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

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

  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.