35 KiB
35 KiB
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 infrontend/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-resourcecrud.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 (theadmin/membersplit no longer exists as separate files). - Wiring:
hono.admin.*(form actions/loads) andmemberApi.*/chat are now direct service calls or SvelteKit/api/*routes (completions/toggle,members/rewards/[id]/claim,members,fam/[famId]/payday,chat,admin/[famId]/assigned-chores). Browser admin calls (chores grid) hit SvelteKit/api/admin/*. The/api/*routes read auth straight fromevent.locals(locals.user+createPbClient(locals.pbToken)) — a separateactingClienthelper was dropped as redundant sincehooks.server.tsalready resolves the session. No Authorization header; the httpOnlypb_tokencookie is the auth for same-origin calls. - Migration relocated:
proxy/src/migrate.ts→frontend/src/lib/server/migrate.ts(env now via$app/env/private+PB_ENDPOINTfrompocketbase.ts), run once per process bymigrate-boot.ts, kicked off inhooks.server.ts(void migrateOnBoot()). Schema source of truth remainsshared/pb/schema.ts. - Infra:
pnpm-workspace.yaml(onlyfrontend), rootpackage.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 tolocation /→ SvelteKit:3000;/pb/api/unchanged). RemovedPROXY_URLenv +PROXY_PORTfromshared/config.tsandfrontend/src/env.ts. DeadmemberApi.myChores/requestAllremoved. - Typecheck: frontend
svelte-check= 12 pre-existing canary errors (.svelteimplicit-any, qrcode decl, RewardType/Frequency casts, signupstring|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 dropPROXY_URL/loadmigrateOnBoot+ the removed/apiVite 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 viacolspropCard— 1/2/3 col span, micro-layout per pageAccordion— for settings / log sectionsButton— consistent CTAs withvariant(primary/secondary/ghost/danger) andsize(sm/md/lg)
Components (frontend/src/lib/components/)
Sidebar.svelte,TopNav.svelte,Footer.svelteViewHeader.svelte,Card.svelte,CardGrid.svelteButton.svelte,Accordion.svelteicons.ts— SVG icon strings (no icon library dep)
Role-Based Auth (Jul 2026)
- Members have
rolefield ('parent' | 'child', added tomemberscollection) - Parents authenticate via email/password (PB session JWT), children via device token
/api/admin/signupnow creates a member record for the parent withrole: 'parent'/api/admin/loginreturnsmemberName,memberColor,rolealongside session info/api/members/verify-tokenreturnsrolefor the frontendrequireAdminmiddleware checksusers(role='parent' && id = userId, scoped byfamId)- Removed
fam_adminscollection (Aug 2026): parent identity fully lives on theusersrecord (famId,role,name,color,email).requireAdmin,authorizeFamReq,resolveChatActor, admin/profileget/patch, and the legacy proxy/signup//loginnow read/writeusersinstead. Signup no longer creates afam_adminsrow; superadmin stats deriveparentEmailfromusers 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. RemovedrandomCode()at signup, the/regen-inviteendpoint +hono.admin.regenInviteaction, theinviteCodetype/field, and added migrate step 34 to drop the field. - Frontend sidebar is role-aware:
isParent = session !== null - No more
/adminprefix — admin pages live under/{fam}/{parent-username}/choresetc.
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. Port3000reserved/conflict. - Shared config:
config.tsat root for dev/build-time values (e.g.PROXY_PORT). Runtime config via env vars..envtracks ports,.env.examplecommitted. - Docker: 2 Dockerfiles —
Dockerfile(prod, multi-stage with nginx) andDockerfile.dev(PocketBase for dev). - Nginx: Prod container uses nginx to route
/api/*→ Hono (:3456),/*→ SvelteKit (:2080). - Dev workflow:
pnpm devat root runs SvelteKit + Hono in parallel. PocketBase viaDockerfile.dev. - Proxy runtime: Uses
process.env.PROXY_PORTinstead of importingconfig.ts(avoidsrootDirissues intsc).
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
/debugpage's PB SDKsubscribe()was experimental only; final apps fetch via Hono proxy and update Svelte state reactively.
2026-07-28 — Auth Bug: Join Flow Set Session Cookie for Children
- Bug: Both
join/[code]/+page.server.tsandjoin/[code]/[member]/+page.server.tscalledsetSessionCookie()withuserId: memberId(the child's PB record ID). This madeevent.locals.sessiontruthy for children, causing[username]/+page.server.tsto enter the admin branch and callhono.admin.*endpoints. The proxy'srequireAdmincheckedfam_adminsfor the child's member ID (which doesn't exist) and returned 401. - Fix: Removed
setSessionCookie()from both join pages. Children only get adevice_tokencookie. The session cookie is only for email/password-authenticated parents, set by/loginand/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 viamigrate.ts5d/5e (field + backfill"auto"). Exposed in settings Payday card (dropdown fromCOMMON_TIMEZONES, ~40 entries, + "Auto (detected)"). Set viaPATCH /api/admin/:famId/famalongside 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/daysLeftused localsetDate+toISOString(). On BST Sunday Aug 9 local midnight → UTC Aug 8, so Monday showed 5 days left instead of 6. Now the child page computesweekStart/todayIso/daysLeftviaweekStart(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) → target2026-08-03T19:00:00Z;autoresolves to server tz. Returns{settled:false, notYet:true, weekStart, target}before the time. - Proxy threads
tzthrough weekly-summary, eow-preview, bonus-configs/progress,evaluateFam, tallies, manual trigger, complete-week,releaseWeek.my-choresreturnstimezone; child+page.server.tspasses it todata.timezone; parent passesdata.fam.timezone. - Frontend child page:
rawFamTz = data.timezone || data.fam?.timezone || 'auto',famTz = resolveTz(rawFamTz).todayChild,todayIso,mondayOf,addDays,isPaydayToday(viaweekdayInTz),paydayTarget(viawallClockToUtc),todayall tz-aware.mondayOfnow delegates totzWeekStart(1, famTz). - Smoke-tested live (fam
v56f0f8o147kj1x): GET fam returns timezone, PATCH sets Europe/London, time-gate returns correct target,autoresolves 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 --noEmit28 errors (all pre-existing rootDir/.ts-import/key: never/implicit-any baseline); frontendsvelte-check9 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, CSSeow-*) left as-is. - daysLeft now counts to settlement day:
daysLefton the child dashboard counts toweekStart + 7(the next payday day) instead ofweekEnd = 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 cardSimulate End-of-Week→Simulate Payday; errorFailed 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.
040826for 4 Aug 2026). Shared helperformatDDMMYY()infrontend/src/lib/format.ts(extracted from the local copy inchores/+page.svelte). Never render rawYYYY-MM-DDto 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-previewreward queries (rewardPointsList/rewardCashList) had NO date filter, summing every claimed reward ever.complete-week(the real settlement snapshot) scopes withdate >= ws. - Fix: added
&& date >= '${ws}'to both reward queries ineow-preview(proxy/src/index.ts) so the preview matches what the rollover actually records. Verified live: zooney now shows this-week220 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-checkstill at 12 baseline errors (no new); proxytscunchanged 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.
?/previewEowaction callseowPreviewthenhono.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 returnssimulateEow: !!settings.simulateEow; child+page.server.tspasses it asdata.simulateEow. Child kanban renders a.preview-noticebanner ("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-onnote + "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
?/previewEowsetssettings.simulateEow=true(GET settings confirms); child SSR page data carriessimulateEow: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()inproxy/src/index.ts— periodsweekly/monthly→{ claimable: 'payday', settleDate: nextPaydayAfter(periodEnd) }; elseimmediate. Manual bonus triggers (/bonus-configs/:id/trigger) stampimmediate(parent-initiated, not a scheduled payout). All 3evaluateFamcreate sites (individual/collaborative/competitive) useclaimableStamp.nextPaydayAfter()helper added totimezone.ts. - Enforcement: member
claimendpoint checksassertPaydayUnlocked()(throws"This bonus pays out on payday (…settleDate) — hang tight!", returned as HTTP 400);request-allskips payday-gated rewards not yet settled. AdminIssue/Issue Allare parent discretion and unaffected. - UI: child wallet renders locked badge for pre-settle payday rewards; admin "Claims → Outstanding" shows a
🔒 {DDMMYY}hint. Both reuseformatDDMMYY(). (Later: switched both toformatShortDate()→ "🔒 pays out 9 Aug"; childowedCashbanner 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 inproxy/src/migrate.ts+proxy/scripts/seed.ts. PB'srequiredselect rejects empty on write; legacy null-claimable rewards are treated asimmediateby both proxy and frontend, so no data backfill was needed. - Verified live:
complete-week→evaluateFamrecreated the weekly Pocket Money reward withclaimable=payday, settleDate=2026-08-09(Sunday payday after Sun→Sat week); member claim pre-payday → 400 with friendly message + status staysunclaimed;request-allreturns{count:0}; claim succeeds after settleDate. - Ops note: the dev proxy's
tsx watchhad 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 relaunchingpnpm dev(nohup →/tmp/proxy_dev.log).pkill -f "tsx watch src/index.ts"hangs the shell — usekill <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 frontendlocalhost:2080(vite HMR). Don't spawnnohup 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 authTOKENacross 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 templatebutton (.add-inline) at the top of the Todos accordion content and the Templates list respectively. - Accordions default open:
accordionStatelookup defaults to{ chores: true, todos: true }(?? truein 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-ctaCSS removed. - Check: frontend
svelte-checkstays 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()tofrontend/src/lib/format.ts— renders5 Aug(adds26when the year isn't the current one). Chores todo card now showsdue 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. ViewHeaderhero variant: addedheroprop tofrontend/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
heroto: 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/.tilepattern + new.tile-members/.tile-chorescolors, from a newadminTilesderived summingsummary.summaries. Also fixed admin subtitleWeek of {YYYY-MM-DD}→Week of {DDMMYY}. - Check: frontend
svelte-checkstays 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.tsis proxy-only. It now holds just the three ports (FRONTEND_PORT/PROXY_PORT/PB_PORT=2080/3456/8090). SvelteKit never importsconfig.ts— SvelteKit env vars are declared infrontend/src/env.tsand read via$app/env/*. Deleted the staleconfig.js/.d.ts/.mapartifacts.frontend/src/env.tsdeclares:PROXY_URL(public, defaulthttp://127.0.0.1:3456),SERVER_IP(public, default192.168.1.225),PB_EMAIL/PB_PASSWORD(private, defaults).PUBLIC_PB_URLremoved (was the source of a startup crash when unset).- Deleted
frontend/src/lib/server/env.ts(untracked). All server modules nowimport { PROXY_URL } from '$app/env/public'(hono.ts,auth.ts,+layout.server.ts,+page.server.ts,preferences,join/[code]/[member]).admin/+page.server.tsimports 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 wherepocketbase.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'sdev/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_ENDPOINTfromDockerfile;EXPOSE 3001(was3005 8090); compose public port is${PORT:-3001}:3001, creds default to the code fallback, redundantFRONTEND_PORT/PROXY_PORTpassthrough dropped;entrypoint.shsimplified (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 buildclean.
2026-08-06 — Dev PB data incident (pb-dev) — see RULES.md "Dev vs Prod PocketBase data"
- Symptom:
pnpm devproxy migrate failed; PB superuser auth returnedHTTP 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 thedebug@famchamp.devsuperuser didn't exist. The realdata.dbwas on the host but the container wasn't seeing it. - Fix: recreated
pb-devwith 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 both127.0.0.1:8090and the TailscaleSERVER_IP:8090. - Watch-out: a careless
docker runwith 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 roottimezone.ts). Imported byfrontend/src/routes/[fam]/[username]/+page.svelte,.../settings/+page.svelte, andproxy/src/index.ts. Deleted the roottimezone.ts. - Created
shared/pb/schema.ts— single source of truth for the PocketBase schema + field builders (SCHEMA_PLANordered collection plan +text/select/rel/...helpers). Bothproxy/src/migrate.ts(ensureSchema) andproxy/scripts/seed.tsnow iterateSCHEMA_PLAN; kills the previous duplicated schema/field-helper definitions in both files. - Reason:
timezone.tsand 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 inshared/, never insidefrontend/orproxy/. - Note: proxy
tsc --noEmitalready errors on.ts-extension imports (allowImportingTsExtensionsunset) — pre-existing, not from this change. Runtime uses esbuild (build) + tsx (dev), both of which bundle theshared/imports correctly. Verifiedpnpm buildclean for both packages.
2026-08-07 — @shared/* import alias (path alias, not a pnpm package)
- Moved
config.ts→shared/config.ts. Allshared/code is now imported as@shared/*instead of relative../../shared/.... - This is a path alias, not a pnpm workspace package (
@sharedalone isn't a valid npm package name; a real package would need@scope/name). - Proxy:
tsconfig.jsonsetspaths: { "@shared/*": ["../shared/*"] }; esbuild build adds--alias:@shared=../shared; tsx resolves via tsconfig paths. Proxy keeps.tsextensions (@shared/config.ts). - Frontend: uses
kit.aliasinvite.config.ts(NOTpathsinfrontend/tsconfig.json, which SvelteKit warns against). Frontend imports shared files without the.tsextension (@shared/timezone) becauserewriteRelativeImportExtensionsonly rewrites relative paths. - Verified: proxy build + frontend build +
svelte-checkall clean for@shared/*(svelte-check still reports pre-existingqrcodetypes + CSS warnings). - Docker note: runtime image only copies
frontend/build+proxy/dist(both already bundleshared/), soshared/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 hostpb_dataas 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 watchdev-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 superuserdebug@famchamp.dev/debug123. - Why recreate pb-dev: v0.25 automigrate had baked stale
pb_migrationsinto 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 appcffd636cc772(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.tsschema-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):- Fresh store:
migrate()bootstraps all 14SCHEMA_PLANcollections + every field migration cleanly (schema field builders are 0.39-compatible). - Existing 0.25
pb_data→ 0.39: opens & serves with--automigrate=falseand no stalepb_migrationsdir. (Without this, automigrate writes a snapshot to./pb_migrationsrelative to CWD that conflicts with existing collections → "Collection name must be unique".) - JS SDK
pocketbase@^0.27.0:authWithPassword+ public reads OK; realtime SSE endpoint servesPB_CONNECT.
- Fresh store:
- Code changes on branch:
docker/DockerfilePOCKETBASE_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-breakingPB_ENDPOINTenv 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/updatedAtto API-created collections (0.25 did). The chat store filters/sorts on a custommessages.createdAt, so a fresh 0.39 store is missing it → raw PB 400. Fixed two ways:shared/pb/schema.ts:messagesnow declaresdate("createdAt")explicitly (source of truth →ensureSchema).proxy/src/migrate.ts: the "messages already exists" branch now addscreatedAtif 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, superuserdebug@famchamp.devre-verified,migrate()bootstraps schema + appliesmessages.createdAtfix cleanly. - Prod upgrade steps: backup
pb_data→ run the 0.39 image (automigrate off) → runmigrate()→ 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
3725c54viaARG 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/DockerfilePOCKETBASE_VERSIONback to0.25.8(matchesdocker/Dockerfile.dev). Devpb-devand the docker app internal PB:8091share host./pb_data. - Migration schema scripts (DO NOT FORGET): the schema single source of truth is
shared/pb/schema.ts(SCHEMA_PLAN), iterated byproxy/src/migrate.ts(ensureSchema) andproxy/scripts/seed.ts. The chatmessages/chat_typingcollections are defined there without an explicitcreatedAt— they rely on PB auto-adding it on first create.- The 0.39 prod
messagesdrift (missingcreatedAt, thenid"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. systemidautogeneratePattern) instead of patching collections by hand.
- The 0.39 prod
2026-08-15 — Members→users migration, OTP child login, cookie httpOnly, slugify
- Migration (members → users, run live + verified): deleted the
memberscollection and repointed the 5memberIdrelations (assigned_chores,completions,rewards,weekly_history,bonus_configs) →users. Addedusers.name+users.color, backfilled child name/color; backfilled parentrole(''→'parent'). Scopedusersrules: 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
usersrecords withrole='child', PBusername = {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 inuser_configs, thenauthWithPassword. Children now hold apb_tokencookie (previously adevice_tokencookie with no session).SessionUsergainedusername(set inhooks.server.ts, storeshandleOf(record.username)); the kanban child redirect comparessession.username, notsession.name. - Cookie httpOnly:
pb_tokenis nowhttpOnly:true. The browser PB SDK is seeded frompage.data.pbTokenviainitPb(token)(layout onMount) — not fromdocument.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 thepbTokenprop. - Typecheck baselines: frontend
svelte-check= 20 pre-existing canary errors (chatjson(status)ResponseInit,$typesAction/SubmitFunction, qrcode decl, vite.config, RewardType/Frequency casts, implicitly-any); proxytsc --noEmit= pre-existing record-typing + implicit-any errors. No new errors in edited files. crypto.randomUUID()unavailable over plain HTTP (non-secure context) → addedgenClientId()fallback (randomUUID if available, elseDate.now().toString(36)+random) inchat.svelte.ts.- Shared slugify util:
shared/slugify.ts(@shared/slugifyalias) — used by proxy signup + fam rename, frontend signup + settingsrenameFam, andmember-otp.createChild(child username + password gen). Removed the 3 local duplicateslugifyhelpers. - Settings UI: CardGrid/Card are now responsive (media queries +
--grid-cols/--card-cols; Cards usecontainer-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, settingscomplete-week/debug/generate-data), member actions viamemberApi.*(toggleCompletion/claimReward/payday) + direct/apifetches (chores assigned-chores, kanban/api/members/me). Not implemented:/api/weekly-cronCRON, Stripe (create-checkout+ webhook), WhatsApp — weekly settlement is manual viacomplete-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."}}— theusers.createin/signupomitted the authusernamefield (PB 0.39 requires it). Reproduced directly against PB: create WITHOUTusername→validation_required; WITHusername→ succeeds. - Fix:
signup/+page.server.tsnow includesusernameon the parentuserscreate. - Username convention (composite + handle): PB
users.usernameis 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}.namekeeps the raw display name, read from DB viaauthRefresh(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 inlogin/+page.server.ts,[fam]/[username]/+page.server.ts(parent + child branches) andpreferences/+page.server.tsusesession.username(the handle). Member-list URLs insettings,[fam]/+page.svelte,[fam]/[username]/+page.sveltebuild/{famSlug}/{handleOf(m.username)}.handle/handleOf/famUsernamelive inshared/slugify.ts(@shared/slugify). - Restored intended multi-step signup (from the guide, adapted to current OTP model):
/signupsteps — (1)?/signupfamilyName/yourName/email/password → create fam + parent (name=yourName, username=famUsername(famSlug, handle(yourName))) + settings, setpb_token; (2)?/childoptional child →issueAccessreturns{ code, joinUrl }; (3) show OTP join code + "Go to dashboard" link. Uses named actions +use:enhance(callback typedanyto avoid the pre-existing canary$typesSubmitFunction error). - Typecheck: frontend
svelte-checkstays at 20 pre-existing errors (no new in edited files).