Files
kvitt/docs/HANDOFF.md
T
jacobh2 ae73f58079 SPEC v0.37: image builds on the Mac, v1.0.3 deployed, web client verified (D-211)
infra/build-images.sh on the Mac with Podman is the normal image build, the
Gitea workflow on the Pi 3 runner the fallback (revises D-156 point 5); tag
rule, registry port 80 from the Mac (the Pi's .env keeps gitea.lan:3001),
Podman insecure-registry files, the script fixes f15fa23 and 8fa9198.
v1.0.3 on the Kvitt Pi; the owner verified the web client from the laptop
browser and S-11's Insights (SPEC §10.3, §10.10, §11.2 to §11.4, §15;
infra/README.md; HANDOFF).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-24 13:14:00 +02:00

45 KiB
Raw Blame History

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-24. main at 8b620c8 (the K0 report, SPEC v0.36), after 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: SPEC v0.37, D-211. 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 on main: server 300 tests, web 609 tests, contracts-check 136.
  • 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; 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). Next: K1 (server, contract) with K2 (Android) and K3 (web) starting in parallel on the spec and fakes; K1 lands first, K2 after it, K3 after K2; their proposed spec text goes into v0.37.
  • 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 ä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 (": 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 " (schema )" 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-v35): SPEC v0.37, docs/decisions.md D-211 (Mac image builds, v1.0.3 deployed, the web client verified), infra/README.md, this file.
  • 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 1c9856b, which fixed the three :data example tests red since 50a0d67; server 299 tests at 50a0d67). 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, K2, K3 (Opus agents, once SPEC v0.36 is on main; decisions from D-181 in disjoint ranges, spec version v0.37 for their proposed text): K1 lands first, K2 after K1, K3 after K2. After deploy, K1's owner steps 5 to 7 verify K0's three assumptions (D-177).
  • 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.
  • Card design-spec sections S-23 to S-27 for docs/ux/design-spec.md (docs agent, from K2's report).
  • 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). The partner's card and the next-day check moved to K1's owner steps (PLAN-K1 §8 steps 5 to 7, after K1 and a client with the card screens are deployed); K1's steps 2 to 4 (key to the Pi, .env, NPM block, deploy) come first.

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).
  • 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-180 and D-211 used, except D-008, D-009 and D-150 (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-210 are reserved for K1 to K3; next free after them: D-212.
  • Spec version: v0.37 written (D-211: Mac image builds, v1.0.3 deployed, the web client verified). Next v0.38. The K plans, prompts/README.md, §2's K item and SPEC §11.1 and §11.3 still name v0.37 for the K1 to K3 text (orchestrator: give them v0.38 in the briefs or have the references renamed).
  • Server schema version 3 in code (migration 0003); 4 specified (migration 0004, §14.1.3) for K1. Room schema version 1 in code; 2 specified (§14.2.3) for K2.
  • 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.
  • 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.