From 7bddc50a53c6ae3c4ad08d7846a66e0bb866c31c Mon Sep 17 00:00:00 2001 From: Jacob Hagstedt Date: Wed, 21 Oct 2026 20:47:42 +0200 Subject: [PATCH] SPEC v0.28: web client programme (D-129 to D-134), plans W0 to W7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New SPEC §15 Web client and FR-21; §1, §2, G-5, AC-14.1 revised; §8.5 and §10.5.2 gate lines for /app/, the invite accept and bearer requests (except /health, /setup/, /i/, /apk/) plus the /app/ CSP via $kvitt_csp, identical in infra/npm/kvitt.conf and infra/README.md step 4 (checked with nginx 1.31.7); §10.3, §10.6, §11.1 to §11.3 (rows W0 to W7), §14.7 ownership; change log is now §16. PLAN-W0..W7 and prompts/W0..W7; HANDOFF numbering D-139 / v0.30. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B --- PLAN-W0.md | 72 +++++++++++++++ PLAN-W1.md | 54 ++++++++++++ PLAN-W2.md | 57 ++++++++++++ PLAN-W3.md | 49 ++++++++++ PLAN-W4.md | 54 ++++++++++++ PLAN-W5.md | 55 ++++++++++++ PLAN-W6.md | 47 ++++++++++ PLAN-W7.md | 50 +++++++++++ SPEC.md | 206 +++++++++++++++++++++++++++++++++++++++---- docs/HANDOFF.md | 19 ++-- docs/decisions.md | 6 ++ infra/README.md | 52 ++++++++--- infra/npm/kvitt.conf | 17 +++- prompts/W0.md | 24 +++++ prompts/W1.md | 24 +++++ prompts/W2.md | 24 +++++ prompts/W3.md | 24 +++++ prompts/W4.md | 24 +++++ prompts/W5.md | 24 +++++ prompts/W6.md | 24 +++++ prompts/W7.md | 24 +++++ 21 files changed, 892 insertions(+), 38 deletions(-) create mode 100644 PLAN-W0.md create mode 100644 PLAN-W1.md create mode 100644 PLAN-W2.md create mode 100644 PLAN-W3.md create mode 100644 PLAN-W4.md create mode 100644 PLAN-W5.md create mode 100644 PLAN-W6.md create mode 100644 PLAN-W7.md create mode 100644 prompts/W0.md create mode 100644 prompts/W1.md create mode 100644 prompts/W2.md create mode 100644 prompts/W3.md create mode 100644 prompts/W4.md create mode 100644 prompts/W5.md create mode 100644 prompts/W6.md create mode 100644 prompts/W7.md diff --git a/PLAN-W0.md b/PLAN-W0.md new file mode 100644 index 0000000..de4247f --- /dev/null +++ b/PLAN-W0.md @@ -0,0 +1,72 @@ +# PLAN-W0: Web foundation + +Status: running since 2026-10-21 from the orchestrator's brief (written before this plan). Where the running agent's brief and this plan differ, the brief governs this run and the W0 report records the difference; the plan is what a rerun or a W0b follow-up uses. + +## 1. Goal +Create the web client's foundation in `web/` (SPEC §15, D-129 to D-134): a Vite + React 19 + TypeScript project with the design tokens, the layout shell, the router with every route, login and the token store, the API client and the household snapshot, i18n, formatting, the shared components, the money functions passing the vectors, the test setup and the web gate. After W0 lands, W1 to W5 build their pages in parallel inside their own feature folders and take everything shared from W0. + +## 2. Read first +1. SPEC §15 (all of it), FR-21, §8.2, §8.3, §8.5 (the D-130 paragraph), §14.3.1, §14.3.2, §14.4.1 to §14.4.3, §14.4.6, §14.4.8, §9.8 Formatting, §9.6 Limits, §9.9, §13.1, §13.3 (rows S-1, S-18), §14.5.2 and §14.5.5. +2. `docs/ux/design-spec.md` §1, §2 (all tokens), §3.1 to §3.6 (rules, money and words, people, categories, ledger, balance), and S-1 and S-18 in §4. +3. `docs/ux/strings.csv` (keys and texts for the shared strings, S-1 and S-18). +4. `contracts/openapi.yaml`, `contracts/test-vectors/{compute_split,compute_balances,propose_allocation,validate_settlement,over_settled_accounts}.json`, `contracts/examples/` (wire shapes). +5. `docs/decisions.md` D-129 to D-134, D-110, D-117, D-126. +6. `CLAUDE.md` (Orchestration, Anti-patterns, Execution protocols). + +## 3. Already established +- Stack (D-131): Vite, React 19, TypeScript `strict`, React Router 7, TanStack Query 5, `openapi-typescript` into `web/src/api/schema.d.ts` (generated, committed) with `openapi-fetch`, Recharts (a dependency for W4; W0 need not use it), Vitest with Testing Library (jsdom), ESLint (flat config) and Prettier, plain CSS modules. npm with a committed `package-lock.json`. Each library at its latest stable version, confirmed from its official documentation or the npm registry at implementation time (CLAUDE.md "Fast-moving APIs"). +- Vite `base` is `/app/`; the build output is `web/dist/`, which W7's Dockerfile stage copies to `/app` in the image (§15.4). The page references only its own files (no CDN, no web font, no external script, AC-21.12); `index.html` names an icon under `/app/` (a browser's automatic `/favicon.ico` is closed by the gate from outside). +- Source layout (§15.3): `web/src/app/` (entry, providers, router, shell, shortcuts), `web/src/api/` (schema, client, token store, snapshot, writes), `web/src/i18n/`, `web/src/design/` (tokens, global CSS), `web/src/components/`, `web/src/lib/` (formatting, `lib/domain/` money functions, utilities), `web/src/features//`. W0 owns all of it except the feature folders of W1 to W5, in which it creates only a placeholder page per route (D-004's shell rule). +- Routes (§15.3): `/app/` Home (W1), `/app/login` (W0), `/app/accounts/:id` (W1), `/app/expenses/new` and `/app/expenses/:id` (W2), `/app/search` (W2), `/app/settle` (W3), `/app/settlements/:id` (W3), `/app/analytics`, `/app/analytics/:groupKind/:groupId`, `/app/insights` (W4), `/app/settings/*` (W5). Each feature folder exports its route element(s) from `web/src/features//index.ts(x)`; the router imports only those exports, so a feature plan replaces its placeholder without touching `web/src/app/`. Sub-routes below a feature's path are the feature's own (nested `` or route objects it exports). +- Login (§15.2): code field (§8.3 normalisation, groups of 4, a pasted `/i/` link fills it), optional name field, E-3 with device name `Web · `; token in `localStorage`; every API call sends `Authorization: Bearer`, `X-Kvitt-Protocol: 1`, `X-Kvitt-App-Version` and `X-Kvitt-App-Version-Name` (from the build: version code `git rev-list --count HEAD`, name at most 32 characters, both injected by Vite at build time, fixed values in tests); a 401 clears the token and routes to `/app/login` (with the S-18 text for `device_revoked`); after a `partner` accept the first page is `/app/settings/profile`. +- Household snapshot (§15.5): pulls `GET /sync` from 0 in pages (`limit` 1000) until `has_more` is false, keeps rows by `server_version` in memory (TanStack Query cache), pulls from its cursor on window focus and after every write, drops and re-pulls on a new epoch or 409 `cursor_ahead`; selectors for users, accounts, categories, templates, expenses, settlements and audit entries (non-deleted and deleted). Writes: a helper for one `POST /sync` mutation (UUIDv7 ids, `mutation_id`, `updated_at` from the browser clock) returning accepted or rejected with the §14.4.6 reason; a helper for REST writes that applies the returned row. No persistence beyond the token and the language; no IndexedDB. +- Money (§15.3): `web/src/lib/domain/` ports `computeSplit`, `computeBalances`, `proposeAllocation`, `validateSettlement`, `overSettledAccounts` with `number` öre (all values stay far inside 2^53, §9.6) and pass the five vector files through a Vitest test that reads `../contracts/test-vectors/*.json` (error tokens as §14.5.3). The analytics and suggestion vectors are W4's and W2's. +- Formatting (§9.8): money (`1 234,50 kr`, signs and words as the app), dates, months, ISO weeks, relative time, in sv and en, with the same outputs as `:core:design`'s formatters for the same inputs (port the tables of D-065; tests per case). +- i18n: `web/src/i18n/{sv,en}.json` for shared strings; each feature keeps `web/src/features//i18n/{sv,en}.json`, loaded by W0's loader (for example `import.meta.glob`) so a feature adds strings without editing W0's files; keys from `docs/ux/strings.csv` where the string exists there, otherwise `web_` keys; named placeholders; a test fails on a key missing from either language. Language: browser preference, overridden by a stored choice (§15.3). +- Shell (§15.6): sidebar from 720 px, bottom bar under it; destinations Home, Analytics, Insights, Search, Settings; a New expense button; keyboard shortcuts `N` → `/app/expenses/new`, `/` → `/app/search`, `?` → W-20 help dialog, `Esc` closes dialogs (none while typing in a field, except `Esc`); instant navigation. +- Shared components (design-spec §3 anatomy, web form): buttons, text and amount fields, select, date field, dialog with `Esc`, confirm dialog, segmented control, avatar (picture from E-11 as a `blob:` URL, initials otherwise, AC-4.5), category icon tile and colour, account chip, balance header, ledger row (expense and settlement, §9.2 effect line), month header, empty, loading and error states, snackbar/toast, page layout, and the expense editor fields of design-spec §3.7 (amount field, category grid and quick row, the four option rows, the split mode control with live shares from `computeSplit`), shared by W-5 (W2) and W-16 (W5) as D-019 shares them on the phone. Each with a Testing Library test. +- Design tokens (design-spec §2) as CSS custom properties for light and dark (`prefers-color-scheme`), system font stack, `font-variant-numeric: tabular-nums` for amounts, Material Symbols as bundled SVG (Apache-2.0; licence noted in the report), focus rings visible, contrast as §13.1. +- Development server: `npm run dev` serves `/app/` and forwards API paths to the server named by an environment variable (default `http://localhost:8080`), documented in `web/README.md`. +- The web gate: `npm run check` = `tsc --noEmit`, `eslint .`, `vitest run`, `vite build`, and `npm run gen:api` followed by a check that `web/src/api/schema.d.ts` is unchanged. Prettier formatting is checked inside ESLint or as its own step of `check`. + +## 4. Owned paths +From SPEC §14.7: `web/**` except `web/src/features/{home,accounts,expenses,search,export,settle,analytics,settings}/**` (W1 to W5), in which W0 creates only the placeholder pages and `index.ts(x)` exports. `web/src/features/auth/**` (W-1) is W0's. Nothing outside `web/`; W7 runs in parallel in `server/**`, `contracts/**`, `.gitea/**`, `infra/build-images.sh`, `infra/dev/**`. + +## 5. Deliverables +- `web/package.json` (scripts `dev`, `build`, `test`, `lint`, `gen:api`, `check`), `package-lock.json`, `vite.config.ts`, `tsconfig*.json`, `eslint.config.js`, Prettier config, `index.html`, `.gitignore` (`node_modules/`, `dist/`), `web/README.md` (commands, dev proxy, layout, how a feature adds strings and routes). +- `web/src/api/schema.d.ts` generated from `contracts/openapi.yaml`; the client, token store, snapshot, writes; tests with a fetch stub over `contracts/examples/` responses. +- `web/src/lib/domain/` with the vector test; `web/src/lib/format/` with tests. +- `web/src/design/`, `web/src/components/`, `web/src/i18n/`, `web/src/app/` (router with every route, shell, shortcuts, W-20), `web/src/features/auth/` (W-1), placeholder pages in the W1 to W5 folders. + +## 6. Steps +1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-w0 -b agent-w0 main`, then in it `git reset --hard main` and `tools/install-git-hooks.sh`. No git-ignored files are needed. +2. Read §2 in order. Confirm SPEC v0.28 is on `main` (§15 exists); if not, work from the brief and say so. +3. Scaffold with the latest stable versions; record each with its source. `npm audit` result in the report. +4. Generated types and the freshness check; the API client, token store, snapshot and writes. +5. Money functions and the vector test; formatting. +6. Tokens, shared components, i18n, shell, router, shortcuts, login. +7. Manual check (optional, no owner data): a throwaway server from the worktree (`server/README.md`; a temporary `KVITT_DATA_DIR`, `KVITT_PUBLIC_URL=http://localhost:8080`, `bootstrap` for an `admin_setup` code), `npm run dev`, log in, see the shell and the placeholders. Never pair against the owner's development stack or the Pi. +8. Gate (§7). Commit on `agent-w0`, `git rebase main`; if `contracts/openapi.yaml` changed on `main` (W7), run `npm run gen:api` and commit; rerun the gate. Never merge, push or remove the worktree. + +## 7. Gate +The web gate (SPEC §15.3, D-131), from `web/`: `npm run check`. Green means every step passed in this session; the report quotes the Vitest summary (files and tests) and the `vite build` size lines. Also `python3 tools/contracts-check.py` (unchanged contracts, 134 checks). + +## 8. HUMAN steps +From SPEC §11.2: none. + +## 9. Out of scope +- The pages of W1 to W5 beyond placeholders; the analytics and suggestion ports (W4, W2). +- Anything in `server/**`, `contracts/**`, `infra/**`, `.gitea/**` (W7); `CLAUDE.md` (the orchestrator adds the third gate). +- A service worker, a manifest, offline storage (§15.1). + +## 10. Report +- Branch `agent-w0`, head commit after rebasing on `main`. +- Gate output lines; Vitest counts; build sizes; `npm audit`. +- Libraries and versions with sources; the Material Symbols package and licence. +- The module map W1 to W5 depend on: each shared module's path and exports (snapshot selectors, write helpers, components, formatters, i18n loader, route export convention), exactly as built. +- Web-only string keys added (`web_…`) with en and sv. +- Proposed decision entries and spec text (numbered from the brief), `TODO(spec)` occurrences, open questions with SPEC section numbers. +- Proposed text for `CLAUDE.md` "Build and verify" if it differs from SPEC §15.3. + +## 11. Numbering +Decision ids and the spec version are assigned in the start prompt; use `TODO(spec)` if none was given. diff --git a/PLAN-W1.md b/PLAN-W1.md new file mode 100644 index 0000000..12f8bc3 --- /dev/null +++ b/PLAN-W1.md @@ -0,0 +1,54 @@ +# PLAN-W1: Web home and accounts + +## 1. Goal +Build the web client's Home (W-2), account detail (W-3) and account editor (W-4) in `web/src/features/home/` and `web/src/features/accounts/`, on W0's foundation, mirroring S-4, S-3, S-5 and S-6 in content, rules and states (SPEC §15.6), with tests. + +## 2. Read first +1. SPEC §15 (all), FR-21, FR-6 (accounts), FR-10 (balances), §9.2 (effect line), §9.3 (balances), §9.6 (limits), §9.8, AC-8.10, AC-11.7, §13.3 rows S-3, S-4, S-5, S-6, D-052, D-068, D-080. +2. `docs/ux/design-spec.md` §3.2 to §3.6 and the S-3, S-4, S-5, S-6 sections; `docs/ux/strings.csv` keys `home_*`, `account_*` and the shared ones they use. +3. `web/README.md` and W0's report (in the start prompt's notes): the module map. +4. Files: `web/src/app/**` (router, shell; read only), `web/src/api/**` (snapshot selectors, write helpers; read only), `web/src/lib/**` (`computeBalances`, formatters; read only), `web/src/components/**` (balance header, ledger rows, month header; read only), `web/src/features/home/**`, `web/src/features/accounts/**` (W0's placeholders). +5. `CLAUDE.md` (Anti-patterns, Execution protocols). + +## 3. Already established +- W0 owns everything shared; this plan edits only its two feature folders. A shared piece that is missing or wrong (a component, a selector, a formatter) is a request to the orchestrator, not an edit in W0's paths; work around it inside the feature only when the report names it. +- Balances, effect lines and month totals are computed in the browser from the snapshot with W0's `computeBalances` and formatters (§15.5), never re-implemented. +- Home (W-2): the balance header with the §9.3 wording and amount, Settle up when non-zero (to `/app/settle`), active accounts with balance wording, amount and this month's expense count, a collapsed "Archived (n)" section, the empty state "Create your first account"; while the household has no partner, the S-3 content instead (explanation; Invite partner for the admin to `/app/settings/invites`). No sync pill, no update banner, no pull to refresh (§15.6). +- Account detail (W-3): name, default split with names (`common_ratio` "60/40"), balance header, expenses and settlements interleaved by date, grouped by month with month totals (household spend), rows per AC-8.10 and AC-11.7 (opening `/app/expenses/:id` or `/app/settlements/:id`), archived badge; actions New expense (not on an archived account; to `/app/expenses/new` with the account), Settle this account (`/app/settle` with the account), Edit, Archive / Unarchive (archive shows the S-5 snackbar with undo, D-071), Delete only when allowed (AC-6.5); list order by `created_at` (AC-6.6); unique names (AC-6.2: `name_taken` shown on the name field). +- Account editor (W-4): name, the default split as two percent fields that always sum to 100 with two decimals and a slider or stepper, the "Of 100,00 kr" preview, Save and Cancel; the discard dialog has a title and no body (D-080). Create (E-18), rename and ratio (E-19), archive and unarchive (E-20, E-21), delete (E-22), each applying the returned row (§15.5); a failure shows "Kunde inte nå servern" or the error code's text and keeps the form. +- Desktop layout is this plan's within W0's shell and tokens: for example Home's accounts as a grid or list beside the balance header, account detail with a wider ledger. Under 720 px it reads as the app's screens. +- Strings from `docs/ux/strings.csv` where they exist, `web_` keys otherwise, in the feature's `i18n/{sv,en}.json`. + +## 4. Owned paths +From SPEC §14.7: `web/src/features/home/**`, `web/src/features/accounts/**`. `web/package.json` only to add a dependency line (the second plan to land resolves `package-lock.json` with `npm install` on rebase). Nothing else. + +## 5. Deliverables +- The three pages with every state of §15.6 (loaded, empty, loading, error, long content: long Swedish names, 7-digit amounts) and the S-3 variant of Home. +- Tests (Vitest, Testing Library) over a snapshot fixture: balance wording per direction and settled, archived section, month grouping and totals, delete allowed or not, ratio fields summing to 100, discard dialog, each write's success and failure path. + +## 6. Steps +1. Worktree: from `/Users/jhag/projects/kvitt`, `git worktree add .claude/worktrees/agent-w1 -b agent-w1 main`; in it `git reset --hard main`, `tools/install-git-hooks.sh`, `cd web && npm ci`. +2. Read §2. Confirm W0 has landed (`web/package.json` and the placeholders in this plan's folders exist on `main`); if not, stop and report. +3. `npm outdated` in `web/`; record it. No new library is expected. +4. Build W-2, then W-3, then W-4, with tests. +5. Manual check (optional): a throwaway server as `web/README.md` describes, `npm run dev`, create accounts and expenses through the pages that exist. Never the owner's development stack or the Pi. +6. Gate (§7). Commit on `agent-w1`, `git rebase main` (other web plans land in parallel; only `package-lock.json` may conflict), rerun the gate. Never merge, push or remove the worktree. + +## 7. Gate +The web gate, from `web/`: `npm run check` (SPEC §15.3). Green: every step passed in this session; the report quotes the Vitest summary. + +## 8. HUMAN steps +From SPEC §11.2: look at Home and an account in a browser against the development stack (yes/no), relayed by the orchestrator after landing and a dev-stack rebuild. + +## 9. Out of scope +- Expense editor and detail, search (W2); settle up and settlement detail (W3); analytics (W4); settings (W5). +- Any edit in W0's paths (`web/src/{app,api,i18n,design,components,lib}/**`) or another feature folder. + +## 10. Report +- Branch `agent-w1`, head commit after rebasing on `main`; gate lines and Vitest counts. +- Pages × states covered; screenshots are not committed (none in the web gate). +- `web_` keys added with en and sv; strings missing from `strings.csv`; design-spec mismatches. +- Requests for W0's modules, workarounds kept inside the feature, proposed decision entries (numbered from the brief), open questions with SPEC section numbers. + +## 11. Numbering +Assigned in the start prompt; use `TODO(spec)` if none was given. diff --git a/PLAN-W2.md b/PLAN-W2.md new file mode 100644 index 0000000..65c0a96 --- /dev/null +++ b/PLAN-W2.md @@ -0,0 +1,57 @@ +# PLAN-W2: Web expenses, search and export + +## 1. Goal +Build the web client's expense editor (W-5) with the category suggestion of §9.11, expense detail with the audit timeline (W-6), and the search page (W-12) with its CSV export, in `web/src/features/expenses/`, `web/src/features/search/` and `web/src/features/export/`, on W0's foundation (SPEC §15.6, §15.7), with tests. + +## 2. Read first +1. SPEC §15 (all), FR-21, FR-8 (expenses), FR-12 (audit, AC-12.4), §9.1 (splits), §9.2, §9.6 (limits), §9.8, §9.11 (CS-1 to CS-7) and §14.5.6, §14.4.3 to §14.4.6 (push, rejections, audit diff), §13.3 rows S-7 and S-8, D-049, D-055, D-080, D-102. +2. `docs/ux/design-spec.md` §3.7 (editor fields) and the S-7 and S-8 sections; `docs/ux/category-suggestion.md`; `docs/ux/strings.csv` keys `expense_*`, `editor_*` and the shared ones. +3. `contracts/test-vectors/suggest_category.json`, `contracts/category-keywords.csv`, `contracts/examples/sync/*.json`. +4. `web/README.md` and W0's report (module map). +5. Files: `web/src/{app,api,lib,components,i18n}/**` (read only), `web/src/features/{expenses,search,export}/**` (W0's placeholders). +6. `CLAUDE.md`. + +## 3. Already established +- W0 owns everything shared; this plan edits only its three feature folders. Missing shared pieces are requests to the orchestrator. +- Editor (W-5, mirrors S-7), built from W0's shared editor fields (design-spec §3.7, D-019; W5's recurring editor uses the same): amount (kr, two decimals, §9.6 range), description, category grid (active categories, the S-7 quick row of `frequentCategoryIds`' rule computed in the browser from the snapshot: the five most used active categories of the account, ties by `sort_order`, D-060), account (active accounts), date (today by default), payer, split modes `default`, `ratio`, `exact`, `full` with live shares from W0's `computeSplit`, notes; account, date, payer and split as four fixed rows (D-102); validation inline; Save writes one `POST /sync` mutation (§15.5); Delete in edit mode (soft delete, with confirmation); Cancel with the discard dialog (title, no body, D-080). After saving, the page returned to shows "Utgiften sparades" (D-071) without undo. +- Category suggestion (§9.11, §15.7): a TypeScript port of `suggestCategory` and its normalisation in `web/src/features/expenses/suggest/`, passing `contracts/test-vectors/suggest_category.json` in a Vitest test; the keyword table imported at build time from `contracts/category-keywords.csv` (Vite raw import of the repository file, never a copy), parsed with the §14.8 rules. For a new expense the category follows the description until the user picks one (CS-2, CS-3, CS-7), with no marker. The history for CS-1 comes from the snapshot. Unicode normalisation with `String.prototype.normalize('NFC')` and `\p{L}`/`\p{Nd}` classes (the `UnicodeText` roles of §14.5.6). +- Detail (W-6, mirrors S-8): every field, each user's share and the split mode, "Recurring: