Files
kvitt/docs/HANDOFF.md
T
jacobh2 7ce3029cb1
Build Kvitt images / build (push) Failing after 3h14m38s
SPEC v0.40: K3's report recorded (D-201 to D-207), web card pages as built
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
2026-10-24 17:04:37 +02:00

146 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Handoff: state of the work
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-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).
- **Images** (D-211, revises D-156 point 5): built on the Mac with Podman, `infra/build-images.sh v1.0.N`, the next `v1.0.N` after the highest tag on `origin` (`git fetch --tags`), pushed as a git tag too; the Gitea workflow on the Pi 3 runner is the fallback (about three hours per run, disk pressure; the owner cancels the runs it queues). `gitea.lan/jacobh2/kvitt-server` and `kvitt-backup`, tags up to `v1.0.3` (the first Mac build, `8fa9198`, the tag the Pi runs) plus `latest`; the packages are public, so the Pi pulls without `docker login`. Registry from the Mac: `gitea.lan` on port 80 (`infra/secrets/registry.env`: `KVITT_REGISTRY=gitea.lan/jacobh2`, user, `write:package` token); the Pi's `.env` keeps `gitea.lan:3001/jacobh2` (same registry). Podman's insecure-registry files: `~/.config/containers/registries.conf.d/kvitt.conf` on the Mac and `/etc/containers/registries.conf.d/kvitt.conf` in the Podman machine (owner's allowance 2026-10-24). Workflow history: run 69 (2026-10-19) built `v1.0.0`, `kvitt-server` 2 h 42 min on the Pi 3. SPEC §10.3, `infra/README.md` "Image build on the Mac".
- **Kvitt Pi** (D-125): Kvitt runs from `/home/jacobhagstedt/kvitt` as the owner's user `jacobhagstedt`, uid and gid 1000:1000 (`KVITT_UID`/`KVITT_GID`); Docker and the `gitea.lan` registry trust were already on the Pi; nothing outside that folder is installed or changed. **The stack is running** (owner, 2026-10-21): image `v1.0.1`, `server` and `backup` both healthy after a `chown` of `data/` (it had been created as root); compose's `mem_limit` warning (the Pi kernel has no memory cgroup, so the 160 MiB and 96 MiB limits are not enforced) is accepted and the lines stay (D-156, SPEC §10.2). `/health` through NPM from outside answered `"status":"ok"` (public health OK). **Live** (2026-10-21): both phones are paired against it and the Splitwise import (M11) is done.
- **App gate** (D-126, owner decision 2026-10-21): outside the home LAN `192.168.1.0/24` NPM closes (`return 444`) every request without `X-Kvitt-App: <key>`, every route included; the key is the build property `kvitt.appKey` (`android/local.properties`, git-ignored), generated by the owner with `openssl rand -hex 32` and pasted into NPM's block (`infra/npm/kvitt.conf`, `7521c01`); the server and `contracts/openapi.yaml` are unchanged. Android side `34e5cad`, docs `2acfc34` (SPEC v0.25, `infra/README.md` step 4a). `android/local.properties` has a `kvitt.appKey` and `infra/pi/out/apk/` holds `kvitt-125.apk` (checked 2026-10-21); both phones run release 140, built with the key like every APK since `kvitt-133` (2026-10-24); whether the gate is in NPM is not recorded here (orchestrator confirms with the owner, §3).
- **Web client programme** (owner interview 2026-10-21, SPEC v0.28, D-129 to D-134, SPEC §15, FR-21): a React web client in `web/`, served by `kvitt-server` under `/app/` from the server image, with the app's features except the phone-only ones, plus search, CSV export, keyboard shortcuts and a language choice; login with an invite code (device `Web · <browser>`); the NPM gate also passes `/app/`, `POST /invites/accept` and bearer-token requests except `/health`, `/setup/`, `/i/`, `/apk/` (new block in `infra/npm/kvitt.conf`, README step 4, SPEC §10.5.2; checked with nginx 1.31.7, 17 requests); advanced analytics on the server (`/analytics/*`, D-133, forecast = recurring templates due + 3-month average of non-recurring spend). Plans `PLAN-W0.md` to `PLAN-W7.md`, prompts `prompts/W0.md` to `prompts/W7.md`. W7 landed `50a0d67` (D-135 to D-138, SPEC v0.29: `/app/` from `KVITT_WEB_DIR`, E-34 `GET /analytics/trends`, E-35 `GET /analytics/forecast`, rules SPEC §14.5.7, the Node stage and the repository-root build context); W0 landed `2ae65a5` (`web/**`: Vite, React 19, TypeScript strict, sync-pull ledger, invite-code login, ported money rules passing the vectors, tokens, i18n generated from `docs/ux/strings.csv` and `web-strings.csv`); W1 to W5 landed 2026-10-21 (`934eee7`, `a65ab71`, `f9a9bfb`, `1aecf51`, `3ebe57d`; D-144 to D-154, SPEC v0.32), each in its own `web/src/features/<name>/`, W2 with the shared editor fields in `web/src/components/editor/`; its loose ends done by W0b (`5305f37`, §2 "Web follow-ups"); W6 (Android Insights) landed `1c9856b` and shows on the phones once the image with W7 and W0 is deployed and a release APK with W6 is published. The web gate is `npm run check` in `web/` (third gate, in `CLAUDE.md` since `415f9f5`).
- **First release APK**: `apk/kvitt-123.apk` (10.0 MB, git-ignored by `*.apk`) handed to the owner from the main checkout for sideloading on both phones (both paired, 2026-10-21); it was built before any `kvitt.appKey` existed, so it sends no `X-Kvitt-App`: it works only until the gate is pasted (on the home Wi-Fi it keeps working afterwards). Both phones have since moved to the keyed release 140 (2026-10-24), so the gate can go in (§3 step 1).
- History rewrite and first push, 2026-10-18 (owner authorised): superseded PNG versions were dropped from the whole history (no LFS), the current goldens and UX mock-ups restored in `18c527f`; pack 45 MiB; `origin/main` pushed at `18c527f`. A bundle of the pre-rewrite history (`kvitt-pre-rewrite.bundle`) is kept outside the repository, in the orchestrator's scratchpad. Every commit from M2b on got a new hash; the oxipng pass (`7c453d8` before the rewrite) no longer exists as a commit (it only touched PNGs). SPEC, `docs/decisions.md`, this file and `prompts/README.md` cite the rewritten hashes since v0.18 (`7c453d8` stays as the oxipng pass's name; `docs/validation/ac-coverage.md` keeps its pre-rewrite hashes verbatim); since v0.19 the plans cite rewritten hashes too (eight pre-rewrite hashes in `PLAN-*.md` replaced by the commit with the same subject); `docs/spikes/pi-image-arm64.md` and the READMEs had none stale. For any other old hash, find the commit by its subject with `git log --oneline --grep`.
- History (rewritten hashes): `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `c277ee3` M2b, `06f48ef` M2a, `6dce4c7` SPEC v0.4, `7869ec4` M3, `8f0e156` M5, `853c038` SPEC v0.5, `15056dc` M4, `d9dd584` SPEC v0.6, `50339ae` M9, `86e08a1` SPEC v0.7, `aa765bb` M12a, `d39eb2c` SPEC v0.8, `b09f322` M12c, `6eaccec` M12b, `0006bd7` UX Kalas categories and tokens, `6da6b1b` UX sprint U2, `e676812` SPEC v0.9, `557af7e` M12d, `7730a31` M6, `9ee67d4` SPEC v0.10, `a41f240` and `6453932` app wiring before M8, `1ea752c` M7a, `ea7d494` M7e, `7380e50` M7d, `0e1d99d` M7b, `0b140aa` M7c, `c4ab35f` M7f, `b24c8b5` M6b, `f4dc10c` SPEC v0.11, `3dda414` M7g, `cb832a7` and `3d9eea8` M8, `6159e18` SPEC v0.12, `8c60715` M12e, `b7b7483` M12e launcher icon, `51d6ff8` SPEC v0.13, `a312758` Vico removal, `f56dcbe` FCM `fid`, `3e6c337` Pi image arm64 check, `e2dba84` SPEC v0.14, `328c466` M8b, `6eb3f85` `FcmMessageBody.fid`, `bfde12c` SPEC v0.15, `75b549f` M8c goldens, `a0c25b0` M8c licences row, `c30a49e` R8 release build, `bacbd18` M8d, `bfc3acb` SPEC v0.16, `2476667` M2c-pi bundle, `a59abeb` M8e, `b4d9467` Dockerfile `CARGO_BUILD_JOBS`, `956908a` M8f, `1328afe` chips cleanup, `400eac9` SPEC v0.17, `18c527f` goldens restored after the rewrite; then `2fac1ec` to `e70fe0d` infra overhaul, `8660b7f` server gap pass, `7fb86f5` infra security, `dc7a296` to `a0259ab` M12f, `8103dae` to `b985ce9` server overhaul, `48fe119` SPEC v0.18, `e4e6e5d` to `c582363` Android overhaul, `3ef2c15` SPEC v0.19, `96e4cbd` APK download rate limit (D-119), `481889a` SPEC v0.20, `f1abb0d` `cargo audit` in the server gate, `4f69ae0` Gitea login in the run line, `d05f13a` Pi stack behind NPM, `62f5279` and `9231a1d` infra README for NPM (D-120), `d090c6c` Android host property (D-121), `4bcff2c` SPEC v0.21, `dd07ca4` temporary workflow diagnostic step, `4a4e45f` S-22's release server and `4b1ae0c` `publish-apk.sh` host (D-122), `c9abd24` and `e381e37` server host defaults and OpenAPI `{host}` (D-123), `ac17b41` `install.sh` step number, `cce3e4e` SPEC v0.22, `a86ad24` server and contracts on port 443, `4852803` bundle on port 443 (D-124), `038c603` Android on port 443 and the pinned-root template, `ad35231` portless Android fixtures, `b1f4c10` SPEC v0.23, `3820f9c` sparse-checkout workflow, `200c99f` `.env` without informational keys, `6e5981d` self-contained Pi compose project (D-125), `28fa71b` compose deploy hint in the workflow and `infra/build-images.sh`.
- Landed since SPEC v0.31 (recorded in v0.32):
- W1 web Home and accounts (`934eee7`, D-144, D-145): W-2 with the balance hero and seesaw, account cards, the archived section (open state per page session) and the "Du och <partner> är med" empty card; W-3 with month groups and the overflow menu; W-4 at `/app/accounts/new` and `/app/accounts/:id/edit`; `?account=<id>` links into W-5 and W-7.
- W2 web expenses, search and CSV export (`a65ab71`, D-146 to D-148): W-5 with the §9.11 suggestion, the rejection banner and the discard dialog; W-6 with the audit timeline; W-12 with filters in the URL; the CSV export (`;`, BOM, decimal comma); the shared editor fields `web/src/components/editor/`; `vite.config.ts` `server.fs.allow` for `contracts/category-keywords.csv`.
- W3 web settle up (`f9a9bfb`, D-144, D-149): W-7 with live allocation errors, `?account=` and `?settlement=`; W-8 with allocations and timeline.
- W4 web analytics (`1aecf51`, D-151, D-152): W-9 stacked bars, W-10 drill-down with the D-128 line chart (bucket route `/app/analytics/bucket/<key>`, filter in the query string), W-11 Insights on E-34/E-35 with 3, 6 or 12 months; "Visa som tabell"; `resolveRange` and `spendSeries` ported with their vectors.
- W5 web settings (`3ebe57d`, D-153, D-154): W-13 hub, W-14 profile with the partner's first-login intro, W-15 categories, W-16 recurring, W-17 devices, W-18 invites at `/app/settings/invite`, W-19 about with the `GET /me` header fallback.
- Web fix (`83f5293`, no decision): Vitest's module cache kept a stale `catalog.ts` glob result in the main checkout (raw keys such as `web_accounts_load_failed` on screen); the glob lost its type argument and `catalog.test.ts` compares the loaded feature catalogs with the files; `web_accounts_load_failed` became the shared `web_ledger_load_failed` in `web-strings.csv`; `suggest_category` moved to PORTED in `lib/vectors.test.ts`; the fake server keeps non-JSON bodies raw.
- APK install failures (`d686ee0`, D-155): installer status reaches `UpdateRepository` by session id; blocked → "Kunde inte installera uppdateringen" with the security-settings text, aborted → `Available`, other failures → the installer's message; "Försök igen" downloads again; new AC-16.7; `strings.csv` 590 rows. Release `kvitt-140` deployed 2026-10-21.
- Landed since SPEC v0.29 (recorded in v0.30 and v0.31):
- Update-check fix (`c0f4345`, D-140, v0.30): S-22 "Check for update" calls `UpdateRepository.checkNow()`, which always asks `GET /apk/latest` and restarts the 6-hour window; the automatic checks keep the throttle (AC-16.2, AC-16.6).
- W6 Android Insights (`1c9856b`, D-141 to D-143, v0.31): `InsightsRepository` (E-34 trends, E-35 forecast, online only, names from Room); an "Insikter" section below S-11's local analytics following the measure toggle, 6 whole months, household and per-category line charts with the server's average and moving average, a forecast card per account ("Återkommande + snitt = prognos"); offline and error banners over the last data; 40 `s11_insights_*` goldens; Android gate green (791 tests in the feature run). `strings.csv` 588 rows (16 `analytics_insights_*` keys); the web dictionaries regenerated.
- W0 web foundation (`2ae65a5`) and `CLAUDE.md` with the web client and three gates (`415f9f5`); W0's report has no decisions recorded here.
- Landed since SPEC v0.28 (recorded in v0.29):
- W7 server web serving and analytics (`50a0d67`, D-135 to D-138): `KVITT_WEB_DIR` (default `/app`) served under `/app/` (308 from `/app`, SPA fallback, missing `assets/*` 404, `assets/*` cached a year immutable, `index.html` `no-store`, the `$kvitt_csp` web policy plus `nosniff`, `DENY`, `no-referrer`, strict path allowlist); `/app` paths on the unauthenticated 60/min per IP (D-136); image build from the repository root with an allowlist `.dockerignore`, a Node 24 LTS stage pinned by digest passed through the Rust stage (D-137); E-34 trends and E-35 forecast with examples, rules in `kvitt-domain::analytics` (D-138); `Cargo.lock` `rustls-webpki` 0.103.16, `webpki-root-certs` 1.0.10 (RUSTSEC-2026-0382, -0383). 299 server tests; contracts-check 136.
- Unattended APK deploy (`70cb25c`, D-139): `infra/publish-apk.sh --deploy` copies the APK, then `latest.json`, with the forced-command key `infra/secrets/pi_deploy_ed25519` and checks `/apk/latest` with `X-Kvitt-App`; first run 2026-10-21 served `version_code` 133.
- Landed since SPEC v0.26 (recorded in v0.27):
- S-12 line chart (`12e2508`, D-128, owner request 2026-10-21): S-12 opened from a category or account breakdown row has its own period chip and Month/Week toggle (starting from S-11's, changing S-12 only; measure and group-by fixed from S-11), the header total, a "Utgifter över tid" card with `SpendLineChart` (one point per bucket, empty as zero), the dashed range average with its amount, a trailing 3-bucket moving average, `LineLegend` and a caption comparing the latest whole bucket with the average ("<Bucket>: N % över snittet" / "under snittet" / "på snittet"), then the rows; a bar drill-down is unchanged. New AC-14.11; seven `strings.csv` keys (569 rows); no model, domain, fakes, server or contract change. Goldens `s12_category_groceries`, `s12_category_week`, `s12_category_empty_range`, `line_chart`, `line_chart_variants` new, `s12_category`, `s12_account`, `s12_long` re-recorded; 977 Android tests.
- Editor keyboard fix (`05ec3f3`, no decision): S-19, S-15 and S-6 editor bodies apply the scaffold padding, consume it and add `imePadding()`, so the keyboard no longer covers a focused field.
- Landed since SPEC v0.25 (recorded in v0.26):
- Pull-to-refresh (`3778f5f`, D-127, owner request 2026-10-21): S-4 and S-5 run `SyncRepository.syncNow()` (the S-16 "Synka nu" call) on a pull; the indicator stays until the sync returns, success or failure (failure only in the sync pill); offline the pull ends at once. `KvittPullToRefresh` in `:core:design` (`component/Refresh.kt`): `PullToRefreshBox` with a `primary-container` disc, the `sync` icon turning with the pull and popping at the threshold (`KvittMotion.spatialFast()`, `GestureThresholdActivate` haptic), a `primary` arc that turns indeterminate; `ContainedLoadingIndicator` swaps in when the pinned Material 3 ships it. TalkBack "Synkar…" (`sync_syncing`, reused). Goldens `S-4-home-refreshing`, `S-5-account-refreshing`; 959 Android tests.
- Landed since SPEC v0.24 (recorded in v0.25, plus `infra/npm/kvitt.conf` `7521c01`, no decision of its own):
- App key header (`34e5cad`, D-126): `kvitt.appKey` (`-Pkvitt.appKey`, else `android/local.properties`, else empty = no header; 16 to 128 printable ASCII characters without white space, else the build stops giving the length only), `BuildConfig.APP_KEY`, `AppBuildInfo.appKey` (`:data`, `toString` hides it); `X-Kvitt-App` in `KvittApi`'s common headers and `PictureAuth`; 952 Android tests.
- Landed since SPEC v0.23 (recorded in v0.24):
- Sparse-checkout workflow (`3820f9c`, no decision): cone-mode `sparse-checkout` of `server` and `infra/backup` (the two build contexts), so the action fetches with `--filter=blob:none` and no PNG is checked out; `fetch-depth: 0` stays because `fetch-tags` with a shallow fetch only brings tags on the tip; the temporary "Diagnose secrets" step removed. Its push built `v1.0.1`.
- `.env` without the informational keys (`200c99f`, no decision; revises D-103): `KVITT_PI_HOST` and `KVITT_PI_SSH_USER` dropped, read by nothing on the Pi; the README's `scp`, `ssh` and NPM steps name `<kvitt-pi-address>`.
- Self-contained Pi compose project (`6e5981d`, D-125, owner decision 2026-10-20: 'It should be a self contained docker compose thing', 'do not install anything in any other place'): `/home/jacobhagstedt/kvitt` holds `compose.yaml`, `.env`, `secrets/` and the bind-mounted `data/`, `backups/`, `import/`; both containers as `KVITT_UID`/`KVITT_GID` (1000 on the Pi), read-only root, 16 MB tmpfs `/tmp`, each mounting only its own secret file (`create_host_path: false`); `.env` exactly the seven keys compose reads, `RCLONE_REMOTE` fixed to `gdrive:`; `install.sh`, `deploy.sh`, `bootstrap.sh`, `backup-now.sh`, `install-apk.sh`, `lib.sh` removed, operations are plain `docker compose` commands (`infra/README.md` "Pi: what the owner does", seven steps, and `infra/RESTORE.md`); no digest record (`docker image inspect` instead); APKs by `scp` into `data/apk/`. Rehearsed with Podman as uid 501: healthy, `/health` 200, bootstrap, APK served, backup in three modes, restore steps.
- Compose deploy hint (`28fa71b`, no decision): the workflow's header and digest-step comment and `infra/build-images.sh`'s closing line name `cd ~/kvitt`, `KVITT_TAG` in `.env`, `docker compose pull && docker compose up -d` and `docker image inspect` instead of `/opt/kvitt/deploy.sh` and `/opt/kvitt/deployed-digests`.
- Landed since SPEC v0.22 (recorded in v0.23):
- Port 443 (D-124, owner decision 2026-10-19): Kvitt is served at `https://<host>` with no explicit port, because NPM listens on 443 and selects by host name; the router's existing external-443 forward to the Home Assistant Pi serves Kvitt, no 8443 rule. Server and contracts (`a86ad24`): OpenAPI server `https://{host}`, E-7 and E-12 examples without a port, `tools/contracts-check.py` follows. Bundle (`4852803`): `KVITT_PUBLIC_URL=https://${KVITT_PUBLIC_HOST}`, health checks through NPM on 443, README step 7 "Router: no new rule", the APK-abuse stop-gap is disabling Kvitt's proxy host in NPM. Android (`038c603`): `releaseServerUrl(host)` = `https://<host>` (`RELEASE_SERVER_PORT` removed), the AC-2.6 intent filter without a port (matches every port, so an old `:8443` link still opens S-1 with the server it names), invite-link parsing drops `:443` and `:80`, 28 S-1 goldens on `https://kvitt.example.org`.
- Portless Android fixtures (`ad35231`, D-124): `android/README.md`, the `AppBuildInfo` and `AboutRepositoryImpl` comments, the S-21 and S-22 fake scenarios and the `:data` test fixtures without `:8443`; 30 goldens re-recorded (6 S-21 invite-created, 24 S-22). What is left of `8443` in `android/` is deliberate: tests that an old `:8443` link still opens S-1 and that a typed port survives.
- Pinned-root template (`038c603`, no decision): with `kvitt.tls.pinnedRoot=true` the build generates the pinned-root network security config from `android/app/src/releasePinnedRoot/network_security_config.xml` with the domain `kvitt.publicHost` and stops with a message while `android/app/src/release/res/raw/kvitt_root_ca.crt` is not committed (SPEC §8.5, §10.5.3). The old host name is out of that config and of the S-1 fixture.
- Landed since SPEC v0.21 (recorded in v0.22):
- D-121 follow-ups (`4a4e45f`, `4b1ae0c`; D-122): `AppBuildInfo.releaseServerUrl` from `RELEASE_SERVER_URL`; S-22 shows the paired server, before pairing the build's release server; `DataModule.DEFAULT_SERVER_URL` removed; fakes and tests on `https://kvitt.example.org:8443` (30 S-21/S-22 goldens re-recorded); `infra/publish-apk.sh` reads `KVITT_PUBLIC_HOST` from `infra/pi/.env` (refuses missing, `CHANGE_ME`, a URL, the old name) and passes `-Pkvitt.publicHost`.
- Server host defaults (`c9abd24`, `e381e37`; D-123): `KVITT_PUBLIC_URL` required (every command exits 1 without it, before opening the database); `KVITT_TRUSTED_PROXY` defaults to `none`; OpenAPI server `https://{host}:8443` (portless since D-124) with variable `host` (default `kvitt.example.invalid`); E-7 and E-12 examples on `kvitt.example.invalid`; `tools/contracts-check.py` follows. 276 server tests.
- `install.sh` names the NPM proxy host as README step 6 (`ac17b41`, no decision).
- Temporary "Diagnose secrets" step in `.gitea/workflows/build-images.yaml` (`dd07ca4`, no decision), to be removed once the login failure's cause is known (removed in `3820f9c`).
- Landed since SPEC v0.20 (recorded in v0.21):
- `cargo audit` in the server gate (`f1abb0d`, no decision): the gate is `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test`; the pre-commit hook runs `cargo audit --no-fetch` only when `server/Cargo.lock` or a server `Cargo.toml` is staged. First run: 0 advisories over 319 crates.
- NPM rework (`4f69ae0`, `d05f13a`, `62f5279`, `9231a1d`; D-120): TLS terminates in the owner's Nginx Proxy Manager on the Home Assistant Pi; Caddy, cert-sync, the Caddyfile, the HA SSH key and the router forward to the Kvitt Pi are gone. Kvitt gets its own host name `KVITT_PUBLIC_HOST` (owner-chosen; a DuckDNS name is being set up), because `icehack.asuscomm.com` is NPM's proxy host for Home Assistant. Router external 8443 → HA Pi 443 (replaced by the existing 443 forward, D-124). The server publishes 8080 on the Kvitt Pi's LAN, `KVITT_TRUSTED_PROXY=${HA_HOST}` (NPM's IPv4, checked by `install.sh`). SPEC §10.5 rewritten (NPM proxy host, the Custom Nginx Configuration with D-117's headers and the `/i/` log exclusion, fallback: no LAN path for release builds). Rehearsed on the Mac: `nginx -t` against NPM v2.16.0 templates, 21 checks of the server behind nginx passed. The Gitea login interpolates the secret in the `run` line (the env form came through empty on act_runner 0.6.1).
- Android host property (`d090c6c`, D-121): `kvitt.publicHost` (Gradle property, else `android/local.properties`, else `kvitt.example.invalid`), `BuildConfig.PUBLIC_HOST`, the manifest placeholder `publicHost`, `RELEASE_SERVER_URL`, `@ReleaseServerUrl` in `:feature:onboarding`. 944 Android tests.
- Landed since SPEC v0.19 (recorded in v0.20):
- APK download rate limit (`96e4cbd`, D-119, security review L-3): E-6 `GET /apk/files/{file_name}` at most 10 requests per minute per client IP in its own window, not counted against the 60 of the other unauthenticated endpoints; a 404 counts; 429 `rate_limited` with `Retry-After`; E-5 and E-4 stay on the 60; unit and integration tests.
- Landed since SPEC v0.18 (recorded in v0.19):
- Android overhaul (`e4e6e5d`, `8229ed6`, `515b60f`, `e08aaf0`, `0714242`, `a92c236`, `74b5fbd`, `c582363`; D-118): shared test support in `:core:design`'s test fixtures (`se.hagstedt.kvitt.design.testing`), `kvitt.android.screenshots` wires Roborazzi and the test libraries; `format/Failures.kt`, `format/Limits.kt`, `savedSplitLabel` in `:core:design`; dead code removed (`KvittMotion` only the seesaw spring, `KvittIcon.Description`, unused dimens and shapes); four catalog entries removed; one `CopyCategoryKeywords` task; one `configureAndroidModule`; S-7's state types in `ExpenseEditorState.kt`. Gate at landing: 941 tests, no golden changed. Release APK 10.46 MB.
- Landed since SPEC v0.17 (recorded in v0.18):
- Infra overhaul (`2fac1ec`, `61f92c8`, `fc6126c`, `b647faf`, `e70fe0d`; no decision): `tools/gradle-lock.sh` shared by `gradle-serial.sh`, `clean-builds.sh` and `infra/build-images.sh`; the pre-commit hook runs through `gradle-serial.sh` with JDK 25 on `PATH` (hooks reinstalled in `main` after `e70fe0d`); `install.sh` keeps the deployed `KVITT_TAG`; `--provenance=false` on buildx pushes, the workflow token through env; `RCLONE_REMOTE` dropped from the dev env.
- Server gap pass (`8660b7f`, D-113): `kvitt-server invite-partner` (0 ok, 2 household full, 3 no admin), so a scripted partner needs no second admin device; `push sent` info line; CLI tests (`invite-partner`, `migrate`, `healthcheck`, SIGTERM drain); `tools/contracts-check.py` 134 checks.
- Infra security (`7fb86f5`, D-117): Caddy log redaction of `/i/<code>` and query strings, CSP and three more response headers, Caddy non-root (uid 10002, read-only, caps dropped), `/certs` 0750, dev stack mounts only the FCM key, `/opt/kvitt/deployed-digests` with a WARNING on change, `kvitt_cicd` with `write:package` only, the plain root key (old variant B) not acceptable.
- M12f (`dc7a296`, `5b2ed37`, `2bd6b58`, `c366fbd`, `a0259ab`; D-109 to D-112, D-116): S-16 lost version read-only, S-22 "<version> (schema <n>)" from `/health`, `SettlementRepository` returns null instead of throwing, E-17 retry after each successful sync, invite links dropped while paired, private notifications on the lock screen, `localeFilters` en/sv, `:data` tests (429, D-040, WorkManager via `work-testing`), the home/account screenshot harness fix (stale `sv_dark` goldens re-recorded), 19 unused string keys removed (`strings.csv` 562 rows). Gate at landing: 941 tests. Release APK 10.0 MB.
- Validation reports (SPEC §11.4): `docs/validation/ac-coverage.md` (taken at `956908a`, `ed1b130` before the rewrite): 270 rows, 260 Done, 5 Partial, 1 Missing, 4 device-only; feature complete in code, not yet against §4's acceptance rule; its S-16 and S-22 mismatches are fixed by M12f. `docs/validation/security-review.md` (taken at `18c527f`): Critical 0, High 1, Medium 3, Low 10, Info 8; L-6 kept (D-116), L-7 fixed (D-116), L-1, L-2, L-5, L-9, M-2 fixed (D-117), L-8 fixed (`e70fe0d`); H-1 (`725901c`, D-115), M-3 (`baa5806`) and L-4 (`b985ce9`, D-114) fixed by the server overhaul; L-10 answered 2026-10-24: `not_found` for the other user's device (D-156), fixed in `07c36d2`. Owner answers of 2026-10-19: M-1 no (backups stay unencrypted, rclone keeps the `drive` scope; accepted risk in SPEC §8.1 and §8.7), L-3 accepted and fixed (`96e4cbd`, D-119), the 16 MP and 512-character limits (D-114) confirmed; M-2's remaining question (the Home Assistant SSH key form) is moot with the NPM rework (D-120) and closed; D-120 also moves L-1 and the headers to NPM, makes L-2 moot, reverses the L-8 fix and replaces I-5's check with a conntrack check (dated notes in the review, SPEC §11.4). H-1 is fixed (`725901c`).
- Server overhaul (landed on `main` `8103dae` to `b985ce9`): shared helpers and one test harness, clippy pedantic wins; H-1 rate limiter (`725901c`, D-115), pictures at most 16 MP and 64 MB decoded (`baa5806`, D-114), `fcm_token` at most 512 characters (`b985ce9`, D-114); shared row inserts, invite creation and CLI helpers, `tests/common/` harness, synchronous `tasks::start`; 268 server tests; all crates at their latest versions. Recorded in SPEC v0.18.
- In flight:
- This docs pass (`agent-docs-k2`): SPEC v0.39, `docs/decisions.md` D-191 to D-200, `docs/ux/design-spec.md` (S-23 to S-27, §3.11, §3.12, S-5, S-7, S-8, S-13), this file.
- Next: the docs pass for K3's report (SPEC v0.40, D-201 to D-210).
- Earlier outcomes (details in SPEC §11.2, §11.3 and `docs/decisions.md`): M8 end-to-end test passed on the owner's S26 against the dev server on 2026-10-18 (pairing, scripted partner, offline expense synced, push by `fid` delivered); M8b to M8f UI fixes; the M2c-pi bundle `infra/pi/` (D-103 to D-106, owner checklist in `infra/README.md` "Pi: what the owner does"); release builds R8-shrunk and signed (alias `kvitt`, RSA 4096, v2).
- Compatibility: an APK from M12e on cannot pair or list devices against a pre-M12e server, so the server is rebuilt and redeployed before a new APK is installed (SPEC §14.8). The dev server already runs schema 3.
- Remote: `origin` on Gitea, SSH key works; pushed at `c13be3b` (2026-10-24). The Gitea secret `kvitt_cicd` (a user-level secret of `jacobh2`, renamed by the owner 2026-10-19) works; the Drive token is `infra/secrets/rclone.conf` (`[gdrive]`; folder id in `.env` as `RCLONE_FOLDER_ID`). Unused on the Mac since D-120: `infra/secrets/ha_ssh_key` and `ha_ssh_key.pub` (git-ignored; may be deleted).
- Both gates exist: Android `tools/gradle-serial.sh check` (from `android/`), server `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test` (from `server/`, `cargo audit` since `f1abb0d`). Both green on `main` as far as reported (Android green at `a8f91ee`, 1154 tests; server 356 tests at `671c00d`). The third gate (W0, D-131): `npm run check` in `web/` (generated files, `tsc --noEmit`, `eslint .`, Prettier, `vitest run`, `vite build`), green on `main` with W1 to W5 (59 files, 598 tests at `83f5293`); a change to `contracts/openapi.yaml` regenerates `web/src/api/schema.d.ts` in the same landing.
- Importer: the dry run of 2026-10-18 reconciled on both real files (Vardag 1746 rows, Hemma 37 rows, 0 unmapped). The production import on the Kvitt Pi is done (owner, 2026-10-21). The CSV files stay in `export/` (git-ignored).
- Development stack (M2c-dev): running on the Mac at `http://192.168.1.210:8080`, database at schema 3, data kept; backups `infra/dev/backups/kvitt-20261018T093143Z.sqlite.gz` and `kvitt-20261018T130443Z.sqlite.gz` (git-ignored). On this Podman setup run `infra/dev/up.sh --down`, then `infra/dev/up.sh`, to swap in a rebuilt image. Since `7fb86f5` `up.sh` copies only `fcm-service-account.json` into `infra/dev/secrets/` (git-ignored). The owner's phone is paired with it; a scripted partner exists (tokens held by the orchestrator outside the repo). Firebase project `kvitt-1337` with apps `se.hagstedt.kvitt` and `se.hagstedt.kvitt.debug`; service account `kvitt-fcm`.
- Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug`, paired against the dev server; the release app `se.hagstedt.kvitt`, rebuilt with the host `kvitt.suorra.duckdns.org` and no port (D-124) and installed 2026-10-19; paired against the Kvitt Pi 2026-10-21. The partner's phone is paired too.
- 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`, 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.
- Closed by this pass: the card design-spec sections S-23 to S-27 (`docs/ux/design-spec.md`, written from K2's goldens).
- Closed by `f15fa23` and `8fa9198`: `infra/build-images.sh` passes `--build-arg KVITT_VERSION_CODE` to the server build as the workflow does (v0.34), and detects Podman without a pipe.
- Closed 2026-10-24 (D-211): image builds moved to the Mac (`infra/build-images.sh v1.0.N`, the workflow the fallback); `v1.0.3` deployed on the Kvitt Pi; the web client checked from the laptop browser and S-11's Insights on the phone.
- Closed by `28fa71b`: the `/opt/kvitt` leftovers in `.gitea/workflows/build-images.yaml` and `infra/build-images.sh` (compose deploy hint, `docker image inspect`). Still stale in the workflow header (agent, next infra pass): "NOT YET RUN" and "First-build duration on the Pi: unknown" (now 2 h 42 min).
- Pushed: `5305f37` (orchestrator, 2026-10-24).
- Closed by `2acfc34` (`agent-gate-docs`): `infra/README.md`'s stale `CARGO_BUILD_JOBS` text and its "Owner decisions (open)" (now answered).
- `infra/backup/Dockerfile`: stale comment "on the Pi the container runs as uid 10001" (since D-125 the owner's uid); left as is, because editing it triggers a 5-minute image build (agent, with the next change there).
- Historical `:8443` and `icehack.asuscomm.com` mentions in `docs/ux/` and `docs/validation/` are left as the record of their time (no action unless the owner wants them rewritten).
- Restore drill AC-18.2 (`docs/validation/restore-drill.md`, M10; the one Missing row of the AC-coverage audit); `infra/RESTORE.md` is the runbook since D-125.
- Firewall rule on the Kvitt Pi limiting port 8080 to the HA Pi (`HA_HOST`): deferred by the owner 2026-10-24 (D-156, SPEC §10.2); it would be the owner's own step, outside `~/kvitt` (D-125).
- AC-20.1 shown on the Pi (M10). AC-18.1 and AC-16.3 observed 2026-10-24 (D-156).
- A `KvittWorkerFactory.createWorker` test (Android agent, no plan yet).
- Closed by `07c36d2`: security review L-10 (E-16 `not_found` for the other user's device, 403 dropped from `contracts/openapi.yaml`; also `ctutils` 0.4.4 for RUSTSEC-2026-0385).
- State-class naming: `*Ui`, `*UiState` and `*State` are mixed across features; needs a decision if a convention is wanted (owner; no plan).
- Unverified risk (the fallback workflow only): the workflow pushes a git tag; whether that re-triggers the path-filtered workflow is not known (watch the next runs).
- `PLAN-M2c-pi.md` is superseded where it names Caddy, cert-sync, scripts or `/opt/kvitt` (note at its top, D-120 and D-125); SPEC §10 and `infra/README.md` are current. `PLAN-M11.md` has a note that `/opt/kvitt/import` is now `~/kvitt/import`.
- Plans (SPEC §11.2 v0.24), in dependency order; prompt `prompts/<id>.md`:
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7g, M8 (phone test passed), M8b to M8f, M9, M12a to M12f, U1, U2, R8 release build, M2c-pi bundle with its NPM rework and the self-contained compose project (D-125), server gap pass, infra overhaul, infra security, the server overhaul, the Android overhaul, the APK download rate limit, `cargo audit`, the Android host property and its follow-ups (D-122), the server host defaults (D-123), port 443 (D-124), pull-to-refresh (D-127), the editor keyboard fix, the S-12 line chart (D-128): landed.
- Web client programme (D-134, SPEC §11.2 rows W0 to W7): all landed: W0 (`2ae65a5`), W1 (`934eee7`), W2 (`a65ab71`), W3 (`f9a9bfb`), W4 (`1aecf51`), W5 (`3ebe57d`), W6 (`1c9856b`), W7 (`50a0d67`); D-144 to D-154 recorded in v0.32; W0b follow-ups `5305f37` (v0.34).
- W0 to W7 and W0b verified on `v1.0.3` 2026-10-24 (D-211).
- Next: the app key and gate if not yet done (D-126, §3); M10 First release (keystore per §10.6, D-101; the reports `first-release.md` and `restore-drill.md`). Done 2026-10-21: release pairing of both phones against the Kvitt Pi and M11 Import history (SPEC §10.8 "Production run").
### Web follow-ups
- Done in W0b (`5305f37`, SPEC §15.3 v0.34): shared pieces once in `components`, `lib`, `api`, `app`; `AmountField` per page language with `hideLabel` and `placeholder`; `ids.ts` typing; Vitest cache off; `/analytics` in the dev proxy and Recharts in its own chunk (vendor 516 → 132 kB); `routes.ts` builders (`links.ts` files deleted); `spend_series` and `resolve_range` in `vectors.test.ts`; W5's recurring editor on `components/editor`; a click wording for the paid-by toggle; the version code from the commit count. Testing Library's wait limit is 5 s (`web/src/test/setup.ts`, load on the Mac during parallel agents).
- Kept: `/app/settings/invites` as an alias of `/app/settings/invite` (the spec names it, D-153).
- The image's version code: `ARG KVITT_VERSION_CODE` in `server/Dockerfile` and the workflow's build argument (`5d948f9`); `infra/build-images.sh` since `f15fa23`.
## 3. HUMAN steps pending (relay in chat)
Done 2026-10-21: the Pi install (stack running, `v1.0.1`, both containers healthy), the public health check through NPM, bootstrap, release pairing of both phones, the Splitwise import. Done 2026-10-24: both phones on release 140 (keyed, so the old step 1 is done), the first backup on Drive (AC-18.1); `v1.0.3` deployed and the web client checked from the laptop browser, S-11's Insights on the phone (the old steps 3 and 4, D-211).
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 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`).
## 5. Paused (waiting on something outside the repo)
- Nothing.
## 6. Numbering
- 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**.
## 7. How the work is run
See CLAUDE.md › Orchestration. HUMAN steps in reports are relayed in chat by the orchestrator; the owner does not read them in files.