From c38ca43f0b67636b87704acf7d97d8ebb0415330 Mon Sep 17 00:00:00 2001 From: JCEEE <0xjceee@proton.me> Date: Tue, 4 Aug 2026 11:38:38 +0100 Subject: [PATCH] fixed payout locks & time sensitive todos & minor ui updates on chores view --- AGENTS.md | 2 + MEMORY.md | 41 +++++ frontend/src/lib/types.ts | 2 + .../routes/[fam]/[username]/+page.server.ts | 6 +- .../src/routes/[fam]/[username]/+page.svelte | 146 ++++++++++++------ .../[fam]/[username]/bonuses/+page.svelte | 39 ----- .../[fam]/[username]/chores/+page.svelte | 110 +++++-------- proxy/scripts/seed.ts | 2 + proxy/src/index.ts | 59 ++++++- proxy/src/migrate.ts | 36 +++++ timezone.ts | 10 ++ 11 files changed, 289 insertions(+), 164 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9fb5228..9460d76 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -200,11 +200,13 @@ All admin and member pages use the following pattern: - **Member → Proxy (server)**: `memberApi.*` in `$lib/client/api.ts` — use inside `+page.server.ts` load/actions; `BASE_URL` resolves to Hono port on server - **Member → Proxy (browser)**: `memberApi.*` in `$lib/client/api.ts` — use inside `+page.svelte`; `BASE_URL` is empty, Vite proxies `/api/*` to Hono - **`$page`**: import `{ page }` from `$app/state` (NOT `$app/stores` — that's the old Svelte 4 API). Reference as `page.params.fam`, `page.url.pathname` etc. without `$` prefix +- **Dates**: all user-facing dates are DDMMYY (compact, e.g. `040826` for 4 Aug 2026). Use the shared `formatDDMMYY()` helper in `frontend/src/lib/format.ts`. Never render raw `YYYY-MM-DD` to users. Exception: single human-readable dates like todo **due dates** should use `formatShortDate()` (also in `format.ts`, renders `5 Aug` / `5 Aug 26`) — the compact DDMMYY code is ambiguous and bad UI for those. - `config.ts` at root for dev/build-time shared config (e.g. `PROXY_PORT`); runtime config via env vars - `.env` at root tracks port values (`PROXY_PORT`, `PORT`); `.env.example` committed as template - Docker: `docker/Dockerfile` (prod, multi-stage + nginx) + `docker/Dockerfile.dev` (PocketBase) - Nginx routes in prod: `/api/*` → Hono (`:3456`), `/*` → SvelteKit (`:2080`) - Ports: frontend `2080`, proxy `3456`, container ext `3001` (port `3000` is reserved) +- **Dev servers: NEVER start your own.** Always reuse the running dev servers — proxy `192.168.1.225:3456` (tsx watch, reloads on edit), frontend `localhost:2080` (vite HMR). Don't spawn `nohup pnpm dev` / `tsx watch` / extra vite instances. Only restart when the user explicitly asks. - Environment: `FRONTEND_PORT`, `PROXY_PORT`, `PB_PORT`, `PB_EMAIL`, `PB_PASSWORD`, `DEBUG_RECORD_ID`, `STRIPE_SECRET_KEY`, `DONATION_MODAL_INTERVAL` - Seed via JSON dump (portable for dev) - Monorepo: SvelteKit in `frontend/`, Hono in `proxy/`, two Dockerfiles diff --git a/MEMORY.md b/MEMORY.md index 5566757..6a1b5d3 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -110,3 +110,44 @@ - **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 ` 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. + + diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index d57c1ca..cd2987e 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -138,6 +138,8 @@ export interface Reward { value: number; rewardType: BonusRewardType; status: 'unclaimed' | 'requested' | 'claimed'; + claimable?: 'immediate' | 'payday'; + settleDate?: string; claimedAt?: string; requestedAt?: string; date: string; diff --git a/frontend/src/routes/[fam]/[username]/+page.server.ts b/frontend/src/routes/[fam]/[username]/+page.server.ts index 7da268c..4e5b87e 100644 --- a/frontend/src/routes/[fam]/[username]/+page.server.ts +++ b/frontend/src/routes/[fam]/[username]/+page.server.ts @@ -120,7 +120,8 @@ export async function load(event) { tallies: chores.tallies || {}, payday: chores.payday, paydayTime: chores.paydayTime || '18:00', - timezone: chores.timezone || 'auto' + timezone: chores.timezone || 'auto', + simulateEow: !!chores.simulateEow }; } catch { return { @@ -161,7 +162,8 @@ export const actions = { const famId = event.locals.session.famId; try { const preview = await hono.admin.eowPreview(event, famId); - return { preview }; + await hono.admin.updateSettings(event, famId, { simulateEow: true }); + return { preview, simulateEow: true }; } catch (e) { return { error: e instanceof Error ? e.message : 'Failed to preview payday' }; } diff --git a/frontend/src/routes/[fam]/[username]/+page.svelte b/frontend/src/routes/[fam]/[username]/+page.svelte index 9ae156b..ddfae4c 100644 --- a/frontend/src/routes/[fam]/[username]/+page.svelte +++ b/frontend/src/routes/[fam]/[username]/+page.svelte @@ -4,6 +4,7 @@ import { onMount } from 'svelte'; import { famStore } from '$lib/stores/fam.svelte'; import { memberApi } from '$lib/client/api'; + import { formatDDMMYY, formatShortDate } from '$lib/format'; import { ViewHeader, CardGrid, Card, Button } from '$lib/components'; import type { AssignedChore, Completion, ChoreTemplate, BonusConfig, Reward } from '$lib/types'; import { @@ -40,9 +41,8 @@ let summary = $state(data.summary); let today = $derived(todayInTz(famTz)); - let simulateEow = $state(!!data.settings?.simulateEow); + let simulateEow = $state(!!(data.settings?.simulateEow ?? data.simulateEow)); let eowPreview = $state(null); - const responseTags = [ '👏 well done', '😊 really pleased', @@ -352,9 +352,17 @@ .reduce((sum, r) => sum + Number(r.value), 0) ); // Money owed to this member across all time (incl. carry-over from past weeks). + // Payday-gated rewards are excluded until their settleDate — the wallet shows + // them separately as a locked "pays out on payday" line. let owedCash = $derived.by(() => rewards - .filter((r) => r.memberId === memberId && r.rewardType === 'cash' && r.status !== 'claimed') + .filter( + (r) => + r.memberId === memberId && + r.rewardType === 'cash' && + r.status !== 'claimed' && + !paydayLocked(r) + ) .reduce((sum, r) => sum + Number(r.value), 0) ); let weekPointsEarned = $derived.by(() => @@ -370,6 +378,10 @@ (r.status === 'unclaimed' || r.status === 'requested') ) ); + // Payday-gated bonus rewards unlock on their settleDate (stamped server-side). + function paydayLocked(r: any) { + return r.claimable === 'payday' && r.settleDate && todayChild < r.settleDate; + } let weekBonusTallies = $derived.by(() => { const map = new Map(); for (const r of weekRewards) { @@ -786,7 +798,12 @@ {#each outstanding as r}
{r.label} - {rewardLabel(r)} + + {rewardLabel(r)} + {#if paydayLocked(r)} + 🔒 {formatShortDate(r.settleDate)} + {/if} +
{/each} {/if} @@ -822,28 +839,10 @@ - +

- Read-only preview of what the payday rollover will produce. Nothing here is written. -

-
{ - return async ({ result }) => { - const d = (result as any).data || {}; - if (d.error) toast = d.error; - else simulateEow = !!d.simulateEow; - }; - }} - > - - -
-

- This toggles the family debug flag only. It does not alter any live data. + Read-only preview of what the payday rollover will settle for this week. Nothing here is + written.

{ const d = (result as any).data || {}; if (d.error) toast = d.error; - else eowPreview = d.preview; + else { + eowPreview = d.preview; + simulateEow = !!d.simulateEow; + } }; }} > - +
+ {#if simulateEow} +

+ 👀 Preview mode is ON — child dashboards show a "payday preview" notice. +

+
{ + return async ({ result }) => { + const d = (result as any).data || {}; + if (d.error) toast = d.error; + else simulateEow = !!d.simulateEow; + }; + }} + > + + +
+ {/if} + {#if eowPreview}
-

Week {eowPreview.weekStart} → {eowPreview.weekEnd}

+

+ Week {formatDDMMYY(eowPreview.weekStart)} → {formatDDMMYY(eowPreview.weekEnd)} +

- On rollover these {eowPreview.completionsThisWeek} completions reset; week starts anew at - {eowPreview.nextWeekStart}. + On payday these {eowPreview.completionsThisWeek} completions reset; week starts anew at + {formatDDMMYY(eowPreview.nextWeekStart)}.

@@ -904,6 +928,12 @@ {:else if error}

{error}

{:else} + {#if simulateEow} +
+ 👀 Payday preview — your parent is checking this week's payday. Nothing is paid + out yet. +
+ {/if} {#if owedCash > 0}
🎉 You've earned £{owedCash.toFixed(2)} — go get it from your parent! @@ -1240,6 +1270,10 @@ ✓ credited {:else if isReq} ⏳ waiting + {:else if paydayLocked(r)} + 🔒 pays out {formatShortDate(r.settleDate)} {:else}
- {#if doneConfigs.length > 0} -
-

Completed

-
- {#each doneConfigs as cfg} -
-
- {cfg.name} -
-
- {formatReward(cfg.rewardType, cfg.rewardValue)} -
-
- {/each} -
-
- {/if} - {#if showCreateModal}
(showCreateModal = false)} role="presentation">