Rebrand to Proof of Work; multi-user 0.4 package with curated library sync
Repo cleanup - Add top-level .gitignore (was missing; node_modules, .next, *.s9pk, image.tar, seed/data/*.db, log files, etc.) and a root README. - Delete legacy start9/0.3.5/ package (StartOS 0.3.5 wrapper, no longer the deploy target). - Delete start9-example-packaging/ (template from another project). - Delete planning docs (START9_PACKAGING_LOG.md, VERSIONING.md, STARTOS_0.4_UPGRADE_PROMPT.md, ICON_FILES_INDEX.md, etc.) — info now lives in the deploy guide and code comments. - Drop the standalone Dockerfile, docker-compose.yml, ICON_*, and dev log/build artifacts from the app dir. - Drop the v0.1.0:18/19/20 version files (they belonged to the legacy workout-log package and don't apply to the new id). Rename + new package - Rename app dir workout-planner/ -> proof-of-work/. - Rename StartOS package id workout-log -> proof-of-work; the new id makes this a brand new StartOS service (clean cutover from the old one rather than in-place upgrade). - Reset version graph; v1.0.0:1 is the seeded cutover release. The Dockerfile bakes a one-time /data snapshot and docker_entrypoint.sh copies it into the new volume on truly-fresh first boot only (both /data/app.db missing AND /data/.seeded absent). - Move start9/0.4-migration/ -> start9/0.4/; the old start9/0.4/ stub is gone. Curated exercise library (multi-user-aware) - proof-of-work/prisma/exercises.seed.json is the canonical library shipped to every install (164 exercises today, dumped from the live snapshot). - proof-of-work/scripts/sync-library.cjs (npm run sync-library) refreshes the JSON from start9/0.4/seed/data/app.db after refresh_seed.sh. - proof-of-work/prisma/seed.ts now reads from the JSON instead of a hardcoded 52-exercise array; runs at Docker build time to seed the fallback DB and on first boot for fresh installs. - proof-of-work/prisma/ensureExerciseLibrary.cjs runs on every container boot (from docker_entrypoint.sh) and INSERT OR IGNOREs every library entry for every user, keyed on (userId, name). Library updates flow to existing installs on package upgrade; user-custom exercises (isCustom=true) and any colliding names are never overwritten; removed exercises stay on existing installs (additive-only). Deploy guide (start9/0.4/DEPLOY_040.md) - Rewritten end-to-end for the workout-log -> proof-of-work cutover: refresh_seed, sync-library, build, sideload, verify, rotate creds, stop the old service, then post-cutover cleanup release v1.0.0:2.
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
# Deploy Proof of Work on StartOS 0.4 (sideload)
|
||||
|
||||
This guide walks the maintainer through cutting over from the legacy
|
||||
`workout-log` package to the new `proof-of-work` package. They share the
|
||||
same upstream code and on-disk schema, but `proof-of-work` is a brand new
|
||||
StartOS service (different package id), so StartOS treats it as a fresh
|
||||
install. Data preservation is handled by baking a one-time snapshot of
|
||||
your live `workout-log` `/data` volume into the `proof-of-work` v1.0.0:1
|
||||
image and copying it into the new volume on first boot.
|
||||
|
||||
Your existing `workout-log` install stays running and untouched the whole
|
||||
time. The cutover is one-way only after you stop and uninstall it; until
|
||||
then you can fall back to it.
|
||||
|
||||
> NEVER click Uninstall on either package mid-cutover. Uninstall destroys
|
||||
> the volume. Use Stop instead. Only Uninstall after you've verified
|
||||
> proof-of-work is happy.
|
||||
|
||||
---
|
||||
|
||||
## 0) Prereqs on your build machine
|
||||
|
||||
You need:
|
||||
|
||||
- Node.js >= 20 and npm
|
||||
- Docker with `buildx`
|
||||
- `start-cli` from Start9 (SDK):
|
||||
<https://docs.start9.com/packaging/0.4.0.x/environment-setup.html>
|
||||
- `jq`, `make`, `git`, `sqlite3`, `rsync`, `ssh`
|
||||
- Reachable SSH access to your existing `workout-log` host
|
||||
|
||||
One-time `start-cli` key setup:
|
||||
|
||||
```sh
|
||||
start-cli init-key
|
||||
```
|
||||
|
||||
`~/.startos/config.yaml` should contain at least:
|
||||
|
||||
```yaml
|
||||
host: http://<your-04-host>.local
|
||||
```
|
||||
|
||||
`make install` uses this to push the `.s9pk` to the target.
|
||||
|
||||
---
|
||||
|
||||
## 1) Refresh the seed snapshot from your live `workout-log` host
|
||||
|
||||
The repo includes `start9/0.4/seed/data/app.db` as a placeholder so the
|
||||
build works without network access, but for an actual cutover you should
|
||||
pull a fresh snapshot from your live host first.
|
||||
|
||||
```sh
|
||||
./start9/0.4/refresh_seed.sh embassy@embassy.local
|
||||
```
|
||||
|
||||
For a 0.3.5 host this auto-detects
|
||||
`/embassy-data/package-data/volumes/workout-log/data/main/`. For a 0.4
|
||||
host pass the volume path explicitly as arg 2 — get it from
|
||||
`start-cli package shell workout-log` and `df` inside the container, or
|
||||
from the StartOS docs.
|
||||
|
||||
Refresh prints expected row counts and runs `PRAGMA integrity_check`. If
|
||||
anything looks off, fix the source before proceeding.
|
||||
|
||||
---
|
||||
|
||||
## 2) (Optional) Refresh the curated exercise library JSON
|
||||
|
||||
The shared library that ships with the package is generated from the same
|
||||
snapshot. After `refresh_seed.sh`, regenerate the JSON if you've added
|
||||
new exercises in the running app:
|
||||
|
||||
```sh
|
||||
cd proof-of-work
|
||||
npm run sync-library
|
||||
git diff prisma/exercises.seed.json # eyeball the new rows
|
||||
```
|
||||
|
||||
Commit the JSON change before building so it's baked into the image.
|
||||
|
||||
---
|
||||
|
||||
## 3) Build the package
|
||||
|
||||
```sh
|
||||
cd start9/0.4
|
||||
npm ci
|
||||
make clean
|
||||
make x86
|
||||
```
|
||||
|
||||
On success you'll get an artifact at:
|
||||
|
||||
```
|
||||
start9/0.4/proof-of-work_x86_64.s9pk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4) Sideload on the 0.4 host
|
||||
|
||||
### Option A — `make install` (fastest)
|
||||
|
||||
```sh
|
||||
make install
|
||||
```
|
||||
|
||||
### Option B — StartOS web UI
|
||||
|
||||
1. Open the StartOS 0.4 web UI on the target host.
|
||||
2. **System -> Sideload Service**.
|
||||
3. Upload `start9/0.4/proof-of-work_x86_64.s9pk`.
|
||||
4. Click **Install**, then **Start**.
|
||||
|
||||
`workout-log` and `proof-of-work` will appear as two separate services in
|
||||
the UI. Both can be running simultaneously — they have different volumes,
|
||||
different ports the SDK assigns, and no shared state.
|
||||
|
||||
---
|
||||
|
||||
## 5) First-boot verification
|
||||
|
||||
### 5a. Confirm the seed branch ran
|
||||
|
||||
In **Services -> Proof of Work -> Logs**, or via CLI:
|
||||
|
||||
```sh
|
||||
start-cli package logs proof-of-work | grep '\[entrypoint\]'
|
||||
```
|
||||
|
||||
On the very first boot you should see:
|
||||
|
||||
```
|
||||
[entrypoint] no /data/app.db and no .seeded marker; copying baked cutover seed from /app/seed/data/app.db
|
||||
[entrypoint] ensuring curated exercise library is present for every user
|
||||
[ensure-library] processed 1 user(s) x 164 exercise(s) (164 INSERT OR IGNORE statements)
|
||||
[entrypoint] launching Next.js on :3000 with DATABASE_URL=file:/data/app.db
|
||||
```
|
||||
|
||||
Subsequent boots:
|
||||
|
||||
```
|
||||
[entrypoint] /data/app.db already present; live data is the source of truth
|
||||
[entrypoint] found .seeded: seeded from baked cutover snapshot at 2026-...
|
||||
[ensure-library] processed N user(s) x 164 exercise(s)
|
||||
```
|
||||
|
||||
### 5b. Log into the app
|
||||
|
||||
Open the web UI from the StartOS service page and log in with the same
|
||||
credentials you used on the legacy `workout-log` install (the bcrypt hash
|
||||
came over with the snapshot, so existing passwords keep working).
|
||||
|
||||
Default seed credentials (only on a brand-new install with no baked
|
||||
seed): `admin@local` / `workout123` — change immediately via the action
|
||||
in section 6.
|
||||
|
||||
### 5c. Spot-check the data
|
||||
|
||||
- Workouts count matches your old install (run-counts in the
|
||||
`refresh_seed.sh` output were the source of truth).
|
||||
- Exercise library shows your 164 exercises.
|
||||
- Open a recent workout and confirm set logs match.
|
||||
|
||||
### 5d. Run a StartOS backup
|
||||
|
||||
**Services -> Proof of Work -> Backup -> Create Backup**. Confirm it
|
||||
completes. This validates `Backups.ofVolumes('main')` wiring on the new
|
||||
package.
|
||||
|
||||
---
|
||||
|
||||
## 6) Rotate admin credentials
|
||||
|
||||
Stop the service first (action is gated on `allowedStatuses: only-stopped`):
|
||||
|
||||
1. **Services -> Proof of Work -> Stop**
|
||||
2. **Actions -> Change admin credentials**
|
||||
3. Fill in new email, new password, confirm. Submit.
|
||||
4. **Start** the service and log in with the new credentials.
|
||||
|
||||
---
|
||||
|
||||
## 7) Stop the legacy `workout-log` service
|
||||
|
||||
Once you've used `proof-of-work` for at least a session and confirmed
|
||||
nothing's missing:
|
||||
|
||||
1. **Services -> Workout Log -> Stop**
|
||||
|
||||
DO NOT Uninstall yet. Keeping the old service installed (just stopped)
|
||||
preserves its `/data` volume in case you need to re-pull the snapshot or
|
||||
roll back. Once you're confident (a week is plenty), Uninstall to free
|
||||
the volume.
|
||||
|
||||
---
|
||||
|
||||
## 8) Post-cutover cleanup release (v1.0.0:2)
|
||||
|
||||
After a few days running on `proof-of-work` with no surprises, ship a
|
||||
cleanup release that:
|
||||
|
||||
- Strips `COPY start9/0.4/seed/data /app/seed/data` from the Dockerfile.
|
||||
- Strips the seed-copy branch out of `docker_entrypoint.sh` (keep the
|
||||
fallback DB branch and the ensure-library step).
|
||||
- Leaves `seed/` and `refresh_seed.sh` on disk as historical artifacts.
|
||||
- Adds `startos/versions/v1.0.0.2.ts` (empty `up`/`down` migrations) and
|
||||
promotes it to `current` in the version graph; v1.0.0:1 moves to `other`.
|
||||
|
||||
Reason: once `/data` is the sole source of truth, a baked seed in the
|
||||
image becomes a foot-gun — removing it eliminates any chance of a future
|
||||
upgrade accidentally stomping live data.
|
||||
|
||||
---
|
||||
|
||||
## 9) Library updates after cutover (steady state)
|
||||
|
||||
```
|
||||
# add new exercises in the running app, then:
|
||||
./start9/0.4/refresh_seed.sh embassy@embassy.local
|
||||
cd proof-of-work && npm run sync-library
|
||||
git add prisma/exercises.seed.json
|
||||
# bump the build rev in start9/0.4/startos/versions, add a new version file,
|
||||
# commit, then:
|
||||
cd start9/0.4 && make clean && make x86 && make install
|
||||
```
|
||||
|
||||
On every upgrade, every user on the instance picks up the new exercises
|
||||
on first boot via the `INSERT OR IGNORE` step. Custom exercises a user
|
||||
added themselves are never overwritten.
|
||||
|
||||
---
|
||||
|
||||
## 10) Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| Login fails with your `workout-log` password | Snapshot was pulled from a stale source | Re-run `refresh_seed.sh` against your live host and rebuild. |
|
||||
| Workouts missing after cutover | Same | Same. |
|
||||
| `/data/app.db` is empty after first boot | `.seeded` marker present without the DB (corrupted prior boot) | `start-cli package shell proof-of-work`, `rm /data/.seeded`, restart. |
|
||||
| Service never reaches Ready | Port 3000 not listening | Check logs: Prisma or Next.js threw at boot. Verify the DB file isn't zero bytes. |
|
||||
| `[ensure-library] WARNING` | sqlite3 failed to apply the curated library | Inspect `/data/app.db` and the JSON. Boot continues; library updates skipped this cycle. |
|
||||
| Container exits immediately | Build context wrong — `proof-of-work/` not visible to Docker | Run `make` from `start9/0.4/`, not the repo root. |
|
||||
| `make x86` fails with `No rule to make target .git/HEAD` | s9pk.mk's awk grabbed two `id:` matches and concatenated BASE_NAME | Confirm `manifest/index.ts` has only one `id:` line. The shipped s9pk.mk uses `{print $$2; exit}` which already guards this. |
|
||||
| Docker fails with `ERROR: failed to build: resolve : lstat start9: no such file or directory` | `dockerBuild.dockerfile` was set to a path including `start9/0.4/` | The shipped manifest sets `dockerfile: './Dockerfile'`. The `dockerfile` field is resolved relative to the package dir, not relative to `workdir`. |
|
||||
|
||||
---
|
||||
|
||||
## 11) Data-preservation summary
|
||||
|
||||
- Volume name (`main`), mount path (`/data`), DB path (`/data/app.db`),
|
||||
internal port (`3000`): identical to the legacy `workout-log` package.
|
||||
- Package id changed: `workout-log` -> `proof-of-work`. StartOS treats
|
||||
this as a brand new service. Migration is via the baked seed in
|
||||
v1.0.0:1, not via in-place upgrade.
|
||||
- Seed only writes to `/data` on truly-fresh first boot (both `app.db`
|
||||
missing AND `.seeded` marker absent).
|
||||
- Library updates (new exercises) flow to every user on every upgrade
|
||||
via additive `INSERT OR IGNORE`. Never deletes; never overwrites a
|
||||
user's own custom exercises.
|
||||
- StartOS Backup captures `/data` in full.
|
||||
- Uninstall destroys `/data`. The `alertUpdate` copy reminds users.
|
||||
@@ -0,0 +1,88 @@
|
||||
# syntax=docker/dockerfile:1.6
|
||||
#
|
||||
# Proof of Work (proof-of-work) — StartOS 0.4 package image.
|
||||
#
|
||||
# Build context: repo root (see manifest.images.main.source.dockerBuild.workdir
|
||||
# which is set to '../..' so all COPY paths below are repo-root-relative).
|
||||
#
|
||||
# This Dockerfile is self-contained: it references only files under
|
||||
# `proof-of-work/` (the upstream app) and `start9/0.4/` (this wrapper).
|
||||
#
|
||||
# Data preservation (v1.0.0:1 — initial seeded cutover):
|
||||
# - This image bakes a one-time snapshot of the maintainer's live /data
|
||||
# volume under /app/seed/data so the cutover from the legacy `workout-log`
|
||||
# package preserves every workout, exercise, and preference.
|
||||
# - docker_entrypoint.sh copies the seed into the StartOS-managed /data
|
||||
# volume only on a truly-fresh first boot (both /data/app.db missing AND
|
||||
# /data/.seeded absent). Every subsequent boot leaves /data alone.
|
||||
# - v1.0.0:2 will strip the seed copy from the image and the seed-copy
|
||||
# branch from the entrypoint once the cutover is verified in production.
|
||||
# - A tiny empty-schema fallback DB is also COPYed from the builder stage
|
||||
# (at /app/prisma/data/app.db) as a safety net for fresh sideloads on a
|
||||
# brand-new host with no existing /data and no baked seed.
|
||||
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache openssl
|
||||
|
||||
COPY proof-of-work/package.json proof-of-work/package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY proof-of-work/ ./
|
||||
RUN npx prisma generate
|
||||
|
||||
# Build a fallback empty-but-schema-correct DB. Used by docker_entrypoint.sh
|
||||
# only when /data has no app.db AND no baked seed is available (i.e. after
|
||||
# v1.0.0:2 strips the seed). Seeded with the curated exercise library via
|
||||
# `npm run db:seed`, so a brand-new install still gets the full library.
|
||||
RUN mkdir -p /tmp-seed \
|
||||
&& DATABASE_URL=file:/tmp-seed/app.db npx prisma db push --skip-generate \
|
||||
&& DATABASE_URL=file:/tmp-seed/app.db npm run db:seed
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache dumb-init curl openssl sqlite \
|
||||
&& addgroup -S nodejs -g 1001 \
|
||||
&& adduser -S nextjs -u 1001 -G nodejs
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
HOSTNAME=0.0.0.0 \
|
||||
PORT=3000 \
|
||||
WORKOUT_DATA_DIR=/data \
|
||||
WORKOUT_DB_PATH=/data/app.db \
|
||||
WORKOUT_FALLBACK_SEED_DB_PATH=/app/prisma/data/app.db \
|
||||
WORKOUT_BAKED_SEED_DB_PATH=/app/seed/data/app.db \
|
||||
WORKOUT_LIBRARY_JSON_PATH=/app/prisma/exercises.seed.json
|
||||
|
||||
# Next.js standalone runtime bundle
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
|
||||
|
||||
# Empty-schema fallback DB (used only when no baked seed is available on a
|
||||
# brand-new sideload).
|
||||
COPY --from=builder --chown=nextjs:nodejs /tmp-seed/app.db /app/prisma/data/app.db
|
||||
|
||||
# Baked one-time cutover seed: the maintainer's live /data snapshot pulled
|
||||
# off the running `workout-log` host via refresh_seed.sh. Copied into /data
|
||||
# only on truly-fresh first boot. Removed in v1.0.0:2.
|
||||
COPY --chown=nextjs:nodejs start9/0.4/seed/data /app/seed/data
|
||||
|
||||
# Container entrypoint and diagnostic healthcheck
|
||||
COPY start9/0.4/docker_entrypoint.sh /usr/local/bin/docker_entrypoint.sh
|
||||
COPY start9/0.4/healthcheck.sh /usr/local/bin/healthcheck.sh
|
||||
|
||||
RUN chmod +x /usr/local/bin/docker_entrypoint.sh /usr/local/bin/healthcheck.sh \
|
||||
&& mkdir -p /data \
|
||||
&& chown -R nextjs:nodejs /app /data
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENTRYPOINT ["dumb-init", "--", "/usr/local/bin/docker_entrypoint.sh"]
|
||||
@@ -0,0 +1,4 @@
|
||||
All rights reserved.
|
||||
|
||||
This StartOS packaging wrapper and associated project materials are provided for
|
||||
internal/private use unless otherwise licensed by the repository owner.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Thin override of s9pk.mk.
|
||||
#
|
||||
# Beta target: x86_64 only, per StartOS 0.4 beta guidance. Change to
|
||||
# "x86 arm" (or "x86 arm riscv") once those architectures are officially
|
||||
# supported and tested on your host.
|
||||
ARCHES := x86
|
||||
|
||||
include s9pk.mk
|
||||
+83
-7
@@ -1,10 +1,86 @@
|
||||
# Start9 Wrapper (0.4.0 planning)
|
||||
<p align="center">
|
||||
<img src="icon.png" alt="Proof of Work Logo" width="21%">
|
||||
</p>
|
||||
|
||||
This directory is reserved for the future StartOS 0.4.0 wrapper.
|
||||
# Proof of Work on StartOS 0.4 (migration package)
|
||||
|
||||
Migration intent from 0.3.5:
|
||||
- Keep package id as `workout-log` when StartOS 0.4.0 migration rules allow it.
|
||||
- Preserve persistent data contract at `/data/app.db`.
|
||||
- Reuse the same backup/restore semantics so migration is low-risk.
|
||||
This directory packages **Proof of Work** (`proof-of-work`) for StartOS 0.4
|
||||
beta. It is the cutover package that carries your 0.3.5 data across to a new
|
||||
x86_64 StartOS 0.4 host.
|
||||
|
||||
When 0.4.0 is stable, adapt manifest/build interfaces to the 0.4 schema and validate with the relevant Start9 docs.
|
||||
> Upstream app lives at `../../proof-of-work/` in this repo.
|
||||
> Legacy 0.3.5 package lives at `../0.3.5/` (kept intact; do not modify).
|
||||
> Codex's WIP 0.4 scaffold lives at `../0.4/` (kept intact; superseded by
|
||||
> this folder).
|
||||
|
||||
## Goals
|
||||
|
||||
- Keep the package id `proof-of-work` so StartOS recognizes it as the same service.
|
||||
- Keep the persistent data volume `main` mounted at `/data`.
|
||||
- Keep the SQLite database at `/data/app.db`.
|
||||
- Preserve every existing workout, set, exercise, and preference.
|
||||
- Ship x86_64 only for 0.4 beta (sideload target).
|
||||
|
||||
## How data preservation works
|
||||
|
||||
1. `seed/data/app.db` holds a one-time snapshot of `/data` from the live
|
||||
0.3.5 host (currently 1 user, 348 workouts, 164 exercises, 5720 set
|
||||
logs).
|
||||
2. The `Dockerfile` bakes that snapshot into the image at
|
||||
`/app/seed/data/`.
|
||||
3. On **first boot only** — `/data/app.db` missing AND `/data/.seeded`
|
||||
absent — `docker_entrypoint.sh` copies the seed into `/data/` and writes
|
||||
a `.seeded` marker.
|
||||
4. On every subsequent boot, `/data/` is the sole source of truth; the seed
|
||||
in the image is ignored.
|
||||
|
||||
See `seed/README.md` for the snapshot provenance and row counts.
|
||||
|
||||
## Image runtime
|
||||
|
||||
| Property | Value |
|
||||
| --- | --- |
|
||||
| Base image | `node:20-alpine` (multi-stage build) |
|
||||
| App runtime | Next.js standalone + Prisma + SQLite |
|
||||
| Entrypoint | `/usr/local/bin/docker_entrypoint.sh` (dumb-init wrapped) |
|
||||
| Internal port | `3000` |
|
||||
| Architectures | `x86_64` (beta) |
|
||||
|
||||
## Build and sideload
|
||||
|
||||
```sh
|
||||
cd start9/0.4
|
||||
npm ci
|
||||
make clean
|
||||
make x86 # outputs proof-of-work_x86_64.s9pk
|
||||
```
|
||||
|
||||
Sideload via StartOS web UI or `make install` (requires `~/.startos/config.yaml`).
|
||||
Step-by-step instructions are in [`DEPLOY_040.md`](./DEPLOY_040.md).
|
||||
|
||||
## What is unchanged from 0.3.5
|
||||
|
||||
- Package id: `proof-of-work`
|
||||
- Volume id: `main`
|
||||
- Mount path: `/data`
|
||||
- DB path: `/data/app.db`
|
||||
- Health endpoint: `/api/health`
|
||||
- Compat `ALTER TABLE` block (idempotent; no-op on a current DB)
|
||||
|
||||
## What is new in 0.4
|
||||
|
||||
- TypeScript SDK manifest under `startos/`
|
||||
- ExVer version (`0.1.0:18`) replaces the 0.3.5 4-part `0.1.0.17`
|
||||
- Seed-on-first-boot with a `.seeded` marker and stderr logging
|
||||
- `alertUpdate` warning users not to Uninstall to troubleshoot
|
||||
- Self-contained Dockerfile — no references to `../0.3.5/` or `../0.4/`
|
||||
|
||||
## Follow-up releases (planned, do not ship yet)
|
||||
|
||||
- **v0.1.0:19** — remove the `COPY seed/data \u2026` line and the seed block
|
||||
from the entrypoint once the cutover is confirmed. Leaves `seed/` on disk
|
||||
unreferenced.
|
||||
- **v0.1.0:19** or **v0.1.0:20** — add a StartOS Package Action
|
||||
`change-admin-credentials` that updates the User row in `/data/app.db`
|
||||
(bcryptjs, salt rounds 10) so you can rename/rotate the admin from the
|
||||
StartOS UI.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# About Proof of Work
|
||||
|
||||
Proof of Work is a private, self-hosted workout planning and training log. It
|
||||
runs entirely on your StartOS server — no third-party cloud, no analytics, no
|
||||
external account. Your workouts, exercises, and body-metric history stay in
|
||||
the StartOS `main` volume and are included in every StartOS backup.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Log workouts, sets, reps, weight, RPE, custom metrics.
|
||||
- Soft-deletes preserve history (deleted workouts are hidden, not erased).
|
||||
- Browser-based UI; single local user by default.
|
||||
- SQLite database at `/data/app.db` — portable, auditable, backup-safe.
|
||||
|
||||
## Contract invariants
|
||||
|
||||
The 0.4 package keeps the exact same data contract as the original 0.3.5
|
||||
release so existing `/data` contents are preserved end-to-end:
|
||||
|
||||
| What | Value |
|
||||
| --- | --- |
|
||||
| Package id | `proof-of-work` |
|
||||
| Volume id | `main` |
|
||||
| Mount path | `/data` |
|
||||
| Primary DB file | `/data/app.db` |
|
||||
| Internal port | `3000` |
|
||||
| Health endpoint | `/api/health` |
|
||||
Executable
+107
@@ -0,0 +1,107 @@
|
||||
#!/bin/sh
|
||||
# Proof of Work (proof-of-work) — StartOS 0.4 container entrypoint.
|
||||
#
|
||||
# Responsibilities (in order):
|
||||
# 1. Ensure the persistent /data directory exists.
|
||||
# 2. Seed /data on truly-fresh first boot:
|
||||
# a. If /app/seed/data/app.db is present (v1.0.0:1 cutover image),
|
||||
# copy it into /data. This is how an operator migrating from
|
||||
# the legacy `workout-log` package preserves their full history.
|
||||
# b. Otherwise fall back to the empty-schema fallback DB at
|
||||
# /app/prisma/data/app.db (already seeded with the curated
|
||||
# exercise library at build time).
|
||||
# Both branches are gated on /data/app.db being absent AND
|
||||
# /data/.seeded being absent — never overwrites live data.
|
||||
# 3. Run idempotent compat ALTERs for columns added after older snapshots.
|
||||
# No-ops on hosts whose schema is already current.
|
||||
# 4. Ensure the curated exercise library is present for every user. New
|
||||
# maintainer-shipped exercises appear on every boot (INSERT OR IGNORE
|
||||
# keyed on (userId, name); never overwrites a user's own exercises).
|
||||
# 5. Exec the Next.js standalone server as PID 1 (under dumb-init).
|
||||
#
|
||||
# Every branch logs to stderr so it is visible in StartOS -> Logs.
|
||||
|
||||
set -eu
|
||||
|
||||
DATA_DIR="${WORKOUT_DATA_DIR:-/data}"
|
||||
DB_PATH="${WORKOUT_DB_PATH:-$DATA_DIR/app.db}"
|
||||
FALLBACK_SEED_DB_PATH="${WORKOUT_FALLBACK_SEED_DB_PATH:-/app/prisma/data/app.db}"
|
||||
BAKED_SEED_DB_PATH="${WORKOUT_BAKED_SEED_DB_PATH:-/app/seed/data/app.db}"
|
||||
LIBRARY_JSON_PATH="${WORKOUT_LIBRARY_JSON_PATH:-/app/prisma/exercises.seed.json}"
|
||||
|
||||
log() {
|
||||
# write to stderr so StartOS log viewer surfaces it immediately
|
||||
printf '[entrypoint] %s\n' "$*" 1>&2
|
||||
}
|
||||
|
||||
mkdir -p "$DATA_DIR"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 1 — first-boot seeding. NEVER overwrites an existing app.db.
|
||||
# Both branches are skipped on every restart of an installed host because
|
||||
# /data/app.db already exists.
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ ! -f "$DB_PATH" ] && [ ! -f "$DATA_DIR/.seeded" ]; then
|
||||
if [ -f "$BAKED_SEED_DB_PATH" ]; then
|
||||
log "no $DB_PATH and no .seeded marker; copying baked cutover seed from $BAKED_SEED_DB_PATH"
|
||||
cp "$BAKED_SEED_DB_PATH" "$DB_PATH"
|
||||
date -u +"seeded from baked cutover snapshot at %Y-%m-%dT%H:%M:%SZ" > "$DATA_DIR/.seeded"
|
||||
elif [ -f "$FALLBACK_SEED_DB_PATH" ]; then
|
||||
log "no $DB_PATH and no baked seed; copying empty-schema fallback from $FALLBACK_SEED_DB_PATH"
|
||||
cp "$FALLBACK_SEED_DB_PATH" "$DB_PATH"
|
||||
date -u +"seeded from empty-schema fallback at %Y-%m-%dT%H:%M:%SZ" > "$DATA_DIR/.seeded"
|
||||
else
|
||||
log "no $DB_PATH, no baked seed, no fallback DB; creating empty $DB_PATH"
|
||||
touch "$DB_PATH"
|
||||
fi
|
||||
else
|
||||
log "$DB_PATH already present; live data is the source of truth"
|
||||
if [ -f "$DATA_DIR/.seeded" ]; then
|
||||
log "found .seeded: $(cat "$DATA_DIR/.seeded")"
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 2 — idempotent compat ALTERs (safety net for older snapshots).
|
||||
# No-ops on hosts whose schema is already current.
|
||||
# -----------------------------------------------------------------------------
|
||||
if command -v sqlite3 >/dev/null 2>&1 && [ -f "$DB_PATH" ]; then
|
||||
if ! sqlite3 "$DB_PATH" "PRAGMA table_info('SetLog');" 2>/dev/null | grep -q "|customMetrics|"; then
|
||||
log "adding missing column SetLog.customMetrics"
|
||||
sqlite3 "$DB_PATH" "ALTER TABLE SetLog ADD COLUMN customMetrics TEXT;"
|
||||
fi
|
||||
|
||||
if ! sqlite3 "$DB_PATH" "PRAGMA table_info('Workout');" 2>/dev/null | grep -q "|deletedAt|"; then
|
||||
log "adding missing column Workout.deletedAt"
|
||||
sqlite3 "$DB_PATH" "ALTER TABLE Workout ADD COLUMN deletedAt DATETIME;"
|
||||
fi
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 3 — ensure curated exercise library for every user (multi-user-aware).
|
||||
# New entries shipped in /app/prisma/exercises.seed.json appear on every boot.
|
||||
# `INSERT OR IGNORE` keyed on (userId, name) so we never overwrite a user's
|
||||
# own custom exercises. Designed to be additive only — exercises removed from
|
||||
# the curated JSON are not deleted from existing installs (users may have
|
||||
# logged sets against them).
|
||||
# -----------------------------------------------------------------------------
|
||||
if [ -f "$LIBRARY_JSON_PATH" ] && [ -f "$DB_PATH" ]; then
|
||||
log "ensuring curated exercise library is present for every user"
|
||||
node /app/prisma/ensureExerciseLibrary.cjs \
|
||||
--db "$DB_PATH" \
|
||||
--json "$LIBRARY_JSON_PATH" \
|
||||
|| log "WARNING: ensureExerciseLibrary failed; continuing boot"
|
||||
else
|
||||
log "skipping library ensure (json or db not found)"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 4 — launch the app.
|
||||
# -----------------------------------------------------------------------------
|
||||
export DATABASE_URL="file:$DB_PATH"
|
||||
export NODE_ENV="${NODE_ENV:-production}"
|
||||
export HOSTNAME="${HOSTNAME:-0.0.0.0}"
|
||||
export PORT="${PORT:-3000}"
|
||||
|
||||
log "launching Next.js on :${PORT} with DATABASE_URL=file:${DB_PATH}"
|
||||
exec node /app/server.js
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# Diagnostic healthcheck for local smoke-tests.
|
||||
#
|
||||
# StartOS 0.4 uses the TypeScript `checkPortListening` probe defined in
|
||||
# `startos/main.ts`; this script is NOT wired into the manifest. It's here
|
||||
# so you can `docker exec` into the container and validate end-to-end DB
|
||||
# connectivity against the app's /api/health endpoint.
|
||||
|
||||
set -eu
|
||||
|
||||
PORT="${PORT:-3000}"
|
||||
curl -fsS "http://127.0.0.1:${PORT}/api/health"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Generated
+345
@@ -0,0 +1,345 @@
|
||||
{
|
||||
"name": "workout-log-startos",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "workout-log-startos",
|
||||
"dependencies": {
|
||||
"@start9labs/start-sdk": "1.0.0",
|
||||
"bcryptjs": "^2.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "^22.19.0",
|
||||
"@vercel/ncc": "^0.38.4",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@iarna/toml": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz",
|
||||
"integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "1.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
|
||||
"integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@start9labs/start-sdk": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@start9labs/start-sdk/-/start-sdk-1.0.0.tgz",
|
||||
"integrity": "sha512-rtAfumVbMy90iw2WRbWH7fGcuwAvvuFfR4YwgSsh5R2Bz9MXtcEfmznwhnrp+ntQ6BOUSQ0wLzePbfsS6kUagg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^3.0.0",
|
||||
"@noble/curves": "^1.8.2",
|
||||
"@noble/hashes": "^1.7.2",
|
||||
"@types/ini": "^4.1.1",
|
||||
"deep-equality-data-structures": "^2.0.0",
|
||||
"fast-xml-parser": "^5.5.6",
|
||||
"ini": "^5.0.0",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"mime": "^4.0.7",
|
||||
"yaml": "^2.7.1",
|
||||
"zod": "^4.3.6",
|
||||
"zod-deep-partial": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bcryptjs": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ini": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ini/-/ini-4.1.1.tgz",
|
||||
"integrity": "sha512-MIyNUZipBTbyUNnhvuXJTY7B6qNI78meck9Jbv3wk0OgNwRyOOVEKDutAkOs1snB/tx0FafyR6/SN4Ps0hZPeg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
|
||||
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vercel/ncc": {
|
||||
"version": "0.38.4",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz",
|
||||
"integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"ncc": "dist/ncc/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
|
||||
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deep-equality-data-structures": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-equality-data-structures/-/deep-equality-data-structures-2.0.0.tgz",
|
||||
"integrity": "sha512-qgrUr7MKXq7VRN+WUpQ48QlXVGL0KdibAoTX8KRg18lgOgqbEKMAW1WZsVCtakY4+XX42pbAJzTz/DlXEFM2Fg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-hash": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
|
||||
"integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-expression-matcher": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.5.9",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.9.tgz",
|
||||
"integrity": "sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-xml-builder": "^1.1.4",
|
||||
"path-expression-matcher": "^1.2.0",
|
||||
"strnum": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz",
|
||||
"integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/isomorphic-fetch": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz",
|
||||
"integrity": "sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-fetch": "^2.6.1",
|
||||
"whatwg-fetch": "^3.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz",
|
||||
"integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/object-hash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/path-expression-matcher": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz",
|
||||
"integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz",
|
||||
"integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-fetch": {
|
||||
"version": "3.6.20",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz",
|
||||
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
|
||||
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zod-deep-partial": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/zod-deep-partial/-/zod-deep-partial-1.4.4.tgz",
|
||||
"integrity": "sha512-aWkPl7hVStgE01WzbbSxCgX4O+sSpgt8JOjvFUtMTF75VgL6MhWQbiZi+AWGN85SfSTtI9gsOtL1vInoqfDVaA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"zod": "^4.1.13"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "proof-of-work-startos",
|
||||
"scripts": {
|
||||
"build": "rm -rf ./javascript && ncc build startos/index.ts -o ./javascript",
|
||||
"prettier": "prettier --write startos",
|
||||
"check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@start9labs/start-sdk": "1.0.0",
|
||||
"bcryptjs": "^2.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "^22.19.0",
|
||||
"@vercel/ncc": "^0.38.4",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
}
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env bash
|
||||
# Refresh seed/data/ from the live StartOS 0.3.5 host over SSH.
|
||||
#
|
||||
# Usage:
|
||||
# ./start9/0.4/refresh_seed.sh <ssh-target> [remote-volume-path]
|
||||
#
|
||||
# Example (most common):
|
||||
# ./start9/0.4/refresh_seed.sh embassy@embassy.local
|
||||
#
|
||||
# Example (custom remote path):
|
||||
# ./start9/0.4/refresh_seed.sh embassy@embassy.local \
|
||||
# /embassy-data/package-data/volumes/proof-of-work/data/main
|
||||
#
|
||||
# Behavior:
|
||||
# - Auto-detects /embassy-data/package-data/volumes/proof-of-work/data/main
|
||||
# if the remote path is omitted.
|
||||
# - Refuses to overwrite a non-empty seed/data/ without confirmation.
|
||||
# - After SCP, runs PRAGMA integrity_check and prints expected row counts.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SEED_DATA_DIR="${SCRIPT_DIR}/seed/data"
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "usage: $0 <ssh-target> [remote-volume-path]" >&2
|
||||
exit 64
|
||||
fi
|
||||
|
||||
SSH_TARGET="$1"
|
||||
REMOTE_PATH="${2:-/embassy-data/package-data/volumes/proof-of-work/data/main}"
|
||||
|
||||
echo "[refresh-seed] target: ${SSH_TARGET}"
|
||||
echo "[refresh-seed] remote path: ${REMOTE_PATH}"
|
||||
echo "[refresh-seed] local seed: ${SEED_DATA_DIR}"
|
||||
|
||||
mkdir -p "${SEED_DATA_DIR}"
|
||||
|
||||
# Prompt if seed/data/ already has files.
|
||||
if [ -n "$(ls -A "${SEED_DATA_DIR}" 2>/dev/null || true)" ]; then
|
||||
echo
|
||||
echo "[refresh-seed] WARNING: ${SEED_DATA_DIR} is non-empty:" >&2
|
||||
ls -la "${SEED_DATA_DIR}" >&2
|
||||
echo
|
||||
read -r -p "Overwrite existing seed? [y/N] " answer
|
||||
case "${answer:-N}" in
|
||||
y|Y|yes|YES) ;;
|
||||
*) echo "[refresh-seed] aborted."; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Verify the remote path exists before copying.
|
||||
echo "[refresh-seed] verifying remote path exists..."
|
||||
if ! ssh "${SSH_TARGET}" "test -d \"${REMOTE_PATH}\""; then
|
||||
echo "[refresh-seed] remote path not found on ${SSH_TARGET}:${REMOTE_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copy everything under the volume root (preserves mode/time).
|
||||
echo "[refresh-seed] copying volume contents via rsync..."
|
||||
rsync -aP --delete-excluded --exclude='.seeded' \
|
||||
"${SSH_TARGET}:${REMOTE_PATH}/" \
|
||||
"${SEED_DATA_DIR}/"
|
||||
|
||||
# Integrity check on the primary SQLite file, if present.
|
||||
DB_PATH="${SEED_DATA_DIR}/app.db"
|
||||
if [ -f "${DB_PATH}" ]; then
|
||||
echo
|
||||
echo "[refresh-seed] sqlite integrity_check:"
|
||||
sqlite3 "${DB_PATH}" "PRAGMA integrity_check;" || {
|
||||
echo "[refresh-seed] WARNING: integrity_check failed" >&2
|
||||
}
|
||||
echo
|
||||
echo "[refresh-seed] row counts:"
|
||||
for t in User Workout Exercise SetLog UserPreferences Program Session; do
|
||||
n="$(sqlite3 "${DB_PATH}" "SELECT COUNT(*) FROM \"${t}\";" 2>/dev/null || echo '-')"
|
||||
printf ' %-18s %s\n' "${t}" "${n}"
|
||||
done
|
||||
else
|
||||
echo "[refresh-seed] NOTE: no app.db present in remote snapshot \u2014 skipped integrity check." >&2
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "[refresh-seed] done. Review ${SEED_DATA_DIR}, commit the snapshot, and rebuild:"
|
||||
echo " cd start9/0.4 && make clean && make"
|
||||
@@ -0,0 +1,163 @@
|
||||
# ** Plumbing. DO NOT EDIT **.
|
||||
# This file is imported by ./Makefile. Make edits there
|
||||
|
||||
PACKAGE_ID := $(shell awk -F"'" '/id:/ {print $$2; exit}' startos/manifest/index.ts)
|
||||
INGREDIENTS := $(shell start-cli s9pk list-ingredients 2>/dev/null)
|
||||
ARCHES ?= x86 arm riscv
|
||||
|
||||
# Nested-package fixes for the upstream Start9 Makefile, which otherwise
|
||||
# assumes the package dir == the git repo root. This package lives at
|
||||
# start9/0.4/, so we need two small adjustments to make Make's
|
||||
# prereq resolution work without rewriting ingredient names.
|
||||
#
|
||||
# (1) Git metadata paths. .git/HEAD and .git/index live at the repo root
|
||||
# (or elsewhere for git worktrees), not at the package dir. Ask git
|
||||
# for the canonical location via `git rev-parse --git-path`. Literal
|
||||
# fallbacks preserve upstream behavior outside a git tree.
|
||||
#
|
||||
# (2) VPATH for ingredient search. `start-cli s9pk list-ingredients` emits
|
||||
# paths in a *mixed* mode for nested packages:
|
||||
#
|
||||
# - package-local inputs (icon.png, assets/, LICENSE, and the
|
||||
# compiled javascript/index.js) come out relative to the package
|
||||
# dir (CWD). javascript/index.js is a build output: it doesn't
|
||||
# exist before the first `npm run build`, and it's produced by
|
||||
# the `javascript/index.js:` rule below.
|
||||
# - Docker build inputs (the Dockerfile) come out relative to the
|
||||
# manifest's images.*.source.dockerBuild.workdir, which for this
|
||||
# package is '../..' -- i.e. the repo root.
|
||||
#
|
||||
# Setting VPATH to the repo root lets Make find workdir-relative
|
||||
# inputs (e.g. start9/0.4/Dockerfile) there while leaving
|
||||
# CWD-relative files that don't exist yet (e.g. javascript/index.js)
|
||||
# to be built by their generating rule. We deliberately do NOT
|
||||
# rewrite ingredient names to absolute paths -- doing that breaks
|
||||
# the match between Make's `javascript/index.js:` rule and the
|
||||
# prereq, which is how we got bitten previously.
|
||||
REPO_ROOT := $(shell git rev-parse --show-toplevel 2>/dev/null)
|
||||
GIT_HEAD_PATH := $(shell git rev-parse --git-path HEAD 2>/dev/null || echo .git/HEAD)
|
||||
GIT_INDEX_PATH := $(shell git rev-parse --git-path index 2>/dev/null || echo .git/index)
|
||||
|
||||
VPATH := $(REPO_ROOT)
|
||||
TARGETS ?= arches
|
||||
ifdef VARIANT
|
||||
BASE_NAME := $(PACKAGE_ID)_$(VARIANT)
|
||||
else
|
||||
BASE_NAME := $(PACKAGE_ID)
|
||||
endif
|
||||
|
||||
.PHONY: all arches aarch64 x86_64 riscv64 arm arm64 x86 riscv arch/* clean install check-deps check-init package ingredients
|
||||
.DELETE_ON_ERROR:
|
||||
.SECONDARY:
|
||||
|
||||
define SUMMARY
|
||||
@manifest=$$(start-cli s9pk inspect $(1) manifest); \
|
||||
size=$$(du -h $(1) | awk '{print $$1}'); \
|
||||
title=$$(printf '%s' "$$manifest" | jq -r .title); \
|
||||
version=$$(printf '%s' "$$manifest" | jq -r .version); \
|
||||
arches=$$(printf '%s' "$$manifest" | jq -r '[.images[].arch // []] | flatten | unique | join(", ")'); \
|
||||
sdkv=$$(printf '%s' "$$manifest" | jq -r .sdkVersion); \
|
||||
gitHash=$$(printf '%s' "$$manifest" | jq -r .gitHash | sed -E 's/(.*-modified)$$/\x1b[0;31m\1\x1b[0m/'); \
|
||||
printf "\n"; \
|
||||
printf "\033[1;32m✅ Build Complete!\033[0m\n"; \
|
||||
printf "\n"; \
|
||||
printf "\033[1;37m📦 $$title\033[0m \033[36mv$$version\033[0m\n"; \
|
||||
printf "───────────────────────────────\n"; \
|
||||
printf " \033[1;36mFilename:\033[0m %s\n" "$(1)"; \
|
||||
printf " \033[1;36mSize:\033[0m %s\n" "$$size"; \
|
||||
printf " \033[1;36mArch:\033[0m %s\n" "$$arches"; \
|
||||
printf " \033[1;36mSDK:\033[0m %s\n" "$$sdkv"; \
|
||||
printf " \033[1;36mGit:\033[0m %s\n" "$$gitHash"; \
|
||||
echo ""
|
||||
endef
|
||||
|
||||
all: $(TARGETS)
|
||||
|
||||
arches: $(ARCHES)
|
||||
|
||||
universal: $(BASE_NAME).s9pk
|
||||
$(call SUMMARY,$<)
|
||||
|
||||
arch/%: $(BASE_NAME)_%.s9pk
|
||||
$(call SUMMARY,$<)
|
||||
|
||||
x86 x86_64: arch/x86_64
|
||||
arm arm64 aarch64: arch/aarch64
|
||||
riscv riscv64: arch/riscv64
|
||||
|
||||
$(BASE_NAME).s9pk: $(INGREDIENTS) $(GIT_HEAD_PATH) $(GIT_INDEX_PATH)
|
||||
@$(MAKE) --no-print-directory ingredients
|
||||
@echo " Packing '$@'..."
|
||||
start-cli s9pk pack -o $@
|
||||
|
||||
$(BASE_NAME)_%.s9pk: $(INGREDIENTS) $(GIT_HEAD_PATH) $(GIT_INDEX_PATH)
|
||||
@$(MAKE) --no-print-directory ingredients
|
||||
@echo " Packing '$@'..."
|
||||
start-cli s9pk pack --arch=$* -o $@
|
||||
|
||||
ingredients: $(INGREDIENTS)
|
||||
@echo " Re-evaluating ingredients..."
|
||||
|
||||
install: | check-deps check-init
|
||||
@HOST=$$(awk -F'/' '/^host:/ {print $$3}' ~/.startos/config.yaml); \
|
||||
if [ -z "$$HOST" ]; then \
|
||||
echo "Error: You must define \"host: http://server-name.local\" in ~/.startos/config.yaml"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
S9PK=$$(ls -t *.s9pk 2>/dev/null | head -1); \
|
||||
if [ -z "$$S9PK" ]; then \
|
||||
echo "Error: No .s9pk file found. Run 'make' first."; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
printf "\n🚀 Installing %s to %s ...\n" "$$S9PK" "$$HOST"; \
|
||||
start-cli package install -s "$$S9PK"
|
||||
|
||||
publish: | all
|
||||
@REGISTRY=$$(awk -F'/' '/^registry:/ {print $$3}' ~/.startos/config.yaml); \
|
||||
if [ -z "$$REGISTRY" ]; then \
|
||||
echo "Error: You must define \"registry: https://my-registry.tld\" in ~/.startos/config.yaml"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
S3BASE=$$(awk -F'/' '/^s9pk-s3base:/ {print $$3}' ~/.startos/config.yaml); \
|
||||
if [ -z "$$S3BASE" ]; then \
|
||||
echo "Error: You must define \"s3base: https://s9pks.my-s3-bucket.tld\" in ~/.startos/config.yaml"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
command -v s3cmd >/dev/null || \
|
||||
(echo "Error: s3cmd not found. It must be installed to publish using s3." && exit 1); \
|
||||
printf "\n🚀 Publishing to %s; indexing on %s ...\n" "$$S3BASE" "$$REGISTRY"; \
|
||||
for s9pk in *.s9pk; do \
|
||||
age=$$(( $$(date +%s) - $$(stat -c %Y "$$s9pk") )); \
|
||||
if [ "$$age" -gt 3600 ]; then \
|
||||
printf "\033[1;33m⚠️ %s is %d minutes old. Publish anyway? [y/N] \033[0m" "$$s9pk" "$$((age / 60))"; \
|
||||
read -r ans; \
|
||||
case "$$ans" in [yY]*) ;; *) echo "Skipping $$s9pk"; continue ;; esac; \
|
||||
fi; \
|
||||
start-cli s9pk publish "$$s9pk"; \
|
||||
done
|
||||
|
||||
check-deps:
|
||||
@command -v start-cli >/dev/null || \
|
||||
(echo "Error: start-cli not found. Please see https://docs.start9.com/latest/developer-guide/sdk/installing-the-sdk" && exit 1)
|
||||
@command -v npm >/dev/null || \
|
||||
(echo "Error: npm not found. Please install Node.js and npm." && exit 1)
|
||||
|
||||
check-init:
|
||||
@if [ ! -f ~/.startos/developer.key.pem ]; then \
|
||||
echo "Initializing StartOS developer environment..."; \
|
||||
start-cli init-key; \
|
||||
fi
|
||||
|
||||
javascript/index.js: $(shell find startos -type f) tsconfig.json node_modules
|
||||
npm run check
|
||||
npm run build
|
||||
|
||||
node_modules: package-lock.json
|
||||
npm ci
|
||||
|
||||
package-lock.json: package.json
|
||||
npm i
|
||||
|
||||
clean:
|
||||
@echo "Cleaning up build artifacts..."
|
||||
@rm -rf $(PACKAGE_ID).s9pk $(PACKAGE_ID)_x86_64.s9pk $(PACKAGE_ID)_aarch64.s9pk $(PACKAGE_ID)_riscv64.s9pk javascript node_modules
|
||||
@@ -0,0 +1,59 @@
|
||||
# Seed snapshot
|
||||
|
||||
This directory contains a one-time snapshot of `/data/` from the live
|
||||
StartOS 0.3.5 install of **Proof of Work** (`proof-of-work`). It is baked into
|
||||
the 0.4 Docker image so the first 0.4 boot comes up with your production data
|
||||
already in place.
|
||||
|
||||
## Source
|
||||
|
||||
| Field | Value |
|
||||
| --- | --- |
|
||||
| Source file | `proof-of-work-2026-04-21T14-36-53-332Z.db` (user-provided backup) |
|
||||
| Exported on | 2026-04-21T14:36:53Z |
|
||||
| Exported from | StartOS 0.3.5 (aarch64) running `proof-of-work` v0.1.0.17 |
|
||||
| SQLite integrity check | `ok` |
|
||||
|
||||
## Row counts (at snapshot time)
|
||||
|
||||
| Table | Rows |
|
||||
| --- | --- |
|
||||
| `User` | 1 (`admin@local`) |
|
||||
| `UserPreferences` | 1 |
|
||||
| `Session` | 9 |
|
||||
| `Exercise` | 164 |
|
||||
| `Workout` | 348 |
|
||||
| `SetLog` | 5720 |
|
||||
| `Equipment` | 0 |
|
||||
| `Program` / `ProgramWeek` / `ProgramDay` / `ProgramExercise` | 0 |
|
||||
| `ContentItem` / `ContentChunk` | 0 |
|
||||
| `AISuggestion` | 0 |
|
||||
|
||||
## What the seed does
|
||||
|
||||
On **first boot** of this 0.4 package, `docker_entrypoint.sh`:
|
||||
|
||||
1. Creates `/data/` if missing.
|
||||
2. If `/data/app.db` does NOT exist AND `/data/.seeded` does NOT exist, copies
|
||||
everything under `/app/seed/data/` into `/data/` and writes `/data/.seeded`
|
||||
with a timestamp.
|
||||
3. Logs which branch it took to stderr (visible in the StartOS log viewer).
|
||||
|
||||
On every subsequent boot, the seed copy is skipped and the live `/data/` is
|
||||
used as the sole source of truth.
|
||||
|
||||
## When to refresh this snapshot
|
||||
|
||||
You usually refresh the seed only if you rebuild the package **before** doing
|
||||
the first-time sideload on the 0.4 host. Once the 0.4 service is running,
|
||||
stop rebuilding with a seed — the next package release (v0.1.0:19) removes
|
||||
the seed step entirely so releases don't risk overwriting live data.
|
||||
|
||||
To refresh before first-time sideload, from the repo root:
|
||||
|
||||
```sh
|
||||
./start9/0.4/refresh_seed.sh embassy@embassy.local
|
||||
```
|
||||
|
||||
This SCPs the live `/embassy-data/package-data/volumes/proof-of-work/data/main/`
|
||||
files back into `start9/0.4/seed/data/` and runs an integrity check.
|
||||
@@ -0,0 +1,201 @@
|
||||
import bcryptjs from 'bcryptjs'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
/**
|
||||
* change-admin-credentials — StartOS Package Action.
|
||||
*
|
||||
* Lets the user rotate the admin email and password for Proof of Work directly
|
||||
* from the StartOS UI, without dropping to a shell. Replaces the manual CLI
|
||||
* fallback documented in DEPLOY_040.md \u00a75b.
|
||||
*
|
||||
* Design notes:
|
||||
*
|
||||
* - allowedStatuses: 'only-stopped'. StartOS forces a Stop before the action
|
||||
* runs, so there is zero risk of two writers (the running Next.js server +
|
||||
* the action's sqlite3 UPDATE) racing on /data/app.db. As a side effect,
|
||||
* any previously-issued session cookies are implicitly invalidated when
|
||||
* the service restarts and re-reads the User row.
|
||||
*
|
||||
* - Bcrypt salt rounds = 10. This MUST match
|
||||
* proof-of-work/lib/auth.ts::hashPassword, which uses bcryptjs.genSalt(10)
|
||||
* followed by bcryptjs.hash. If the app ever changes its rounds, change them
|
||||
* here too \u2014 otherwise login will fail.
|
||||
*
|
||||
* - We compute the bcrypt hash in the action's own JS runtime (bcryptjs is
|
||||
* bundled via package.json), then push only the finished hash into the
|
||||
* subcontainer. The plaintext password never lands in /proc, the SQL log,
|
||||
* or anywhere persistent.
|
||||
*
|
||||
* - The UPDATE is keyed on `id = (SELECT id FROM User ORDER BY createdAt ASC
|
||||
* LIMIT 1)` rather than `WHERE email = 'admin@local'` (the original 0.3.5
|
||||
* default). That makes the action safe to re-run after a previous rotation.
|
||||
* The app is single-user by design, so this targets the only User row.
|
||||
*
|
||||
* - We assert exactly 1 row was updated (`changes() == 1`). Anything else
|
||||
* means the schema/data is in an unexpected state and we abort without
|
||||
* reporting success, so the user is forced to investigate before assuming
|
||||
* credentials rotated successfully.
|
||||
*
|
||||
* Available from package version 0.1.0:20 onward.
|
||||
*/
|
||||
|
||||
const EMAIL_PATTERN = '^[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}$'
|
||||
|
||||
/** Escape a string for safe inclusion inside SQLite single-quoted literal. */
|
||||
const sqlQuote = (s: string): string => `'${s.replace(/'/g, "''")}'`
|
||||
|
||||
export const changeAdminCredentials = sdk.Action.withInput(
|
||||
'change-admin-credentials',
|
||||
// ---------------------------------------------------------------------------
|
||||
// metadata
|
||||
// ---------------------------------------------------------------------------
|
||||
async () => ({
|
||||
name: 'Change admin credentials',
|
||||
description:
|
||||
'Rotate the admin email and password stored in /data/app.db. The service must be stopped first; you will log in with the new credentials after starting it again.',
|
||||
warning:
|
||||
'This permanently overwrites the existing User row. Any browser sessions issued under the old credentials will stop working as soon as the service restarts. Make sure you can receive the new email at the address you enter.',
|
||||
visibility: 'enabled',
|
||||
allowedStatuses: 'only-stopped',
|
||||
group: null,
|
||||
}),
|
||||
// ---------------------------------------------------------------------------
|
||||
// input form
|
||||
// ---------------------------------------------------------------------------
|
||||
sdk.InputSpec.of({
|
||||
email: sdk.Value.text({
|
||||
name: 'New email address',
|
||||
description: 'The email you will use to log in.',
|
||||
required: true,
|
||||
default: null,
|
||||
inputmode: 'email',
|
||||
placeholder: 'you@example.com',
|
||||
patterns: [
|
||||
{
|
||||
regex: EMAIL_PATTERN,
|
||||
description: 'Must be a valid email address (e.g. you@example.com).',
|
||||
},
|
||||
],
|
||||
}),
|
||||
password: sdk.Value.text({
|
||||
name: 'New password',
|
||||
description: 'Minimum 8 characters. No other complexity rules.',
|
||||
required: true,
|
||||
default: null,
|
||||
masked: true,
|
||||
minLength: 8,
|
||||
placeholder: 'At least 8 characters',
|
||||
}),
|
||||
passwordConfirm: sdk.Value.text({
|
||||
name: 'Confirm new password',
|
||||
description: 'Retype the new password to guard against typos.',
|
||||
required: true,
|
||||
default: null,
|
||||
masked: true,
|
||||
minLength: 8,
|
||||
placeholder: 'Retype new password',
|
||||
}),
|
||||
}),
|
||||
// ---------------------------------------------------------------------------
|
||||
// getInput (prefill) \u2014 we deliberately don't prefill the email. Prefilling
|
||||
// would require spinning up a subcontainer just to read the current row,
|
||||
// which is overkill for what is a once-in-a-blue-moon action.
|
||||
// ---------------------------------------------------------------------------
|
||||
async () => null,
|
||||
// ---------------------------------------------------------------------------
|
||||
// run
|
||||
// ---------------------------------------------------------------------------
|
||||
async ({ effects, input }) => {
|
||||
if (input.password !== input.passwordConfirm) {
|
||||
throw new Error(
|
||||
'New password and confirmation do not match. Re-enter both fields.',
|
||||
)
|
||||
}
|
||||
|
||||
// Compute the bcrypt hash in the action's runtime. Salt rounds 10 to
|
||||
// match proof-of-work/lib/auth.ts::hashPassword.
|
||||
const passwordHash = await bcryptjs.hash(input.password, 10)
|
||||
|
||||
// Run the UPDATE inside a temp subcontainer with /data mounted. The
|
||||
// subcontainer uses the same image as the main service, so sqlite3 is
|
||||
// available (the runner image apks it in for the entrypoint's compat
|
||||
// ALTERs).
|
||||
await sdk.SubContainer.withTemp(
|
||||
effects,
|
||||
{ imageId: 'main' },
|
||||
sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: '/data',
|
||||
readonly: false,
|
||||
}),
|
||||
'change-admin-credentials',
|
||||
async (sc) => {
|
||||
const sql = [
|
||||
'BEGIN IMMEDIATE;',
|
||||
`UPDATE User`,
|
||||
`SET email = ${sqlQuote(input.email)},`,
|
||||
` passwordHash = ${sqlQuote(passwordHash)},`,
|
||||
` updatedAt = (strftime('%s','now') * 1000)`,
|
||||
`WHERE id = (SELECT id FROM User ORDER BY createdAt ASC LIMIT 1);`,
|
||||
'SELECT changes();',
|
||||
'COMMIT;',
|
||||
].join('\n')
|
||||
|
||||
const res = await sc.execFail(
|
||||
['sqlite3', '/data/app.db'],
|
||||
{ input: sql },
|
||||
30_000,
|
||||
)
|
||||
|
||||
// sqlite3 prints `changes()` on its own line. Be defensive about
|
||||
// trailing whitespace / multiple lines.
|
||||
const changes = parseInt(
|
||||
res.stdout
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.pop() ?? '0',
|
||||
10,
|
||||
)
|
||||
if (changes !== 1) {
|
||||
throw new Error(
|
||||
`Aborting: expected exactly 1 user row updated, but sqlite3 reported changes()=${changes}. The User table may be empty or contain unexpected rows. Inspect /data/app.db before retrying.`,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: 'Credentials updated',
|
||||
message:
|
||||
'The admin email and password have been rotated. Start the service and log in with your new credentials.',
|
||||
result: {
|
||||
type: 'group',
|
||||
value: [
|
||||
{
|
||||
type: 'single',
|
||||
name: 'New login email',
|
||||
description: 'What you will enter on the Proof of Work login page.',
|
||||
value: input.email,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
masked: false,
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
name: 'Password',
|
||||
description:
|
||||
'Stored as a bcrypt hash (salt rounds 10). Not displayed.',
|
||||
value: '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022',
|
||||
copyable: false,
|
||||
qr: false,
|
||||
masked: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { changeAdminCredentials } from './changeAdminCredentials'
|
||||
|
||||
/**
|
||||
* Package actions registered with StartOS.
|
||||
*
|
||||
* - change-admin-credentials (added v0.1.0:20): rotate the admin email +
|
||||
* password from the StartOS UI without dropping to a shell. See
|
||||
* ./changeAdminCredentials.ts for full design notes.
|
||||
*/
|
||||
export const actions = sdk.Actions.of().addAction(changeAdminCredentials)
|
||||
@@ -0,0 +1,10 @@
|
||||
import { sdk } from './sdk'
|
||||
|
||||
/**
|
||||
* Back up the entire `main` volume. StartOS handles snapshotting everything
|
||||
* under /data (app.db, sidecar WAL/SHM files, future additions), matching
|
||||
* the 0.3.5 backup semantics.
|
||||
*/
|
||||
export const { createBackup, restoreInit } = sdk.setupBackups(
|
||||
async () => sdk.Backups.ofVolumes('main'),
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
import { sdk } from './sdk'
|
||||
|
||||
/** Proof of Work is fully self-contained and has no StartOS dependencies. */
|
||||
export const setDependencies = sdk.setupDependencies(async () => ({}))
|
||||
@@ -0,0 +1,14 @@
|
||||
export const DEFAULT_LANG = 'en_US'
|
||||
|
||||
const dict = {
|
||||
'Starting Proof of Work': 0,
|
||||
'Web Interface': 1,
|
||||
'The web interface is ready': 2,
|
||||
'The web interface is not ready': 3,
|
||||
'Web UI': 4,
|
||||
'The browser interface for Proof of Work': 5,
|
||||
} as const
|
||||
|
||||
export type I18nKey = keyof typeof dict
|
||||
export type LangDict = Record<(typeof dict)[I18nKey], string>
|
||||
export default dict
|
||||
@@ -0,0 +1,36 @@
|
||||
import { LangDict } from './default'
|
||||
|
||||
export default {
|
||||
es_ES: {
|
||||
0: 'Iniciando Proof of Work',
|
||||
1: 'Interfaz web',
|
||||
2: 'La interfaz web esta lista',
|
||||
3: 'La interfaz web no esta lista',
|
||||
4: 'Interfaz web',
|
||||
5: 'La interfaz del navegador para Proof of Work',
|
||||
},
|
||||
de_DE: {
|
||||
0: 'Proof of Work wird gestartet',
|
||||
1: 'Weboberflaeche',
|
||||
2: 'Die Weboberflaeche ist bereit',
|
||||
3: 'Die Weboberflaeche ist nicht bereit',
|
||||
4: 'Weboberflaeche',
|
||||
5: 'Die Browseroberflaeche fuer Proof of Work',
|
||||
},
|
||||
pl_PL: {
|
||||
0: 'Uruchamianie Proof of Work',
|
||||
1: 'Interfejs webowy',
|
||||
2: 'Interfejs webowy jest gotowy',
|
||||
3: 'Interfejs webowy nie jest gotowy',
|
||||
4: 'Interfejs webowy',
|
||||
5: 'Interfejs przegladarkowy dla Proof of Work',
|
||||
},
|
||||
fr_FR: {
|
||||
0: 'Demarrage de Proof of Work',
|
||||
1: 'Interface web',
|
||||
2: "L'interface web est prete",
|
||||
3: "L'interface web n'est pas prete",
|
||||
4: 'Interface web',
|
||||
5: "L'interface navigateur pour Proof of Work",
|
||||
},
|
||||
} satisfies Record<string, LangDict>
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Plumbing. DO NOT EDIT this file.
|
||||
*/
|
||||
import { setupI18n } from '@start9labs/start-sdk'
|
||||
import defaultDict, { DEFAULT_LANG } from './dictionaries/default'
|
||||
import translations from './dictionaries/translations'
|
||||
|
||||
export const i18n = setupI18n(defaultDict, translations, DEFAULT_LANG)
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Plumbing. DO NOT EDIT.
|
||||
*/
|
||||
export { createBackup } from './backups'
|
||||
export { main } from './main'
|
||||
export { init, uninit } from './init'
|
||||
export { actions } from './actions'
|
||||
import { buildManifest } from '@start9labs/start-sdk'
|
||||
import { manifest as sdkManifest } from './manifest'
|
||||
import { versionGraph } from './versions'
|
||||
export const manifest = buildManifest(versionGraph, sdkManifest)
|
||||
@@ -0,0 +1,16 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { setDependencies } from '../dependencies'
|
||||
import { setInterfaces } from '../interfaces'
|
||||
import { versionGraph } from '../versions'
|
||||
import { actions } from '../actions'
|
||||
import { restoreInit } from '../backups'
|
||||
|
||||
export const init = sdk.setupInit(
|
||||
restoreInit,
|
||||
versionGraph,
|
||||
setInterfaces,
|
||||
setDependencies,
|
||||
actions,
|
||||
)
|
||||
|
||||
export const uninit = sdk.setupUninit(versionGraph)
|
||||
@@ -0,0 +1,30 @@
|
||||
import { i18n } from './i18n'
|
||||
import { sdk } from './sdk'
|
||||
import { uiPort } from './utils'
|
||||
|
||||
/**
|
||||
* Expose the Next.js UI over StartOS's standard HTTP multi-host interface.
|
||||
* The UI is unmasked (no shared secret in the URL) because the app has its
|
||||
* own password-protected login screen.
|
||||
*/
|
||||
export const setInterfaces = sdk.setupInterfaces(async ({ effects }) => {
|
||||
const uiMulti = sdk.MultiHost.of(effects, 'ui-multi')
|
||||
const uiMultiOrigin = await uiMulti.bindPort(uiPort, {
|
||||
protocol: 'http',
|
||||
})
|
||||
const ui = sdk.createInterface(effects, {
|
||||
name: i18n('Web UI'),
|
||||
id: 'ui',
|
||||
description: i18n('The browser interface for Proof of Work'),
|
||||
type: 'ui',
|
||||
masked: false,
|
||||
schemeOverride: null,
|
||||
username: null,
|
||||
path: '',
|
||||
query: {},
|
||||
})
|
||||
|
||||
const uiReceipt = await uiMultiOrigin.export([ui])
|
||||
|
||||
return [uiReceipt]
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import { i18n } from './i18n'
|
||||
import { sdk } from './sdk'
|
||||
import { uiPort } from './utils'
|
||||
|
||||
/**
|
||||
* Main daemon definition.
|
||||
*
|
||||
* Mounts the `main` volume at /data (same contract as 0.3.5) and runs the
|
||||
* container's ENTRYPOINT (docker_entrypoint.sh -> Next.js standalone).
|
||||
*
|
||||
* Readiness: we check the listening port rather than hitting /api/health
|
||||
* because the Next.js server starts listening before Prisma has warmed up;
|
||||
* the entrypoint's own DB seeding logic is responsible for DB readiness.
|
||||
* If you want a stricter gate later, switch to `sdk.healthCheck.checkWebUrl`
|
||||
* pointed at `http://localhost:3000/api/health` and look for `status: ok`.
|
||||
*/
|
||||
export const main = sdk.setupMain(async ({ effects }) => {
|
||||
console.info(i18n('Starting Proof of Work'))
|
||||
|
||||
return sdk.Daemons.of(effects).addDaemon('main', {
|
||||
subcontainer: await sdk.SubContainer.of(
|
||||
effects,
|
||||
{ imageId: 'main' },
|
||||
sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: '/data',
|
||||
readonly: false,
|
||||
}),
|
||||
'proof-of-work-main',
|
||||
),
|
||||
exec: { command: sdk.useEntrypoint() },
|
||||
ready: {
|
||||
display: i18n('Web Interface'),
|
||||
fn: () =>
|
||||
sdk.healthCheck.checkPortListening(effects, uiPort, {
|
||||
successMessage: i18n('The web interface is ready'),
|
||||
errorMessage: i18n('The web interface is not ready'),
|
||||
}),
|
||||
},
|
||||
requires: [],
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
export const short = {
|
||||
en_US: 'Self-hosted workout planning and training log app.',
|
||||
es_ES: 'Aplicacion autoalojada para planificar y registrar entrenamientos.',
|
||||
de_DE: 'Selbst gehostete App fuer Trainingsplanung und Trainingsprotokolle.',
|
||||
pl_PL: 'Samodzielnie hostowana aplikacja do planowania i rejestrowania treningow.',
|
||||
fr_FR: "Application auto-hebergee pour planifier et enregistrer les entrainements.",
|
||||
}
|
||||
|
||||
export const long = {
|
||||
en_US:
|
||||
'Proof of Work is a private workout planning and logging app. This StartOS 0.4 package preserves the existing /data volume contract from 0.3.5: the SQLite database at /data/app.db, workout history, exercise library, and imported records all survive the 0.3.5 \u2192 0.4 migration. The first 0.4 release ships with a one-time data seed baked into the image; subsequent releases use the live /data volume as the sole source of truth.',
|
||||
es_ES:
|
||||
'Proof of Work es una aplicacion privada para planificar y registrar entrenamientos. Este paquete de StartOS 0.4 conserva el contrato del volumen /data de 0.3.5: la base SQLite en /data/app.db, el historial de entrenamientos, la biblioteca de ejercicios y los registros importados sobreviven a la migracion 0.3.5 \u2192 0.4. La primera version 0.4 incluye una semilla unica; las siguientes usan el volumen /data vivo como unica fuente de verdad.',
|
||||
de_DE:
|
||||
'Proof of Work ist eine private App fuer Trainingsplanung und Trainingsprotokolle. Dieses StartOS-0.4-Paket behaelt den /data-Vertrag von 0.3.5 bei: die SQLite-Datenbank unter /data/app.db, der Trainingsverlauf, die Uebungsbibliothek und importierte Daten ueberstehen die Migration 0.3.5 \u2192 0.4. Das erste 0.4-Release liefert einen einmaligen Daten-Seed; spaetere Releases nutzen /data als einzige Wahrheit.',
|
||||
pl_PL:
|
||||
'Proof of Work to prywatna aplikacja do planowania i zapisywania treningow. Ten pakiet StartOS 0.4 zachowuje kontrakt /data z 0.3.5: baza SQLite w /data/app.db, historia treningow, biblioteka cwiczen i zaimportowane dane przetrwaja migracje 0.3.5 \u2192 0.4. Pierwsze wydanie 0.4 zawiera jednorazowy seed danych; kolejne wydania uzywaja /data jako jedynego zrodla prawdy.',
|
||||
fr_FR:
|
||||
"Proof of Work est une application privee de planification et de suivi d'entrainement. Ce paquet StartOS 0.4 conserve le contrat /data de la 0.3.5 : la base SQLite dans /data/app.db, l'historique, la bibliotheque d'exercices et les donnees importees survivent a la migration 0.3.5 \u2192 0.4. La premiere 0.4 inclut un seed unique ; les suivantes utilisent /data comme seule source de verite.",
|
||||
}
|
||||
|
||||
export const alertInstall = {
|
||||
en_US:
|
||||
'This package bakes a one-time snapshot of your 0.3.5 /data volume into the image. On first boot, it will populate /data only if it is empty. After first boot, /data is the sole source of truth; do NOT Uninstall (that destroys /data).',
|
||||
es_ES:
|
||||
'Este paquete incluye una instantanea unica del volumen /data de 0.3.5. En el primer arranque poblara /data solo si esta vacio. Despues, /data es la unica fuente de verdad; NO desinstale (eso destruye /data).',
|
||||
de_DE:
|
||||
'Dieses Paket enthaelt eine einmalige Momentaufnahme Ihres 0.3.5 /data-Volumes. Beim ersten Start wird /data nur befuellt, wenn es leer ist. Danach ist /data die einzige Wahrheit; NICHT deinstallieren (das zerstoert /data).',
|
||||
pl_PL:
|
||||
'Ten pakiet zawiera jednorazowa migawke Twojego woluminu /data z 0.3.5. Przy pierwszym starcie wypelni /data tylko, jesli jest pusty. Pozniej /data jest jedynym zrodlem prawdy; NIE odinstalowuj (to zniszczy /data).',
|
||||
fr_FR:
|
||||
"Ce paquet integre un instantane unique de votre volume /data 0.3.5. Au premier demarrage, /data ne sera rempli que s'il est vide. Ensuite, /data est la seule source de verite ; NE PAS desinstaller (cela detruit /data).",
|
||||
}
|
||||
|
||||
export const alertUpdate = {
|
||||
en_US:
|
||||
'Updating is safe: the entrypoint never overwrites an existing /data. NEVER choose Uninstall to troubleshoot \u2014 Uninstall destroys /data. If you need to disable the service, use Stop. If you need to roll back, run a StartOS Backup first.',
|
||||
es_ES:
|
||||
'Actualizar es seguro: el entrypoint nunca sobrescribe /data existente. NUNCA desinstale para solucionar problemas \u2014 Desinstalar destruye /data. Para desactivar el servicio use Detener. Para revertir, haga primero una copia de seguridad de StartOS.',
|
||||
de_DE:
|
||||
'Updaten ist sicher: der Entrypoint ueberschreibt niemals ein vorhandenes /data. NIE zur Fehlersuche deinstallieren \u2014 Deinstallieren zerstoert /data. Dienst deaktivieren: Stop. Zurueckrollen: erst StartOS-Backup.',
|
||||
pl_PL:
|
||||
'Aktualizacja jest bezpieczna: entrypoint nigdy nie nadpisze istniejacego /data. NIGDY nie odinstalowuj w ramach diagnostyki \u2014 Odinstalowanie niszczy /data. Aby wylaczyc usluge, uzyj Stop. Aby wycofac zmiany, najpierw wykonaj StartOS Backup.',
|
||||
fr_FR:
|
||||
"La mise a jour est sure : l'entrypoint ne remplace jamais un /data existant. NE PAS desinstaller pour depanner \u2014 Desinstaller detruit /data. Pour arreter le service, utilisez Stop. Pour revenir en arriere, faites d'abord une sauvegarde StartOS.",
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { setupManifest } from '@start9labs/start-sdk'
|
||||
import { alertInstall, alertUpdate, long, short } from './i18n'
|
||||
|
||||
/**
|
||||
* Proof of Work (proof-of-work) — StartOS 0.4 manifest.
|
||||
*
|
||||
* Contract invariants that MUST stay stable across all future releases so the
|
||||
* 0.3.5 \u2192 0.4 data migration and subsequent upgrades remain safe:
|
||||
* - package identifier = 'proof-of-work' (matches the 0.3.5 package)
|
||||
* - volume name = 'main' (matches the 0.3.5 volume)
|
||||
* - mount path = '/data' (matches the 0.3.5 mount)
|
||||
*
|
||||
* NOTE: do NOT write the literal two-character token "i"+"d" followed by ":"
|
||||
* anywhere else in this file. s9pk.mk extracts PACKAGE_ID with a naive awk
|
||||
* that matches on that substring and will concatenate multiple hits into a
|
||||
* broken BASE_NAME (symptom: make warning "overriding commands for target
|
||||
* 'proof-of-work'" and "No rule to make target .git/HEAD").
|
||||
*/
|
||||
export const manifest = setupManifest({
|
||||
id: 'proof-of-work',
|
||||
title: 'Proof of Work',
|
||||
license: 'Proprietary',
|
||||
packageRepo: 'https://github.com/your-org/proof-of-work-startos',
|
||||
upstreamRepo: 'https://github.com/your-org/proof-of-work',
|
||||
marketingUrl: 'https://github.com/your-org/proof-of-work',
|
||||
donationUrl: null,
|
||||
docsUrls: ['https://docs.start9.com/packaging/0.4.0.x/'],
|
||||
description: { short, long },
|
||||
volumes: ['main'],
|
||||
images: {
|
||||
main: {
|
||||
source: {
|
||||
dockerBuild: {
|
||||
// Both `workdir` and `dockerfile` are resolved by start-cli
|
||||
// relative to the PACKAGE directory (where this manifest
|
||||
// lives), per the 0.4.0.x packaging docs. That means:
|
||||
// workdir: '../..' -> Docker build context = repo root
|
||||
// (two levels up from
|
||||
// start9/0.4/). The
|
||||
// Dockerfile's COPY paths such as
|
||||
// 'proof-of-work/...' and
|
||||
// 'start9/0.4/...' are
|
||||
// resolved from there.
|
||||
// dockerfile: './Dockerfile'
|
||||
// -> <package-dir>/Dockerfile. That's
|
||||
// where ours actually lives; the
|
||||
// Dockerfile sitting OUTSIDE the
|
||||
// workdir is fine -- Docker
|
||||
// supports it via `-f <path>
|
||||
// <context>` and buildkit resolves
|
||||
// COPY paths against the context
|
||||
// (workdir), not the Dockerfile's
|
||||
// directory.
|
||||
// DO NOT set dockerfile to './start9/0.4/Dockerfile'.
|
||||
// That would resolve to
|
||||
// start9/0.4/start9/0.4/Dockerfile
|
||||
// (nonexistent), producing
|
||||
// `resolve : lstat start9: no such file or directory`
|
||||
// from buildkit before any layer runs.
|
||||
workdir: '../..',
|
||||
dockerfile: './Dockerfile',
|
||||
},
|
||||
},
|
||||
// 0.4 beta is x86_64-only; expand to ['x86_64', 'aarch64'] post-beta.
|
||||
arch: ['x86_64'],
|
||||
},
|
||||
},
|
||||
alerts: {
|
||||
install: alertInstall,
|
||||
update: alertUpdate,
|
||||
uninstall: null,
|
||||
restore: null,
|
||||
start: null,
|
||||
stop: null,
|
||||
},
|
||||
dependencies: {},
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
import { StartSdk } from '@start9labs/start-sdk'
|
||||
import { manifest } from './manifest'
|
||||
|
||||
/**
|
||||
* Plumbing. DO NOT EDIT.
|
||||
*/
|
||||
export const sdk = StartSdk.of().withManifest(manifest).build(true)
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Shared constants for the Proof of Work StartOS 0.4 package.
|
||||
*
|
||||
* Keep these in lockstep with the manifest (`./manifest/index.ts`) and with
|
||||
* the Dockerfile environment variables. Changing any of these is effectively
|
||||
* a breaking change for the on-disk contract with the `/data` volume.
|
||||
*/
|
||||
|
||||
export const PACKAGE_ID = 'proof-of-work'
|
||||
export const PACKAGE_TITLE = 'Proof of Work'
|
||||
export const IMAGE_ID = 'main'
|
||||
export const VOLUME_ID = 'main'
|
||||
export const DATA_MOUNT_PATH = '/data'
|
||||
|
||||
/** Internal HTTP port the Next.js standalone server listens on. */
|
||||
export const uiPort = 3000
|
||||
|
||||
/** Path the health route is mounted at inside the app. */
|
||||
export const HEALTH_PATH = '/api/health'
|
||||
@@ -0,0 +1,18 @@
|
||||
import { VersionGraph } from '@start9labs/start-sdk'
|
||||
import { v_1_0_0_1 } from './v1.0.0.1'
|
||||
|
||||
/**
|
||||
* Version graph for the `proof-of-work` package.
|
||||
*
|
||||
* v1.0.0:1 — initial release, seeded cutover from the legacy `workout-log`
|
||||
* package. No prior version to upgrade from.
|
||||
*
|
||||
* StartOS picks `current` as the install target; `other` lists every node
|
||||
* that can upgrade into `current`. Fresh sideloads land directly on
|
||||
* `current`. Once we ship the post-cutover cleanup release, it goes here as
|
||||
* the new `current` and v1.0.0:1 moves into `other`.
|
||||
*/
|
||||
export const versionGraph = VersionGraph.of({
|
||||
current: v_1_0_0_1,
|
||||
other: [],
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import { IMPOSSIBLE, VersionInfo } from '@start9labs/start-sdk'
|
||||
|
||||
/**
|
||||
* v1.0.0:1 — initial Proof of Work release.
|
||||
*
|
||||
* Upstream version: 1.0.0
|
||||
* Wrapper rev: 1
|
||||
*
|
||||
* This is a one-shot "seeded cutover" release for users migrating from the
|
||||
* old `workout-log` StartOS package. The Docker image bakes in a snapshot of
|
||||
* the maintainer's live /data volume under /app/seed/data; the entrypoint
|
||||
* copies that snapshot into the new StartOS-managed /data volume only on a
|
||||
* truly-fresh first boot (both /data/app.db missing AND /data/.seeded
|
||||
* absent). Every subsequent boot leaves /data untouched.
|
||||
*
|
||||
* Because StartOS treats `proof-of-work` as a brand new service (different
|
||||
* package id from `workout-log`), the old install stays running until the
|
||||
* operator confirms the cutover and stops it manually. There is no
|
||||
* downgrade path; `down` is IMPOSSIBLE.
|
||||
*
|
||||
* The post-cutover cleanup release (v1.0.0:2) will strip the baked seed and
|
||||
* the seed-copy branch from docker_entrypoint.sh.
|
||||
*/
|
||||
export const v_1_0_0_1 = VersionInfo.of({
|
||||
version: '1.0.0:1',
|
||||
releaseNotes: {
|
||||
en_US:
|
||||
'Initial Proof of Work release. Replaces the legacy `workout-log` package with multi-user support and a curated exercise library shared across all users on the instance. Bakes a one-time seed of /data into the image and copies it into the new volume only on truly-fresh first boot, so an operator migrating from `workout-log` keeps every workout, exercise, and preference.',
|
||||
},
|
||||
migrations: {
|
||||
up: async () => {},
|
||||
down: IMPOSSIBLE,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"include": ["startos/**/*.ts", "node_modules/**/startos"],
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user