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
+18 -15
View File
@@ -1,6 +1,6 @@
# Kvitt: specification
Version **0.39** (2026-10-24). Schema version **4**. Sync protocol version **1**. Push payload version **1**.
Version **0.40** (2026-10-24). Schema version **4**. Sync protocol version **1**. Push payload version **1**.
This file is the single source of truth for Kvitt. It is written for implementation agents that have no other context. If code, a plan or `CLAUDE.md` disagrees with this file, this file is right. Words in `code` are literal identifiers, paths, enum values or error codes and must be used exactly as written. Anything not decided here is listed in §12 as `TODO(spec)`; do not invent a value for it.
@@ -1425,7 +1425,7 @@ SPEC v0.28 ─┬─► W0 web foundation (web/**) ─────────
└─► W7 server: /app/ serving, /analytics/* (D-135..D-138, v0.29) ─┬─► W4's W-11
└─► W6 Android Insights
Card purchases (v0.35, D-158 to D-176; K0 done, v0.36; K1 landed `671c00d`, its report recorded in v0.38, D-181 to D-188; K2's report goes into v0.39, K3's into v0.40):
Card purchases (v0.35, D-158 to D-176; K0 done, v0.36; K1 landed `671c00d`, its report recorded in v0.38, D-181 to D-188; K2 landed `a8f91ee`, its report recorded in v0.39, D-191 to D-200; K3 landed `faeb752`, its report recorded in v0.40, D-201 to D-207):
SPEC v0.35 ─► owner steps (Enable Banking account, ToS, application, key, linked cards) ─► K0 local test app on the Mac
─► K0 report ─► SPEC v0.36 (K0's answers, D-177 to D-180) ────────┬─► K1 server (schema 4, adapter, E-36..E-44, poller, NPM text) ──┐ lands first
├─► K2 Android (:feature:cards, Room 2, Kortköp tab) ──────────┼─► lands after K1
@@ -1478,7 +1478,7 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
| K0 | Card purchases spike: local test app (D-176) | The test app landed `2ce317f`: `docs/spikes/k0/app.py` and its `README.md`, one command (`python3 docs/spikes/k0/app.py`, the standard library only, the RS256 JWT signed with `openssl`), a page on `http://localhost:8765/` that runs the real Enable Banking flow (bank list, BankID, paste-back of the redirect address, accounts, "Uppdatera" with PSU headers, "Bakgrundshämtning" without, balances, the purchases, an aggregate "Analys", "Koppla från"), key and state in the git-ignored `docs/spikes/k0/.local/`. Done 2026-10-24: run with the owner on his card; report `docs/spikes/K0-enable-banking.md` (aggregates only), its answers recorded in SPEC v0.36 (D-177 to D-180); the partner's card and the next-day checks were not run (owner's decision, D-177) and move to K1's owner steps | The flow worked end to end with the owner's card (owner's yes); the report answers every K0 item; `git status` shows nothing from `.local/`; no gate (not part of any gate) | Done 2026-10-24: Enable Banking account and Terms of Service, the restricted production application, the key in `.local/`, the owner's card linked with BankID, the test app run (PLAN-K0 §8) | SPEC v0.35 |
| K1 | Card purchases: server (D-158 to D-175) | Landed `671c00d` (2026-10-24; D-181 to D-188, SPEC v0.38). Migration 0004 with its contract text and migration test, server schema 4; `src/cards/` (registry, Enable Banking adapter, the windowed fetch and de-duplication of §9.12 CP-7 and CP-8, poller, callback page); E-36 to E-44 in the server and `contracts/openapi.yaml` with examples; new error codes; the link clearing in §14.4.4 step 7; `contracts/test-vectors/match_card_purchase.json` and its Python reference in `tools/contracts-check.py`; config (§10.4), `infra/pi/compose.yaml`, `.env.example`, `infra/dev/**`, `infra/npm/kvitt.conf`, `infra/README.md` (step 4 block, card runbook); `web/src/api/schema.d.ts` regenerated | Server gate; `python3 tools/contracts-check.py`; the web gate (the regenerated types); an integration test suite against a local Enable Banking stub. At landing: server 356 tests (34 in `tests/cards_api/`), contracts-check 157 (17 `match_card_purchase` cases), web 609 (`match_card_purchase` listed as not ported until K3), the nginx check and both compose configs ok; the Android gate red by design until K2 lands (`VectorFilesTest`, `ExampleRoundTripTest` pin the contract's vector and example sets), so the commit bypassed the pre-commit hook (contract-first landing) | Copy the key (already on the Mac from K0) to `~/kvitt/secrets/cards/`, set the application id in `.env`, paste the new NPM block, deploy; once a client with the card screens runs: the partner's card, the next-day fetch without BankID and pending→booked (D-177) (PLAN-K1 §8) | SPEC v0.36 (K0's answers) |
| K2 | Card purchases: Android (D-169, D-170, D-172, D-173) | Landed `a8f91ee` (2026-10-24; D-191 to D-200, SPEC v0.39). `:feature:cards` (S-23 to S-27 and the sheet), Room version 2 with its migration test, the `CardRepository` UI contract in `:core:model`, `matchCardPurchase` in `:domain` passing its vectors, `:data` (DTOs, card sync, queued link changes, Custom Tab, deep link), fakes and scenarios, the fourth bottom destination, S-5's "Från kortköp", S-7's prefill and link, S-8's card line, the committed card artwork mapped (D-180), goldens; strings in `docs/ux/strings.csv` (77 rows); out-of-path edits ratified by the orchestrator: `ArchitectureRules.kt` knows `cards`, `:core:design` gains `ic_credit_card` / `KvittIcon.CreditCard`, the card measures in `KvittDimens` (D-200), the nav-label shrink-to-fit (D-197), `StringsCatalogueTest` takes `cards_` as a feature prefix | Android gate. At landing: `BUILD SUCCESSFUL`, 791 actionable tasks, 1154 tests, 0 failures; the 11 card examples (E-36 to E-44) round-trip, the 17 `match_card_purchase` cases pass, `VectorFilesTest` and `ExampleRoundTripTest` green again on `main`; 150 goldens added (`:feature:cards` 135 for every state of S-23 to S-27 with `_sv_light_200` for the long ones; `S-5-account-menu-card`, `s7_from_card`, `s7_from_card_foreign`, `s8_from_card`, `s13_hub_card_connected`) and 147 re-recorded for the fourth bar destination (S-3, S-4, S-11, S-13, S-17 and the navigation-bar component; only the bar changed) | Connect the card on the phone with BankID; create an expense from a purchase; link one; disconnect and reconnect (PLAN-K2 §8 steps 1 to 7, pending: the phone was not attached; the release APK with the card screens is installed by the orchestrator once K1's image runs on the Pi) | SPEC v0.36; may start then on the spec and fakes; lands after K1 |
| K3 | Card purchases: web (D-174) | W-21 to W-23 in `web/src/features/cards/`, the sidebar entry, the S-7-style prefill in W-5 and the card line in W-6, the committed card artwork mapped (D-180), `matchCardPurchase` in TypeScript passing its vectors | Web gate | Connect the card from the laptop browser with BankID QR; create and link from W-21 (PLAN-K3 §8) | SPEC v0.36; lands after K1 and K2 (it takes the card strings K2 adds to `docs/ux/strings.csv`) |
| K3 | Card purchases: web (D-174) | Landed `faeb752` (2026-10-24; D-201 to D-207, SPEC v0.40). W-21 to W-23 in `web/src/features/cards/` (every §13.3 state but offline, the S-23 sheet actions, `?attempt=` polling, `?account=` as S-27), the Kortköp entry after Översikt in the sidebar and bottom bar, W-5's `?cardPurchase=` prefill (CP-9) and link on save, W-6's card line with "Ta bort koppling", W-3's "Från kortköp", W-13's "Anslut kort" row, the committed card artwork mapped (D-180), the card snapshot in `web/src/api/cards.ts`, `matchCardPurchase` in TypeScript passing its 17 vectors | Web gate. At landing: web 70 files, 684 tests, build ok; contracts-check 157 | Pending after image `v1.0.5` is deployed: connect the card from the laptop browser with BankID QR; create and link from W-21 (PLAN-K3 §8, three steps) | SPEC v0.36; lands after K1 and K2 (it takes the card strings K2 adds to `docs/ux/strings.csv`) |
| W7 | Server: web serving and analytics endpoints (D-132, D-133) | Landed `50a0d67` (2026-10-21, D-135 to D-138, SPEC v0.29; 299 server tests, contracts-check 136). `/app/` from `KVITT_WEB_DIR` with the SPA fallback, cache headers and the `/app/` CSP; the `/analytics/*` endpoints (D-135 to D-138, SPEC v0.29) in `contracts/openapi.yaml` and the server; the Node stage in `server/Dockerfile` with the repository root as build context (workflow, `infra/build-images.sh`, `infra/dev/**`) | Server gate; `python3 tools/contracts-check.py`; the web gate when `web/` exists and the contract changed (`web/src/api/schema.d.ts` regenerated) | Paste the v0.28 NPM block (`infra/README.md` step 4); deploy the new image (§10.10); deployed 2026-10-24 as `v1.0.3` (D-211) | SPEC v0.28 |
### 11.3 Notes per milestone
@@ -1511,7 +1511,7 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
* **M11** runs the import per §10.8 "Production run" (D-025). The rehearsal on 2026-10-18 ran both real files against a throwaway database and reconciled on both (Vardag 1746 rows, Hemma 37 rows); 21 rows were unmapped before D-044. The dry run of 2026-10-18 with M12b's mapping confirmed it: Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped; both reconciled.
* **W0 to W7** (D-134, §15.9): the web client programme. W0 and W7 run first in parallel (disjoint paths); W1 to W5 run in parallel once W0 has landed; W4's W-11 and W6 wait for W7. W0 owns `web/**` except the feature folders, registers every route with a placeholder page per feature folder (as M2a registered every route, D-004), and lists its module paths in its report; W1 to W5 build only inside their feature folders and take everything shared from W0's modules (a missing shared piece is a request to the orchestrator, as M7x's requests went to M6b). A second agent to land that changed `web/package.json` resolves `web/package-lock.json` with `npm install` on rebase (as `Cargo.lock`, §14.7). A plan that changes `contracts/openapi.yaml` regenerates `web/src/api/schema.d.ts` (§15.3). Web agents do not use the phone or `tools/gradle-serial.sh`; a manual check runs against a throwaway server, never the owner's development or Pi data (§15.4). The orchestrator adds the web gate to `CLAUDE.md` "Build and verify" (D-131). W1 to W5 landed 2026-10-21 (`934eee7`, `a65ab71`, `f9a9bfb`, `1aecf51`, `3ebe57d`; D-144 to D-154, D-150 unused) with the web gate green on `main` (59 test files, 532 tests at `1aecf51`); a web fix (`83f5293`) followed (598 tests). Pieces built twice or three times in feature folders and other loose ends are listed for a W0b plan in `docs/HANDOFF.md` "Web follow-ups". W0b landed `5305f37` (v0.34). All of W0 to W7 are landed and verified: `v1.0.3` (built on the Mac from `8fa9198`, D-211) runs on the Kvitt Pi and the owner checked the web client and the phone's Insights against it on 2026-10-24 (§11.4).
* **K0 to K3** (D-176, §11.1): the card-purchase programme. K0 runs first, on the Mac only, with the owner (a local test app for fast feedback before any Android work, owner's refinement 2026-10-24; the app landed `2ce317f`); K0 is done (run 2026-10-24, report `docs/spikes/K0-enable-banking.md`) and its answers are in SPEC v0.36 (D-177 to D-180), so K1 to K3 can start. The reports of K1 to K3 propose spec text and decisions that a docs agent records: K1's in SPEC v0.38 (landed `671c00d`, D-181 to D-188), K2's in v0.39 (landed `a8f91ee`, D-191 to D-200), K3's in v0.40 (landed `faeb752`) (decisions from D-191 to D-210, disjoint ranges per plan). Then K1, K2 and K3 run in parallel in their own worktrees (disjoint paths, §14.7): K1 owns the contract (`contracts/**`, the migration text, the vectors) and lands first; K2 and K3 build against the spec and fakes, and rebase on K1 before their gates (K2's DTO tests and K3's generated types need K1's contract); K3 lands after K2, whose `docs/ux/strings.csv` rows it imports (`npm run i18n:import`). K2 uses the phone only when its brief names it; K1 and K3 never use the phone. No plan connects to the owner's real card except through the owner's own BankID step; no plan stores, prints or commits a real purchase.
* **K0 to K3** (D-176, §11.1): the card-purchase programme. K0 runs first, on the Mac only, with the owner (a local test app for fast feedback before any Android work, owner's refinement 2026-10-24; the app landed `2ce317f`); K0 is done (run 2026-10-24, report `docs/spikes/K0-enable-banking.md`) and its answers are in SPEC v0.36 (D-177 to D-180), so K1 to K3 can start. The reports of K1 to K3 propose spec text and decisions that a docs agent records: K1's in SPEC v0.38 (landed `671c00d`, D-181 to D-188), K2's in v0.39 (landed `a8f91ee`, D-191 to D-200), K3's in v0.40 (landed `faeb752`, D-201 to D-207) (decisions from D-191 to D-210, disjoint ranges per plan). Then K1, K2 and K3 run in parallel in their own worktrees (disjoint paths, §14.7): K1 owns the contract (`contracts/**`, the migration text, the vectors) and lands first; K2 and K3 build against the spec and fakes, and rebase on K1 before their gates (K2's DTO tests and K3's generated types need K1's contract); K3 lands after K2, whose `docs/ux/strings.csv` rows it imports (`npm run i18n:import`). K2 uses the phone only when its brief names it; K1 and K3 never use the phone. No plan connects to the owner's real card except through the owner's own BankID step; no plan stores, prints or commits a real purchase.
### 11.4 Validation reports
@@ -3422,7 +3422,7 @@ Ownership (an agent edits only paths its milestone owns; anything else needs the
| K0 (D-176): `docs/spikes/k0/**` (the test app of `2ce317f`; its key, config and state in the git-ignored `docs/spikes/k0/.local/`, root `.gitignore`), `docs/spikes/K0-enable-banking.md` | K0 | docs agent (the report is a deliverable, kept) |
| K1 (D-158 to D-175): `server/**`, `contracts/**`, `tools/contracts-check.py`, `infra/pi/compose.yaml`, `infra/pi/.env.example`, `infra/dev/**`, `infra/npm/kvitt.conf`, `infra/README.md` (step 4's block and the card runbook entry), and `web/src/api/schema.d.ts` for its regeneration only | K1 (contract-change plan for `contracts/**`) | the usual owners |
| K2 (D-169, D-170, D-172, D-173): `android/feature/cards/**` (new module), and as a contract-change plan `android/core/model/**`, `android/domain/**` (`matchCardPurchase`), `android/data/**` (Room 2, exported `2.json`), `android/core/fakes/**`; `android/core/design/**` (the fourth destination, `nav_cards`), `android/feature/account/**` (S-5's action), `android/feature/expense/**` (S-7's prefill, S-8's line), `android/feature/settings/**` (S-13's row), `android/app/**` (navigation, deep link, bindings), `android/settings.gradle.kts` and `android/gradle/libs.versions.toml` (the module and `androidx.browser`), `docs/ux/strings.csv` (the card keys of §13.3) | K2 | the usual owners |
| K3 (D-174): `web/src/features/cards/**` (new), and in W0's shared code the route, the sidebar entry and `web/src/lib/domain/` (`matchCardPurchase`); `web/src/features/expenses/**` (W-5's prefill, W-6's card line); `web/src/features/accounts/**` (W-3's "Från kortköp"); `web/src/features/settings/**` (W-13's row); the regenerated i18n dictionaries | K3 | later web plans |
| K3 (D-174): `web/src/features/cards/**` (new), and in W0's shared code the route, the sidebar entry and `web/src/lib/domain/` (`matchCardPurchase`); `web/src/features/expenses/**` (W-5's prefill, W-6's card line); `web/src/features/accounts/**` (W-3's "Från kortköp"); `web/src/features/settings/**` (W-13's row); the regenerated i18n dictionaries; as built also `web/src/api/cards.ts` and its test, `web/src/test/cards.ts`, `web/vite.config.ts` (`/cards` in the dev proxy), `web/scripts/gen-icons.mjs` and `web/src/design/icons.generated.ts`, `web/src/app/Shell.test.tsx` (ratified by the orchestrator at landing, `faeb752`) | K3 | later web plans |
| `docs/validation/<file>` | the milestone that names the file | |
Module shells (D-004): a shell file or module created by M2a or M2b in a path owned by a later milestone holds only what makes the build and the 501 stubs work; the later milestone owns it from the moment the shell lands and writes the body.
@@ -3462,7 +3462,7 @@ Rule: a change to any other contract file, or a semantic change to `contracts/op
Owner interview 2026-10-21 (D-129 to D-134). Kvitt has a web client, built in `web/` and served by `kvitt-server` under `/app/`. It is a second way into the same household: the same server, the same rules, the same data, read and written through the same API (§7, §14.3, §14.4). Nothing in the phone app changes because of it, except the Insights section of W6 (§15.8). This section is binding for the web plans W0 to W5 and for the web parts of W7; FR-21 holds the acceptance criteria.
**Status (2026-10-24, D-211).** W0 to W7 and W0b are landed; the web client is live at `https://kvitt.suorra.duckdns.org/app/` in image `v1.0.3`, paired from the owner's laptop browser and checked by the owner across its pages (§11.4).
**Status (2026-10-24, D-211).** W0 to W7 and W0b are landed; the web client is live at `https://kvitt.suorra.duckdns.org/app/` in image `v1.0.3`, paired from the owner's laptop browser and checked by the owner across its pages (§11.4). K3 (the card purchases of FR-22 on the web, W-21 to W-23) landed `faeb752` (D-201 to D-207, v0.40) and ships in image `v1.0.5`; its owner steps (PLAN-K3 §8) follow that deploy.
### 15.1 Scope (D-129)
@@ -3489,9 +3489,10 @@ Owner interview 2026-10-21 (D-129 to D-134). Kvitt has a web client, built in `w
* **Design.** Tokens from `docs/ux/design-spec.md` §2 as CSS custom properties: the same colour roles (light and dark), category colours (§9.7), type scale, spacing, the 4/2 corner-radius language of the charts (D-097), the same icons (Material Symbols, Apache-2.0, bundled as SVG, never from a CDN). The typeface is the platform's system font stack (D-020's "no bundled font"), amounts with tabular figures. Components follow the design spec's anatomy where one exists (ledger rows, balance header, category tile, avatar, segmented controls); desktop layouts are W0's and each plan's, within the tokens.
* **Strings.** A small JSON dictionary per language (`sv.json`, `en.json`). A string that exists in `docs/ux/strings.csv` uses its key and its text; web-only strings get keys prefixed `web_` and are listed in the plan's report (they are not added to `strings.csv`, which is the app's). Placeholders are named (`{name}`), not Android format arguments. As built (W0, `web/README.md` "Strings"): `web/src/i18n/{sv,en}.json` are generated from `docs/ux/strings.csv` plus `web/src/i18n/web-strings.csv` (the shared web-only strings, W0's), with numbered placeholders `{1}`, `{2}` in place of `%1$s`; each feature keeps its own strings in `web/src/features/<name>/i18n/{sv,en}.json` (keys `web_<name>_…`), merged at build time; a string two features render moves to `web-strings.csv` (`83f5293`). Both languages always have every key; the gate checks it.
* **Language.** Swedish when the browser's preferred language is Swedish, English otherwise (G-8); W-13 offers a choice that overrides it in this browser (`localStorage`).
* **Money and dates.** Integer öre everywhere, formatting per §9.8 (the web formats with the same rules and the same outputs as the app for the same input). The money functions of §14.5.2 (`computeSplit`, `computeBalances`, `proposeAllocation`, `validateSettlement`, `overSettledAccounts`) exist in TypeScript and pass `contracts/test-vectors/*.json` for those functions in a Vitest test; `resolveRange` and `spendSeries` (§14.5.4) pass their vectors (W4), the category suggestion of §9.11 passes `suggest_category.json` (W2). The rules live in `kvitt-domain`, `:domain` and the web's ports of them; the vectors keep the three equal (G-1).
* **Money and dates.** Integer öre everywhere, formatting per §9.8 (the web formats with the same rules and the same outputs as the app for the same input). The money functions of §14.5.2 (`computeSplit`, `computeBalances`, `proposeAllocation`, `validateSettlement`, `overSettledAccounts`) exist in TypeScript and pass `contracts/test-vectors/*.json` for those functions in a Vitest test; `resolveRange` and `spendSeries` (§14.5.4) pass their vectors (W4), the category suggestion of §9.11 passes `suggest_category.json` (W2), `matchCardPurchase` (§14.5.8, `web/src/lib/domain/`) passes `match_card_purchase.json` (K3, listed as ported). The rules live in `kvitt-domain`, `:domain` and the web's ports of them; the vectors keep the three equal (G-1).
* **Source layout** (agreed for W0 to W5): `web/src/app/` (entry, providers, router with every route, layout shell, keyboard shortcuts), `web/src/api/` (generated schema, client, token store, household snapshot and writes, §15.5), `web/src/i18n/`, `web/src/design/` (tokens, global styles), `web/src/components/` (shared components), `web/src/lib/` (formatting, the money functions under `lib/domain/`, utilities), `web/src/features/<name>/` (one folder per feature plan, each with its own pages, components, tests and strings). Routes: `/app/` Home, `/app/login`, `/app/accounts/:id`, `/app/expenses/new`, `/app/expenses/:id`, `/app/settle`, `/app/settlements/:id`, `/app/analytics`, `/app/analytics/:groupKind/:groupId`, `/app/insights`, `/app/search`, `/app/cards` (K3, D-174), `/app/settings/*` (`/app/settings/cards` and `/app/settings/cards/:id` K3's); sub-routes and query parameters are the owning plan's.
* **As built (W0b, `5305f37`).** Shared web pieces live once in `web/src/components` (AccountTile, PageStates, Timeline, OverflowMenu, SwitchRow, `editor/`), `lib` (categories, timeline, accountColors), `api` (errors, invites) and `app` (useToday). A feature never imports another feature (`catalog.test.ts` also fails when a feature uses another feature's `web_` key). Amount fields follow the page language: sv `,` or `.` decimal with space grouping, en `.` decimal with `,` grouping. Recharts is its own chunk loaded by the analytics pages only (vendor 516 → 132 kB). The Vitest cache is off. The web's version code is the git commit count at build time, else `KVITT_VERSION_CODE`, else 1.
* **As built (K3, `faeb752`).** Shared card pieces: `web/src/api/cards.ts` (the card snapshot of §15.5 and the E-44 write, with `cards.test.ts`; in `api/` because W-21 to W-23, W-5, W-6, W-3 and W-13 read it), `web/src/test/cards.ts` (fixtures and the fake server's card endpoints), `/cards` in the dev proxy of `web/vite.config.ts`, the icons `credit_card`, `link_off` and `visibility_off` in `web/scripts/gen-icons.mjs` and the generated table, the query names `cardPurchase`, `attempt` and `provider` in `routes.ts` `QUERY` with the builders `cardsPath`, `connectCardPath` and `expensesNewPath({ cardPurchase })` (D-202). Web-only strings `web_cards_*` (load and catalogue failures, the catalogue's loading line, "Filter", the never-fetched failure, `expense_already_linked`, `card_bank_unavailable`, the empty "Välj en annan utgift" list) and `web_expenses_card_load_failed`, `web_expenses_card_missing_title`, `web_expenses_card_link_failed`; the rest are the §13.3 keys.
* **The web gate** (the third gate, beside the Android and server gates): `npm run check` in `web/`, which runs `tsc --noEmit`, `eslint .`, `vitest run`, `vite build` and the generated-types freshness check (it fails when `web/src/api/schema.d.ts` differs from what `openapi-typescript` generates from `contracts/openapi.yaml`). A change to `contracts/openapi.yaml` regenerates `web/src/api/schema.d.ts` in the same landing, by the agent that changes the contract; it is the one file under `web/` that a contract-change plan may write.
### 15.4 Build and serving (D-132)
@@ -3503,6 +3504,7 @@ Owner interview 2026-10-21 (D-129 to D-134). Kvitt has a web client, built in `w
### 15.5 Data: reads, writes, freshness
* **Reads.** Accounts, categories, templates, expenses, settlements, users and audit entries are readable only through `GET /sync` (§7). The web keeps an in-memory household snapshot built from it: after login and on every page load it pulls from `since=0` in pages until `has_more` is false and applies rows as §14.4.2 step 3 does (by `server_version`; the web has no pending rows); afterwards it pulls from its cursor on window focus and after every write. A different `epoch` or a 409 `cursor_ahead` drops the snapshot and pulls from 0. Nothing is persisted across a reload except the token, the language choice and per-viewer conveniences (a remembered filter); there is no IndexedDB copy. `GET /me` (E-7), `GET /devices`, `GET /invites` and the `/analytics/*` endpoints (D-133) are read directly.
* **Card purchases** (K3, D-201). The card snapshot (E-40) is held in memory beside the household snapshot, pulled from 0 on load, from its cursor on focus, every 60 s and after every card write; card writes are sent at once. A 409 `cursor_ahead` pulls it from 0; rows apply by `card_version`, tombstones included; only the viewer's own rows ever arrive (CP-11). W-21 re-reads it every 2 s while a connection is fetching or in its first fetch (CP-2's interval), otherwise at the shared 60 s (D-201). Refreshing it re-reads only the snapshot, not the catalogue (E-36) or an attempt (E-38) (`web/src/api/cards.ts`).
* **Computed in the browser**, as the phone does: balances (§9.3, `computeBalances`), effect lines (§9.2), the settle-up proposal (§9.4, `proposeAllocation`), the S-11 and S-12 analytics (§14.5.4) and the D-128 line chart. The advanced analytics of D-133 are not computed in the browser.
* **Writes.** Expenses and settlements are written with `POST /sync`, one mutation per user action (a new UUIDv7 id for a new row, a new `mutation_id` per attempt, the browser's clock for `updated_at`), and the result is applied to the snapshot at once: `accepted` updates the row; a rejection shows its §14.4.6 reason on the page and changes nothing. Everything else uses its REST endpoint (§7) and applies the returned row. The web never queues a write: a network error shows "Kunde inte nå servern" (`common_could_not_reach_server`) and the form keeps its values (§9.9's rule, for every write). Pictures (E-11) are fetched with the bearer header and shown from a `blob:` URL.
@@ -3516,15 +3518,15 @@ The web's pages. "Mirrors" names the app screen whose content, rules and states
| W-2 | Home | `/app/` | S-4; S-3 while there is no partner | Balance header, Settle up, active accounts with balance and this month's count, collapsed archived section (its open state kept for the page session, D-145), empty state with the "Du och <partner> är med" card in place of the balance header (D-145); no sync pill, no update banner, no pull to refresh | New expense (`N`), Settle up, open account, New account | W1 (`934eee7`) |
| W-3 | Account detail | `/app/accounts/:id` | S-5 | Month groups with month totals, expense and settlement rows, archived badge | New expense (`/app/expenses/new?account=<id>`), Settle this account (`/app/settle?account=<id>`, D-144), Edit, Archive / Unarchive, Delete; "Från kortköp" (`/app/cards?account=<id>`, while the viewer has a card connection, K3, AC-22.9) | W1 (`934eee7`) |
| W-4 | Account editor | `/app/accounts/new`, `/app/accounts/:id/edit` | S-6 | Name, default split with the 100,00 kr preview | Save, Cancel | W1 (`934eee7`) |
| W-5 | Expense editor | `/app/expenses/new` (`?account=<id>` presets the account, D-144), `/app/expenses/:id/edit` | S-7 | Amount, description, category grid with the §9.11 suggestion for a new expense, account, date, payer, split modes `default`, `ratio`, `exact`, `full` with live shares, notes; validation per §9.6 inline; a rejected or failed save in a banner with "Visa nuvarande version", the typed values kept; browser controls in place of S-7's keypad, slider and summary row (D-148) | Save, Delete (edit), Cancel (`Esc`, with the discard dialog) | W2 (`a65ab71`) |
| W-6 | Expense detail | `/app/expenses/:id` | S-8 | Every field, shares and mode, "Recurring: <template>", the audit timeline (AC-12.4, AC-12.5), deleted state | Edit, Delete | W2 (`a65ab71`) |
| W-5 | Expense editor | `/app/expenses/new` (`?account=<id>` presets the account, D-144), `/app/expenses/:id/edit` | S-7 | Amount, description, category grid with the §9.11 suggestion for a new expense, account, date, payer, split modes `default`, `ratio`, `exact`, `full` with live shares, notes; validation per §9.6 inline; a rejected or failed save in a banner with "Visa nuvarande version", the typed values kept; browser controls in place of S-7's keypad, slider and summary row (D-148) | Save, Delete (edit), Cancel (`Esc`, with the discard dialog); from a card purchase `?cardPurchase=<id>` (D-202) prefills per CP-9 and an accepted save links it with E-44: an unknown or deleted purchase shows "Kortköpet finns inte längre", a failed link keeps the expense and says "Utgiften sparades men kunde inte kopplas till kortköpet" (D-205; K3) | W2 (`a65ab71`); K3 (`faeb752`) |
| W-6 | Expense detail | `/app/expenses/:id` | S-8 | Every field, shares and mode, "Recurring: <template>", the audit timeline (AC-12.4, AC-12.5), deleted state; for the card holder the card line of AC-22.10 with the purchase date in the list form (D-206) | Edit, Delete; Ta bort koppling (K3) | W2 (`a65ab71`); K3 (`faeb752`) |
| W-7 | Settle up | `/app/settle` (`?account=<id>` one account, `?settlement=<id>` editing a payment, D-144) | S-9 | Direction with swap, amount, allocation rows with debts and over-settle warnings, allocation errors as you type (D-149), remaining indicator, add account, date, note, "Föreslå igen" | Confirm, Cancel | W3 (`f9a9bfb`) |
| W-8 | Settlement detail | `/app/settlements/:id` | S-10 | Payer, recipient, amount, date, note, allocations, timeline | Edit (`/app/settle?settlement=<id>`), Delete | W3 (`f9a9bfb`) |
| W-9 | Analytics | `/app/analytics` | S-11 | Range, granularity, measure, group-by; total; stacked bars with legend; category and account breakdowns; "Visa som tabell" under each chart, bar labels as keyboard stops (D-152) | Open a drill-down: a bar opens `/app/analytics/bucket/<key>`, a breakdown row `/app/analytics/:groupKind/:groupId`, the filter in the query string (D-151) | W4 (`1aecf51`) |
| W-10 | Drill-down | `/app/analytics/:groupKind/:groupId` (`groupKind` `category`, `account` or `bucket`; query `range`, `from`, `to`, `interval`, `measure`, `group`, `bucket`, D-151) | S-12 with D-128's line chart and average overlays | Title naming the filter, total, line chart for a category or account over the range, expense rows; its own period changes replace the query string (D-151) | Open expense | W4 (`1aecf51`) |
| W-11 | Insights | `/app/insights` | none (D-133) | Per-category averages and trends per month (overall and per category) over 3, 6 or 12 whole months (default 6, D-152), the 2-month forecast per account with its parts (recurring, average) | Choose category or account; choose months | W4 (`1aecf51`) |
| W-12 | Search | `/app/search` | none (D-129) | Free-text search in descriptions and notes (case-insensitive), filters for amount range, date range, account, category, payer, kept in the URL query string (D-147); result count and sum; expense rows | Open expense; Export CSV of the filtered list (format D-146) | W2 (`a65ab71`) |
| W-13 | Settings | `/app/settings` | S-13 | Own and partner's avatar and name; rows Profile, Categories, Recurring expenses, Devices, Invites (admin), About, Language; Log out | Navigate; Log out | W5 (`3ebe57d`) |
| W-13 | Settings | `/app/settings` | S-13 | Own and partner's avatar and name; rows Profile, Categories, Recurring expenses, Devices, Invites (admin), About, Language; "Anslut kort" (S-13's row: none, connected with `valid_until` in the detail form (D-206), or `settings_cards_expired`; K3); Log out | Navigate; Log out | W5 (`3ebe57d`); K3 (`faeb752`) |
| W-14 | Profile | `/app/settings/profile` | S-14 (and S-2 once after a `partner` login, as an intro remembered per browser device in `kvitt.profileStepDone`, D-154) | Avatar with change and remove (file picker), display name | Save | W5 (`3ebe57d`) |
| W-15 | Categories | `/app/settings/categories`, editor `/app/settings/categories/new` and `/app/settings/categories/:id` (D-153) | S-15 | List with archived section; editor with name, reset name, 33 icons, 14 colours, delete or archive with the reason | Add, Edit, Reorder (drag, and by keyboard), Archive, Delete | W5 (`3ebe57d`) |
| W-16 | Recurring expenses | `/app/settings/recurring`, editor `/app/settings/recurring/new` and `/app/settings/recurring/:id` (D-153) | S-19 | List and editor as S-19 | Add, Edit, Pause / Resume, Delete | W5 (`3ebe57d`) |
@@ -3532,11 +3534,11 @@ The web's pages. "Mirrors" names the app screen whose content, rules and states
| W-18 | Invites | `/app/settings/invite` (D-153; `/app/settings/invites` opens it too) | S-21 | Invite partner (before the partner joins) and pair a device; a created invite's code in groups of 4, link and expiry; pending invites | Create, Copy (code and link, to the clipboard), Cancel | W5 (`3ebe57d`) |
| W-19 | About | `/app/settings/about` | S-22 | The web build's version name and code; the server's version and schema from E-1 `/health` when it answers (on the LAN), otherwise the `X-Kvitt-Server-Version` header of a fresh `GET /me` without the schema (the S-22 fallback, D-110, D-154; `/health` is closed from outside, §15.2) | none | W5 (`3ebe57d`) |
| W-20 | Keyboard help | dialog on `?` | none (D-129) | The shortcuts and what they do | Close (`Esc`) | W0 |
| W-21 | Card purchases | `/app/cards` (`?attempt=<id>` after the bank's return, CP-2; `?account=<id>` from W-3's "Från kortköp", which shows the purchases to handle and presets that account for "Skapa utgift", as S-27) | S-23, S-27 (D-174) | The header card with artwork, "Uppdaterad", "Uppdatera"; filters; day groups; rows with the match chip; the purchase as a side panel from 720 px (a dialog below) with the S-23 sheet's actions; "Skapa utgift" opens W-5 at `/app/expenses/new?cardPurchase=<id>` (plus `&account=<id>` when W-21 was opened from an account; D-144's parameter style), prefilled per CP-9, and saving links the purchase; W-6 shows the card holder the line of AC-22.10; with `?attempt=` it polls E-38 until the attempt ends, then drops the parameter; states as S-23 without the offline one | Uppdatera; filter; Skapa utgift; link; Välj en annan utgift; Ignorera; Ta bort koppling; Förnya med BankID | K3 |
| W-22 | Connect card | `/app/settings/cards` | S-24, S-25 (D-174) | The catalogue with artwork; for a card not connected, the history choice and "Anslut med BankID", which opens the bank's page in the same tab (BankID by QR on the computer); returning through the callback page's link lands on W-21 | Anslut med BankID; open a connected card (W-23) | K3 |
| W-23 | Card connection | `/app/settings/cards/:id` | S-26 (D-174) | As S-26 | Change history; Förnya med BankID; Koppla från | K3 |
| W-21 | Card purchases | `/app/cards` (`?attempt=<id>` after the bank's return, CP-2; `?account=<id>` from W-3's "Från kortköp", which shows the purchases to handle and presets that account for "Skapa utgift", as S-27) | S-23, S-27 (D-174) | The header card with artwork, "Uppdaterad", "Uppdatera"; filters; day groups; rows with the match chip; the purchase as a side panel from 720 px (a dialog below) with the S-23 sheet's actions; "Skapa utgift" opens W-5 at `/app/expenses/new?cardPurchase=<id>` (plus `&account=<id>` when W-21 was opened from an account; D-144's parameter style), prefilled per CP-9, and saving links the purchase; W-6 shows the card holder the line of AC-22.10; with `?attempt=` it polls E-38 until the attempt ends, then drops the parameter; states as S-23 without the offline one. As built: polling E-38 every 2 s; once `?attempt=` is dropped, a connected attempt shows the snackbar "Kortet är anslutet", a failed one a banner "Det gick inte att ansluta kortet" and an expired one "Anslutningsförsöket tog för lång tid", each with "Försök igen" to W-22 `?provider=` (D-204); E-40 re-read every 2 s while fetching (D-201); the expiring banner shows `valid_until` in the detail form (D-206); with `?account=` it is S-27: title "Från kortköp", rows open W-5 directly, no filter chips, no panel (D-207) | Uppdatera; filter; Skapa utgift; link; Välj en annan utgift; Ignorera; Ta bort koppling; Förnya med BankID | K3 (`faeb752`) |
| W-22 | Connect card | `/app/settings/cards` | S-24, S-25 (D-174) | The catalogue with artwork; for a card not connected, the history choice and "Anslut med BankID", which opens the bank's page in the same tab (BankID by QR on the computer); returning through the callback page's link lands on W-21. As built: `?provider=<id>` opens the connect step (S-25) for that card, also from "Förnya med BankID" (D-202); history 7, 30, 90 or 180 days; E-37 with `client` `web`; only an `https:` `auth_url` is followed, anything else shows "Det gick inte att ansluta kortet" (D-203); a connected card's tile shows `valid_until` in the detail form (D-206) | Anslut med BankID; open a connected card (W-23) | K3 (`faeb752`) |
| W-23 | Card connection | `/app/settings/cards/:id` | S-26 (D-174) | As S-26: status, `valid_until`, last fetch, history (a shorter one through E-41), the disconnect dialog with "erase handled purchases" (E-43 `erase_handled`) | Change history; Förnya med BankID (W-22 `?provider=`); Koppla från | K3 (`faeb752`) |
Navigation (W0): a sidebar from 720 px wide and a bottom bar under it, with Home, Card purchases (Kortköp, after Home, D-172, D-174; K3 adds it), Analytics, Insights, Search and Settings; New expense is a button in the sidebar and on Home and account detail (the app's FAB). Dialogs close with `Esc`. Destination changes are instant (D-108).
Navigation (W0): a sidebar from 720 px wide and a bottom bar under it, with Home, Card purchases (Kortköp, after Home, D-172, D-174; added by K3, `faeb752`), Analytics, Insights, Search and Settings; New expense is a button in the sidebar and on Home and account detail (the app's FAB). Dialogs close with `Esc`. Destination changes are instant (D-108).
### 15.7 Search, export and shortcuts (D-129)
@@ -3565,7 +3567,7 @@ Plans with disjoint paths (§14.7), one Opus agent each in its own worktree. W0
| W5 Settings | `web/src/features/settings/**` | W-13 to W-19 |
| W6 Android Insights | `android/feature/analytics/**`, and as a UI-contract change (§14.8) `android/core/model/**`, `android/data/**`, `android/core/fakes/**` | The D-133 endpoints on S-11 |
| W7 Server web serving and analytics | `server/**`, `contracts/**`, `tools/contracts-check.py`, `.gitea/**`, `infra/build-images.sh`, `infra/dev/**`, and `web/src/api/schema.d.ts` when it changes `contracts/openapi.yaml` | §15.4, §15.8 |
| K3 Web card purchases (D-174) | `web/src/features/cards/**`; in shared code the route, the sidebar entry and `web/src/lib/domain/` (`matchCardPurchase`); `web/src/features/expenses/**`, `web/src/features/accounts/**` and `web/src/features/settings/**` for W-5's prefill, W-6's card line, W-3's "Från kortköp" and W-13's row (§14.7) | W-21 to W-23, landing after K1 and K2 |
| K3 Web card purchases (D-174) | `web/src/features/cards/**`; in shared code the route, the sidebar entry and `web/src/lib/domain/` (`matchCardPurchase`); `web/src/features/expenses/**`, `web/src/features/accounts/**` and `web/src/features/settings/**` for W-5's prefill, W-6's card line, W-3's "Från kortköp" and W-13's row (§14.7) | W-21 to W-23; landed `faeb752` after K1 and K2 |
---
@@ -3613,3 +3615,4 @@ Plans with disjoint paths (§14.7), one Opus agent each in its own worktree. W0
| 0.37 | 2026-10-24 | D-211: image builds on the Mac with Podman (`infra/build-images.sh v1.0.N`) are the normal path, the Gitea workflow on the Pi 3 runner the fallback (revises D-156 point 5); tag rule (the next `v1.0.N` after the highest tag on `origin`, pushed as a git tag too, `v1.0.3` = `8fa9198`); the registry from the Mac is `gitea.lan` on port 80, the Pi's `.env` keeps `gitea.lan:3001`; Podman's insecure-registry files on the Mac and in the Podman machine; the script fixes `f15fa23` and `8fa9198` (§10.3, §10.10). `v1.0.3` deployed on the Kvitt Pi and the web client and S-11's Insights verified by the owner (§11.2 W1 to W7, §11.3, §11.4, §15). No contract file changes. |
| 0.38 | 2026-10-24 | K1's report (card purchases on the server, landed `671c00d`: migration 0004 byte-identical to §14.1.3, server schema 4 in code, E-36 to E-44 with four new error codes and a 502 `BadGateway` response in `contracts/openapi.yaml`, examples, `match_card_purchase.json` with its Python reference; gates at landing: server 356 tests, contracts-check 157, web 609, nginx check and compose configs ok, the Android gate red until K2 lands), D-181 to D-188 (D-189 and D-190 unused). D-181: the history counts today, `date_from` = today − (`history_days` − 1), 6 windows for 180 days (revises CP-3 and CP-7). D-182: the server's CS-4 first token for CP-8 uses Rust's standard Unicode classes without NFC. D-183: Enable Banking's errors mapped to CP-14 (`EXPIRED_SESSION`, `CLOSED_SESSION`, `REVOKED_SESSION`, `SESSION_DOES_NOT_EXIST` → `consent_expired` and `expired`; `ASPSP_RATE_LIMIT_EXCEEDED` or 429 → `rate_limited`; `ASPSP_ERROR`, `ASPSP_TIMEOUT`, 408, 5xx or no answer → `bank_unavailable`; anything else `failed`); E-37's `card_bank_unavailable` when `/aspsps` or `/auth` fails. D-184: the callback (an expired `state` stores `expired`, revising CP-2's `failed`; a used one changes nothing; neither `code` nor `error` → `failed`/`bank_error`; a session without accounts is revoked; the atomic claim). D-185: the poller's start-up slot, background starts counted whatever their outcome, deletion after `WRONG_TRANSACTIONS_PERIOD` only over the windows that answered, a split window's cut rows discarded. D-186: PSU headers only with a value, `/aspsps` cached 24 hours, the two fixed headers when it fails. D-187: E-41 with the current value and E-43 on a disconnected connection make no new version (`erase_handled=true` still erases), E-44's `field` for a credit purchase, `CardAttempt.failure` a plain nullable string. D-188: `src/wire/card.rs`, row helpers in `src/cards/mod.rs`, the client IP in a request extension, and the Pi's `compose.yaml` refusing to start until `secrets/cards/` exists. Changed: §9.12 CP-2, CP-3, CP-4, CP-5, CP-7, CP-8, CP-14; §10.2; §11.1, §11.2 K1 row, §11.3 (K2's text goes into v0.39, K3's into v0.40); §14.1.3; §14.3.3; §14.3.4 E-1 (`schema_version` 4), E-37, E-39, E-41, E-43, E-44; §14.3.5; §14.7. |
| 0.39 | 2026-10-24 | K2's report (card purchases on Android, landed `a8f91ee`: Room version 2 with `MIGRATION_1_2`, its test and the exported `2.json`; the `CardRepository` UI contract of §14.2.4 unchanged in its names; the card cycle; `:feature:cards` S-23 to S-27; S-5, S-7, S-8, S-13 and the fourth bottom destination; Android gate at landing `BUILD SUCCESSFUL`, 791 actionable tasks, 1154 tests, 0 failures, `VectorFilesTest` and `ExampleRoundTripTest` green again; 150 goldens added, 147 re-recorded for the bar), D-191 to D-200. D-191: the card cycle after each successful household cycle and after `refresh`, never changing the household outcome; E-40's 404 is "no card support", retried every cycle. D-192: E-44's `expense_already_linked` or `validation_failed` resets the row's `card_version` to 0 and moves the card cursor back before it (§14.2.1). D-193: links to expenses no longer on the phone cleared before E-44 and at every local delete or rejection that removes an expense. D-194: the E-36 catalogue cached in DataStore (`card_providers`, §14.2.2). D-195: after a connect or E-42's `fetching`, E-40 every 3 s, at most 40 times, until `last_fetch_at` or `last_fetch_error` changes. D-196: the return link held by `AppViewModel`: S-25 polls it at once, else one E-38 call and S-23. D-197: bottom-bar labels shrink to fit down to 70 % (design-spec §3.11). D-198: S-23's filter chips wrap; S-25's and S-26's history as wrapping chips (revises S-25's segmented choice). D-199: S-13 "Har upphört" for an expired connection; S-5's "Från kortköp" hidden on archived accounts. D-200: four added string keys and the card artwork measures in `KvittDimens`. §13.2 (the destination enum as built), §13.3 S-5, S-13, S-23 to S-27 as built and the added keys, §14.2.1 to §14.2.4 (Room 2 landed; readings fixed in v0.39), §11.2 K2 row, §11.3, new OQ-9 (`CardPurchase.linked` while the expense is missing, §12). `docs/ux/design-spec.md`: S-23 to S-27 sections from the goldens, §3.11 four destinations and shrink-to-fit, §3.12 card artwork, the `credit_card` icon, S-5, S-7, S-8, S-13 card states. No contract file changes in this pass (Room 2 is K2's). contracts-check 157. |
| 0.40 | 2026-10-24 | K3's report (card purchases on the web, landed `faeb752`; web gate at landing 70 files, 684 tests, build ok; contracts-check 157), D-201 to D-207 (D-208 to D-210 unused). Built: W-21 `/app/cards` (every §13.3 state but offline, the S-23 sheet actions, `?attempt=` polling E-38 every 2 s, `?account=` as S-27), W-22 `/app/settings/cards` (catalogue, connect step `?provider=`), W-23 `/app/settings/cards/:id` (history, renew, disconnect with erase); W-5's `?cardPurchase=` prefill (CP-9) and link on save; W-6's card line with "Ta bort koppling"; W-3's "Från kortköp"; W-13's "Anslut kort" row; Kortköp after Översikt in the sidebar and bottom bar; `matchCardPurchase` in `web/src/lib/domain/` passing the 17 vectors (ported). Shared pieces outside PLAN-K3 §4, ratified at landing: `web/src/api/cards.ts` with its test, `web/src/test/cards.ts`, `/cards` in the dev proxy, the icons `credit_card`, `link_off`, `visibility_off`, `Shell.test.tsx`; a refresh of the card snapshot no longer re-reads the catalogue and attempt queries (shared key prefix, now exact). D-201: W-21 re-reads E-40 every 2 s while a connection is fetching or in its first fetch, otherwise every 60 s. D-202: query names `cardPurchase` (W-5), `attempt` (W-21), `provider` (W-22, S-25, also "Förnya med BankID"). D-203: only an `https:` `auth_url` is followed. D-204: the outcome after `?attempt=` is dropped (snackbar "Kortet är anslutet"; a banner with "Försök igen" to W-22 `?provider=`). D-205: W-5 for an unknown or deleted purchase, and a failed E-44 after save. D-206: `valid_until` in the detail date form (W-21 banner, W-22 tile, W-13 row), W-6's line in the list form. D-207: W-21 with `?account=` is S-27 (title "Från kortköp", rows open W-5, no chips, no panel). Changed: §11.1, §11.2 K3 row, §11.3, §14.7 K3 row, §15 status, §15.3, §15.5 (the card snapshot), §15.6 W-5, W-6, W-13, W-21 to W-23 and the navigation paragraph, §15.9. No contract file changes. |