59 KiB
59 KiB
FamDone v2 — Development Memory
2026-08-17 — Schema collapse to single source of truth (Option 1)
- Decision: abandoned incremental migration history.
SCHEMA_PLAN(shared/pb/schema.ts) is now the true, current schema;migrate.tscollapsed from 2096 → 183 lines to a fresh-only bootstrap (ensureSchema()skips iffamsexists — no incremental steps). The app isn't live and data is disposable, so there's nothing to preserve; a schema change = updateSCHEMA_PLAN+ wipe PB + reboot. - Folded the net effect of ~40 hand-written steps into
SCHEMA_PLAN(verified against the live PB):fams.{payday,lastIssued,paydayTime,timezone}(dropped staleseasons),settings.simulateEow,messages.clientId,assigned_chores.{isTodo,startDate,completeBy}.bonus_configs.memberId,weekly_history/rewards/assigned_chores/completions.memberId→users. - Out of
SCHEMA_PLAN(handled inmigrate.ts): the nativeusersauth collection (customfamId/role/username/colorfields +usernameunique index + password-auth identity + famId-scoped rules, viaensureUsers) and the superuser-onlyotpcollection (null rules —col()can't expressnull, viaensureOtp). - Gotcha:
col()coercesrules.listRule ?? ""→ can't emitnullrules, sootpstays out of the plan.ensureSchemaneeds the live PB to confirm the true schema (SCHEMA_PLAN was stale before this). - Verify path: wipe dev PB + restart frontend (needs user OK) so
migrateOnBootrebuilds fromSCHEMA_PLAN.
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 (FamDone)
- 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).
2026-08-21 — Stripe embedded checkout live + access-code gating (fams.paymentMode)
- Embedded Checkout fixes (
frontend/src/lib/server/stripe.ts):ui_mode: 'embedded'→'embedded_page'(Stripe deprecated'embedded'); removedcustomer_creation: 'always'— only valid inpaymentmode; subscription mode auto-creates the customer fromcustomer_email. Client side already usedcreateEmbeddedCheckoutPage({ clientSecret }). - Secure-context gotcha: embedded Checkout requires HTTPS or localhost. Dev host is reached over Tailscale IP via plain HTTP → checkout hangs silently (the
muid/guid/sidJSON fromm.stripe.comis Radar device fingerprinting, not an error; Stripe CLI websocket errors are benign). Fix: SSH port-forwardssh -L 2080:localhost:2080and usehttp://localhost:2080. Webhook listener is now a root script:pnpm stripe:listen(=stripe listen -e customer.subscription.updated,customer.subscription.deleted,checkout.session.completed --forward-to http://127.0.0.1:2080/account/webhook). - Gating model: the platform is gated.
fams.paymentMode=none | code | sub | canceled;fams.active(bool) is the derived "usable now" flag, re-persisted byensureFamAccess()when it drifts. New superuser-onlyaccesscodescollection (all rules null likeotp, so it lives inmigrate.tsnotSCHEMA_PLAN):value(unique, required),name,duration(months from entry date; 0=continuous),expiry(months after the code's owncreatedAt; 0=never),activefailsafe,createdAt. Idempotently seeded withdev123/ developer / 0 / 0. - Core module
frontend/src/lib/server/access.ts:addMonthsUTC,codeIsValid,computeFamAccess(mode →{disabled, reason}; reasonsno_access|code_expired|code_disabled|subscription_inactive|canceled),ensureFamAccess(famId)(reads fam+code, persists driftedactive, returns{fam, access}),applyAccessCode(famId, value)(sets mode=code + accessCodeId + accessCodeEnteredAt). Wired into[fam]/+layout.server.tsload →data.famAccess(both roles). - UI gating:
[fam]/+layout.svelteblurs.page-content.lockedbehind a non-blocking overlay card + admin TopNav announcement (sidebar/chat stay usable). Member kanban gate is frontend-only by decision:[fam]/[username]/+page.sveltederivesaccessDisabledfrompage.data.famAccess?.disabled, early-returns intoggle(), and renders three empty locked columns instead of the board. Signup takes an optional code (blank → gated fam; invalid → 400); settings has an Access card (?/applyCode). Webhooks maintainpaymentMode: checkout completed / subscription sync →sub; subscription deleted →canceled. - Bug found while verifying: the live
famscollection was missing theactivebool entirely (schema.ts declared it; this PB predated it) →activewrites were silently dropped. Fixed by adding it toensureFamFields()(idempotent; runs outsideensureSchema's early-return alongsideensureAccessCodes) and patching the live collection. Verified end-to-end against PB: fam with validdev123→active=true; fam with empty mode →active=false(gated). - PB curl gotcha: single-record endpoints are
/api/collections/{name}/records/{id}— omitting/records/returns PB's"File not found."404 which masquerades as a missing record. The JS SDK always builds the correct path (an earlier "fams by-id 404" scare was a bad curl URL, not an app bug).
2026-08-22 — Routes reshuffle: [famSlug]→[fam] merge, /pricing public, signup wizard with inline checkout
- Join route merged:
[famSlug]/join/{username}→[fam]/join/{username}(same URL shape, singlefamparam). Fixedparams.famSlug→params.famin join page files. - Public pricing page:
/subscriptions→/pricing(untracked dir renamed). NewPricingPlans.sveltecomponent (reusable tier cards; props:action,hideTrial,selected,error,onsubmithandler)./pricingis public: logged-out "Choose monthly" → redirect/signup?plan=monthly; logged-in → existing embedded checkout. - Signup wizard rewritten as state machine (
fam → child → code → plan → done):- Step 1 (fam): family + parent creation (access code field REMOVED from here)
- Step 2 (child): add child or skip (unchanged)
- Step 3 (code): "Have an access code?" Apply (→ done) or Skip (→ plan)
- Step 4 (plan):
PricingPlansembedded (hideTrial=true); selecting mounts embedded checkout INLINE (user authenticated);?plan=Xfrom /pricing pre-highlights tier - Step 5 (done): "Go to dashboard" — webhook flips
paymentMode=sub, overlay lifts - Server actions:
signup(no code),child(unchanged),access(reusesapplyAccessCode),choose(embedded checkout session)
- Webhook moved to
/api/webhooks/stripe(machine-to-machine endpoint belongs in/api/*namespace).pnpm stripe:listenforward URL updated. - Links updated: account "Change plan", settings "Plans",
stripe.tscancel_url →/pricing. - Docs updated: AGENTS.md routes, ARCHITECTURE.md (routes, Stripe flow, architecture diagram, project structure), MEMORY.md this entry.
2026-08-21 — Platform feature flags (platform collection) + debug-gated revoke CTA
fams.featureFlagsdeprecated (removed from SCHEMA_PLAN,Famtype, live PB; field dropped). Replaced by a globalplatformcollection: single recordlabel='global', jsonflags. Rules: list/view =""(public read — the one rule shapecol()CAN express), create/update/delete = null (superuser-only) → created inmigrate.ts(ensurePlatform+ idempotentseedPlatform, like otp/accesscodes).- Public load: new root
frontend/src/routes/+layout.server.tsexposespage.data.platformFlagson every page viagetPlatformFlags()(lib/server/platform.ts, 10s TTL cache;setPlatformFlagfor superuser writes). debugflag gates dev-only UI: settings "Revoke code" CTA (?/revokeCode) — clears an applied code (paymentMode→none, accessCodeId/EnteredAt→'', active=false, fam re-gates). Server action checks the flag itself (hidden CTA is not the boundary). Settings' old per-famfeatureFlags.debugModeDebug Tools card now keys offpage.data.platformFlags.debug./adminPlatform Flags card replaces the per-fam Debug column:?/togglePlatformFlagtoggles any flag on the global record. Dev PB seeded withdebug: true.- Also:
[fam]/+layout.svelteexempts/settingsfrom the paused blur overlay (admins can apply a code while gated) anddisabled/accessReasonare$derivedso applying/revoking updates the overlay without a refresh.
2026-08-22 — Settings reorg: Accordion groups, paymentMode-only billing, notices system
- Settings grouped into 4 Accordions (Family / App / Invites / Billing).
Accordion.svelterewritten as a styled snippet wrapper + new self-containedAccordionItem(own open state,$bindable,{@render children()}) — no items-array API. - Gating model simplified (user decision):
fams.paymentModealone drives the FE (none= gated/paused;codevalid = active;subfollows webhooks;canceled= gated). Nopausedfield added; the local pause toggle was removed entirely.fams.activeremains an internal derived flag maintained byensureFamAccess/webhooks only. - Access card: shows countdown from
accessCodeEnteredAt+ codedurationmonths (days when <1 month, "never expires" when duration=0) — settings load now fetches theaccesscodesrecord (data.accessCode). Once a code is applied the input/Apply are hidden and Revoke is always visible (debug-flag requirement dropped); revoke just setspaymentMode:'none'+ clears code fields (fam-scoped only — global code management is a platform-admin concern). - Billing card replaces
/account(route deleted): sub → Change plan (/pricing) + Open billing portal (Stripe Customer Portal; dummy mode opens returned URL); code → Switch to subscription; none/canceled → Choose plan. Portal + checkout both return to/{fam}?checkout=return. - Checkout-return welcome notice:
[fam]/+page.svelte$effectwatches?checkout=return→ fires a success notice via the new notices store (lib/stores/notices.ts: typed add/success/info/warning/error + auto-dismiss helper) rendered by global<NoticeDialog />in the root layout; query param scrubbed viahistory.replaceStateso refresh doesn't re-fire. - Gotchas fixed along the way: duplicate NoticeDialog export; Svelte 5 forbids
class:directives on components unless declared (Card gotselectedprop instead); second<script>block in a component is invalid.
2026-08-22 — famSlug single source of truth + notices store to runes
- famSlug convention: the URL param surfaced by
[fam]/+layout.server.tsas top-leveldata.famSlugis canonical. Client code readspage.data.famSlug(fallback?? page.params.famacceptable); server loads/actions under[fam]readevent.params.fam. Never copy it into local$state— settings had a frozen-snapshot bug doing exactly that (now$derived(page.data.famSlug ...)). Nestedsession.famSlugremoved (no consumers). Only exception with no URL param: signupchildaction resolves via DB (fam?.slug || famId) with a comment. - Sweep results: removed dead/mislabeled
const famId = $derived(page.params.fam)in bonuses page; zeroparams.famSlugreferences remain post[famSlug]→[fam]merge. - Notices store converted to Svelte 5 runes:
lib/stores/notices.svelte.ts(class with$state<Notice[]>list, add/remove/clear/success/info/warning/error +addAutoDismissNotice). Consumers:notices.listinNoticeDialog.svelte; no more svelte-storewritable/$noticesauto-subscription.
2026-08-22 — famSlug single source of truth + docs Hono purge
- famSlug convention:
[fam]/+layout.server.tsreturns top-leveldata.famSlug(from the URL param) — canonical. Client:page.data.famSlug; server under[fam]:event.params.fam. Never copy into local$state(settings had that frozen-snapshot bug). Nestedsession.famSlugremoved; signupchildaction is the only DB-fallback case. Fixed platform-admin links pointing at nonexistent/[slug]/admin. - Docs: purged all stale Hono-proxy references from AGENTS.md + ARCHITECTURE.md (proxy deleted 2026-08-17); data-flow sections now describe SvelteKit services/
/api/*routes. Remaining "Hono" mentions are struck-through historical build phases.
2026-08-22 — Trial codes in PB, pause=cancel decision, settings reorder
- "Pause" = cancel (decision): no separate pause concept. Pausing a plan means cancelling the card subscription via the billing portal; data is kept, resubscribing restores access (
paymentModewebhook-driven). Copy lives in settings Account card. - Trial codes now PB-backed:
accesscodes.trialDays(number).resolveTrialDays()(stripe.ts) is async — queriesaccesscodesfor an active record withvaluematch andtrialDays > 0; staticTRIAL_CODESmap deleted. Seeded:FAM3MONTHS= 90 days.ensureAccessCodeFields()hardens existing installs; seeds unified inseedAccessCodes(). - Settings accordion order: Family (name/payday/seasons, opens by default via
<AccordionItem open>) → Invites → Account (Access + Subscription) → App last. clearLegacyCookies(cookies)added to$lib/server/session.ts; auth/join/logout use it instead of inlinedevice_tokendeletes.
2026-08-31 — All-time earnings + weekly trend chart (family root), lifetime wallet (member)
- New server stats service
frontend/src/lib/server/services/stats.ts(familyStats(pb, famId), exposed ass.stats.family(famId)): scans full-historycompletions+rewardsonce, filtered/summed on the server, returns fixed-size{ allTimePts, allTimeCash, allTimeChores, series }(per-member weekly buckets{weekStart, points, cash, chores}). Server-side aggregation keeps the client payload constant-size regardless of history depth — this is the chosen scaling win for all-time totals + the retrospective graph. - Totals definition (matches the app's own accounting, no double count): lifetime points = points-type completions' chore values + claimed points rewards; lifetime cash = money-type completions' chore values + claimed cash rewards; chores = count of all completions. Unclaimed/requested rewards stay out of totals (they're the "to chase" list).
- Reading
weekly_historyNOT needed — totals derive from livecompletions+rewards(both havelistRule: ""in SCHEMA_PLAN = open to any authenticated session, which is why parent/child token reads already work today). - Member wallet (
[fam]/[username]/+page.svelte): the existingallTimeCash/allTimePointsderiveds now include chore completion earnings (previously claimed-rewards only). Wallet UI restyled: big lifetime cells (cash "earned all time" + points "accrued") on top, then a "To collect from parent" divider + the existing pendingRewards list (unclaimed/requested/payday-locked) so members still see what to chase. - Family root (
[fam]/+page.svelte++page.server.ts): load now fetchesfamilyStats(+ drops the removed templates). ViewHeader gained an optional default slot (children?: Snippet); the fam hero renders large all-time points | cash inside the header. Chores card shows "this week | total all-time" withjustify-content: space-between. Templates card removed. Chart.js line graph "Over time" plots per-member lines with a Points/Cash/Chores tab; window starts at the current month and expands a month at a time up to 3 months based on data span (≤31d→1, ≤62d→2, else 3). - Typecheck/build: still 6 pre-existing canary errors only (none in edited files);
pnpm build(adapter-node) clean. New code is picked up by HMR; no schema/migration change so no restart required.
2026-08-31 — Bonus progress window: completeBy + startDate implemented
- Problem: a standalone cash bonus (core reward with a points threshold) displayed progress differently on the child dashboard (current week → "0/500") vs the admin dashboard (cumulative since records began). Both were "correct" because
bonus_configshad no way to scope tracking — with noperiodset, progress()/evaluateFam totalled all completions, and the child dashboard forcedcfg.period || 'weekly'. - Fix: added to
bonus_configs(schema +ensureBonusFieldsidempotent field-add in migrate.ts, so existing installs upgrade without wiping):completeBy(selectunlimited|week|custom),startDate(text),completeByDate(text). New shared helpers inshared/timezone.ts:bonusWindow(cfg, payday, tz)+completionInWindow(c, {from,to})('' = unbounded side). - Window semantics (unified across server
progress()/evaluateFam()and the childthresholdGoals): recurring configs (period set) keep their period window; standalone configs usecompleteBy:unlimited(default) →[startDate, ∞]cumulativeweek→ current week[weekStart, weekEnd]custom→[startDate, completeByDate]- No
startDate/completeBy(legacy) → unbounded both sides (== the pre-fix admin behaviour, so existing cash bonuses stay correct).
- UI: rewards modal gained a "Progress counts" group (shown when no period / not manual):
completeByradios (Unlimited | Until a date) + optionalcompleteByDatedate input + "Progress starts on"startDatedate input. Creating from a template'sstartMode(Today/Next week) now setsstartDate(Today = fam-tz today; Next week = +7 days).openEditConfig/openCreateFromTemplatedefaultstartDateto today, so saving an existing cash bonus re-scopes it to count from today (progress since the reward begins), not since records started. - Server actions (
rewards/+page.server.ts):createConfig/updateConfig/createFromTemplatenow persistcompleteBy/startDate/completeByDate. - Typecheck/build:
svelte-checkstill at the 6 pre-existing canary errors (chores RewardType/Frequency, qrcode decl, settings unknown→string) — none in edited files;pnpm build(adapter-node) clean. - Ops note: the schema fields are added to live PB by
ensureBonusFields()on nextmigrateOnBoot(dev server restart). A restart of the dev frontend is needed to apply the migration + load the new code.
2026-08-22 — Graceful post-checkout activation (webhook-lag UX)
[fam]/+layout.svelteowns the?checkout=returnflow (moved out of the fam page). On landing: if unlocked → welcome notice. If still gated (webhook lag) →activatingstate: paused overlay swaps to a spinner card ("Activating your subscription…"), TopNav paused announcement suppressed, andinvalidateAll()revalidates every 1.5s (max 12 tries). The$effectwatchingactivating && !disabledcancels polling and fires "Subscription active!" the instant the gate lifts; exhaustion degrades to a refresh-hint warning.- Mechanics:
pollTokenguards against stale loops;history.replaceStatescrubs the query cosmetically +returnHandledflag prevents double-handling. Webhook remains the sole source of truth forpaymentMode/active. - Upgrade (same day): activation is event-driven, not polled.
startActivatingsubscribes the browser PB client to its ownfamsrecord (pb.collection('fams').subscribe(famId)— allowed by viewRuleid = @request.auth.famId). Webhook (superuser) writes → PB SSE push → singleinvalidateAll(); unlock$effectstops the subscription + fires success. 20s timer kept purely as a degrade-gracefully fallback. Rejected: onComplete-as-source-of-truth (untrusted); optional future hardening = server-side session verification on return.
2026-08-22 — Platform admin: access-code management + richer stats
/adminextended (same route/embedded login — env-var check +platform_sessioncookie, no PB auth or hooks involved):- Access Codes card: issue codes via
?/createCode— blank value auto-generatesXXXX-XXXX(unambiguous charset); fields name/duration/expiry/trialDays (trialDays > 0= Stripe trial code). Table shows type badge, active/disabled, used-by count (from fams.accessCodeId map), copy-to-clipboard.?/toggleCodeflips active;?/deleteCodeblocked while in use (must disable). - Overview: added chores completed (completions length), active subs (
paymentMode='sub'), paused/gated (!active || none/canceled), active codes. Families table gained plan+paused badges. - All actions guarded by
requirePlatform(cookies); data still viapbAdminsuperuser facade.
- Access Codes card: issue codes via
- Note:
svelte-kit syncneeded after changing load return shapes or$typesstaleness doubles the error count. - /admin login pattern: action sets
platform_sessioncookie + returns{success:true}; the form'suse:enhancecallback flips a localauthedview state — no redirect, no reliance on inline invalidation or fetch-time Set-Cookie behavior (which proved flaky in-browser despite curl proving both response paths carried it). Cookie still covers subsequent loads; load errors surface asdata.loadErroron the login card instead of silently masquerading as logged-out. - Platform-admin auth via hooks:
hooks.server.tsresolveslocals.platformAdminfrom theplatform_sessioncookie (=== 'authenticated') on every request;/adminload/actions consumeevent.locals.platformAdmin(requirePlatform(event)) instead of raw cookie reads. Same central pattern aspb_token→locals.user. isDummyStriperemoved: real test keys made every dummy branch dead code — and the webhook's|| !signaturefallback accepted UNSIGNED events (forgeable access grants). Signature is now mandatory (400 without it); settings' simulated endSubscription/portal branches deleted. Dev verification stays via stripe-cli signed events (pnpm stripe:listen).- Platform-admin auth hardened (supersedes the constant-cookie version):
/adminlogin now does a real_superusers.authWithPasswordvia PB; the minted superuser JWT goes inplatform_session(setPlatformSessionin session.ts).hooks.server.tsdeviates on/admin: verifies the token with_superusers.authRefresh→locals.platformAdmin(forged values fail authRefresh and get cleared); fam-user pb_token flow skipped on that route. FINAL login shape (user-amended, working): action returns{success:true}(no redirect); form's enhance callback doesgoto('/admin', { invalidateAll: true })on success — forcing the load re-run with the fresh cookie; view branches ondata.authenticated. Verified: real token renders dashboard, forged cookie gets login. - use:enhance redirect gotcha (session-wide lesson): action-thrown redirects surface as
result.type === 'redirect'in the RESOLVE callback — handlers checking only'success'silently drop them (bit /admin login, pricing choose, and settings billingPortal). Shared fix:lib/forms.ts→handleResult(handler?)factory follows redirects viagoto, delegates the rest to the handler or defaultupdate(). Use it for any form whose action can throw a redirect. Also: enhance is two-stage —{result}only exists in the resolve fn, not the submit params (was mis-handled in pricing/signup handlers).
2026-09-02 — Count-type reward: Target Chore + TODO (period semantics)
- Feature: Count-type rewards now support pinning to a single assigned chore (
bonus_configs.targetChoreId). On the fam-admin Rewards modal (Step 2), when Type = "Count - (chores)" a Target Chore dropdown appears after the Target Value field — options are the selected member's assigned (non-todo) chores, default "All Chores". Evaluation (bonuses.evaluateFam) + display (bonuses.progress, dashboardthresholdGoals) now filter Count progress to only that chore's completions when set. Platform-level template form only needed the relabeled "Count - (chores)" — no target chore at template level. Pocket-money checkbox removed from the platform template form (only the auto-created per-child droplet needs it). - TODO (logic, not yet fixed): Count rewards with a
period(e.g. weekly) reset + re-earn each period like a points/cash threshold. Once a Count reward is pinned to a target (or all chores), the intended semantics are ambiguous: should it be continuous (unlimited, measured once since startDate until reached) or limited to the period window (re-earn each week)? Currently it follows the periodic-reset behavior. Decide whether Count rewards should ignoreperiod(behave as standalone/unlimited since startDate) and adjustbonusWindow/evaluation accordingly. Not attempted in this change.