Files
2026-08-07 11:37:46 +01:00

22 KiB
Raw Permalink Blame History

FamChore v2 — Development Memory

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 unchanged (still checks fam_admins)
  • 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 leftdays until payday; debug card Simulate End-of-WeekSimulate Payday; error Failed to preview EOWFailed 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-weekevaluateFam 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.tsshared/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.