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
52 KiB
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.
mainat6f63751(SPEC v0.39, K2's report, D-191 to D-200, design-spec S-23 to S-27,agent-docs-k2), afterfaeb752(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.shPodman detection),f15fa23(infra/build-images.shpassesKVITT_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) and5305f37(W0b web shared follow-ups); before them07c36d2(L-10, D-156) and593c727(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.mdandprompts/README.md;mainwith this pass is pushed by the orchestrator after it lands. Deployed (owner's report): imagev1.0.4(git tagv1.0.4=671c00d, K1's server, schema 4) runs on the Kvitt Pi, so K1's owner steps 1 to 4 are done; imagev1.0.5(faeb752, the web card pages of K3) is being built on the Mac; release APKkvitt-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): imagev1.0.3(git tagv1.0.3=8fa9198, built on the Mac) runs on the Kvitt Pi,serverandbackuphealthy; the web client is live athttps://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 untilkvitt-164; whether the phones have updated is not recorded here (PLAN-K2 §8 step 1 starts with it). Gates: server 356 tests at671c00d; contracts-check 157 (17match_card_purchasecases); Android green again ata8f91ee(BUILD SUCCESSFUL, 791 actionable tasks, 1154 tests, 0 failures; the card examples E-36 to E-44 round-trip,VectorFilesTestandExampleRoundTripTestpass), so the pre-commit hook needs no bypass any more; web atfaeb75270 files, 684 tests, build ok (match_card_purchaseported). - 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. PlansPLAN-K0.mdtoPLAN-K3.md, promptsprompts/K0.mdtoprompts/K3.md, written byagent-a3b7abf183f883261, landedb5114ad. K0 done (2026-10-24): the test app (2ce317f) ran with the owner on his card; reportdocs/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 fromremittance_information, the datebooking_dateelsevalue_date, everything in SEK,entry_referenceon 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 itsentry_referencewhen 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 URLhttps://kvitt.suorra.duckdns.org/cards/callback, the owner's card linked); its key file lives only in the main checkout's git-ignoreddocs/spikes/k0/.local/<application id>.pemand 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 landed671c00d(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 ofsecrets/cards/and the NPM/cards/callbackblock; its report is SPEC v0.38 (D-181 to D-188). K2 landeda8f91ee(2026-10-24): Room version 2 (MIGRATION_1_2with its test,2.json), the card cycle after each household cycle (D-191 to D-196),:feature:cardsS-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), 77strings.csvrows, new catalog entryandroidx.browser1.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 landedfaeb752(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 inweb/src/api/cards.ts,matchCardPurchasepassing 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 atinfra/secrets/cards/enable-banking.pemin 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 runsdocker builder prune -afbetween builds (SPEC §10.3). - Kvitt host name (owner decision 2026-10-19):
kvitt.suorra.duckdns.org, a subdomain of the owner's DuckDNS family domainsuorra.duckdns.org; NPM issues its Let's Encrypt certificate;KVITT_PUBLIC_HOST=kvitt.suorra.duckdns.org, soKVITT_PUBLIC_URL=https://kvitt.suorra.duckdns.org(port 443, D-124). It is ininfra/pi/.env.exampleandinfra/pi/.env(KVITT_PUBLIC_HOST) andandroid/local.properties(kvitt.publicHost, git-ignored). SPEC §10.1 and §10.5.1 point here. - NPM proxy host for
kvitt.suorra.duckdns.orgis 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 nextv1.0.Nafter the highest tag onorigin(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-serverandkvitt-backup, tags up tov1.0.3(the first Mac build,8fa9198, the tag the Pi runs) pluslatest; the packages are public, so the Pi pulls withoutdocker login. Registry from the Mac:gitea.lanon port 80 (infra/secrets/registry.env:KVITT_REGISTRY=gitea.lan/jacobh2, user,write:packagetoken); the Pi's.envkeepsgitea.lan:3001/jacobh2(same registry). Podman's insecure-registry files:~/.config/containers/registries.conf.d/kvitt.confon the Mac and/etc/containers/registries.conf.d/kvitt.confin the Podman machine (owner's allowance 2026-10-24). Workflow history: run 69 (2026-10-19) builtv1.0.0,kvitt-server2 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/kvittas the owner's userjacobhagstedt, uid and gid 1000:1000 (KVITT_UID/KVITT_GID); Docker and thegitea.lanregistry trust were already on the Pi; nothing outside that folder is installed or changed. The stack is running (owner, 2026-10-21): imagev1.0.1,serverandbackupboth healthy after achownofdata/(it had been created as root); compose'smem_limitwarning (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)./healththrough 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/24NPM closes (return 444) every request withoutX-Kvitt-App: <key>, every route included; the key is the build propertykvitt.appKey(android/local.properties, git-ignored), generated by the owner withopenssl rand -hex 32and pasted into NPM's block (infra/npm/kvitt.conf,7521c01); the server andcontracts/openapi.yamlare unchanged. Android side34e5cad, docs2acfc34(SPEC v0.25,infra/README.mdstep 4a).android/local.propertieshas akvitt.appKeyandinfra/pi/out/apk/holdskvitt-125.apk(checked 2026-10-21); both phones run release 140, built with the key like every APK sincekvitt-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 bykvitt-serverunder/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 (deviceWeb · <browser>); the NPM gate also passes/app/,POST /invites/acceptand bearer-token requests except/health,/setup/,/i/,/apk/(new block ininfra/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). PlansPLAN-W0.mdtoPLAN-W7.md, promptsprompts/W0.mdtoprompts/W7.md. W7 landed50a0d67(D-135 to D-138, SPEC v0.29:/app/fromKVITT_WEB_DIR, E-34GET /analytics/trends, E-35GET /analytics/forecast, rules SPEC §14.5.7, the Node stage and the repository-root build context); W0 landed2ae65a5(web/**: Vite, React 19, TypeScript strict, sync-pull ledger, invite-code login, ported money rules passing the vectors, tokens, i18n generated fromdocs/ux/strings.csvandweb-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 ownweb/src/features/<name>/, W2 with the shared editor fields inweb/src/components/editor/; its loose ends done by W0b (5305f37, §2 "Web follow-ups"); W6 (Android Insights) landed1c9856band shows on the phones once the image with W7 and W0 is deployed and a release APK with W6 is published. The web gate isnpm run checkinweb/(third gate, inCLAUDE.mdsince415f9f5). - 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 anykvitt.appKeyexisted, so it sends noX-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/mainpushed at18c527f. 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 (7c453d8before the rewrite) no longer exists as a commit (it only touched PNGs). SPEC,docs/decisions.md, this file andprompts/README.mdcite the rewritten hashes since v0.18 (7c453d8stays as the oxipng pass's name;docs/validation/ac-coverage.mdkeeps its pre-rewrite hashes verbatim); since v0.19 the plans cite rewritten hashes too (eight pre-rewrite hashes inPLAN-*.mdreplaced by the commit with the same subject);docs/spikes/pi-image-arm64.mdand the READMEs had none stale. For any other old hash, find the commit by its subject withgit log --oneline --grep. - History (rewritten hashes):
5e1be38plans and prompts,c885abaM2c-dev,76ee957SPEC v0.3,c277ee3M2b,06f48efM2a,6dce4c7SPEC v0.4,7869ec4M3,8f0e156M5,853c038SPEC v0.5,15056dcM4,d9dd584SPEC v0.6,50339aeM9,86e08a1SPEC v0.7,aa765bbM12a,d39eb2cSPEC v0.8,b09f322M12c,6eaccecM12b,0006bd7UX Kalas categories and tokens,6da6b1bUX sprint U2,e676812SPEC v0.9,557af7eM12d,7730a31M6,9ee67d4SPEC v0.10,a41f240and6453932app wiring before M8,1ea752cM7a,ea7d494M7e,7380e50M7d,0e1d99dM7b,0b140aaM7c,c4ab35fM7f,b24c8b5M6b,f4dc10cSPEC v0.11,3dda414M7g,cb832a7and3d9eea8M8,6159e18SPEC v0.12,8c60715M12e,b7b7483M12e launcher icon,51d6ff8SPEC v0.13,a312758Vico removal,f56dcbeFCMfid,3e6c337Pi image arm64 check,e2dba84SPEC v0.14,328c466M8b,6eb3f85FcmMessageBody.fid,bfde12cSPEC v0.15,75b549fM8c goldens,a0c25b0M8c licences row,c30a49eR8 release build,bacbd18M8d,bfc3acbSPEC v0.16,2476667M2c-pi bundle,a59abebM8e,b4d9467DockerfileCARGO_BUILD_JOBS,956908aM8f,1328afechips cleanup,400eac9SPEC v0.17,18c527fgoldens restored after the rewrite; then2fac1ectoe70fe0dinfra overhaul,8660b7fserver gap pass,7fb86f5infra security,dc7a296toa0259abM12f,8103daetob985ce9server overhaul,48fe119SPEC v0.18,e4e6e5dtoc582363Android overhaul,3ef2c15SPEC v0.19,96e4cbdAPK download rate limit (D-119),481889aSPEC v0.20,f1abb0dcargo auditin the server gate,4f69ae0Gitea login in the run line,d05f13aPi stack behind NPM,62f5279and9231a1dinfra README for NPM (D-120),d090c6cAndroid host property (D-121),4bcff2cSPEC v0.21,dd07ca4temporary workflow diagnostic step,4a4e45fS-22's release server and4b1ae0cpublish-apk.shhost (D-122),c9abd24ande381e37server host defaults and OpenAPI{host}(D-123),ac17b41install.shstep number,cce3e4eSPEC v0.22,a86ad24server and contracts on port 443,4852803bundle on port 443 (D-124),038c603Android on port 443 and the pinned-root template,ad35231portless Android fixtures,b1f4c10SPEC v0.23,3820f9csparse-checkout workflow,200c99f.envwithout informational keys,6e5981dself-contained Pi compose project (D-125),28fa71bcompose deploy hint in the workflow andinfra/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/newand/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 fieldsweb/src/components/editor/;vite.config.tsserver.fs.allowforcontracts/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";resolveRangeandspendSeriesported 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 theGET /meheader fallback. - Web fix (
83f5293, no decision): Vitest's module cache kept a stalecatalog.tsglob result in the main checkout (raw keys such asweb_accounts_load_failedon screen); the glob lost its type argument andcatalog.test.tscompares the loaded feature catalogs with the files;web_accounts_load_failedbecame the sharedweb_ledger_load_failedinweb-strings.csv;suggest_categorymoved to PORTED inlib/vectors.test.ts; the fake server keeps non-JSON bodies raw. - APK install failures (
d686ee0, D-155): installer status reachesUpdateRepositoryby 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.csv590 rows. Releasekvitt-140deployed 2026-10-21.
- W1 web Home and accounts (
- 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" callsUpdateRepository.checkNow(), which always asksGET /apk/latestand 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; 40s11_insights_*goldens; Android gate green (791 tests in the feature run).strings.csv588 rows (16analytics_insights_*keys); the web dictionaries regenerated. - W0 web foundation (
2ae65a5) andCLAUDE.mdwith the web client and three gates (415f9f5); W0's report has no decisions recorded here.
- Update-check fix (
- 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, missingassets/*404,assets/*cached a year immutable,index.htmlno-store, the$kvitt_cspweb policy plusnosniff,DENY,no-referrer, strict path allowlist);/apppaths 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 inkvitt-domain::analytics(D-138);Cargo.lockrustls-webpki0.103.16,webpki-root-certs1.0.10 (RUSTSEC-2026-0382, -0383). 299 server tests; contracts-check 136. - Unattended APK deploy (
70cb25c, D-139):infra/publish-apk.sh --deploycopies the APK, thenlatest.json, with the forced-command keyinfra/secrets/pi_deploy_ed25519and checks/apk/latestwithX-Kvitt-App; first run 2026-10-21 servedversion_code133.
- W7 server web serving and analytics (
- 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 withSpendLineChart(one point per bucket, empty as zero), the dashed range average with its amount, a trailing 3-bucket moving average,LineLegendand 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; sevenstrings.csvkeys (569 rows); no model, domain, fakes, server or contract change. Goldenss12_category_groceries,s12_category_week,s12_category_empty_range,line_chart,line_chart_variantsnew,s12_category,s12_account,s12_longre-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 addimePadding(), so the keyboard no longer covers a focused field.
- S-12 line chart (
- 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 runSyncRepository.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.KvittPullToRefreshin:core:design(component/Refresh.kt):PullToRefreshBoxwith aprimary-containerdisc, thesyncicon turning with the pull and popping at the threshold (KvittMotion.spatialFast(),GestureThresholdActivatehaptic), aprimaryarc that turns indeterminate;ContainedLoadingIndicatorswaps in when the pinned Material 3 ships it. TalkBack "Synkar…" (sync_syncing, reused). GoldensS-4-home-refreshing,S-5-account-refreshing; 959 Android tests.
- Pull-to-refresh (
- Landed since SPEC v0.24 (recorded in v0.25, plus
infra/npm/kvitt.conf7521c01, no decision of its own):- App key header (
34e5cad, D-126):kvitt.appKey(-Pkvitt.appKey, elseandroid/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,toStringhides it);X-Kvitt-AppinKvittApi's common headers andPictureAuth; 952 Android tests.
- App key header (
- Landed since SPEC v0.23 (recorded in v0.24):
- Sparse-checkout workflow (
3820f9c, no decision): cone-modesparse-checkoutofserverandinfra/backup(the two build contexts), so the action fetches with--filter=blob:noneand no PNG is checked out;fetch-depth: 0stays becausefetch-tagswith a shallow fetch only brings tags on the tip; the temporary "Diagnose secrets" step removed. Its push builtv1.0.1. .envwithout the informational keys (200c99f, no decision; revises D-103):KVITT_PI_HOSTandKVITT_PI_SSH_USERdropped, read by nothing on the Pi; the README'sscp,sshand 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/kvittholdscompose.yaml,.env,secrets/and the bind-mounteddata/,backups/,import/; both containers asKVITT_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);.envexactly the seven keys compose reads,RCLONE_REMOTEfixed togdrive:;install.sh,deploy.sh,bootstrap.sh,backup-now.sh,install-apk.sh,lib.shremoved, operations are plaindocker composecommands (infra/README.md"Pi: what the owner does", seven steps, andinfra/RESTORE.md); no digest record (docker image inspectinstead); APKs byscpintodata/apk/. Rehearsed with Podman as uid 501: healthy,/health200, bootstrap, APK served, backup in three modes, restore steps. - Compose deploy hint (
28fa71b, no decision): the workflow's header and digest-step comment andinfra/build-images.sh's closing line namecd ~/kvitt,KVITT_TAGin.env,docker compose pull && docker compose up -danddocker image inspectinstead of/opt/kvitt/deploy.shand/opt/kvitt/deployed-digests.
- Sparse-checkout workflow (
- 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 serverhttps://{host}, E-7 and E-12 examples without a port,tools/contracts-check.pyfollows. 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_PORTremoved), the AC-2.6 intent filter without a port (matches every port, so an old:8443link still opens S-1 with the server it names), invite-link parsing drops:443and:80, 28 S-1 goldens onhttps://kvitt.example.org. - Portless Android fixtures (
ad35231, D-124):android/README.md, theAppBuildInfoandAboutRepositoryImplcomments, the S-21 and S-22 fake scenarios and the:datatest fixtures without:8443; 30 goldens re-recorded (6 S-21 invite-created, 24 S-22). What is left of8443inandroid/is deliberate: tests that an old:8443link still opens S-1 and that a typed port survives. - Pinned-root template (
038c603, no decision): withkvitt.tls.pinnedRoot=truethe build generates the pinned-root network security config fromandroid/app/src/releasePinnedRoot/network_security_config.xmlwith the domainkvitt.publicHostand stops with a message whileandroid/app/src/release/res/raw/kvitt_root_ca.crtis not committed (SPEC §8.5, §10.5.3). The old host name is out of that config and of the S-1 fixture.
- Port 443 (D-124, owner decision 2026-10-19): Kvitt is served at
- Landed since SPEC v0.21 (recorded in v0.22):
- D-121 follow-ups (
4a4e45f,4b1ae0c; D-122):AppBuildInfo.releaseServerUrlfromRELEASE_SERVER_URL; S-22 shows the paired server, before pairing the build's release server;DataModule.DEFAULT_SERVER_URLremoved; fakes and tests onhttps://kvitt.example.org:8443(30 S-21/S-22 goldens re-recorded);infra/publish-apk.shreadsKVITT_PUBLIC_HOSTfrominfra/pi/.env(refuses missing,CHANGE_ME, a URL, the old name) and passes-Pkvitt.publicHost. - Server host defaults (
c9abd24,e381e37; D-123):KVITT_PUBLIC_URLrequired (every command exits 1 without it, before opening the database);KVITT_TRUSTED_PROXYdefaults tonone; OpenAPI serverhttps://{host}:8443(portless since D-124) with variablehost(defaultkvitt.example.invalid); E-7 and E-12 examples onkvitt.example.invalid;tools/contracts-check.pyfollows. 276 server tests. install.shnames 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 in3820f9c).
- D-121 follow-ups (
- Landed since SPEC v0.20 (recorded in v0.21):
cargo auditin the server gate (f1abb0d, no decision): the gate iscargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test; the pre-commit hook runscargo audit --no-fetchonly whenserver/Cargo.lockor a serverCargo.tomlis 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 nameKVITT_PUBLIC_HOST(owner-chosen; a DuckDNS name is being set up), becauseicehack.asuscomm.comis 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 byinstall.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 -tagainst NPM v2.16.0 templates, 21 checks of the server behind nginx passed. The Gitea login interpolates the secret in therunline (the env form came through empty on act_runner 0.6.1). - Android host property (
d090c6c, D-121):kvitt.publicHost(Gradle property, elseandroid/local.properties, elsekvitt.example.invalid),BuildConfig.PUBLIC_HOST, the manifest placeholderpublicHost,RELEASE_SERVER_URL,@ReleaseServerUrlin: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-6GET /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; 429rate_limitedwithRetry-After; E-5 and E-4 stay on the 60; unit and integration tests.
- APK download rate limit (
- 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.screenshotswires Roborazzi and the test libraries;format/Failures.kt,format/Limits.kt,savedSplitLabelin:core:design; dead code removed (KvittMotiononly the seesaw spring,KvittIcon.Description, unused dimens and shapes); four catalog entries removed; oneCopyCategoryKeywordstask; oneconfigureAndroidModule; S-7's state types inExpenseEditorState.kt. Gate at landing: 941 tests, no golden changed. Release APK 10.46 MB.
- Android overhaul (
- Landed since SPEC v0.17 (recorded in v0.18):
- Infra overhaul (
2fac1ec,61f92c8,fc6126c,b647faf,e70fe0d; no decision):tools/gradle-lock.shshared bygradle-serial.sh,clean-builds.shandinfra/build-images.sh; the pre-commit hook runs throughgradle-serial.shwith JDK 25 onPATH(hooks reinstalled inmainaftere70fe0d);install.shkeeps the deployedKVITT_TAG;--provenance=falseon buildx pushes, the workflow token through env;RCLONE_REMOTEdropped 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 sentinfo line; CLI tests (invite-partner,migrate,healthcheck, SIGTERM drain);tools/contracts-check.py134 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),/certs0750, dev stack mounts only the FCM key,/opt/kvitt/deployed-digestswith a WARNING on change,kvitt_cicdwithwrite:packageonly, 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,SettlementRepositoryreturns null instead of throwing, E-17 retry after each successful sync, invite links dropped while paired, private notifications on the lock screen,localeFiltersen/sv,:datatests (429, D-040, WorkManager viawork-testing), the home/account screenshot harness fix (stalesv_darkgoldens re-recorded), 19 unused string keys removed (strings.csv562 rows). Gate at landing: 941 tests. Release APK 10.0 MB.
- Infra overhaul (
- Validation reports (SPEC §11.4):
docs/validation/ac-coverage.md(taken at956908a,ed1b130before 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 at18c527f): 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_foundfor the other user's device (D-156), fixed in07c36d2. Owner answers of 2026-10-19: M-1 no (backups stay unencrypted, rclone keeps thedrivescope; 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
main8103daetob985ce9): 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_tokenat most 512 characters (b985ce9, D-114); shared row inserts, invite creation and CLI helpers,tests/common/harness, synchronoustasks::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.mdD-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).
- This docs pass (
- 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 byfiddelivered); M8b to M8f UI fixes; the M2c-pi bundleinfra/pi/(D-103 to D-106, owner checklist ininfra/README.md"Pi: what the owner does"); release builds R8-shrunk and signed (aliaskvitt, 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:
originon Gitea, SSH key works; pushed atc13be3b(2026-10-24). The Gitea secretkvitt_cicd(a user-level secret ofjacobh2, renamed by the owner 2026-10-19) works; the Drive token isinfra/secrets/rclone.conf([gdrive]; folder id in.envasRCLONE_FOLDER_ID). Unused on the Mac since D-120:infra/secrets/ha_ssh_keyandha_ssh_key.pub(git-ignored; may be deleted). - Both gates exist: Android
tools/gradle-serial.sh check(fromandroid/), servercargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test(fromserver/,cargo auditsincef1abb0d). Both green onmainas far as reported (Android green ata8f91ee, 1154 tests; server 356 tests at671c00d). The third gate (W0, D-131):npm run checkinweb/(generated files,tsc --noEmit,eslint ., Prettier,vitest run,vite build), green onmainwith W1 to W5 (59 files, 598 tests at83f5293); a change tocontracts/openapi.yamlregeneratesweb/src/api/schema.d.tsin 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; backupsinfra/dev/backups/kvitt-20261018T093143Z.sqlite.gzandkvitt-20261018T130443Z.sqlite.gz(git-ignored). On this Podman setup runinfra/dev/up.sh --down, theninfra/dev/up.sh, to swap in a rebuilt image. Since7fb86f5up.shcopies onlyfcm-service-account.jsonintoinfra/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 projectkvitt-1337with appsse.hagstedt.kvittandse.hagstedt.kvitt.debug; service accountkvitt-fcm. - Installed on the owner's phone (SM-S942B): the debug app
se.hagstedt.kvitt.debug, paired against the dev server; the release appse.hagstedt.kvitt, rebuilt with the hostkvitt.suorra.duckdns.organd 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.shwhen 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 APKkvitt-164(K2) is on the Pi. Next: finish and deploy imagev1.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, afterv1.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.linkedcannot tell "unlinked" from "linked to an expense not yet on the phone"; alinkedExpenseIdfield 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
f15fa23and8fa9198:infra/build-images.shpasses--build-arg KVITT_VERSION_CODEto 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.3deployed 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/kvittleftovers in.gitea/workflows/build-images.yamlandinfra/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 staleCARGO_BUILD_JOBStext 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
:8443andicehack.asuscomm.commentions indocs/ux/anddocs/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.mdis 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.createWorkertest (Android agent, no plan yet). - Closed by
07c36d2: security review L-10 (E-16not_foundfor the other user's device, 403 dropped fromcontracts/openapi.yaml; alsoctutils0.4.4 for RUSTSEC-2026-0385). - State-class naming:
*Ui,*UiStateand*Stateare 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.mdis superseded where it names Caddy, cert-sync, scripts or/opt/kvitt(note at its top, D-120 and D-125); SPEC §10 andinfra/README.mdare current.PLAN-M11.mdhas a note that/opt/kvitt/importis 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-ups5305f37(v0.34). - W0 to W7 and W0b verified on
v1.0.32026-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.mdandrestore-drill.md). Done 2026-10-21: release pairing of both phones against the Kvitt Pi and M11 Import history (SPEC §10.8 "Production run").
- 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,
Web follow-ups
- Done in W0b (
5305f37, SPEC §15.3 v0.34): shared pieces once incomponents,lib,api,app;AmountFieldper page language withhideLabelandplaceholder;ids.tstyping; Vitest cache off;/analyticsin the dev proxy and Recharts in its own chunk (vendor 516 → 132 kB);routes.tsbuilders (links.tsfiles deleted);spend_seriesandresolve_rangeinvectors.test.ts; W5's recurring editor oncomponents/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/invitesas an alias of/app/settings/invite(the spec names it, D-153). - The image's version code:
ARG KVITT_VERSION_CODEinserver/Dockerfileand the workflow's build argument (5d948f9);infra/build-images.shsincef15fa23.
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.
- 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 openshttps://kvitt.suorra.duckdns.org/i/TESTand 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). - 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.confcontent (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) andhttps://kvitt.suorra.duckdns.org/healthstill gives a connection error (yes/no) (step 4a item 5 d and e). - 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.4deployed on the Pi); for the record they were: step 1 is done by the orchestrator (the key staged atinfra/secrets/cards/enable-banking.pemin the main checkout, git-ignored, mode 0600); step 2 on the Pi in~/kvitt:mkdir -p secrets/cards && chmod 700 secrets/cardsbefore anything else (the newcompose.yamlrefuses to start without that folder, even with no card configured, D-188),scpthe key,chmod 600 secrets/cards/*, copy the newcompose.yaml, setKVITT_EB_APP_ID=in.env(the orchestrator gives the id in chat) and leaveKVITT_EB_MEMBER_APP_ID=empty; step 3 paste the currentinfra/npm/kvitt.confinto NPM (row Online, yes/no); step 4 backup,KVITT_TAG,docker compose pull && docker compose up -d(serverhealthy,/healthshows"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. - Card purchases on the phone (PLAN-K2 §8, all pending; K1's image runs on the Pi and
kvitt-164is 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). - Card purchases on the laptop (PLAN-K3 §8, all pending; after image
v1.0.5is deployed on the Pi): 1https://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
linkedExpenseIdto tell it apart (a UI-contract change)? Default: not now. Note only, no question: S-25's "timed out" relies on the server answeringexpired(15 minutes), the phone does not check the clock. - K3, SPEC §13.3: the date form of "upphör " / "giltig till " and of "Från kortköp: …, ". 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.csvnotes the list form forsettings_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,*Statemixed): 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-10not_foundfor the other user's device;mem_limitkept, 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 onmainby 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~/kvittwith nothing installed elsewhere (D-125). Answered 2026-10-19: Kvitt's host name iskvitt.suorra.duckdns.org(§1); Kvitt on port 443 through the existing router forward (D-124). Moot:write:repositoryfor the registry secret (the tag push works withwrite: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.3deploy (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 APKkvitt-164(K2) on the Pi. - Server schema version 4 in code (migration 0004, K1
671c00d); the Kvitt Pi runs it sincev1.0.4; the dev stack still runs schema 3 until its next deploy. Room schema version 2 in code (K2a8f91ee,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.