Commit Graph

8 Commits

Author SHA1 Message Date
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 990f5582b8 Typed Prisma queries, bcrypt native, CSP nonces, /api/me/import, more tests
Typed Prisma queries
- where: any in app/api/workouts/route.ts (GET + POST) and
  lib/db/workouts.ts replaced with Prisma.WorkoutWhereInput +
  Prisma.WorkoutCreateInput + Prisma.DateTimeFilter. Catches typos
  at compile time and surfaces query shape directly in tooltips.

Workout import endpoint tests (tests/routes-import.test.ts)
- 7 tests covering /api/workouts/import/save: 401 unauthenticated,
  empty workouts rejected, case-insensitive name matching against
  existing exercises, new-exercise creation with isCustom=true and
  type='other' default, explicit existingExerciseId honored over
  name lookup, multiple workouts per call, sequential setNumber
  per exercise per workout.

bcryptjs -> bcrypt (native)
- Roughly 10x faster than the pure-JS implementation under load —
  login latency drops from ~250ms to ~25ms. Hash format is fully
  cross-compatible with bcryptjs ($2a$ / $2b$ both verify), so
  existing user passwords keep working without migration.
- Dockerfile builder stage adds python3 + make + g++ as a safety net
  for native node-gyp compilation on alpine when prebuilt binaries
  aren't available.
- Runner stage explicitly COPYs node_modules/bcrypt so the .node
  binding is unambiguously present even if Next.js standalone
  tracing somehow misses it.
- StartOS package's changeAdminCredentials.ts keeps bcryptjs (it's
  bundled by ncc into a single JS file and runs only on the rare
  admin action; native bcrypt would require shipping the .node
  binding through ncc which it doesn't handle gracefully).

CSP nonces (middleware.ts + next.config.js)
- Per-request nonce generated in middleware. Forwarded to Next via
  the x-nonce request header, which Next 13.4+ automatically stamps
  onto its inline bootstrap scripts. CSP response header includes
  `'nonce-${nonce}' 'strict-dynamic'`, dropping the previous
  `'unsafe-inline'` from script-src.
- Static CSP removed from next.config.js (middleware-set headers
  override static ones, so keeping both was redundant).
- Middleware matcher widened to all paths except static assets so
  the CSP applies to every page response. Existing /main + /api
  auth gating preserved.
- style-src keeps 'unsafe-inline' — Next/Tailwind still inject
  critical inline <style>; tightening that requires hash-based
  style-src or per-style nonce stamping (Next doesn't auto-do
  either). Worth a follow-up if you want the cleanest possible CSP.

/api/me/import (mirror of /api/me/export)
- Accepts the same JSON shape /api/me/export emits (schema string
  validated: only `proof-of-work-export@1` accepted today).
- mode: 'merge' (default) — adds imported rows; existing exercises
  with matching names are NOT overwritten (the user's custom version
  wins). All workout sets with a known exercise get rebound to the
  user's actual exercise id via name lookup.
- mode: 'replace' — wipes the user's exercises/workouts/sets first,
  then imports. Requires `confirm: "REPLACE"` in the body.
- Always scoped to the actor — never touches other users' data.
- Profile/admin flag/sessions/InstanceSettings deliberately not
  imported (account identity stays put).
- 7 tests cover: 401, schema rejection, merge create+skip, replace
  confirmation gate, replace wipes-then-imports, isolation across
  users.
- ExportMyData component grew Import (merge) + Import (replace)
  buttons with native browser confirm() before the destructive
  replace.

Test suite now 81 tests across 9 files in ~2.6s.
2026-05-09 11:05:03 -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 d51400c2a9 Robustness: WAL mode, security headers, last-login, delete-my-account
SQLite WAL mode (start9/0.4/docker_entrypoint.sh)
- Switches journal_mode to WAL on every boot. WAL persists in the DB
  header so this is effectively a one-shot but rerunning is harmless.
- Crucial for the "background StartOS Backup while users are using the
  app" case: under the default rollback journal, a long backup can
  capture an inconsistent snapshot. WAL keeps readers and the writer
  from blocking each other.
- synchronous=NORMAL paired with WAL: still crash-consistent at every
  checkpoint, ~10x faster than FULL.

Security headers (proof-of-work/next.config.js)
- Content-Security-Policy with frame-ancestors 'none', base-uri 'self',
  form-action 'self', object-src 'none'. Keeps 'unsafe-inline' for
  script/style because Next.js emits inline bootstrap; tightening to
  nonce-based CSP is a follow-up.
- Strict-Transport-Security: max-age=31536000; includeSubDomains.
- Referrer-Policy: strict-origin-when-cross-origin (don't leak workout
  IDs etc. to third-party sites).
- Permissions-Policy: deny camera, mic, geolocation, USB, etc. across
  the board (none of those APIs are used today; explicit deny means
  vulnerability scanners have one less thing to flag).

Last-login tracking
- New User.lastLoginAt column. createSession stamps it inside the same
  transaction as the new Session row.
- Compat ALTER in entrypoint adds the column to legacy snapshots.
- Admin Users table now shows a relative-age cell (today / Nd ago /
  Nmo ago / Ny ago / "never" if the user hasn't signed in since the
  column was added). Hover reveals the exact ISO timestamp.

Self-serve delete-my-account (Settings -> Danger Zone)
- Requires both the user's current password AND typing the literal
  phrase "delete my account" (defense against a stolen-session
  attacker nuking the account in one click).
- Refused for the last admin (instance can't be left with no admin —
  the user is told to promote someone first).
- Cascades through Prisma onDelete: Cascade on every relation owned by
  User, so workouts, exercises, sessions, preferences all go in one
  shot. Session cookie cleared, redirected to /auth/login.
2026-05-09 10:19:31 -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
MacPro 1b64c45c52 Initial commit for Start9 packaging 2026-02-28 09:27:26 -06:00