154 lines
15 KiB
Markdown
154 lines
15 KiB
Markdown
# 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}/rewards/+page.svelte ← parent only
|
||
├── [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}/rewards Parent: reward 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.
|
||
|
||
|