SPEC v0.40: K3's report recorded (D-201 to D-207), web card pages as built
Build Kvitt images / build (push) Failing after 3h14m38s

K3 (card purchases on the web, landed faeb752; web gate 70 files, 684 tests,
build ok; contracts-check 157):
- SPEC §15.6 W-21 to W-23 as built, with W-5's ?cardPurchase= prefill and link,
  W-6's card line, W-13's "Anslut kort" row; §15.5 the card snapshot; §15.3
  the shared card pieces (api/cards.ts, fixtures, dev proxy, three icons) and
  matchCardPurchase ported; §11.1, §11.2 K3 row, §11.3, §14.7, §15.9; v0.40 row.
- decisions D-201 to D-207 (refresh while fetching, query names, https-only
  auth_url, attempt outcome, W-5 purchase errors, date forms, S-27 on the web);
  D-208 to D-210 unused.
- HANDOFF: K0 to K3 landed, v1.0.4 deployed, v1.0.5 being built, kvitt-164 on
  the Pi; pending owner steps PLAN-K2 §8, PLAN-K3 §8, PLAN-K1 §8 steps 5 to 7;
  K3's open questions; next free D-212, v0.41.
- web/README.md: card routes, the card snapshot, query names, the new icons;
  prompts/README.md: K2 and K3 done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-24 17:02:07 +02:00
parent 6f637514e6
commit 7ce3029cb1
5 changed files with 68 additions and 36 deletions
+24 -10
View File
@@ -20,7 +20,7 @@ npm run build # web/dist/, what kvitt-server serves under /app/
The build splits libraries into `react` (React, React DOM), `charts` (Recharts and its d3 and Redux dependencies,
loaded only with the analytics pages) and `vendor` (the rest), each under Vite's 500 kB warning. The dev proxy forwards
every API prefix of `contracts/openapi.yaml`, `/analytics` included.
every API prefix of `contracts/openapi.yaml`, `/analytics` and `/cards` included.
To sign in, create a "Pair a device" invite on the phone (Settings › Pair a device) and type the code on
`/app/login`. The browser becomes a device named `Web · <browser>` and keeps its token in `localStorage`.
@@ -60,7 +60,7 @@ web/
useDocumentTitle
api/ client.ts (openapi-fetch, headers, 401, ApiError, unwrap), schema.d.ts (generated),
types.ts (wire objects), session.ts, ledger.ts (sync-pull ledger), me.ts, invites.ts,
pictures.ts, push.ts (POST /sync), errors.ts (error and rejection texts),
pictures.ts, push.ts (POST /sync), cards.ts (card snapshot, E-44), errors.ts (error and rejection texts),
queryClient.ts, ApiProvider.tsx, useApi.ts
i18n/ sv.json, en.json (generated), web-strings.csv, catalog.ts, I18nProvider, useI18n, language.ts
design/ tokens.css, global.css, Icon.tsx, icons.generated.ts (generated)
@@ -71,18 +71,21 @@ web/
lib/ money, split, balances, settlement (kvitt-domain port), dates, effect, csv, ids,
shortcuts, storage, categories (categoryName), timeline (audit lines),
accountColors
features/<name>/ auth (sign-in, built), home, accounts, expenses, settle, analytics (with insights), search,
settings (placeholders for W1 to W5)
test/ setup.ts, fakeServer.ts (hand-written fetch fake), fixtures.ts, render.tsx
features/<name>/ auth (sign-in), home, accounts, expenses, settle, analytics (with insights), search,
export, settings, cards (K3: W-21 to W-23)
test/ setup.ts, fakeServer.ts (hand-written fetch fake), fixtures.ts, cards.ts (card fixtures),
render.tsx
```
## Routes
Fixed for the feature plans; always link with the builders in `src/app/routes.ts` (`paths.account(id)`,
`paths.drillDown('bucket', key)`, `expensesNewPath({ account })`, `settlePath({ account })`,
`settlementEditPath(id)`; the query parameter names are `QUERY.account` and `QUERY.settlement`, D-144). Each
feature exports its route objects from `src/features/<name>/index.ts` (`homeRoutes`, `accountsRoutes`,
`expensesRoutes`, `searchRoutes`, `settleRoutes`, `analyticsRoutes`, `settingsRoutes`, `authRoutes`), paths relative
`settlementEditPath(id)`, `cardsPath({ account })`, `connectCardPath({ provider })`,
`expensesNewPath({ cardPurchase, account })`, `paths.settingsCardConnection(id)`; the query parameter names are
`QUERY.account` and `QUERY.settlement` (D-144), `QUERY.cardPurchase`, `QUERY.attempt` and `QUERY.provider` (D-202)).
Each feature exports its route objects from `src/features/<name>/index.ts` (`homeRoutes`, `accountsRoutes`,
`expensesRoutes`, `searchRoutes`, `settleRoutes`, `analyticsRoutes`, `settingsRoutes`, `cardsRoutes`, `authRoutes`), paths relative
to `/app`, pages loaded lazily with `lazyPage(() => import('./Page'))` (`src/app/lazyPage.ts`). `src/app/router.tsx`
only spreads those arrays, so a feature plan changes its pages and sub-routes without editing `src/app/`.
@@ -99,6 +102,9 @@ only spreads those arrays, so a feature plan changes its pages and sub-routes wi
| `/analytics` | `features/analytics/AnalyticsPage` | S-11 |
| `/analytics/:groupKind/:groupId` | `features/analytics/DrillDownPage` | S-12 (`groupKind` `category`, `account` or `bucket`) |
| `/insights` | `features/analytics/InsightsPage` | web |
| `/cards` (`?attempt=<id>`, `?account=<id>`) | `features/cards/CardsPage` | S-23, S-27 (W-21) |
| `/settings/cards` (`?provider=<id>`) | `features/cards/ConnectCardPage` | S-24, S-25 (W-22) |
| `/settings/cards/:id` | `features/cards/CardConnectionPage` | S-26 (W-23) |
| `/search` | `features/search/SearchPage` | web |
| `/settings` | `features/settings/SettingsPage` | S-13 (holds the web's language/theme card and sign-out) |
| `/settings/categories`, `recurring`, `devices`, `profile`, `invite`, `about` | `features/settings/*Page` | S-15, S-19, S-20, S-14, S-21, S-22 |
@@ -114,6 +120,13 @@ The web has no local database. What it reads:
ported §9.3 rules; `live(map)` drops tombstones. Pass a `select` to `useLedger` for derived views.
- **REST reads**: `useMe()` (E-7), and the feature plans add devices (E-15), invites (E-13), health (E-1) the same
way (`useQuery` + `unwrap(api.GET(…))`). Profile pictures: `useUserPicture(userId, pictureVersion)`.
- **The card snapshot** (SPEC §15.5, K3): the viewer's own card connections and purchases come through E-40
`GET /cards/sync`, never `GET /sync`. `useCards(select?)` (`api/cards.ts`) holds them in memory beside the ledger:
the first read pulls from cursor 0 in pages of 1000, later reads (focus, every 60 s, after every card write) from the
cursor it holds; 409 `cursor_ahead` starts again from 0; rows apply by `card_version`. W-21 re-reads every 2 s while
a connection is fetching or in its first fetch (D-201). `useCardWriter().refresh()` re-reads only the snapshot
(`exact` key), not the catalogue (E-36) or an attempt (E-38). Card writes (E-44 through `useSetCardPurchase`) are
sent at once, never queued.
- **Refresh**: every query refetches when the window gets focus and every 60 s while visible
(`api/queryClient.ts`). `UpdatedLine` shows "Uppdaterad för 3 min sedan" with a button that reads now.
@@ -184,7 +197,8 @@ web-only strings live in `src/i18n/web-strings.csv` (W0's).
`propose_allocation`, `validate_settlement`, `over_settled_accounts`); `src/lib/vectors.test.ts` runs every case
of `contracts/test-vectors/*.json` against them, like `server/crates/kvitt-domain/tests/vectors.rs`, and against
the ports that live in features: `suggest_category` (W2, `features/expenses/suggest`), `resolve_range` and
`spend_series` (W4, `features/analytics/domain/analytics`). Every vector file is listed as ported; a new file
`spend_series` (W4, `features/analytics/domain/analytics`), and `match_card_purchase` against
`lib/domain/matchCardPurchase` (K3). Every vector file is listed as ported; a new file
fails the test until it is.
- `lib/effect.ts` is the §9.2 effect line.
@@ -216,7 +230,7 @@ set is used. `scripts/gen-icons.mjs` copies the path data of the listed icons in
`<Icon name="home" />` or `<Icon name="home" filled />` renders one inline. Five spec names have another file name
in Material Symbols 0.48 (same glyph): `card_giftcard` → `redeem`, `expand_more`/`expand_less` →
`keyboard_arrow_down`/`up`, `smartphone` → `mobile`, `phonelink_erase` → `mobile_cancel`; the tables keep the spec
names. To add an icon: add its name to the list in the script and run `npm run icons:import`.
names. K3 added `credit_card`, `link_off` and `visibility_off` for the card pages. To add an icon: add its name to the list in the script and run `npm run icons:import`.
## Adding a feature page (W1 to W5)