Two surfaces had invisible 50-row caps that this commit removes.
Exercise history popup (clock button in WorkoutForm):
- /api/exercises/[id] now accepts ?offset=N&limit=N (default 25,
max 100) and returns { exercise, history, hasMore }. Pagination
uses take: limit + 1 to detect hasMore without a second COUNT
round-trip.
- Query rewritten to use Prisma's setLogs.some filter — single SQL
that hits the (userId, deletedAt, date) composite index, instead
of fetching all set logs then grouping in JS.
- ExerciseHistoryPopup now uses an IntersectionObserver on a
sentinel div. When sentinel scrolls into view (root: the popup
itself, not the viewport), fetches next page and appends. Status
row at the bottom shows a spinner while loading and "End of
history" when done.
- Container max height bumped from h-64 -> h-80 for a bit more
breathing room on first render.
Workout history page (/main/workouts):
- Page still server-renders the first 50 workouts (instant paint
+ correct date filter forwarding). Now uses take: PAGE_SIZE + 1
to detect hasMore.
- New WorkoutsList client component takes initial workouts +
hasMore + filter values as props. IntersectionObserver on a
sentinel below the cards auto-fetches the next page from
/api/workouts?offset=N&limit=50&q=...&dateFrom=...&dateTo=...
when scrolled to. Filters round-trip through URL params, so a
filter change re-renders the page from scratch with a fresh
first page.
- "End of history · N workouts" line shown once everything is
loaded.
Tests:
- tests/routes-exercise-history.test.ts: 6 new tests covering
auth, cross-user 404, first-page hasMore=true, second-page
hasMore=false + no overlap, set-log filter scoped to the
queried exerciseId, soft-deleted workouts excluded.
- All 87 tests pass.
No schema changes, no migration. /data untouched.
The three exported helpers in lib/prisma.ts (getCaloriesBurned,
setCaloriesBurned, getCaloriesBurnedBulk) existed because an early
Prisma client generation didn't include the column. Schema and
client have been aligned for several releases — the workaround is
dead weight.
Removed: the helpers from lib/prisma.ts (~30 lines of
$queryRawUnsafe / $executeRawUnsafe).
Updated callers to use plain caloriesBurned field references:
- app/api/workouts/route.ts (GET list + POST create)
- app/api/workouts/[id]/route.ts (GET detail + PATCH update)
- app/api/settings/export-csv/route.ts (CSV export)
All call sites now go through normal type-safe Prisma queries.
Net effect for users: zero. Net effect for the codebase: cleaner
read paths, stronger TS coverage on caloriesBurned, fewer SQL
strings to audit.
No schema changes, no migration. Existing /data is untouched.
v1.0.0:5 promoted to current; :1, :2, :3, :4 in other.
Security: shipping admin@local / workout123 as a default that the
operator was supposed-to-rotate-but-might-not is the kind of footgun
that turns into "default-credential exposure" headlines. Eliminated.
prisma/seed.ts now ONLY seeds the InstanceSettings singleton — no
admin user, no UserPreferences, no exercises in the build-time
fallback DB. The image still ships with prisma/exercises.seed.json
(curated 164-exercise library) but those rows aren't inserted until
an admin is created via the StartOS Action.
The change-admin-credentials Action now does INSERT-or-UPDATE in one
shot. CREATE mode (no admin exists) inserts the User row, inserts
UserPreferences with sensible defaults, and runs
ensureExerciseLibrary.cjs for the new admin so they don't have to
wait for the next service start to see the curated library. UPDATE
mode (admin exists) keeps the v1.0.0:1-3 rotation behavior. The
mode is auto-detected by counting `WHERE isAdmin = 1`.
The login page is now a server component that reads the admin count
upfront. Zero admins -> renders a "needs setup" panel pointing at
the StartOS Action ("Services -> Proof of Work -> Actions -> Set
admin credentials"). Otherwise renders the existing LoginForm
(extracted to LoginForm.tsx). Eliminates the
"I tried admin@local/workout123 and it failed, what's wrong"
fresh-installer confusion.
Backward compatible for upgrades from v1.0.0:1-3:
- /data already has an admin user; the no-admin detection never
triggers; login behaves identically to before.
- The Action's UPDATE mode still works for rotation.
Version graph: v1.0.0:4 promoted to current; v1.0.0:1, :2, :3 all
listed as `other` for in-place upgrade paths.
README updated to call out the explicit no-default-account design
and how to bootstrap an admin in local dev (Prisma Studio, since
the StartOS action isn't available off-StartOS).
Removes the one-time `/data` snapshot from the deployed Docker image now
that the cutover from the legacy `workout-log` package is verified done
(v1.0.0:1 + :2 in production).
Dockerfile
- Drops `COPY start9/0.4/seed/data /app/seed/data`.
- Drops the `WORKOUT_BAKED_SEED_DB_PATH` env var.
- Comment block explains the rationale + how to re-seed if ever needed.
docker_entrypoint.sh
- Step 1 collapses to single-branch fallback: if /data is empty AND
/app/prisma/data/app.db exists, copy the empty-schema fallback. The
baked-seed branch is gone.
- Comment cross-references v1.0.0:3 for the rationale.
start9/0.4/seed/README.md rewritten to reflect historical-only status
+ how to re-seed for the rare "spin up another instance with this
history" case.
Version graph
- Adds startos/versions/v1.0.0.3.ts with empty up/down migrations and
release notes.
- Promotes v1.0.0:3 to `current`; v1.0.0:1 and :2 move to `other` so
hosts on either upgrade in place.
No schema changes, no data migration. /data on existing installs is
left exactly as-is. Image size drops by ~1.7MB (the snapshot size).
v1.0.0:1 shipped a per-request nonce-based CSP via Next.js middleware.
In production it produced a blank first paint: Next 14.2.x's bootstrap
inline scripts weren't picking up the nonce reliably from the x-nonce
request header, so the browser blocked them.
This release reverts to the pre-experiment posture:
- middleware.ts back to auth gating only (no nonce, no CSP).
- next.config.js restores the static CSP with `'unsafe-inline'` allowed
for script-src and style-src. Same headers (HSTS, Referrer-Policy,
Permissions-Policy, frame-ancestors 'none', etc.) all stay.
- New startos/versions/v1.0.0.2.ts with empty up/down migrations and
a release note explaining the bug + revert. Promoted to `current`
in the version graph; v1.0.0:1 moves to `other` so existing
installs upgrade in place.
No schema changes, no data migration. Existing v1.0.0:1 installs
keep their /data.
Re-attempt path documented in middleware.ts and next.config.js
comments: future PR can revisit nonce CSP using Next's documented
pattern verbatim (notably setting CSP on BOTH request headers and
response headers — we only set it on response).
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.