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. |
+13 -8
View File
@@ -3,8 +3,8 @@
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
## 1. Where things stand
- State as of 2026-10-24. `main` at `faeb752` (**K3 landed**: card purchases on the web, W-21 to W-23; its report goes into SPEC v0.40, next docs pass), after `a8f91ee` (**K2 landed**: card purchases on Android, Room 2, S-23 to S-27; recorded in SPEC v0.39 by this pass), `07df30d` (SPEC v0.38, K1's report), `671c00d` (**K1 landed**: card purchases on the server, schema 4, E-36 to E-44), `ae73f58` (SPEC v0.37, D-211), `8b620c8` (the K0 report, SPEC v0.36), `8fa9198` (`infra/build-images.sh` Podman detection), `f15fa23` (`infra/build-images.sh` passes `KVITT_VERSION_CODE`), `4339d01` (the owner's SJ Prio card artwork), `b5114ad` (SPEC v0.35, card purchases), `2ce317f` (the K0 card test app), `5d948f9` (SPEC v0.34) and `5305f37` (W0b web shared follow-ups); before them `07c36d2` (L-10, D-156) and `593c727` (SPEC v0.33, D-156, D-157). This pass (`agent-docs-k2`): SPEC v0.39, K2's report as D-191 to D-200, the design-spec sections S-23 to S-27; `main` with this pass is pushed by the orchestrator after it lands. **Deployed and verified 2026-10-24** (owner, D-211): image `v1.0.3` (git tag `v1.0.3` = `8fa9198`, built on the Mac) runs on the Kvitt Pi, `server` and `backup` healthy; the web client is live at `https://kvitt.suorra.duckdns.org/app/`, paired from the laptop browser with a device invite, and the owner reports "All seems to work" for Home and an account, add, edit and delete an expense, CSV export, record and delete a payment, analytics with category drill-down and Insights, the settings pages, revoking the browser from the phone, and the Android Insights section on S-11 against the new server. W0 to W7 are landed and verified (SPEC §11.2, §15). Release 140 (`kvitt-140`, deployed 2026-10-21) runs on both phones. Gates: server 356 tests at `671c00d`; contracts-check 157 (17 `match_card_purchase` cases); Android green again at `a8f91ee` (`BUILD SUCCESSFUL`, 791 actionable tasks, 1154 tests, 0 failures; the card examples E-36 to E-44 round-trip, `VectorFilesTest` and `ExampleRoundTripTest` pass), so the pre-commit hook needs no bypass any more; the web gate at `faeb752` is K3's to report (v0.40).
- **Card purchases programme** (owner request and answers 2026-10-24, SPEC v0.35, D-158 to D-176, FR-22, §9.12): each user connects their own SJ Prio Mastercard (SEB Kort) through Enable Banking's restricted production mode with BankID; the server alone holds the key and sessions, fetches purchases up to 4 times a day in the background plus "Uppdatera", and only the card holder sees them (not in `GET /sync`, audit, push or analytics); a fourth bottom destination "Kortköp" (Översikt, Kortköp, Statistik, Inställningar, revising D-048), "Anslut kort" in Settings with the SJ Prio artwork, "Skapa utgift" prefilled into S-7, one-tap links to matching expenses (same amount, ±3 days), "Från kortköp" on account detail; server schema 4 (migration 0004), Room 2, endpoints E-36 to E-44, screens S-23 to S-27, web W-21 to W-23. Plans `PLAN-K0.md` to `PLAN-K3.md`, prompts `prompts/K0.md` to `prompts/K3.md`, written by `agent-a3b7abf183f883261`, landed `b5114ad`. **K0 done** (2026-10-24): the test app (`2ce317f`) ran with the owner on his card; report [`docs/spikes/K0-enable-banking.md`](spikes/K0-enable-banking.md); its answers are SPEC v0.36 (D-177 to D-180). Main findings: the card returns at most 100 transactions per response and never a continuation key, so history is fetched in windows of at most 30 days, split in half at 100 rows (D-178); merchant text from `remittance_information`, the date `booking_date` else `value_date`, everything in SEK, `entry_reference` on pending rows too (D-179); consent 180 days; background fetch accepted; at least 150 days of history right after BankID. **Owner's assumptions** (2026-10-24, "let's just assume it all works now given your test", D-177), verified by K1's owner steps (PLAN-K1 §8 steps 5 to 7): the partner's card links to the owner's application (fallback: her own application, D-175), a pending purchase keeps its `entry_reference` when booked (fallback: CP-8's amount-and-date rule), the session is reused the next day without BankID. The Enable Banking application exists (restricted production, redirect URL `https://kvitt.suorra.duckdns.org/cards/callback`, the owner's card linked); its key file lives only in the main checkout's git-ignored `docs/spikes/k0/.local/<application id>.pem` and reaches the Pi through K1's owner steps (`~/kvitt/secrets/cards/enable-banking.pem`, D-175), never downloaded again. The artwork is committed for Android and the web, cropped to the card (D-180). **K1 landed** `671c00d` (2026-10-24): migration 0004 (byte-identical contract and server copies), `src/cards/` with the registry, the Enable Banking adapter, the windowed fetch, the poller and the callback page, E-36 to E-44 with four new error codes and a 502 response, examples, `match_card_purchase.json` (17 cases) with its Python reference, `tests/cards_api/` (34 tests against a local Enable Banking stub; the real API is never called), the Pi compose mount of `secrets/cards/` and the NPM `/cards/callback` block; its report is SPEC v0.38 (D-181 to D-188). **K2 landed** `a8f91ee` (2026-10-24): Room version 2 (`MIGRATION_1_2` with its test, `2.json`), the card cycle after each household cycle (D-191 to D-196), `:feature:cards` S-23 to S-27 with 135 goldens, S-5 "Från kortköp", S-7 prefill and link, S-8 card line, S-13 "Anslut kort", the fourth bottom destination "Kortköp" (labels shrink to fit, D-197), 77 `strings.csv` rows, new catalog entry `androidx.browser` 1.10.0; recorded in SPEC v0.39 (D-191 to D-200, OQ-9). The phone was not attached, so nothing of K2 has run on a device yet; the orchestrator builds and installs the release APK with the card screens after landing. **K3 landed** `faeb752` (web W-21 to W-23); its report is recorded next (v0.40, decisions from D-201 to D-210). The key is staged at `infra/secrets/cards/enable-banking.pem` in the main checkout (git-ignored); the application id is known to the orchestrator (not written here, CP-13).
- State as of 2026-10-25. **K0 to K3 are all landed.** `main` at `6f63751` (SPEC v0.39, K2's report, D-191 to D-200, design-spec S-23 to S-27, `agent-docs-k2`), after `faeb752` (**K3 landed**: card purchases on the web, W-21 to W-23; recorded in SPEC v0.40 by this pass), `a8f91ee` (**K2 landed**: card purchases on Android, Room 2, S-23 to S-27), `07df30d` (SPEC v0.38, K1's report), `671c00d` (**K1 landed**: card purchases on the server, schema 4, E-36 to E-44), `ae73f58` (SPEC v0.37, D-211), `8b620c8` (the K0 report, SPEC v0.36), `8fa9198` (`infra/build-images.sh` Podman detection), `f15fa23` (`infra/build-images.sh` passes `KVITT_VERSION_CODE`), `4339d01` (the owner's SJ Prio card artwork), `b5114ad` (SPEC v0.35, card purchases), `2ce317f` (the K0 card test app), `5d948f9` (SPEC v0.34) and `5305f37` (W0b web shared follow-ups); before them `07c36d2` (L-10, D-156) and `593c727` (SPEC v0.33, D-156, D-157). This pass (`agent-docs-k3`): SPEC v0.40, K3's report as D-201 to D-207 (D-208 to D-210 unused), `web/README.md` and `prompts/README.md`; `main` with this pass is pushed by the orchestrator after it lands. **Deployed** (owner's report): image `v1.0.4` (git tag `v1.0.4` = `671c00d`, K1's server, schema 4) runs on the Kvitt Pi, so K1's owner steps 1 to 4 are done; image `v1.0.5` (`faeb752`, the web card pages of K3) is being built on the Mac; release APK `kvitt-164` (K2's card screens) is on the Pi since 2026-10-25 for the in-app update. **Deployed and verified 2026-10-24** (owner, D-211): image `v1.0.3` (git tag `v1.0.3` = `8fa9198`, built on the Mac) runs on the Kvitt Pi, `server` and `backup` healthy; the web client is live at `https://kvitt.suorra.duckdns.org/app/`, paired from the laptop browser with a device invite, and the owner reports "All seems to work" for Home and an account, add, edit and delete an expense, CSV export, record and delete a payment, analytics with category drill-down and Insights, the settings pages, revoking the browser from the phone, and the Android Insights section on S-11 against the new server. W0 to W7 are landed and verified (SPEC §11.2, §15). Release 140 (`kvitt-140`, deployed 2026-10-21) ran on both phones until `kvitt-164`; whether the phones have updated is not recorded here (PLAN-K2 §8 step 1 starts with it). Gates: server 356 tests at `671c00d`; contracts-check 157 (17 `match_card_purchase` cases); Android green again at `a8f91ee` (`BUILD SUCCESSFUL`, 791 actionable tasks, 1154 tests, 0 failures; the card examples E-36 to E-44 round-trip, `VectorFilesTest` and `ExampleRoundTripTest` pass), so the pre-commit hook needs no bypass any more; web at `faeb752` 70 files, 684 tests, build ok (`match_card_purchase` ported).
- **Card purchases programme** (owner request and answers 2026-10-24, SPEC v0.35, D-158 to D-176, FR-22, §9.12): each user connects their own SJ Prio Mastercard (SEB Kort) through Enable Banking's restricted production mode with BankID; the server alone holds the key and sessions, fetches purchases up to 4 times a day in the background plus "Uppdatera", and only the card holder sees them (not in `GET /sync`, audit, push or analytics); a fourth bottom destination "Kortköp" (Översikt, Kortköp, Statistik, Inställningar, revising D-048), "Anslut kort" in Settings with the SJ Prio artwork, "Skapa utgift" prefilled into S-7, one-tap links to matching expenses (same amount, ±3 days), "Från kortköp" on account detail; server schema 4 (migration 0004), Room 2, endpoints E-36 to E-44, screens S-23 to S-27, web W-21 to W-23. Plans `PLAN-K0.md` to `PLAN-K3.md`, prompts `prompts/K0.md` to `prompts/K3.md`, written by `agent-a3b7abf183f883261`, landed `b5114ad`. **K0 done** (2026-10-24): the test app (`2ce317f`) ran with the owner on his card; report [`docs/spikes/K0-enable-banking.md`](spikes/K0-enable-banking.md); its answers are SPEC v0.36 (D-177 to D-180). Main findings: the card returns at most 100 transactions per response and never a continuation key, so history is fetched in windows of at most 30 days, split in half at 100 rows (D-178); merchant text from `remittance_information`, the date `booking_date` else `value_date`, everything in SEK, `entry_reference` on pending rows too (D-179); consent 180 days; background fetch accepted; at least 150 days of history right after BankID. **Owner's assumptions** (2026-10-24, "let's just assume it all works now given your test", D-177), verified by K1's owner steps (PLAN-K1 §8 steps 5 to 7): the partner's card links to the owner's application (fallback: her own application, D-175), a pending purchase keeps its `entry_reference` when booked (fallback: CP-8's amount-and-date rule), the session is reused the next day without BankID. The Enable Banking application exists (restricted production, redirect URL `https://kvitt.suorra.duckdns.org/cards/callback`, the owner's card linked); its key file lives only in the main checkout's git-ignored `docs/spikes/k0/.local/<application id>.pem` and reaches the Pi through K1's owner steps (`~/kvitt/secrets/cards/enable-banking.pem`, D-175), never downloaded again. The artwork is committed for Android and the web, cropped to the card (D-180). **K1 landed** `671c00d` (2026-10-24): migration 0004 (byte-identical contract and server copies), `src/cards/` with the registry, the Enable Banking adapter, the windowed fetch, the poller and the callback page, E-36 to E-44 with four new error codes and a 502 response, examples, `match_card_purchase.json` (17 cases) with its Python reference, `tests/cards_api/` (34 tests against a local Enable Banking stub; the real API is never called), the Pi compose mount of `secrets/cards/` and the NPM `/cards/callback` block; its report is SPEC v0.38 (D-181 to D-188). **K2 landed** `a8f91ee` (2026-10-24): Room version 2 (`MIGRATION_1_2` with its test, `2.json`), the card cycle after each household cycle (D-191 to D-196), `:feature:cards` S-23 to S-27 with 135 goldens, S-5 "Från kortköp", S-7 prefill and link, S-8 card line, S-13 "Anslut kort", the fourth bottom destination "Kortköp" (labels shrink to fit, D-197), 77 `strings.csv` rows, new catalog entry `androidx.browser` 1.10.0; recorded in SPEC v0.39 (D-191 to D-200, OQ-9). The phone was not attached, so nothing of K2 has run on a device yet; the orchestrator builds and installs the release APK with the card screens after landing. **K3 landed** `faeb752` (2026-10-24): 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 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, the card snapshot in `web/src/api/cards.ts`, `matchCardPurchase` passing its 17 vectors; files outside PLAN-K3 §4 ratified at landing (`api/cards.ts`, `test/cards.ts`, `vite.config.ts`, the icon generator and table, `Shell.test.tsx`); recorded in SPEC v0.40 (D-201 to D-207). The key is staged at `infra/secrets/cards/enable-banking.pem` in the main checkout (git-ignored); the application id is known to the orchestrator (not written here, CP-13).
- **Verified 2026-10-24** (owner, D-156): Samsung Auto Blocker was the cause of the silent update failure; turning it off let the update install, and the owner turned it back on, so every in-app update on the Samsung phones needs it off (the D-155 blocked text names it; SPEC AC-16.3, `infra/README.md`). The editor keyboard fix (S-19, S-15, S-6, `05ec3f3`) works on the device. The first backup on the Kvitt Pi ran and a snapshot pair is in the Drive folder (AC-18.1). The runner Pi's disk reached 11 of 15 GB during image builds; the owner runs `docker builder prune -af` between builds (SPEC §10.3).
- **Kvitt host name** (owner decision 2026-10-19): `kvitt.suorra.duckdns.org`, a subdomain of the owner's DuckDNS family domain `suorra.duckdns.org`; NPM issues its Let's Encrypt certificate; `KVITT_PUBLIC_HOST=kvitt.suorra.duckdns.org`, so `KVITT_PUBLIC_URL=https://kvitt.suorra.duckdns.org` (port 443, D-124). It is in `infra/pi/.env.example` and `infra/pi/.env` (`KVITT_PUBLIC_HOST`) and `android/local.properties` (`kvitt.publicHost`, git-ignored). SPEC §10.1 and §10.5.1 point here.
- **NPM proxy host** for `kvitt.suorra.duckdns.org` is Online with a Let's Encrypt certificate (owner, 2026-10-19). The router needs no new rule: external TCP 443 already forwards to the Home Assistant Pi (owner confirmed, D-124).
@@ -79,7 +79,7 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
- Disk: the MacBook had about 72 GB free during the v0.20 docs pass (2026-10-19); run `tools/clean-builds.sh` when it drops under about 5 GB.
## 2. Open work (one line each, with who owns it: agent or owner)
- K1 (`671c00d`, v0.38), K2 (`a8f91ee`, v0.39) and K3 (`faeb752`, report pending for v0.40) landed. The image with K1 is built as `v1.0.4` (git tag on `671c00d`); its deploy on the Pi is K1's owner steps 1 to 4 (§3; the orchestrator records the owner's report). Then the release APK with the card screens (orchestrator), PLAN-K2 §8 steps 1 to 7 and PLAN-K1 §8 steps 5 to 7 (§3), which verify K0's three assumptions (D-177). Docs agent next: K3's report as SPEC v0.40.
- K1 (`671c00d`, v0.38), K2 (`a8f91ee`, v0.39) and K3 (`faeb752`, v0.40) landed and recorded. `v1.0.4` (K1) is deployed on the Pi (owner); release APK `kvitt-164` (K2) is on the Pi. Next: finish and deploy image `v1.0.5` (`faeb752`, orchestrator builds, owner deploys), then the owner steps of §3: PLAN-K2 §8 steps 1 to 7, PLAN-K3 §8 steps 1 to 3 (laptop, after `v1.0.5`), PLAN-K1 §8 steps 5 to 7 (relayed one at a time; they verify K0's three assumptions, D-177).
- OQ-9 (owner, §4): `CardPurchase.linked` cannot tell "unlinked" from "linked to an expense not yet on the phone"; a `linkedExpenseId` field would be a contract change.
- K2's dependency report (agent, next Android pass): datastore 1.2.1 → 1.3.0, kotlinx-serialization 1.11.0 → 1.12.0, roborazzi 1.76.0 → 1.77.0, firebaseBom 35.0.2 → 35.1.0 (each one minor behind); Gradle 9.8.1 → 9.8.2 (patch).
- K0 test app session in `docs/spikes/k0/.local/state.json` (owner): keep it or press "9 Koppla från" in the test app (PLAN-K0 §6 step 7); K1 does not use it. The key file stays in `.local/` either way.
@@ -115,13 +115,17 @@ Done 2026-10-21: the Pi install (stack running, `v1.0.1`, both containers health
Standing note for every in-app update on the Samsung phones: turn Auto Blocker off before tapping "Uppdatera", back on afterwards (D-156). Images are built on the Mac (D-211); only if the fallback workflow runs: `docker builder prune -af` on the runner Pi between two of its builds.
1. Gate (step 4a items 4 and 5), both phones run the keyed APK (release 140): NPM, Hosts, Proxy Hosts, Kvitt row, Edit, Advanced: paste the step 4 block with the key in place of `KVITT_APP_KEY`, Save (row Online, yes/no). Acceptance test: on home Wi-Fi Chrome opens `https://kvitt.suorra.duckdns.org/i/TEST` and the invite page shows (yes/no); with Wi-Fi off the same URL gives a connection error (yes/no); with Wi-Fi off Kvitt's About screen shows the server version (yes/no).
2. Web client gate lines (D-130, SPEC v0.28): when the NPM block is pasted (item 1, or again if the D-126 block is already in NPM), paste the current `infra/npm/kvitt.conf` content (step 4), which also lets the web client through and sets its Content-Security-Policy; Save (row Online, yes/no). The image with the web client is deployed (`v1.0.3`, D-211); acceptance test with Wi-Fi off on the phone: `https://kvitt.suorra.duckdns.org/app/` shows the Kvitt login page (yes/no) and `https://kvitt.suorra.duckdns.org/health` still gives a connection error (yes/no) (step 4a item 5 d and e).
3. Card purchases: K0's owner steps are done (2026-10-24: Enable Banking account and Terms of Service, the application, the key in `docs/spikes/k0/.local/`, the owner's card linked, the test app run); the artwork is committed (D-180). **K1's owner steps 1 to 4 are pending** (PLAN-K1 §8), relayed by the orchestrator in chat after the image with K1 is built: step 1 is done by the orchestrator (the key staged at `infra/secrets/cards/enable-banking.pem` in the main checkout, git-ignored, mode 0600); step 2 on the Pi in `~/kvitt`: `mkdir -p secrets/cards && chmod 700 secrets/cards` before anything else (the new `compose.yaml` refuses to start without that folder, even with no card configured, D-188), `scp` the key, `chmod 600 secrets/cards/*`, copy the new `compose.yaml`, set `KVITT_EB_APP_ID=` in `.env` (the orchestrator gives the id in chat) and leave `KVITT_EB_MEMBER_APP_ID=` empty; step 3 paste the current `infra/npm/kvitt.conf` into NPM (row Online, yes/no); step 4 backup, `KVITT_TAG`, `docker compose pull && docker compose up -d` (`server` healthy, `/health` shows `"schema_version":4`, yes/no). The partner's card, the next-day check and pending to booked (PLAN-K1 §8 steps 5 to 7) follow once a client with the card screens is deployed.
4. Card purchases on the phone (PLAN-K2 §8, all pending; after K1's image runs on the Pi and the release APK with the card screens is installed, Auto Blocker off for the update and back on afterwards): 1 Inställningar › Anslut kort › SJ Prio Mastercard › 30 dagar › "Anslut med BankID", sign in BankID, back in Kvitt "Kortet är anslutet" and the Kortköp tab lists the purchases (yes/no each); 2 Kortköp › a purchase › "Skapa utgift" › "Spara": the expense is in its account and the purchase under "Kopplade" (yes/no); 3 on a purchase the partner already entered, the row's "Koppla till …" moves it to "Kopplade" (yes/no); 4 flight mode on, "Ignorera" a purchase, flight mode off: still ignored after a sync (yes/no); 5 an account › menu › "Från kortköp" › a purchase › "Spara": the expense is in that account (yes/no); 6 Inställningar › Anslut kort › SJ Prio Mastercard › "Koppla från" › "Koppla från": Kortköp shows "Anslut ditt kort", and after reconnecting the linked purchases are under "Kopplade" (yes/no); 7 on the partner's phone after she connects her card (PLAN-K1 §8 step 5): Kortköp shows only her purchases and none of the owner's (yes/no).
3. Card purchases: K0's owner steps are done (2026-10-24: Enable Banking account and Terms of Service, the application, the key in `docs/spikes/k0/.local/`, the owner's card linked, the test app run); the artwork is committed (D-180). **K1's owner steps 1 to 4 are done** (owner's report: `v1.0.4` deployed on the Pi); for the record they were: step 1 is done by the orchestrator (the key staged at `infra/secrets/cards/enable-banking.pem` in the main checkout, git-ignored, mode 0600); step 2 on the Pi in `~/kvitt`: `mkdir -p secrets/cards && chmod 700 secrets/cards` before anything else (the new `compose.yaml` refuses to start without that folder, even with no card configured, D-188), `scp` the key, `chmod 600 secrets/cards/*`, copy the new `compose.yaml`, set `KVITT_EB_APP_ID=` in `.env` (the orchestrator gives the id in chat) and leave `KVITT_EB_MEMBER_APP_ID=` empty; step 3 paste the current `infra/npm/kvitt.conf` into NPM (row Online, yes/no); step 4 backup, `KVITT_TAG`, `docker compose pull && docker compose up -d` (`server` healthy, `/health` shows `"schema_version":4`, yes/no). **Pending: PLAN-K1 §8 steps 5 to 7**, relayed one at a time now that a client with the card screens exists (`kvitt-164`): the partner's card, the next-day fetch without BankID, pending to booked.
4. Card purchases on the phone (PLAN-K2 §8, all pending; K1's image runs on the Pi and `kvitt-164` is on the Pi, so the first step is the in-app update, Auto Blocker off for it and back on afterwards): 1 Inställningar › Anslut kort › SJ Prio Mastercard › 30 dagar › "Anslut med BankID", sign in BankID, back in Kvitt "Kortet är anslutet" and the Kortköp tab lists the purchases (yes/no each); 2 Kortköp › a purchase › "Skapa utgift" › "Spara": the expense is in its account and the purchase under "Kopplade" (yes/no); 3 on a purchase the partner already entered, the row's "Koppla till …" moves it to "Kopplade" (yes/no); 4 flight mode on, "Ignorera" a purchase, flight mode off: still ignored after a sync (yes/no); 5 an account › menu › "Från kortköp" › a purchase › "Spara": the expense is in that account (yes/no); 6 Inställningar › Anslut kort › SJ Prio Mastercard › "Koppla från" › "Koppla från": Kortköp shows "Anslut ditt kort", and after reconnecting the linked purchases are under "Kopplade" (yes/no); 7 on the partner's phone after she connects her card (PLAN-K1 §8 step 5): Kortköp shows only her purchases and none of the owner's (yes/no).
5. Card purchases on the laptop (PLAN-K3 §8, all pending; after image `v1.0.5` is deployed on the Pi): 1 `https://kvitt.suorra.duckdns.org/app/` › Inställningar › Anslut kort: the SJ Prio card shows "Ansluten" if the phone connected it (yes/no), and Kortköp lists the same purchases as on the phone (yes/no); 2 Kortköp › a purchase › "Skapa utgift" › "Spara": the purchase is under "Kopplade" on the laptop and on the phone after a sync (yes/no); 3 if the card is not connected yet, or after "Koppla från": Anslut kort › SJ Prio Mastercard › "Anslut med BankID": the bank's page shows a QR (yes/no), scan it with BankID on the phone (yes/no), "Tillbaka till Kvitt" opens Kortköp with the purchases (yes/no).
## 4. Owner questions (asked, unanswered)
- OQ-7 (SPEC §12): a notification for new card purchases? Default: none in this version (D-171).
- OQ-8 (SPEC §12): turning several card purchases into expenses at once? Default: not in this version (D-171).
- OQ-9 (SPEC §12): a purchase linked to an expense the phone has not pulled yet shows under "Kopplade" without its line and offers "Skapa utgift" in its sheet until the next sync; add a `linkedExpenseId` to tell it apart (a UI-contract change)? Default: not now. Note only, no question: S-25's "timed out" relies on the server answering `expired` (15 minutes), the phone does not check the clock.
- K3, SPEC §13.3: the date form of "upphör <date>" / "giltig till <date>" and of "Från kortköp: …, <date>". The web uses the detail form for `valid_until` (W-21 banner, W-22 tile, W-13 row) and the list form on W-6 (D-206); `docs/ux/strings.csv` notes the list form for `settings_cards_connected` (K2). One answer sets both clients.
- K3, SPEC §15.3: the English "Card purchases" wraps to two lines in the web's phone-width bottom bar (Swedish "Kortköp" fits). Shorten it to "Cards" (a `web_` label), or accept the wrap?
- K3, SPEC §15.6: remember W-21's filter in this browser (`localStorage`, a per-viewer convenience of §15.5)? Left out; default: not now.
- Firewall rule limiting the Kvitt Pi's port 8080 to the HA Pi: deferred (2026-10-24, D-156); ask again only if the owner raises it.
- State-class naming (`*Ui`, `*UiState`, `*State` mixed): fix one convention, or leave as built?
- Answered 2026-10-24 (D-211): image builds move to the Mac (`infra/build-images.sh`), the Pi workflow is the fallback. Answered 2026-10-24 (D-156, D-157): OQ-6 no confirm step on S-1 for an unusual server; L-10 `not_found` for the other user's device; `mem_limit` kept, the warning accepted; image builds stay on the Pi for now; the web client is first used from the laptop browser; small edits by a Haiku or Sonnet agent in the main checkout, committed on `main` by the orchestrator (D-157). Answered 2026-10-21: auth on everything, the key is the app itself, the landing page on the LAN only is OK (D-126). Answered 2026-10-20: Kvitt on the Pi is a self-contained compose project in `~/kvitt` with nothing installed elsewhere (D-125). Answered 2026-10-19: Kvitt's host name is `kvitt.suorra.duckdns.org` (§1); Kvitt on port 443 through the existing router forward (D-124). Moot: `write:repository` for the registry secret (the tag push works with `write:package`).
@@ -130,9 +134,10 @@ Standing note for every in-app update on the Samsung phones: turn Auto Blocker o
- Nothing.
## 6. Numbering
- Decisions: D-001 to D-200 and D-211 used, except D-008, D-009, D-150, D-189 and D-190 (never used); D-158 to D-176 are the card purchases (v0.35), D-177 to D-180 K0's results and the artwork (v0.36), D-211 the Mac image builds and the verified `v1.0.3` deploy (v0.37), D-181 to D-188 K1's report (v0.38), D-191 to D-200 K2's report (v0.39). **D-201 to D-210 are reserved for K3's report**; next free after them: **D-212**.
- Spec version: v0.39 written (K2's report, D-191 to D-200). Next **v0.40** (K3's report).
- Server schema version **4** in code (migration 0004, K1 `671c00d`); the Kvitt Pi and the dev stack still run schema 3 until their next deploy. Room schema version **2** in code (K2 `a8f91ee`, `MIGRATION_1_2`).
- Decisions: D-001 to D-211 used, except D-008, D-009, D-150, D-189, D-190 and D-208 to D-210 (never used); D-158 to D-176 are the card purchases (v0.35), D-177 to D-180 K0's results and the artwork (v0.36), D-211 the Mac image builds and the verified `v1.0.3` deploy (v0.37), D-181 to D-188 K1's report (v0.38), D-191 to D-200 K2's report (v0.39), D-201 to D-207 K3's report (v0.40); D-208 to D-210 unused like D-150. Next free: **D-212**.
- Spec version: v0.40 written (K3's report, D-201 to D-207). Next **v0.41**.
- Release tags: `v1.0.3` = `8fa9198`, `v1.0.4` = `671c00d` (K1, deployed), `v1.0.5` = `faeb752` (K3, being built). Release APK `kvitt-164` (K2) on the Pi.
- Server schema version **4** in code (migration 0004, K1 `671c00d`); the Kvitt Pi runs it since `v1.0.4`; the dev stack still runs schema 3 until its next deploy. Room schema version **2** in code (K2 `a8f91ee`, `MIGRATION_1_2`).
- New ids in v0.35: FR-22 (AC-22.1 to AC-22.15), US-18, US-19, CP-1 to CP-14 (§9.12), S-23 to S-27, W-21 to W-23, E-36 to E-44, OQ-7, OQ-8; plans K0 to K3. New in v0.39: OQ-9.
- Sync protocol **1**, push payload **1**.
+10
View File
@@ -205,4 +205,14 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| D-198 | 2026-10-24 | Wrapping chips (K2; revises §13.3 S-25 "segmented choice"): S-23's four filter chips wrap to a second row rather than scroll, so the selected one is always in view; S-25's and S-26's history choice ("7 dagar" … "180 dagar") is a row of wrapping filter chips with a check on the selected one, not a segmented button row. | The Swedish labels with their counts do not fit one row, and a segmented row cuts "180 dagar" at large font scales. | SPEC §13.3 S-23, S-25, S-26; design-spec S-23, S-25, S-26; `feature/cards/.../CardsScreen.kt` (`Filters`), `CardParts.kt` (`HistoryChips`) |
| D-199 | 2026-10-24 | Two small screen rules (K2): S-13's "Anslut kort" subtitle reads "Har upphört" (`settings_cards_expired`) for an expired connection, besides "Ansluten · giltig till <date>" and "Läs in dina kortköp"; S-5's overflow item "Från kortköp" is hidden on an archived account (it has no add-expense FAB either, AC-6.4). | An expired consent is not "Ansluten"; an archived account takes no new expenses. | SPEC §13.3 S-5, S-13; design-spec S-5, S-13; `feature/settings/.../SettingsHubScreen.kt`, `feature/account/.../AccountDetailScreen.kt` |
| D-200 | 2026-10-24 | K2's string keys and artwork measures: four keys beyond §13.3's card table, in `docs/ux/strings.csv` (77 rows added in all): `cards_foreign_amount` ("%1$s %2$s", the foreign amount, decimal comma in Swedish), `cards_connection_settings` ("Kortanslutning", TalkBack click label of S-23's header card), `cards_connect_offline_body` (S-24's offline banner body), `settings_cards_expired` (D-199). Card artwork measures in `KvittDimens` (`:core:design`): `CARD_ART_ASPECT` 1.586 (the box; the bundled picture keeps its own 1313 × 829 inside it), `cardArtCorner` 16 dp and `cardArtNameInset` 16 dp (the drawn fallback card), `cardArtHeader` 72 dp wide (S-23's header card), `cardArtLarge` at most 280 dp wide (S-23's empty state, S-25, S-26); S-24's tiles are full width. | §13.3 left TalkBack labels and unlisted keys to K2; measures live in one design file (`CLAUDE.md`). | SPEC §13.3 "Card purchase strings"; design-spec §3.12; `docs/ux/strings.csv`; `android/core/design/.../theme/KvittDimens.kt` |
| D-201 | 2026-10-24 | W-21 refresh while fetching (K3): W-21 re-reads the card snapshot (E-40) every 2 s while a connection is fetching or in its first fetch (its `fetching` flag set or its first fetch not yet done, CP-2's interval), otherwise every 60 s like every other web read; window focus and every card write still re-read it at once. | The first fetch after BankID and a user-present "Uppdatera" finish within seconds; the phone polls at the same interval (CP-2), and 60 s would leave the page stale just after the user asked for fresh data. | SPEC §15.5, §15.6 W-21; `web/src/features/cards/CardsPage.tsx` (`cardsInterval`), `model.ts` (`ATTEMPT_POLL_MS`) |
| D-202 | 2026-10-24 | Web query names for the card flows (K3; extends D-144's style): `/app/expenses/new?cardPurchase=<id>` opens W-5 prefilled from a purchase (CP-9, plus `&account=<id>` from S-27); `/app/cards?attempt=<id>` is W-21 polling an attempt after the bank's return (CP-2; the callback page links there); `/app/settings/cards?provider=<id>` opens W-22's connect step for one card (S-25), also the target of "Förnya med BankID" on W-21 and W-23. All three are in `routes.ts` `QUERY` with builders. | One name per parameter, readable in a bookmarked URL, built in one place like D-144's `account` and `settlement`. | SPEC §15.3, §15.6 W-5, W-21, W-22, W-23; `web/src/app/routes.ts` |
| D-203 | 2026-10-24 | The web follows only an `https:` `auth_url` (K3): after E-37, W-22 opens the returned URL in the same tab only when its protocol is `https:`; any other URL (or one that does not parse) is not opened and shows "Det gick inte att ansluta kortet" (`cards_connect_failed`). | The URL comes from the server but is the bank's; refusing `javascript:`, `data:` and plain `http:` keeps a bad or tampered answer from running in the page's origin or sending BankID over plain HTTP. | SPEC §15.6 W-22; `web/src/features/cards/cardsApi.ts` |
| D-204 | 2026-10-24 | The outcome on W-21 after the bank's return (K3; refines CP-2 for the web): when E-38 reports the attempt ended, W-21 drops `?attempt=` from the URL and shows: connected, the snackbar "Kortet är anslutet" (`cards_connected`); failed, a banner "Det gick inte att ansluta kortet" (`cards_connect_failed`); expired, a banner "Anslutningsförsöket tog för lång tid" (`cards_connect_timeout`); each banner with "Försök igen" (`common_retry`), which opens W-22 at `?provider=<id>` (D-202). | A success needs no further action, so a transient snackbar; a failure needs one, so a banner that stays with the way to try again, as S-25 offers on the phone. | SPEC §15.6 W-21; `web/src/features/cards/CardsPage.tsx` (`useAttemptReturn`) |
| D-205 | 2026-10-24 | W-5 from a card purchase (K3; refines D-169 for the web): when `?cardPurchase=<id>` names a purchase not in the card snapshot or deleted, W-5 shows the empty state "Kortköpet finns inte längre" (`web_expenses_card_missing_title`) instead of the editor; when the expense is saved but the E-44 link fails, the expense stays saved and the snackbar says "Utgiften sparades men kunde inte kopplas till kortköpet" (`web_expenses_card_link_failed`); the purchase stays to handle on W-21. | The expense is the user's data and was accepted; the link is a separate write the web never queues (§15.5), so the user is told and can link it again from W-21. | SPEC §15.6 W-5; `web/src/features/expenses/ExpenseEditorPage.tsx` |
| D-206 | 2026-10-24 | Date forms of the card lines on the web (K3; proposed answer to the open §13.3 date form): the consent's end `valid_until` shows in the §9.8 detail date form on the W-21 expiring banner (`cards_expiring`), the W-22 connected tile (`cards_provider_connected`) and the W-13 row (`settings_cards_connected`); W-6's "Från kortköp: …, <date>" line uses the list form. `docs/ux/strings.csv` notes the list form for `settings_cards_connected` (K2); the owner's answer to the open question decides both clients. | An end date is a single date read on its own, where the detail form is clearer; W-6's line sits among list-style metadata. | SPEC §15.6 W-6, W-13, W-21, W-22; `web/src/features/cards/`, `web/src/features/settings/SettingsPage.tsx`, `web/src/features/expenses/ExpenseDetailPage.tsx` |
| D-207 | 2026-10-24 | W-21 opened from W-3 (K3; S-27 on the web): `/app/cards?account=<id>` shows the purchases to handle with the title "Från kortköp", no filter chips and no side panel or dialog; a row opens W-5 at once with `?cardPurchase=<id>&account=<id>` (D-202). | S-27 is a picker for one account's new expense, so it mirrors the phone's direct path; the filters and the sheet's other actions stay on the full W-21. | SPEC §15.6 W-21; `web/src/features/cards/CardsPage.tsx` |
| D-208 | 2026-10-24 | Unused (reserved for K3; K3 needed D-201 to D-207). | | |
| D-209 | 2026-10-24 | Unused (reserved for K3; K3 needed D-201 to D-207). | | |
| D-210 | 2026-10-24 | Unused (reserved for K3; K3 needed D-201 to D-207). | | |
| D-211 | 2026-10-24 | Image builds on the Mac (revises D-156 point 5 and D-104's "fallback" wording). `infra/build-images.sh v1.0.N` on the owner's Mac with Podman is the normal way to build and push `kvitt-server` and `kvitt-backup`; the Gitea Actions workflow on the Pi 3 runner stays as the fallback (about three hours per run, disk pressure; the owner cancels the runs it queues). Tag: after `git fetch --tags`, the next `v1.0.N` after the highest `v1.0.*` tag on `origin`, also pushed as a git tag on the built commit (`v1.0.3` = `8fa9198`). Registry from the Mac: `gitea.lan` on port 80, plain HTTP; `infra/secrets/registry.env` (git-ignored) holds `KVITT_REGISTRY=gitea.lan/jacobh2`, the user and a `write:package` token; the Pi's `.env` keeps `gitea.lan:3001/jacobh2`, the port it reaches locally; both name the same registry. Podman needs the registry marked insecure in `~/.config/containers/registries.conf.d/kvitt.conf` on the Mac and `/etc/containers/registries.conf.d/kvitt.conf` inside the Podman machine (`[[registry]] location = "gitea.lan" insecure = true`), owner's allowance 2026-10-24. Script fixes: the server build gets `KVITT_VERSION_CODE` as in the workflow (`f15fa23`); Podman detection without a pipe (`8fa9198`: `grep -q` under `pipefail` closed the pipe, `docker version` died of SIGPIPE and the script chose docker). Deployed and verified by the owner 2026-10-24: `v1.0.3` on the Kvitt Pi (`server` and `backup` healthy); the web client at `https://kvitt.suorra.duckdns.org/app/`, a laptop browser paired with a device invite; "All seems to work" for Home and an account, add, edit and delete an expense, CSV export, record and delete a payment, analytics with category drill-down and Insights, the settings pages, revoking the browser from the phone, and the Android Insights section on S-11 against the new server. This answers the open question "image-build location". | A cold arm64 server build took 48 s on the M4 (2026-10-18, before the web stage) against 2 h 42 min on the 1 GB Pi 3 (run 69), whose disk fills during builds; the workflow is kept because it needs no Mac. One tag sequence for both paths keeps `KVITT_TAG` monotonic whichever path built it. | SPEC §10.3, §10.10, §11.2 W1 to W7, §11.3, §11.4, §15; `infra/README.md` (Layout, How it fits together, the image-build sections, "Pi: what the owner does" steps 2 and 7, Owner decisions); `docs/HANDOFF.md` |
+3 -3
View File
@@ -42,8 +42,8 @@ Plans in the same wave can run in parallel (they own disjoint paths, SPEC §14.7
| 7 | M11 Import history | `M11.md` | M9, M10 | none |
| 8 | K0 Card purchases spike (local test app, D-176) | `K0.md` | SPEC v0.35; the owner's Enable Banking account, application and key (PLAN-K0 §8) | done 2026-10-24 (app `2ce317f`; report `docs/spikes/K0-enable-banking.md`; answers in SPEC v0.36, D-177 to D-180) |
| 9 | K1 Card purchases: server (schema 4, E-36 to E-44) | `K1.md` | SPEC v0.36 (K0's answers) | done 2026-10-24 (landed `671c00d`; report recorded in SPEC v0.38, D-181 to D-188) |
| 9 | K2 Card purchases: Android (`:feature:cards`, Room 2) | `K2.md` | SPEC v0.36; rebases on K1 before its gate | K1, K3; lands after K1 |
| 9 | K3 Card purchases: web (W-21 to W-23) | `K3.md` | SPEC v0.36; rebases on K1 and K2 before its gate | K1, K2; lands after K2 (card strings) |
| 9 | K2 Card purchases: Android (`:feature:cards`, Room 2) | `K2.md` | SPEC v0.36; rebases on K1 before its gate | done 2026-10-24 (landed `a8f91ee`; report recorded in SPEC v0.39, D-191 to D-200) |
| 9 | K3 Card purchases: web (W-21 to W-23) | `K3.md` | SPEC v0.36; rebases on K1 and K2 before its gate | done 2026-10-24 (landed `faeb752`; report recorded in SPEC v0.40, D-201 to D-207) |
## Constraints the orchestrator must keep in mind
@@ -54,5 +54,5 @@ Plans in the same wave can run in parallel (they own disjoint paths, SPEC §14.7
- **M6** landed `7730a31` (D-063 to D-066): charts on `Canvas`, Expressive fallbacks against material3 1.4.0, formatting tables in code, one golden per sheet × theme × language (+200 %), which M7a to M7f follow. Its M8 contract requests are in SPEC §11.3.
- **M12** (D-054): done; the importer test `category_table_matches_the_spec` is green again since M12b (`6eaccec`). M12d (D-059) landed `557af7e` (D-060 to D-062); M7a to M7f run on top of both.
- **Phone:** M2b (one `installDebug`), M5 (only if instrumented tests), M8 and M10 use the phone; only one at a time, named in the notes. M8's on-phone test was run by the orchestrator with the owner on 2026-10-18; M12e did not use the phone; its APK needs a schema-3 server (SPEC §14.8), and the dev server already runs it.
- **K0 to K3** (D-176, SPEC §11.3): K0 ran with the owner on the Mac only and is done; its answers are in SPEC v0.36 (D-177 to D-180), so K1 to K3 can start. Fill `{{SPEC_VERSION}}` with v0.38 for K1 (done: landed `671c00d`, recorded in v0.38 with D-181 to D-188), v0.39 for K2 and v0.40 for K3, and `{{NEXT_DECISION}}` from D-191 to D-210 in disjoint ranges for K2 and K3. K1 owns the contract (migration 0004, OpenAPI E-36 to E-44, examples, `match_card_purchase.json`) and lands first; K2 and K3 may start on the spec and fakes and rebase before their gates. Give K1, K2 and K3 disjoint decision ranges. No agent ever prints, logs or commits a real card purchase; the owner's BankID steps are relayed in chat.
- **K0 to K3** (D-176, SPEC §11.3): K0 ran with the owner on the Mac only and is done; its answers are in SPEC v0.36 (D-177 to D-180), so K1 to K3 can start. Fill `{{SPEC_VERSION}}` with v0.38 for K1 (done: landed `671c00d`, recorded in v0.38 with D-181 to D-188), v0.39 for K2 (done: landed `a8f91ee`, recorded in v0.39 with D-191 to D-200) and v0.40 for K3 (done: landed `faeb752`, recorded in v0.40 with D-201 to D-207), and `{{NEXT_DECISION}}` from D-191 to D-210 in disjoint ranges for K2 and K3 (D-208 to D-210 unused; next free D-212, v0.41). K1 owns the contract (migration 0004, OpenAPI E-36 to E-44, examples, `match_card_purchase.json`) and lands first; K2 and K3 may start on the spec and fakes and rebase before their gates. Give K1, K2 and K3 disjoint decision ranges. No agent ever prints, logs or commits a real card purchase; the owner's BankID steps are relayed in chat.
- **Outside the repository:** M2c-dev (D-022, done), M2c-pi needs none: it writes the `infra/pi/` compose project and the owner runs it on the Pi (D-100, revises D-023; D-125); M8 (dev server on the Mac), M10 and M11 (production) need the owner's allowance in the notes.
+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)