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
11 KiB
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
- 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.
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.docs/ux/strings.csv(keys and texts for the shared strings, S-1 and S-18).contracts/openapi.yaml,contracts/test-vectors/{compute_split,compute_balances,propose_allocation,validate_settlement,over_settled_accounts}.json,contracts/examples/(wire shapes).docs/decisions.mdD-129 to D-134, D-110, D-117, D-126.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-typescriptintoweb/src/api/schema.d.ts(generated, committed) withopenapi-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 committedpackage-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
baseis/app/; the build output isweb/dist/, which W7's Dockerfile stage copies to/appin the image (§15.4). The page references only its own files (no CDN, no web font, no external script, AC-21.12);index.htmlnames an icon under/app/(a browser's automatic/favicon.icois 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/newand/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) fromweb/src/features/<name>/index.ts(x); the router imports only those exports, so a feature plan replaces its placeholder without touchingweb/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 nameWeb · <browser>; token inlocalStorage; every API call sendsAuthorization: Bearer,X-Kvitt-Protocol: 1,X-Kvitt-App-VersionandX-Kvitt-App-Version-Name(from the build: version codegit 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 fordevice_revoked); after apartneraccept the first page is/app/settings/profile. - Household snapshot (§15.5): pulls
GET /syncfrom 0 in pages (limit1000) untilhas_moreis false, keeps rows byserver_versionin memory (TanStack Query cache), pulls from its cursor on window focus and after every write, drops and re-pulls on a new epoch or 409cursor_ahead; selectors for users, accounts, categories, templates, expenses, settlements and audit entries (non-deleted and deleted). Writes: a helper for onePOST /syncmutation (UUIDv7 ids,mutation_id,updated_atfrom 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/portscomputeSplit,computeBalances,proposeAllocation,validateSettlement,overSettledAccountswithnumberö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}.jsonfor shared strings; each feature keepsweb/src/features/<name>/i18n/{sv,en}.json, loaded by W0's loader (for exampleimport.meta.glob) so a feature adds strings without editing W0's files; keys fromdocs/ux/strings.csvwhere the string exists there, otherwiseweb_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,Esccloses dialogs (none while typing in a field, exceptEsc); 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 ablob: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 fromcomputeSplit), 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-numsfor 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 devserves/app/and forwards API paths to the server named by an environment variable (defaulthttp://localhost:8080), documented inweb/README.md. - The web gate:
npm run check=tsc --noEmit,eslint .,vitest run,vite build, andnpm run gen:apifollowed by a check thatweb/src/api/schema.d.tsis unchanged. Prettier formatting is checked inside ESLint or as its own step ofcheck.
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(scriptsdev,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.tsgenerated fromcontracts/openapi.yaml; the client, token store, snapshot, writes; tests with a fetch stub overcontracts/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
- Worktree. From the main checkout
/Users/jhag/projects/kvitt:git worktree add .claude/worktrees/agent-w0 -b agent-w0 main, then in itgit reset --hard mainandtools/install-git-hooks.sh. No git-ignored files are needed. - Read §2 in order. Confirm SPEC v0.28 is on
main(§15 exists); if not, work from the brief and say so. - Scaffold with the latest stable versions; record each with its source.
npm auditresult in the report. - Generated types and the freshness check; the API client, token store, snapshot and writes.
- Money functions and the vector test; formatting.
- Tokens, shared components, i18n, shell, router, shortcuts, login.
- Manual check (optional, no owner data): a throwaway server from the worktree (
server/README.md; a temporaryKVITT_DATA_DIR,KVITT_PUBLIC_URL=http://localhost:8080,bootstrapfor anadmin_setupcode),npm run dev, log in, see the shell and the placeholders. Never pair against the owner's development stack or the Pi. - Gate (§7). Commit on
agent-w0,git rebase main; ifcontracts/openapi.yamlchanged onmain(W7), runnpm run gen:apiand 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 onmain. - 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.