aaa09d9073
Build Kvitt images / build (push) Has started running
v0.30: D-140 S-22 "Check for update" calls UpdateRepository.checkNow(), which always asks
GET /apk/latest and restarts the 6-hour window (AC-16.2, AC-16.6, S-22, §14.2.4).
v0.31: D-141 InsightsRepository UI contract (§14.2.4); D-142 Insights placement and loading
on S-11; D-143 display rules; design-spec S-11 "Insights" and §3.10; strings.csv 588 rows
(16 analytics_insights_* keys), web dictionaries regenerated; §11.2 W0 and W6 landed.
HANDOFF: state at 1c9856b, Pi image deploy step, numbering (D-144..D-155 reserved, next v0.32).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
36 KiB
36 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-21 late.
mainat1c9856b(W6 Android Insights on S-11, D-141 to D-143), pushed toorigin/main(Gitea), after415f9f5(CLAUDE.md: web client, three gates),2ae65a5(W0 web foundation),3fbd393(SPEC v0.29) andc0f4345(update-check fix, D-140). The Android gate is green again onmain(W6's E-34/E-35 DTOs fixed the three:dataexample tests red since50a0d67). This docs pass (agent-docs-v31) records D-140 as SPEC v0.30 and D-141 to D-143 as v0.31. Running: web W1 to W5 (D-144 to D-154 reserved for them). Landed after1c9856b, not yet recorded in the spec:d686ee0, the APK install-failure fix (D-155; twostrings.csvkeysupdate_install_failed,update_install_blocked, so the file has 590 rows against SPEC §13.4's 588; the web dictionaries are regenerated with them in this pass). The push of1c9856bstarted the Pi image build with the web client and E-34/E-35 (in progress on the Pi); then the owner deploys it (§3 step 4), and after the Pi runs the new image the release APK with Insights is published (infra/publish-apk.sh --deploy). - 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:
gitea.lan/jacobh2/kvitt-serverandgitea.lan/jacobh2/kvitt-backup, tagsv1.0.0(run 69) andv1.0.1(the sparse-checkout push; the tag the Pi uses), pluslatest; the packages are public, so the Pi pulls withoutdocker login. Gitea Actions on the owner's Pi runner (act_runner 0.6.1, Alpine job container): the first failures were an empty secret (the user-level secret was misnamed on the owner's side, fixed 2026-10-19; a user-level secret of the repository owner reaches the repository); run 69 succeeded: login ok,v1.0.0,kvitt-backup5 min 20 s,kvitt-server2 h 42 min on the Pi 3 (first build,CARGO_BUILD_JOBS=1; later runs reuse the builder's cargo cache); the tag push works with thewrite:packagetoken. SPEC §10.3. - 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./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); whether both phones run a keyed APK and 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 are running in parallel, each in its ownweb/src/features/<name>/; 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). The phones need a keyed APK before the gate goes in (§3). - 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.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 left as the spec says, decision pending (§4). 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-v31: SPEC v0.30 and v0.31, D-140 to D-143,strings.csv, the web dictionaries, design-spec S-11 and §3.10, this file). - Web W1 to W5 (
web/src/features/**, D-144 to D-154 reserved); the Pi image build with the web client and E-34/E-35. - Record D-155 (
d686ee0, install failures visible on S-17 and S-22) in the next spec version, v0.32, withstrings.csvat 590 rows (docs agent).
- 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 at7521c01. 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 at1c9856b, which fixed the three:dataexample tests red since50a0d67; server 299 tests at50a0d67). The third gate (W0, D-131):npm run checkinweb/(tsc --noEmit,eslint .,vitest run,vite build, generated-types freshness); 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)
- Land
agent-docs-v31(orchestrator): SPEC v0.30 and v0.31, D-140 to D-143,strings.csv,web/src/i18n/*.json, design-spec, HANDOFF; gatepython3 tools/contracts-check.py(the web gate for the regenerated dictionaries). - Pi image with the web client and E-34/E-35 (built from the
1c9856bpush): watch the workflow run, then the owner deploys it (§3 step 4), then HUMAN step 3; then publish a release APK with W6 (infra/publish-apk.sh --deploy) so the phones show Insights. - 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: through
1c9856b(W6 Android Insights). - 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. - Possible firewall rule on the Kvitt Pi limiting port 8080 to the HA Pi (
HA_HOST); not done, not decided (owner). D-125 makes no system changes, so it would be the owner's own step. - AC-18.1, AC-20.1 and AC-16.3 shown on the Pi and the phones (M2c-pi run, M10).
- A
KvittWorkerFactory.createWorkertest (Android agent, no plan yet). - Security review L-6: an S-1 design state if the owner wants a confirm step for an unusual server (OQ-6, default not in v1; owner, §4).
- Security review L-10:
forbiddenornot_foundfor the other user's device id (owner decision, §4). - State-class naming:
*Ui,*UiStateand*Stateare mixed across features; needs a decision if a convention is wanted (owner; no plan). - Unverified risk: 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): W0 (
2ae65a5), W6 (1c9856b) and W7 (50a0d67) landed; W1 to W5 running in parallel (briefs fromprompts/W1.mdtoprompts/W5.md); their decisions D-144 to D-154 are recorded by a docs agent when they land. - 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,
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.
- App key (D-126;
infra/README.mdstep 4a items 1 to 3; the key exists on the Mac and the Pi serves the keyedkvitt-133.apksince the first--deployrun, D-139; whether both phones run it is unconfirmed): generatekvitt.appKeyinandroid/local.propertieson the Mac (an agent may run the one-lineopensslcommand; the value is never printed); build and publish a keyed APK (infra/publish-apk.sh, copy todata/apk/); both phones on it (update banner, or sideload). - Gate (step 4a items 4 and 5), only after both phones run the keyed APK: 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 2, 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). After the first image with the web client is deployed (W7, §10.10), 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). - Deploy the new image (web client and E-34/E-35, SPEC §10.10), once the Gitea workflow run for
1c9856bhas succeeded: on the Kvitt Pi in~/kvitt, setKVITT_TAGin.envto the tag the run printed, thendocker compose pull && docker compose up -d;serverandbackuphealthy (yes/no). Then the orchestrator publishes the release APK with Insights.
4. Owner questions (asked, unanswered)
- OQ-6 (SPEC §12): a confirm step on S-1 for an unusual server; default "not in v1".
- Security review L-10: should a member get
not_foundinstead offorbiddenfor a device id of the other user (hides whose it is)? As built and as the spec says:forbidden. - State-class naming (
*Ui,*UiState,*Statemixed): fix one convention, or leave as built? - 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-143 used, except D-008 and D-009 (never used). Reserved: D-144 to D-154 for the web feature agents W1 to W5, D-155 for the APK install-failure fix. Next free after those: D-156.
- Spec version: v0.31 written (v0.30 D-140, v0.31 D-141 to D-143). Next v0.32.
- Server schema version 3 (migration 0003); next 4. Room schema version 1 (
android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json); next 2. Unchanged by v0.27 to v0.31 (W7 and W6 needed no migration). - 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.