Files
famdone/MEMORY.md
T
2026-08-07 11:37:46 +01:00

194 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
### 2026-07-28 — Auth Bug: Join Flow Set Session Cookie for Children
- **Bug**: Both `join/[code]/+page.server.ts` and `join/[code]/[member]/+page.server.ts` called `setSessionCookie()` with `userId: memberId` (the child's PB record ID). This made `event.locals.session` truthy for children, causing `[username]/+page.server.ts` to enter the admin branch and call `hono.admin.*` endpoints. The proxy's `requireAdmin` checked `fam_admins` for the child's member ID (which doesn't exist) and returned 401.
- **Fix**: Removed `setSessionCookie()` from both join pages. Children only get a `device_token` cookie. The session cookie is only for email/password-authenticated parents, set by `/login` and `/signup`.
- **Lesson**: Children must never get a session cookie. The auth table in AGENTS.md says "Member → device token, no expiry" — the code must match.
### 2026-08-03 — Family Timezone Setting + Tz-Aware Week Math
- **Feature**: `fams.timezone` (IANA name or `"auto"`) added via `migrate.ts` 5d/5e (field + backfill `"auto"`). Exposed in settings Payday card (dropdown from `COMMON_TIMEZONES`, ~40 entries, + "Auto (detected)"). Set via `PATCH /api/admin/:famId/fam` alongside payday/paydayTime.
- **Shared module** `timezone.ts` (root, imported by both proxy and frontend): `resolveTz`, `dateStrInTz`, `weekdayInTz`, `todayInTz`, `addDaysStr` (pure UTC), `weekStart(payday, tz)`, `wallClockToUtc` (iterative 4-pass Intl, DST-safe), `COMMON_TIMEZONES`.
- **Bug fixed** ("5 days left on Monday"): old `mondayOf`/`addDays`/`daysLeft` used local `setDate` + `toISOString()`. On BST Sunday Aug 9 local midnight → UTC Aug 8, so Monday showed 5 days left instead of 6. Now the child page computes `weekStart`/`todayIso`/`daysLeft` via `weekStart(1, famTz)` + `addDaysStr` + `todayInTz`, giving 6. Verified: Mon Aug 3 → weekStart 2026-08-03, weekEnd 2026-08-09, daysLeft 6.
- **releaseWeek time gate**: now tz-aware. `weekStart(payday, tz)` for idempotency check; `target = new Date(wallClockToUtc(weekStartToday, paydayTime, tz))`. Verified: payday Mon 20:00 Europe/London (BST) → target `2026-08-03T19:00:00Z`; `auto` resolves to server tz. Returns `{settled:false, notYet:true, weekStart, target}` before the time.
- **Proxy threads `tz`** through weekly-summary, eow-preview, bonus-configs/progress, `evaluateFam`, tallies, manual trigger, complete-week, `releaseWeek`. `my-chores` returns `timezone`; child `+page.server.ts` passes it to `data.timezone`; parent passes `data.fam.timezone`.
- **Frontend child page**: `rawFamTz = data.timezone || data.fam?.timezone || 'auto'`, `famTz = resolveTz(rawFamTz)`. `todayChild`, `todayIso`, `mondayOf`, `addDays`, `isPaydayToday` (via `weekdayInTz`), `paydayTarget` (via `wallClockToUtc`), `today` all tz-aware. `mondayOf` now delegates to `tzWeekStart(1, famTz)`.
- **Smoke-tested live** (fam `v56f0f8o147kj1x`): GET fam returns timezone, PATCH sets Europe/London, time-gate returns correct target, `auto` resolves to server tz, settings page SSR shows the dropdown, child page 200. Fam restored to payday=0, paydayTime=18:00, timezone=auto, lastIssued=2026-08-02.
- **Typecheck**: proxy `tsc --noEmit` 28 errors (all pre-existing rootDir/`.ts`-import/`key: never`/implicit-any baseline); frontend `svelte-check` 9 errors (baseline; no new errors in edited files).
### 2026-08-04 — Terminology: "Payday" + Countdown to Settlement Day
- **Decision**: Standardize the user-facing term on **"payday"** for the weekly settlement event/day. "EOW" is ambiguous (window-close vs settlement day) and is now dropped from user-facing strings. Keep "week" for the Sun→Sat earning window. Internal identifiers (`eow*`, `simulateEow`, `eowPreview`, CSS `eow-*`) left as-is.
- **daysLeft now counts to settlement day**: `daysLeft` on the child dashboard counts to `weekStart + 7` (the next payday day) instead of `weekEnd = weekStart + 6`. So Tue Aug 4 with payday=Sun shows 5 days until payday. Hero label updated to "days until payday".
- **User-facing renames**: hero `days left``days until payday`; debug card `Simulate End-of-Week``Simulate Payday`; error `Failed to preview EOW``Failed to preview payday`; settings hint reworded to lead with "Payday:".
### 2026-08-04 — DDMMYY Date Rule + Debug Payday Preview Fix
- **Rule added (AGENTS.md)**: all user-facing dates are DDMMYY (compact, e.g. `040826` for 4 Aug 2026). Shared helper `formatDDMMYY()` in `frontend/src/lib/format.ts` (extracted from the local copy in `chores/+page.svelte`). Never render raw `YYYY-MM-DD` to users.
- **Bug**: the admin "Preview payday" card showed all-time totals (e.g. "280 pts £288.00 14 chores" for zooney) because the proxy's `eow-preview` reward queries (`rewardPointsList`/`rewardCashList`) had NO date filter, summing every claimed reward ever. `complete-week` (the real settlement snapshot) scopes with `date >= ws`.
- **Fix**: added `&& date >= '${ws}'` to both reward queries in `eow-preview` (`proxy/src/index.ts`) so the preview matches what the rollover actually records. Verified live: zooney now shows this-week `220 pts / £16.00 / 14 chores / bonus 10`.
- **UI**: removed the no-op **Simulation ON/OFF** toggle (settings.simulateEow flag drives no behavior) — it was the source of "simulation on/off vs preview rollover" confusion. Card is now just "Debug: Preview payday" → "Preview payday" button. Debug card dates now render via `formatDDMMYY`.
- **Typecheck**: frontend `svelte-check` still at 12 baseline errors (no new); proxy `tsc` unchanged pre-existing baseline.
### 2026-08-04 — Preview Payday Extends to Child Dashboard
- **Feature**: "Preview payday" now enables a family-wide preview mode that the child dashboard reacts to. `?/previewEow` action calls `eowPreview` then `hono.admin.updateSettings({ simulateEow: true })`, returning `{ preview, simulateEow: true }`. A "Turn off preview" button (`?/setEow`, `on=false`) clears it.
- **Child notice**: `my-chores` (`proxy/src/index.ts`) now returns `simulateEow: !!settings.simulateEow`; child `+page.server.ts` passes it as `data.simulateEow`. Child kanban renders a `.preview-notice` banner ("Payday preview — your parent is checking this week's payday. Nothing is paid out yet.") when the flag is set. Admin card shows a `.eow-mode-on` note + "Turn off preview" when active.
- **Note**: `simulateEow` (settings.simulateEow) was previously a no-op debug flag; it now meaningfully drives preview mode across admin + child views.
- **Verified live**: POST `?/previewEow` sets `settings.simulateEow=true` (GET settings confirms); child SSR page data carries `simulateEow:true`; control case (flag off) renders no notice. Test data restored afterwards (zooney deviceToken + flag reset to false).
### 2026-08-04 — Payday-Gated Bonus Payouts
- **Feature**: weekly/monthly period bonus rewards are now **claimable only on payday** (not the moment they're met). Rewards gain `claimable: 'immediate' | 'payday'` + `settleDate` (YYYY-MM-DD, server-side). The bonus-met notice stays exciting on the child dash — the reward line shows a locked "🔒 pays out {DDMMYY}" badge and skips the request button pre-payday.
- **Stamp logic**: `claimableStamp()` in `proxy/src/index.ts` — periods `weekly`/`monthly``{ claimable: 'payday', settleDate: nextPaydayAfter(periodEnd) }`; else `immediate`. Manual bonus triggers (`/bonus-configs/:id/trigger`) stamp `immediate` (parent-initiated, not a scheduled payout). All 3 `evaluateFam` create sites (individual/collaborative/competitive) use `claimableStamp`. `nextPaydayAfter()` helper added to `timezone.ts`.
- **Enforcement**: member `claim` endpoint checks `assertPaydayUnlocked()` (throws `"This bonus pays out on payday (…settleDate) — hang tight!"`, returned as HTTP 400); `request-all` skips payday-gated rewards not yet settled. Admin `Issue`/`Issue All` are parent discretion and unaffected.
- **UI**: child wallet renders locked badge for pre-settle payday rewards; admin "Claims → Outstanding" shows a `🔒 {DDMMYY}` hint. Both reuse `formatDDMMYY()`. (Later: switched both to `formatShortDate()` → "🔒 pays out 9 Aug"; child `owedCash` banner excludes payday-locked rewards so "You've earned £X — go get it!" no longer shows for rewards that aren't claimable yet.)
- **Schema**: `rewards.claimable` (select, required) + `rewards.settleDate` (text) added in `proxy/src/migrate.ts` + `proxy/scripts/seed.ts`. PB's `required` select rejects empty on write; legacy null-claimable rewards are treated as `immediate` by both proxy and frontend, so no data backfill was needed.
- **Verified live**: `complete-week``evaluateFam` recreated the weekly Pocket Money reward with `claimable=payday, settleDate=2026-08-09` (Sunday payday after Sun→Sat week); member claim pre-payday → 400 with friendly message + status stays `unclaimed`; `request-all` returns `{count:0}`; claim succeeds after settleDate.
- **Ops note**: the dev proxy's `tsx watch` had silently frozen (file edits at 09:49 weren't picked up by a child started 09:47). Fixed by killing the watcher tree with explicit PIDs and relaunching `pnpm dev` (nohup → `/tmp/proxy_dev.log`). `pkill -f "tsx watch src/index.ts"` hangs the shell — use `kill <pid>` instead.
### 2026-08-04 — Dev Servers: Always Reuse Existing 2080/3456
- **Rule**: NEVER start our own dev servers. Always use the already-running ones: proxy `192.168.1.225:3456` (tsx watch, reloads on edit) and frontend `localhost:2080` (vite HMR). Don't spawn `nohup pnpm dev`, `tsx watch`, or extra vite instances — it wastes time/tokens. Only kill/restart when the user explicitly asks (or a watcher is demonstrably stale, and then only after asking). Prefer short targeted curls and reuse one auth `TOKEN` across commands in the persistent shell.
### 2026-08-04 — Chores Page Accordion Quick Fixes
- **Add actions moved into sections**: removed the blue round `+` from the member swimlane header and the Templates column header. Both replaced by a shared full-width dashed ` Add a todo` / ` New template` button (`.add-inline`) at the top of the Todos accordion content and the Templates list respectively.
- **Accordions default open**: `accordionState` lookup defaults to `{ chores: true, todos: true }` (`?? true` in the template + toggle), so both sections load expanded on page load; still toggleable. Redundant empty-state "+ Add a todo" button and `.add-todo-btn`/`.empty-cta` CSS removed.
- **Check**: frontend `svelte-check` stays at 12 baseline errors.
### 2026-08-04 — Human Dates for Todo "Due" (Not DDMMYY)
- **Problem**: the chores todo card rendered `due 050826` (DDMMYY code) — ambiguous/terrible for a due date.
- **Fix**: added `formatShortDate()` to `frontend/src/lib/format.ts` — renders `5 Aug` (adds ` 26` when the year isn't the current one). Chores todo card now shows `due 5 Aug`. AGENTS.md date rule updated: DDMMYY for dense/range contexts, `formatShortDate()` for single human-readable dates like due dates.
### 2026-08-04 — Child-Dashboard Design Lead Applied to All Pages
- **Design principal** (from `[fam]/[username]` child dash): gradient hero lead (`linear-gradient(135deg, #6366f1, #8b5cf6 55%, #a855f7)`, radius 16px, glow shadow, white text), gradient stat tiles, rounded white cards.
- **`ViewHeader` hero variant**: added `hero` prop to `frontend/src/lib/components/ViewHeader.svelte` — renders the title/subtitle/tools on the gradient hero (tabs/nav/sort get tinted-on-white styling). Off by default, so no behavior change elsewhere.
- **Applied `hero` to**: admin dashboard (fam name), chores, bonuses, rewards, settings, preferences, platform admin `/admin`.
- **Admin dashboard stat tiles**: added 4 gradient tiles (members / points / cash / chores done) reusing the child `.tiles`/`.tile` pattern + new `.tile-members`/`.tile-chores` colors, from a new `adminTiles` derived summing `summary.summaries`. Also fixed admin subtitle `Week of {YYYY-MM-DD}``Week of {DDMMYY}`.
- **Check**: frontend `svelte-check` stays at 12 baseline errors. Note: frontend dev server on :2080 was not running when verified (proxy :3456 up).
### 2026-08-06 — Env Consolidation: `SERVER_IP`, `PROXY_URL`, and SvelteKit env only
- **`config.ts` is proxy-only.** It now holds just the three ports (`FRONTEND_PORT`/`PROXY_PORT`/`PB_PORT` = `2080`/`3456`/`8090`). SvelteKit **never imports `config.ts`** — SvelteKit env vars are declared in `frontend/src/env.ts` and read via `$app/env/*`. Deleted the stale `config.js/.d.ts/.map` artifacts.
- **`frontend/src/env.ts`** declares: `PROXY_URL` (public, default `http://127.0.0.1:3456`), `SERVER_IP` (public, default `192.168.1.225`), `PB_EMAIL`/`PB_PASSWORD` (private, defaults). `PUBLIC_PB_URL` removed (was the source of a startup crash when unset).
- **Deleted `frontend/src/lib/server/env.ts`** (untracked). All server modules now `import { PROXY_URL } from '$app/env/public'` (`hono.ts`, `auth.ts`, `+layout.server.ts`, `+page.server.ts`, `preferences`, `join/[code]/[member]`). `admin/+page.server.ts` imports creds from `$app/env/private`.
- **`frontend/src/lib/pocketbase.ts` (browser) + `pb-admin.ts`**: `PB_ENDPOINT = import.meta.env.PROD ? '/pb' : \`http://${SERVER_IP}:8090\``. (Fixed a bug where `pocketbase.ts` used `import.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`/`seed` scripts via `tsx --env-file-if-exists=../.env` (pnpm has no `--env-file`; `NODE_OPTIONS='--env-file=…'` is rejected by Node). No `loadEnvFile` hack in code.
- **Docker**: removed dead `ENV PB_ENDPOINT` from `Dockerfile`; `EXPOSE 3001` (was `3005 8090`); compose public port is `${PORT:-3001}:3001`, creds default to the code fallback, redundant `FRONTEND_PORT`/`PROXY_PORT` passthrough dropped; `entrypoint.sh` simplified (`PB_DATA=/app/pb_data`, `PORT=$FRONTEND_PORT`, no `:-` fallbacks).
- **Frontend deps added** (were missing imports): `chart.js`, `qrcode`, `@hiseb/confetti`.
- **Build checks**: proxy + frontend `pnpm build` clean.
### 2026-08-06 — Dev PB data incident (pb-dev) — see RULES.md "Dev vs Prod PocketBase data"
- **Symptom**: `pnpm dev` proxy migrate failed; PB superuser auth returned `HTTP 500 "Something went wrong"`; could not log into the PB admin UI.
- **Root cause**: the permanent dev PB container **`pb-dev`** (publishes `:8090`, data in host `./pb_data`) had a **broken bind mount** — it was serving an empty throwaway store, so the `debug@famchamp.dev` superuser didn't exist. The real `data.db` was on the host but the container wasn't seeing it.
- **Fix**: recreated `pb-dev` with the mount correctly attached (`-v "$PWD/pb_data:/pb_data"`, `pocketbase serve --http=0.0.0.0:8090 --dir=/pb_data`). Superuser auth then returned 200 on both `127.0.0.1:8090` and the Tailscale `SERVER_IP:8090`.
- **Watch-out**: a careless `docker run` with a **fresh volume** (my first attempt, aborted in time) would have wiped the permanent PB data. Restore command is in RULES.md. Two containers (`pb-dev` :8090 and the docker app's internal PB :8091) currently **share the same host `./pb_data`** — be careful with both.
### 2026-08-06 — Added root `shared/` for cross-package code
- Created `shared/timezone.ts` (moved from root `timezone.ts`). Imported by `frontend/src/routes/[fam]/[username]/+page.svelte`, `.../settings/+page.svelte`, and `proxy/src/index.ts`. Deleted the root `timezone.ts`.
- Created `shared/pb/schema.ts` — single source of truth for the PocketBase schema + field builders (`SCHEMA_PLAN` ordered collection plan + `text/select/rel/...` helpers). Both `proxy/src/migrate.ts` (`ensureSchema`) and `proxy/scripts/seed.ts` now iterate `SCHEMA_PLAN`; kills the previous duplicated schema/field-helper definitions in both files.
- Reason: `timezone.ts` and the PB schema are consumed by more than one package; `shared/` is the root location both can reach. Rule added to RULES.md: shared code lives in `shared/`, never inside `frontend/` or `proxy/`.
- Note: proxy `tsc --noEmit` already errors on `.ts`-extension imports (`allowImportingTsExtensions` unset) — pre-existing, not from this change. Runtime uses esbuild (build) + tsx (dev), both of which bundle the `shared/` imports correctly. Verified `pnpm build` clean for both packages.
### 2026-08-07 — `@shared/*` import alias (path alias, not a pnpm package)
- Moved `config.ts``shared/config.ts`. All `shared/` code is now imported as `@shared/*` instead of relative `../../shared/...`.
- This is a **path alias**, not a pnpm workspace package (`@shared` alone isn't a valid npm package name; a real package would need `@scope/name`).
- Proxy: `tsconfig.json` sets `paths: { "@shared/*": ["../shared/*"] }`; esbuild build adds `--alias:@shared=../shared`; tsx resolves via tsconfig paths. Proxy keeps `.ts` extensions (`@shared/config.ts`).
- Frontend: uses `kit.alias` in `vite.config.ts` (NOT `paths` in `frontend/tsconfig.json`, which SvelteKit warns against). Frontend imports shared files **without** the `.ts` extension (`@shared/timezone`) because `rewriteRelativeImportExtensions` only rewrites relative paths.
- Verified: proxy build + frontend build + `svelte-check` all clean for `@shared/*` (svelte-check still reports pre-existing `qrcode` types + CSS warnings).
- Docker note: runtime image only copies `frontend/build` + `proxy/dist` (both already bundle `shared/`), so `shared/` needn't be copied into the image.