Commit Graph

11 Commits

Author SHA1 Message Date
Keysat 3a5b929284 v1.1.0:1 — Programs UI (manual create / save / follow)
Schema
- Workout.programDayId added (nullable FK to ProgramDay) so a
  Workout logged from a program day can be tied back to the planned
  session for adherence analytics. Compat ALTER in entrypoint adds
  the column + index to existing /data; ON DELETE SET NULL so
  deleting a program doesn't remove historical workouts logged
  against it.
- Back-relation `workouts: Workout[]` added to ProgramDay.

API (proof-of-work/app/api/programs/...)
- GET    /api/programs                       — list user's programs
- POST   /api/programs                       — create with full nested
                                                 weeks/days/exercises
                                                 tree in one transaction
- GET    /api/programs/[id]                  — full tree
- PATCH  /api/programs/[id]                  — update metadata AND/OR
                                                 replace entire weeks
                                                 tree (same shape as
                                                 POST). UI editor + AI
                                                 apply flow share this.
- DELETE /api/programs/[id]                  — cascading
- POST   /api/programs/[id]/days/[dayId]/start
                                              — creates a Workout
                                                 pre-populated with
                                                 empty SetLogs (one per
                                                 planned set), tagged
                                                 with programDayId.

UI (proof-of-work/app/main/programs/...)
- /main/programs               — list with cards, today's-session
                                  callout, "active" badge
- /main/programs/new           — create form using ProgramEditor
- /main/programs/[id]          — detail + edit using same editor;
                                  today's-session card + Start button
                                  if program is active
- ProgramEditor component (components/programs/ProgramEditor.tsx) —
  expandable tree editor for weeks -> days -> exercises with
  per-row sets/reps/RPE/rest/notes fields + library exercise picker
- ProgramActions: delete button
- StartSessionButton: POSTs to start endpoint, redirects to new
  workout

Navigation
- "Programs" link added to bottom nav + sidebar (between Workouts
  and Exercises).
- /main/programs page itself shows the today's-session card; the
  same component pattern can be lifted into the dashboard later
  if we want.

lib/db/programs.ts
- getPrograms, getProgramById, getActivePrograms,
  computeTodaysSessionForProgram, getTodaysSession helpers.
- Today's session math: floor((todayUTC - startDateUTC) / 1day),
  weekNumber = floor(.../7) + 1, dayOfWeek = today.getUTCDay().
  Returns null if not started, past durationWeeks, or no day
  matching today's slot (= rest day).

Tests (tests/routes-programs.test.ts)
- 11 new tests covering: 401 unauthenticated, full-tree create
  with nested weeks+days+exercises, cross-user exerciseId
  rejection, list scoped to actor, GET detail returns 404 for
  another user's program, PATCH replace-tree atomicity,
  cascading DELETE, start-day Workout creation with the right
  number of empty SetLogs + programDayId stamped, start-day
  refused for cross-user program day.
- Total: 96 tests across 11 files.

This is the foundation for v1.1.0:2's AI-generated programs —
the AI will produce the same JSON shape POST /api/programs
already accepts, so the apply path is `editor.tsx + POST
/api/programs` with no new API surface.
2026-05-10 07:15:31 -05:00
Keysat 55c17614b8 v1.0.0:7 — exercise library cleanup, photo-import removal, AI-section honesty
Library JSON cleanup (proof-of-work/prisma/exercises.seed.json)
  19 exercises corrected:
  - Cycling/Jump Rope/Rowing/Running: type=cardio with proper
    inputFields (duration/distance/calories — no more reps/weight).
  - Walking Lunge/Wall Sit/Headstand/Hip Extension: reclassified
    out of cardio into bodyweight.
  - Plank/Mace warmup/Hollow Body Landmine/Soccer: inputFields
    fixed.
  - Descriptions added for ~10 cryptic exercises (Core, Resistance
    Band, Stir the pot, Slide Board, Neck Circuit, TGU, Captains
    of Crush, etc.).

Reconcile-on-boot (ensureExerciseLibrary.cjs)
  Changed from INSERT-OR-IGNORE to INSERT-OR-UPDATE keyed on
  (userId, name). Existing rows where isCustom = 0 get
  description/type/muscleGroups/inputFields/defaultWeightUnit
  refreshed from the curated JSON. Rows where isCustom = 1 are
  skipped — user customizations always win.

  Verified end-to-end: applied patches propagate to a copy of the
  user's snapshot DB; manually-tampered isCustom=1 rows survive a
  second reconcile pass untouched.

PATCH /api/exercises/[id] flips isCustom -> true on user edits
  Once you edit a library exercise via the in-app UI, the row's
  isCustom flag becomes 1 and the boot-time reconcile leaves it
  alone forever. Closes the only failure mode where a maintainer
  curated-library refresh could overwrite user edits.

Photo-import (Claude vision) removed
  - app/api/workouts/import/route.ts deleted.
  - components/import/WorkoutImportClient.tsx deleted (orphan
    component — wasn't referenced anywhere by the live UI).
  - CSV import (app/main/import → page-csv.tsx →
    /api/workouts/import/save) is unchanged. The save endpoint
    stays — it's used by the CSV flow too.

Settings UI: "Claude AI Integration" section removed
  The toggle + API key input promised "personalized workout
  recommendations" that the codebase never delivered (the only
  actually-wired use was the photo-import we just removed).
  Schema columns User.enableClaudeAI / User.claudeApiKey stay
  as harmless dead fields — they'll get cleaned up or repurposed
  when the model-agnostic AI work lands. The preferences API
  no longer accepts or returns those fields.

No data migration. /data on existing installs is untouched.
v1.0.0:7 promoted to current; :1-:6 in other.
2026-05-09 21:24:00 -05:00
Keysat ffa8e0d480 v1.0.0:6 — paginate workout history (infinite scroll)
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.
2026-05-09 20:18:31 -05:00
Keysat dc6a3b1116 v1.0.0:5 — remove caloriesBurned raw-SQL workaround
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.
2026-05-09 19:42:45 -05:00
Keysat 5f7b3b6b7a v1.0.0:4 — remove default admin@local credentials; require StartOS action to bootstrap
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).
2026-05-09 19:13:49 -05:00
Keysat a64fee4873 Replace placeholder manifest URLs with real keysat-xyz/proof-of-work
packageRepo + upstreamRepo now point at the public GitHub repo where
this code will live. marketingUrl set to null (not yet a landing page).
2026-05-09 16:37:04 -05:00
Keysat 97ed07fd07 v1.0.0:3 — post-cutover seed strip
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).
2026-05-09 13:40:58 -05:00
Keysat edeb1eb148 v1.0.0:2 — revert CSP nonces; restore inline-friendly CSP
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).
2026-05-09 12:05:11 -05:00
Keysat 54fa77f2eb Sessions UI, CSV parser tests, route tests, composite indexes, verify-db action
Per-user sessions UI (Settings -> Active sessions)
- listMySessions returns the current user's still-valid sessions with
  last-8-char token suffix (UX hint) and an isCurrent flag (the
  authoritative "this device" marker).
- revokeSession refuses if the target is the actor's current token —
  use Sign out for that flow. Per-row Revoke button on every other.
- revokeAllOtherSessions = the previously-internal `deleteOtherSessions`
  helper exposed as a single button "Sign out other devices".
- All gated to the actor's own userId (never lets a user touch another
  user's sessions).

CSV parser refactor + tests
- Extracted parseCSV, NAME_MAP, parseFloatMaybe, parseIntMaybe,
  getVariationNote, resolveExerciseName, parseDate from
  app/api/import/parse/route.ts to lib/csvParser.ts. Behavior
  byte-identical; route is now a thin wrapper that imports from the
  lib.
- 18 tests covering: empty input, simple rows, lowercased headers,
  quoted-field commas, escaped double quotes, CRLF normalization,
  empty-line handling; numeric maybe-parsers; getVariationNote known
  patterns + null pass-through; ALL 27 NAME_MAP entries map to their
  canonical target; named CSV-shorthand examples; M/D/YYYY + ISO date
  parsing with noon-UTC anchoring (so US negative-offset zones still
  see the same calendar day).

Workout + exercise CRUD route tests
- New tests/routes-crud.test.ts: GET/POST /api/exercises, GET/POST
  /api/workouts. 401 on unauthenticated, per-user data isolation,
  query filtering, soft-delete exclusion, isCustom stamping, duplicate
  detection, type-driven inputFields defaults (cardio gets
  duration+calories), Zod validation rejection, set creation with
  weight/reps/rpe persisted, negative-reps rejected.
- Helper builds NextRequest objects so the routes' nextUrl.searchParams
  access works.

Composite indexes for hot query paths (schema.prisma + entrypoint)
- Session: (userId, expiresAt) for "list my still-valid sessions" and
  per-user cleanup.
- Workout: (userId, deletedAt, date) for the workout list query
  (filter by user + alive + date order).
- SetLog: (workoutId, setNumber) for the always-ordered set fetch
  under each workout.
- Existing single-column indexes kept; composites are additive.
- Entrypoint runs CREATE INDEX IF NOT EXISTS so live snapshots pick
  up the new indexes on first boot after upgrade.

verify-database StartOS action (start9/0.4/startos/actions/verifyDatabase.ts)
- Read-only. Runs PRAGMA integrity_check + quick_check + row-count
  queries against /data/app.db, reports as a structured result.
- allowedStatuses: only-running. Mounts the volume read-only.
- Use after a StartOS Backup, after a host crash, or after a fresh
  sideload to confirm the data is sound before relying on it.

Test suite now 67 tests across 7 files in ~2.4s.
2026-05-09 10:53:30 -05:00
Keysat d9c4e6c4a0 Multi-user: self-serve sign-up gated by admin-toggleable flag
Schema
- User.isAdmin: Boolean default false (Prisma)
- New InstanceSettings singleton (id=1) holding signupsOpen flag

Boot-time compat ALTERs (docker_entrypoint.sh)
- Adds User.isAdmin column to legacy snapshots; auto-promotes the oldest
  user to admin if no admin exists yet, so workout-log -> proof-of-work
  cutover preserves admin functionality with no manual SQL.
- Creates InstanceSettings table + singleton row (signupsOpen=0) for any
  snapshot that doesn't have it.

App: sign-up flow
- /auth/signup page: server component that reads InstanceSettings
  upfront. If sign-ups are closed it shows a closed-instance message and
  a back-to-sign-in link rather than a dead form. If open it renders
  SignupForm (client) which calls signupAction (server).
- signupAction: re-checks the flag (defense in depth), validates email
  format / 8-char password / matching confirm, blocks duplicate-email
  enumeration with a generic error, creates the user with isAdmin=false,
  seeds default UserPreferences, ensures the curated exercise library
  for the new user (lib/library.ts upserts every entry), then issues a
  session cookie.
- Login page now links to /auth/signup; old "Demo: admin@example.com /
  password" footer (which was wrong anyway) removed.

App: admin in-app toggle
- Settings page renders new AdminInstanceSettings component for admins
  only. Optimistic toggle posts to /api/admin/signups; error rollback
  on failure.
- /api/admin/signups: GET returns current flag (any authed user, so the
  UI knows whether to show the sign-up CTA later); POST flips it
  (admin only).

StartOS package action
- toggle-signups: same setter as the in-app toggle, accessible from the
  StartOS UI without an admin login. Single boolean input. Asserts the
  read-back value matches what was written before reporting success.
- changeAdminCredentials now keys the UPDATE on
  `WHERE isAdmin = 1 ORDER BY createdAt ASC LIMIT 1` (was: just
  ORDER BY createdAt) — correct under multi-user.

Release notes / docs
- v1.0.0:1 release notes expanded to call out multi-user as part of
  the cutover release (no separate version needed since this is the
  first proof-of-work release shipping to anyone).
- Root README: short Multi-user section explaining both toggle paths
  and that new users get the curated library automatically.
- README dev setup adds `npx prisma generate` step (required after
  schema changes for local dev).
2026-05-08 20:59:45 -05:00
Keysat aa407b5f67 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.
2026-05-08 20:12:25 -05:00