Files
kvitt/PLAN-W0.md
T
jacobh2 7bddc50a53 SPEC v0.28: web client programme (D-129 to D-134), plans W0 to W7
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-21 20:49:00 +02:00

73 lines
11 KiB
Markdown

# 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/<name>/`. 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/<name>/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 `<Routes>` or route objects it exports).
- Login (§15.2): code field (§8.3 normalisation, groups of 4, a pasted `/i/<CODE>` link fills it), optional name field, E-3 with device name `Web · <browser>`; 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/<name>/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.