Files
famdone/MEMORY.md
T
2026-08-17 07:41:26 +01:00

35 KiB
Raw Blame History

FamChore v2 — Development Memory

2026-08-17 — Hono proxy removed: everything runs in SvelteKit services

  • Decision: deleted the proxy/ Hono service entirely. All business logic (admin CRUD, member kanban, weekly summary/EOW, bonus evaluation/trigger/progress, rewards claim/issue, chat, payday settlement, debug data-gen) now lives in frontend/src/lib/server/services/, grouped by app area (not by role): fam.ts, chores.ts, completions.ts, rewards.ts, bonuses.ts, chat.ts, settings.ts, crud.ts, debug.ts, plus a generic per-resource crud.ts. createServices(pb, user) returns a per-feature binder; servicesFor(event) is the shorthand for loads/form actions. No role guard — PB collection rules on the acting user's token are the security boundary (the admin/member split no longer exists as separate files).
  • Wiring: hono.admin.* (form actions/loads) and memberApi.*/chat are now direct service calls or SvelteKit /api/* routes (completions/toggle, members/rewards/[id]/claim, members/me, fam/[famId]/payday, chat, admin/[famId]/assigned-chores). Browser admin calls (chores grid) hit SvelteKit /api/admin/*. routeAuth.actingClient(event) resolves the acting user's PB client from the Bearer header or the pb_token cookie.
  • Migration relocated: proxy/src/migrate.ts → frontend/src/lib/server/migrate.ts (env now via $app/env/private + PB_ENDPOINT from pocketbase.ts), run once per process by migrate-boot.ts, kicked off in hooks.server.ts (void migrateOnBoot()). Schema source of truth remains shared/pb/schema.ts.
  • Infra: pnpm-workspace.yaml (only frontend), root package.json (dev = pnpm --filter frontend dev), docker/Dockerfile (no proxy build/deploy), docker/entrypoint.sh (no proxy start; app runs schema migration on boot), docker/nginx.conf (/api/ block removed → falls through to location / → SvelteKit :3000; /pb/api/ unchanged). Removed PROXY_URL env + PROXY_PORT from shared/config.ts and frontend/src/env.ts. Dead memberApi.myChores/requestAll removed.
  • Typecheck: frontend svelte-check = 12 pre-existing canary errors (.svelte implicit-any, qrcode decl, RewardType/Frequency casts, signup string|undefined); zero errors in the migration's files. pnpm build (adapter-node) succeeds.
  • Note: dev servers were left running; the now-deleted proxy tsx watch (:3456) will error and the frontend dev server needs a restart to drop PROXY_URL/load migrateOnBoot + the removed /api Vite proxy.

UI Component Architecture (Jul 2026)

Layout Hierarchy

+layout.svelte  ← global styles, meta, favicon
├── /login, /signup, /join/*  ← auth pages (no shell)
└── [fam]/+layout.svelte  ← Shell: Sidebar + TopNav + Footer + claim toast
    ├── [fam]/+page.svelte                   ← fam dashboard
    ├── [fam]/{username}/+page.svelte       ← parent=admin overview, child=kanban
    ├── [fam]/{username}/chores/+page.svelte  ← parent only
    ├── [fam]/{username}/ledger/+page.svelte ← parent only (rewards/chores/todos)
    ├── [fam]/{username}/bonuses/+page.svelte ← parent only
    ├── [fam]/{username}/settings/+page.svelte ← parent only
    └── [fam]/{username}/preferences/+page.svelte ← both roles

Sidebar (collapsible to mini-mode)

  • Header: app name (FamChore)
  • Admin CTAs: Dashboard, Chores, Rewards (badge count), Bonuses
  • Member CTAs: Dashboard, Preferences
  • Footer: family name, Settings (admin only), Log out
  • Role-aware: items differ based on admin vs member route

TopNav

  • Slot announcement (center) — system/family messages
  • Slot actions (right) — user status, claim/message

Page Content

  • ViewHeader — title + subtitle + tool bar (tabs, weeknav, sort)
  • CardGrid — 3-column grid, Cards span columns via cols prop
  • Card — 1/2/3 col span, micro-layout per page
  • Accordion — for settings / log sections
  • Button — consistent CTAs with variant (primary/secondary/ghost/danger) and size (sm/md/lg)

Components (frontend/src/lib/components/)

  • Sidebar.svelte, TopNav.svelte, Footer.svelte
  • ViewHeader.svelte, Card.svelte, CardGrid.svelte
  • Button.svelte, Accordion.svelte
  • icons.ts — SVG icon strings (no icon library dep)

Role-Based Auth (Jul 2026)

  • Members have role field ('parent' | 'child', added to members collection)
  • Parents authenticate via email/password (PB session JWT), children via device token
  • /api/admin/signup now creates a member record for the parent with role: 'parent'
  • /api/admin/login returns memberName, memberColor, role alongside session info
  • /api/members/verify-token returns role for the frontend
  • requireAdmin middleware checks users (role='parent' && id = userId, scoped by famId)
  • Removed fam_admins collection (Aug 2026): parent identity fully lives on the users record (famId, role, name, color, email). requireAdmin, authorizeFamReq, resolveChatActor, admin /profile get/patch, and the legacy proxy /signup//login now read/write users instead. Signup no longer creates a fam_admins row; superadmin stats derive parentEmail from users role='parent'. Migrate step 34 drops the collection.
  • Removed fams.inviteCode (Aug 2026): join flow is OTP-based (user_configs.otp), so the stored/displayed/regenerated invite code was never consumed. Removed randomCode() at signup, the /regen-invite endpoint + hono.admin.regenInvite action, the inviteCode type/field, and added migrate step 34 to drop the field.
  • Frontend sidebar is role-aware: isParent = session !== null
  • No more /admin prefix — admin pages live under /{fam}/{parent-username}/chores etc.

Routes (Jul 2026)

/                              Landing (SaaS marketing)
/signup                        Signup (creates parent user + member)
/login                         Login (returns member info, redirects to /{fam}/{memberName})
/join/:code                    Member invite (child)
/join/:code/:member            Member invite with pre-selected name
/admin                         Super admin dashboard (unchanged)
/{fam}                         Fam dashboard
/{fam}/{username}              Parent → admin overview, Child → kanban
/{fam}/{username}/chores       Parent: chore management
/{fam}/{username}/ledger      Parent: rewards / chores / todos ledger
/{fam}/{username}/bonuses      Parent: bonus configs
/{fam}/{username}/settings     Parent: family settings
/{fam}/{username}/preferences  Both: edit name/color
/api/*                         Hono proxy

Architecture Decisions

2026-06-23 — Monorepo & Docker Setup

  • Ports: Frontend = 2080, Proxy = 3456, Container ext = 3001. Port 3000 reserved/conflict.
  • Shared config: config.ts at root for dev/build-time values (e.g. PROXY_PORT). Runtime config via env vars. .env tracks ports, .env.example committed.
  • Docker: 2 Dockerfiles — Dockerfile (prod, multi-stage with nginx) and Dockerfile.dev (PocketBase for dev).
  • Nginx: Prod container uses nginx to route /api/* → Hono (:3456), /* → SvelteKit (:2080).
  • Dev workflow: pnpm dev at root runs SvelteKit + Hono in parallel. PocketBase via Dockerfile.dev.
  • Proxy runtime: Uses process.env.PROXY_PORT instead of importing config.ts (avoids rootDir issues in tsc).

2026-06-23 — Hono Proxy for All Data; Svelte Reactivity Only

  • All data operations (reads and writes) go through the Hono proxy, never directly to PB SDK.
  • UI reactivity is purely Svelte $state / $derived / $effect — no PB SDK .subscribe() / SSE.
  • The /debug page's PB SDK subscribe() was experimental only; final apps fetch via Hono proxy and update Svelte state reactively.
  • Bug: Both join/[code]/+page.server.ts and join/[code]/[member]/+page.server.ts called setSessionCookie() with userId: memberId (the child's PB record ID). This made event.locals.session truthy for children, causing [username]/+page.server.ts to enter the admin branch and call hono.admin.* endpoints. The proxy's requireAdmin checked fam_admins for the child's member ID (which doesn't exist) and returned 401.
  • Fix: Removed setSessionCookie() from both join pages. Children only get a device_token cookie. The session cookie is only for email/password-authenticated parents, set by /login and /signup.
  • Lesson: Children must never get a session cookie. The auth table in AGENTS.md says "Member → device token, no expiry" — the code must match.

2026-08-03 — Family Timezone Setting + Tz-Aware Week Math

  • Feature: fams.timezone (IANA name or "auto") added via migrate.ts 5d/5e (field + backfill "auto"). Exposed in settings Payday card (dropdown from COMMON_TIMEZONES, ~40 entries, + "Auto (detected)"). Set via PATCH /api/admin/:famId/fam alongside payday/paydayTime.
  • Shared module timezone.ts (root, imported by both proxy and frontend): resolveTz, dateStrInTz, weekdayInTz, todayInTz, addDaysStr (pure UTC), weekStart(payday, tz), wallClockToUtc (iterative 4-pass Intl, DST-safe), COMMON_TIMEZONES.
  • Bug fixed ("5 days left on Monday"): old mondayOf/addDays/daysLeft used local setDate + toISOString(). On BST Sunday Aug 9 local midnight → UTC Aug 8, so Monday showed 5 days left instead of 6. Now the child page computes weekStart/todayIso/daysLeft via weekStart(1, famTz) + addDaysStr + todayInTz, giving 6. Verified: Mon Aug 3 → weekStart 2026-08-03, weekEnd 2026-08-09, daysLeft 6.
  • releaseWeek time gate: now tz-aware. weekStart(payday, tz) for idempotency check; target = new Date(wallClockToUtc(weekStartToday, paydayTime, tz)). Verified: payday Mon 20:00 Europe/London (BST) → target 2026-08-03T19:00:00Z; auto resolves to server tz. Returns {settled:false, notYet:true, weekStart, target} before the time.
  • Proxy threads tz through weekly-summary, eow-preview, bonus-configs/progress, evaluateFam, tallies, manual trigger, complete-week, releaseWeek. my-chores returns timezone; child +page.server.ts passes it to data.timezone; parent passes data.fam.timezone.
  • Frontend child page: rawFamTz = data.timezone || data.fam?.timezone || 'auto', famTz = resolveTz(rawFamTz). todayChild, todayIso, mondayOf, addDays, isPaydayToday (via weekdayInTz), paydayTarget (via wallClockToUtc), today all tz-aware. mondayOf now delegates to tzWeekStart(1, famTz).
  • Smoke-tested live (fam v56f0f8o147kj1x): GET fam returns timezone, PATCH sets Europe/London, time-gate returns correct target, auto resolves to server tz, settings page SSR shows the dropdown, child page 200. Fam restored to payday=0, paydayTime=18:00, timezone=auto, lastIssued=2026-08-02.
  • Typecheck: proxy tsc --noEmit 28 errors (all pre-existing rootDir/.ts-import/key: never/implicit-any baseline); frontend svelte-check 9 errors (baseline; no new errors in edited files).

2026-08-04 — Terminology: "Payday" + Countdown to Settlement Day

  • Decision: Standardize the user-facing term on "payday" for the weekly settlement event/day. "EOW" is ambiguous (window-close vs settlement day) and is now dropped from user-facing strings. Keep "week" for the Sun→Sat earning window. Internal identifiers (eow*, simulateEow, eowPreview, CSS eow-*) left as-is.
  • daysLeft now counts to settlement day: daysLeft on the child dashboard counts to weekStart + 7 (the next payday day) instead of weekEnd = weekStart + 6. So Tue Aug 4 with payday=Sun shows 5 days until payday. Hero label updated to "days until payday".
  • User-facing renames: hero days left → days until payday; debug card Simulate End-of-Week → Simulate Payday; error Failed to preview EOW → Failed to preview payday; settings hint reworded to lead with "Payday:".

2026-08-04 — DDMMYY Date Rule + Debug Payday Preview Fix

  • Rule added (AGENTS.md): all user-facing dates are DDMMYY (compact, e.g. 040826 for 4 Aug 2026). Shared helper formatDDMMYY() in frontend/src/lib/format.ts (extracted from the local copy in chores/+page.svelte). Never render raw YYYY-MM-DD to users.
  • Bug: the admin "Preview payday" card showed all-time totals (e.g. "280 pts £288.00 14 chores" for zooney) because the proxy's eow-preview reward queries (rewardPointsList/rewardCashList) had NO date filter, summing every claimed reward ever. complete-week (the real settlement snapshot) scopes with date >= ws.
  • Fix: added && date >= '${ws}' to both reward queries in eow-preview (proxy/src/index.ts) so the preview matches what the rollover actually records. Verified live: zooney now shows this-week 220 pts / £16.00 / 14 chores / bonus 10.
  • UI: removed the no-op Simulation ON/OFF toggle (settings.simulateEow flag drives no behavior) — it was the source of "simulation on/off vs preview rollover" confusion. Card is now just "Debug: Preview payday" → "Preview payday" button. Debug card dates now render via formatDDMMYY.
  • Typecheck: frontend svelte-check still at 12 baseline errors (no new); proxy tsc unchanged pre-existing baseline.

2026-08-04 — Preview Payday Extends to Child Dashboard

  • Feature: "Preview payday" now enables a family-wide preview mode that the child dashboard reacts to. ?/previewEow action calls eowPreview then hono.admin.updateSettings({ simulateEow: true }), returning { preview, simulateEow: true }. A "Turn off preview" button (?/setEow, on=false) clears it.
  • Child notice: my-chores (proxy/src/index.ts) now returns simulateEow: !!settings.simulateEow; child +page.server.ts passes it as data.simulateEow. Child kanban renders a .preview-notice banner ("Payday preview — your parent is checking this week's payday. Nothing is paid out yet.") when the flag is set. Admin card shows a .eow-mode-on note + "Turn off preview" when active.
  • Note: simulateEow (settings.simulateEow) was previously a no-op debug flag; it now meaningfully drives preview mode across admin + child views.
  • Verified live: POST ?/previewEow sets settings.simulateEow=true (GET settings confirms); child SSR page data carries simulateEow:true; control case (flag off) renders no notice. Test data restored afterwards (zooney deviceToken + flag reset to false).

2026-08-04 — Payday-Gated Bonus Payouts

  • Feature: weekly/monthly period bonus rewards are now claimable only on payday (not the moment they're met). Rewards gain claimable: 'immediate' | 'payday' + settleDate (YYYY-MM-DD, server-side). The bonus-met notice stays exciting on the child dash — the reward line shows a locked "🔒 pays out {DDMMYY}" badge and skips the request button pre-payday.
  • Stamp logic: claimableStamp() in proxy/src/index.ts — periods weekly/monthly → { claimable: 'payday', settleDate: nextPaydayAfter(periodEnd) }; else immediate. Manual bonus triggers (/bonus-configs/:id/trigger) stamp immediate (parent-initiated, not a scheduled payout). All 3 evaluateFam create sites (individual/collaborative/competitive) use claimableStamp. nextPaydayAfter() helper added to timezone.ts.
  • Enforcement: member claim endpoint checks assertPaydayUnlocked() (throws "This bonus pays out on payday (…settleDate) — hang tight!", returned as HTTP 400); request-all skips payday-gated rewards not yet settled. Admin Issue/Issue All are parent discretion and unaffected.
  • UI: child wallet renders locked badge for pre-settle payday rewards; admin "Claims → Outstanding" shows a 🔒 {DDMMYY} hint. Both reuse formatDDMMYY(). (Later: switched both to formatShortDate() → "🔒 pays out 9 Aug"; child owedCash banner excludes payday-locked rewards so "You've earned £X — go get it!" no longer shows for rewards that aren't claimable yet.)
  • Schema: rewards.claimable (select, required) + rewards.settleDate (text) added in proxy/src/migrate.ts + proxy/scripts/seed.ts. PB's required select rejects empty on write; legacy null-claimable rewards are treated as immediate by both proxy and frontend, so no data backfill was needed.
  • Verified live: complete-week → evaluateFam recreated the weekly Pocket Money reward with claimable=payday, settleDate=2026-08-09 (Sunday payday after Sun→Sat week); member claim pre-payday → 400 with friendly message + status stays unclaimed; request-all returns {count:0}; claim succeeds after settleDate.
  • Ops note: the dev proxy's tsx watch had silently frozen (file edits at 09:49 weren't picked up by a child started 09:47). Fixed by killing the watcher tree with explicit PIDs and relaunching pnpm dev (nohup → /tmp/proxy_dev.log). pkill -f "tsx watch src/index.ts" hangs the shell — use kill <pid> instead.

2026-08-04 — Dev Servers: Always Reuse Existing 2080/3456

  • Rule: NEVER start our own dev servers. Always use the already-running ones: proxy 192.168.1.225:3456 (tsx watch, reloads on edit) and frontend localhost:2080 (vite HMR). Don't spawn nohup pnpm dev, tsx watch, or extra vite instances — it wastes time/tokens. Only kill/restart when the user explicitly asks (or a watcher is demonstrably stale, and then only after asking). Prefer short targeted curls and reuse one auth TOKEN across commands in the persistent shell.

2026-08-04 — Chores Page Accordion Quick Fixes

  • Add actions moved into sections: removed the blue round + from the member swimlane header and the Templates column header. Both replaced by a shared full-width dashed + Add a todo / + New template button (.add-inline) at the top of the Todos accordion content and the Templates list respectively.
  • Accordions default open: accordionState lookup defaults to { chores: true, todos: true } (?? true in the template + toggle), so both sections load expanded on page load; still toggleable. Redundant empty-state "+ Add a todo" button and .add-todo-btn/.empty-cta CSS removed.
  • Check: frontend svelte-check stays at 12 baseline errors.

2026-08-04 — Human Dates for Todo "Due" (Not DDMMYY)

  • Problem: the chores todo card rendered due 050826 (DDMMYY code) — ambiguous/terrible for a due date.
  • Fix: added formatShortDate() to frontend/src/lib/format.ts — renders 5 Aug (adds 26 when the year isn't the current one). Chores todo card now shows due 5 Aug. AGENTS.md date rule updated: DDMMYY for dense/range contexts, formatShortDate() for single human-readable dates like due dates.

2026-08-04 — Child-Dashboard Design Lead Applied to All Pages

  • Design principal (from [fam]/[username] child dash): gradient hero lead (linear-gradient(135deg, #6366f1, #8b5cf6 55%, #a855f7), radius 16px, glow shadow, white text), gradient stat tiles, rounded white cards.
  • ViewHeader hero variant: added hero prop to frontend/src/lib/components/ViewHeader.svelte — renders the title/subtitle/tools on the gradient hero (tabs/nav/sort get tinted-on-white styling). Off by default, so no behavior change elsewhere.
  • Applied hero to: admin dashboard (fam name), chores, bonuses, rewards, settings, preferences, platform admin /admin.
  • Admin dashboard stat tiles: added 4 gradient tiles (members / points / cash / chores done) reusing the child .tiles/.tile pattern + new .tile-members/.tile-chores colors, from a new adminTiles derived summing summary.summaries. Also fixed admin subtitle Week of {YYYY-MM-DD} → Week of {DDMMYY}.
  • Check: frontend svelte-check stays at 12 baseline errors. Note: frontend dev server on :2080 was not running when verified (proxy :3456 up).

2026-08-06 — Env Consolidation: SERVER_IP, PROXY_URL, and SvelteKit env only

  • config.ts is proxy-only. It now holds just the three ports (FRONTEND_PORT/PROXY_PORT/PB_PORT = 2080/3456/8090). SvelteKit never imports config.ts — SvelteKit env vars are declared in frontend/src/env.ts and read via $app/env/*. Deleted the stale config.js/.d.ts/.map artifacts.
  • frontend/src/env.ts declares: PROXY_URL (public, default http://127.0.0.1:3456), SERVER_IP (public, default 192.168.1.225), PB_EMAIL/PB_PASSWORD (private, defaults). PUBLIC_PB_URL removed (was the source of a startup crash when unset).
  • Deleted frontend/src/lib/server/env.ts (untracked). All server modules now import { PROXY_URL } from '$app/env/public' (hono.ts, auth.ts, +layout.server.ts, +page.server.ts, preferences, join/[code]/[member]). admin/+page.server.ts imports creds from $app/env/private.
  • frontend/src/lib/pocketbase.ts (browser) + pb-admin.ts: PB_ENDPOINT = import.meta.env.PROD ? '/pb' : \http://${SERVER_IP}:8090`. (Fixed a bug where pocketbase.tsusedimport.meta.env.SERVER_IP` → undefined.)
  • proxy/src/env.ts (new): PB_ENDPOINT = SERVER_IP ? \http://${SERVER_IP}:8090` : `http://127.0.0.1:8090`. Dev env is loaded by the proxy's dev/seedscripts viatsx --env-file-if-exists=../.env(pnpm has no--env-file; NODE_OPTIONS='--env-file=…'is rejected by Node). NoloadEnvFile` hack in code.
  • Docker: removed dead ENV PB_ENDPOINT from Dockerfile; EXPOSE 3001 (was 3005 8090); compose public port is ${PORT:-3001}:3001, creds default to the code fallback, redundant FRONTEND_PORT/PROXY_PORT passthrough dropped; entrypoint.sh simplified (PB_DATA=/app/pb_data, PORT=$FRONTEND_PORT, no :- fallbacks).
  • Frontend deps added (were missing imports): chart.js, qrcode, @hiseb/confetti.
  • Build checks: proxy + frontend pnpm build clean.

2026-08-06 — Dev PB data incident (pb-dev) — see RULES.md "Dev vs Prod PocketBase data"

  • Symptom: pnpm dev proxy migrate failed; PB superuser auth returned HTTP 500 "Something went wrong"; could not log into the PB admin UI.
  • Root cause: the permanent dev PB container pb-dev (publishes :8090, data in host ./pb_data) had a broken bind mount — it was serving an empty throwaway store, so the debug@famchamp.dev superuser didn't exist. The real data.db was on the host but the container wasn't seeing it.
  • Fix: recreated pb-dev with the mount correctly attached (-v "$PWD/pb_data:/pb_data", pocketbase serve --http=0.0.0.0:8090 --dir=/pb_data). Superuser auth then returned 200 on both 127.0.0.1:8090 and the Tailscale SERVER_IP:8090.
  • Watch-out: a careless docker run with a fresh volume (my first attempt, aborted in time) would have wiped the permanent PB data. Restore command is in RULES.md. Two containers (pb-dev :8090 and the docker app's internal PB :8091) currently share the same host ./pb_data — be careful with both.

2026-08-06 — Added root shared/ for cross-package code

  • Created shared/timezone.ts (moved from root timezone.ts). Imported by frontend/src/routes/[fam]/[username]/+page.svelte, .../settings/+page.svelte, and proxy/src/index.ts. Deleted the root timezone.ts.
  • Created shared/pb/schema.ts — single source of truth for the PocketBase schema + field builders (SCHEMA_PLAN ordered collection plan + text/select/rel/... helpers). Both proxy/src/migrate.ts (ensureSchema) and proxy/scripts/seed.ts now iterate SCHEMA_PLAN; kills the previous duplicated schema/field-helper definitions in both files.
  • Reason: timezone.ts and the PB schema are consumed by more than one package; shared/ is the root location both can reach. Rule added to RULES.md: shared code lives in shared/, never inside frontend/ or proxy/.
  • Note: proxy tsc --noEmit already errors on .ts-extension imports (allowImportingTsExtensions unset) — pre-existing, not from this change. Runtime uses esbuild (build) + tsx (dev), both of which bundle the shared/ imports correctly. Verified pnpm build clean for both packages.

2026-08-07 — @shared/* import alias (path alias, not a pnpm package)

  • Moved config.ts → shared/config.ts. All shared/ code is now imported as @shared/* instead of relative ../../shared/....
  • This is a path alias, not a pnpm workspace package (@shared alone isn't a valid npm package name; a real package would need @scope/name).
  • Proxy: tsconfig.json sets paths: { "@shared/*": ["../shared/*"] }; esbuild build adds --alias:@shared=../shared; tsx resolves via tsconfig paths. Proxy keeps .ts extensions (@shared/config.ts).
  • Frontend: uses kit.alias in vite.config.ts (NOT paths in frontend/tsconfig.json, which SvelteKit warns against). Frontend imports shared files without the .ts extension (@shared/timezone) because rewriteRelativeImportExtensions only rewrites relative paths.
  • Verified: proxy build + frontend build + svelte-check all clean for @shared/* (svelte-check still reports pre-existing qrcode types + CSS warnings).
  • Docker note: runtime image only copies frontend/build + proxy/dist (both already bundle shared/), so shared/ needn't be copied into the image.

2026-08-10 — Dev runtime cleanup (PB instances / containers)

  • Removed test container 31e74178b3f0 (famdone-service-app-1, host :3010 + :8092). It ran PB 0.39.10 and bound the same host pb_data as pb-dev → two PBs (v0.25 + v0.39) writing one SQLite DB = corruption/lock risk (likely source of dev instability/login lag).
  • Killed 7 stale host tsx watch dev-proxy processes (Jul 28–Aug 5) + my throwaway 0.39 PBs.
  • Reset pb_data: stopped pb-dev, wiped /home/threejjjs/development/famchamp/pb_data, recreated pb-dev container (fresh v0.25 store) with --automigrate=false, recreated dev superuser debug@famchamp.dev/debug123.
  • Why recreate pb-dev: v0.25 automigrate had baked stale pb_migrations into the old container's writable layer; on a fresh store they failed (Failed to apply migration ...: no rows in result set).
  • Desired end state (confirmed): 8090 = pb-dev (v0.25, single instance, automigrate off); 3001+8091 = PROD app cffd636cc772 (kept, not live); PROD pb_data at /data/coolify/.../pb_data (separate from dev).

2026-08-10 — PB 0.25 → 0.39 migration (branch feature/migrate-pocketbase)

  • Decision: keep our own migrate.ts schema-as-code (API-driven, does data migrations + rule locking), NOT PocketBase's built-in automigrate (schema-only, generates version-specific migration files, and generates conflicting snapshots on upgraded stores). Disable PB automigrate in the runtime.
  • Verified against 0.39.10 (throwaway binaries on 127.0.0.1:8098/8099, temp data dirs):
    1. Fresh store: migrate() bootstraps all 14 SCHEMA_PLAN collections + every field migration cleanly (schema field builders are 0.39-compatible).
    2. Existing 0.25 pb_data → 0.39: opens & serves with --automigrate=false and no stale pb_migrations dir. (Without this, automigrate writes a snapshot to ./pb_migrations relative to CWD that conflicts with existing collections → "Collection name must be unique".)
    3. JS SDK pocketbase@^0.27.0: authWithPassword + public reads OK; realtime SSE endpoint serves PB_CONNECT.
  • Code changes on branch:
    • docker/Dockerfile POCKETBASE_VERSION → 0.39.10.
    • docker/Dockerfile.dev → 0.39.10 + CMD ... --automigrate=false.
    • docker/entrypoint.sh → pocketbase serve ... --automigrate=false.
    • proxy/src/env.ts → added non-breaking PB_ENDPOINT env override (used to point migrate at a throwaway PB on another port; default dev/prod split unchanged).
  • ⚠️ 0.39 schema breaking change: PB 0.39 does NOT auto-add createdAt/updatedAt to API-created collections (0.25 did). The chat store filters/sorts on a custom messages.createdAt, so a fresh 0.39 store is missing it → raw PB 400. Fixed two ways:
    • shared/pb/schema.ts: messages now declares date("createdAt") explicitly (source of truth → ensureSchema).
    • proxy/src/migrate.ts: the "messages already exists" branch now adds createdAt if missing (idempotent hardening for drifted/upgraded stores).
  • Dev now on 0.39.10: pb-dev rebuilt from branch Dockerfile.dev (0.39.10 + --automigrate=false), fresh store on :8090, superuser debug@famchamp.dev re-verified, migrate() bootstraps schema + applies messages.createdAt fix cleanly.
  • Prod upgrade steps: backup pb_data → run the 0.39 image (automigrate off) → run migrate() → verify no drift (messages.createdAt, id.autogeneratePattern).

2026-08-10 — Revert PB to 0.25.8 (backtrack from 0.39)

  • Decision: backtrack off the PocketBase 0.39 bump (introduced in commit 3725c54 via ARG POCKETBASE_VERSION=0.39.10) and work from a 0.25.8 baseline in BOTH dev and prod, then migrate to 0.39 deliberately later.
  • Reverted docker/Dockerfile POCKETBASE_VERSION back to 0.25.8 (matches docker/Dockerfile.dev). Dev pb-dev and the docker app internal PB :8091 share host ./pb_data.
  • Migration schema scripts (DO NOT FORGET): the schema single source of truth is shared/pb/schema.ts (SCHEMA_PLAN), iterated by proxy/src/migrate.ts (ensureSchema) and proxy/scripts/seed.ts. The chat messages / chat_typing collections are defined there without an explicit createdAt — they rely on PB auto-adding it on first create.
    • The 0.39 prod messages drift (missing createdAt, then id "Cannot be blank" after a raw field PATCH) came from schema mismatch during the bump. When migrating 0.25→0.39, reconcile the schema scripts against 0.39's field semantics (incl. system id autogeneratePattern) instead of patching collections by hand.
  • Migration (members → users, run live + verified): deleted the members collection and repointed the 5 memberId relations (assigned_chores, completions, rewards, weekly_history, bonus_configs) → users. Added users.name + users.color, backfilled child name/color; backfilled parent role ('' → 'parent'). Scoped users rules: list/view = famId = @request.auth.famId, update/delete = famId = @request.auth.famId && @request.auth.role = 'parent'. Re-runs are idempotent.
  • PB relation quirk: PB forbids changing a relation's target collection in place (validation_field_relation_change) — you must drop the field and re-add it targeting the new collection in two separate collection updates.
  • Child (member) auth: children are users records with role='child', PB username = {famSlug}:{handle} (composite, globally unique), name = raw display name. Server derives the PB password = MEMBER_SECRET + famSlug + handle; the join gate is a 20-min OTP in user_configs, then authWithPassword. Children now hold a pb_token cookie (previously a device_token cookie with no session). SessionUser gained username (set in hooks.server.ts, stores handleOf(record.username)); the kanban child redirect compares session.username, not session.name.
  • Cookie httpOnly: pb_token is now httpOnly:true. The browser PB SDK is seeded from page.data.pbToken via initPb(token) (layout onMount) — not from document.cookie (the client can no longer read it). Logout is server-side only. Note: the JWT is still shipped to the client in SSR HTML via the pbToken prop.
  • Typecheck baselines: frontend svelte-check = 20 pre-existing canary errors (chat json(status) ResponseInit, $types Action/SubmitFunction, qrcode decl, vite.config, RewardType/Frequency casts, implicitly-any); proxy tsc --noEmit = pre-existing record-typing + implicit-any errors. No new errors in edited files.
  • crypto.randomUUID() unavailable over plain HTTP (non-secure context) → added genClientId() fallback (randomUUID if available, else Date.now().toString(36)+random) in chat.svelte.ts.
  • Shared slugify util: shared/slugify.ts (@shared/slugify alias) — used by proxy signup + fam rename, frontend signup + settings renameFam, and member-otp.createChild (child username + password gen). Removed the 3 local duplicate slugify helpers.
  • Settings UI: CardGrid/Card are now responsive (media queries + --grid-cols/--card-cols; Cards use container-type: inline-size). Members card split into two columns: add-child form | member list (space-between rows, larger 22px colour circle, "Preview" CTA → /{famSlug}/{m.username}, Remove). Family Name card gained an explainer + mono /{fam.slug} slug line.
  • Hono audit: the proxy is the live data layer — admin CRUD/reads via hono.admin.* (kanban load, bonuses ~17 calls, ledger 6, preferences 2, fam dash 4, settings complete-week/debug/generate-data), member actions via memberApi.* (toggleCompletion/claimReward/payday) + direct /api fetches (chores assigned-chores, kanban /api/members/me). Not implemented: /api/weekly-cron CRON, Stripe (create-checkout + webhook), WhatsApp — weekly settlement is manual via complete-week/simulateEow. Chat endpoints exist in the proxy but the frontend talks to PB directly.

2026-08-15 — Signup: multi-step flow restored + family-creation bug fixed

  • Bug (blocker): new family creation failed with PB {"username":{"code":"validation_required","message":"Cannot be blank."}} — the users.create in /signup omitted the auth username field (PB 0.39 requires it). Reproduced directly against PB: create WITHOUT username → validation_required; WITH username → succeeds.
  • Fix: signup/+page.server.ts now includes username on the parent users create.
  • Username convention (composite + handle): PB users.username is the composite {famSlug}:{handle} for BOTH parents and children — globally unique (PB auth-identity needs a single-column unique index) even though the URL segment is per-family. handle(name) = lowercase, strips all non-[a-z0-9] ("Jakey Boy" → jakeyboy); slugify() (hyphenated) is kept only for fam slugs. URL segment = handleOf(username) (part after the last :) → /{famSlug}/{handle}. name keeps the raw display name, read from DB via authRefresh (not plucked into the cookie). Parent's handle captured at signup step 1 (yourName) → username = famUsername(famSlug, handle(yourName)); parents authenticate email+password and land on the fam dashboard /{famSlug} (not username-routed). Children authenticate via OTP → authWithPassword(famUsername(...), derivePassword(famSlug, handle)). Redirects in login/+page.server.ts, [fam]/[username]/+page.server.ts (parent + child branches) and preferences/+page.server.ts use session.username (the handle). Member-list URLs in settings, [fam]/+page.svelte, [fam]/[username]/+page.svelte build /{famSlug}/{handleOf(m.username)}. handle/handleOf/famUsername live in shared/slugify.ts (@shared/slugify).
  • Restored intended multi-step signup (from the guide, adapted to current OTP model): /signup steps — (1) ?/signup familyName/yourName/email/password → create fam + parent (name=yourName, username=famUsername(famSlug, handle(yourName))) + settings, set pb_token; (2) ?/child optional child → issueAccess returns { code, joinUrl }; (3) show OTP join code + "Go to dashboard" link. Uses named actions + use:enhance (callback typed any to avoid the pre-existing canary $types SubmitFunction error).
  • Typecheck: frontend svelte-check stays at 20 pre-existing errors (no new in edited files).