b1f4c10a3d
D-124 (owner decision 2026-10-19;a86ad24,4852803,038c603): Kvitt is served at https://<host> with no explicit port; the router's existing external 443 forward to the Home Assistant Pi serves it, no 8443 rule. Every :8443 in SPEC replaced; D-120 to D-123 struck through with pointers. The pinned-root config is generated from its template with kvitt.publicHost (038c603). HANDOFF, prompts/README.md status and the S-1 TalkBack line follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
22 KiB
22 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
mainatad35231(portless Android fixtures after D-124; on top of D-124'sa86ad24server and contracts,4852803bundle,038c603Android with the pinned-root template, SPEC v0.22cce3e4eandac17b41) plus the SPEC v0.23 docs commit fromagent-docs-v023once it lands.origin/main(Gitea) at038c603:ad35231is not pushed yet.- 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(KVITT_PUBLIC_HOST) andandroid/local.properties(kvitt.publicHost), both 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). From the Mac,https://kvitt.suorra.duckdns.org/healthpresents a valid certificate and returns NPM's 502, as expected while the Kvitt Pi is not running. The router needs no new rule: external TCP 443 already forwards to the Home Assistant Pi (owner confirmed, D-124). - Gitea secret diagnosis (the temporary "Diagnose secrets" step,
dd07ca4): the user-level secretbabymonitor_cicdreaches the repository (length 40),kvitt_cicdhas length 0, so the secret is not stored under that name on thejacobh2account. The owner is fixing the name (§3 item 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. - 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.
- 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-v023: SPEC v0.23, D-124, HANDOFF,prompts/README.md). - Gitea Actions: the login step interpolates the secret in the
runline (4f69ae0); the diagnosis foundkvitt_cicdempty (§1); the owner is storing it under the right name (§3 item 1).
- 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 at038c603. The owner created a Gitea secret meant askvitt_cicd(not found under that name, §1) and the Drive token (infra/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 944 tests atd090c6c, one more test in4a4e45f; server 276 tests atc9abd24). - Importer: the dry run of 2026-10-18 reconciled on both real files (Vardag 1746 rows, Hemma 37 rows, 0 unmapped). 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; release pairing waits for the Kvitt Pi (release builds requirehttps, AC-2.8; §3 item 3). - 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-v023(orchestrator): SPEC v0.23, D-124, HANDOFF,prompts/README.md, the S-1 TalkBack line ofdocs/ux/design-spec.md. - Closed by D-124,
038c603andad35231: the server's and the bundle's:8443,RELEASE_SERVER_PORT, the intent filter's port, the pinned-root config's old host name and the stale "pinned Caddy root" comment,OnboardingStates.kt'sGOLDEN_SERVER, the:datacomments, theFakeScenariosandSettingsScenariosfixture URLs,android/README.md. - Push
ad35231and the v0.23 docs commit toorigin(orchestrator). - Remove the temporary "Diagnose secrets" workflow step (
dd07ca4) once a run passes the login (agent, after §3 item 1). - Possible firewall rule on the Kvitt Pi limiting access to port 8080 to the HA Pi (
HA_HOST); not done, not decided (owner). PLAN-M2c-pi.mdis superseded where it names Caddy and cert-sync (note at its top); SPEC §10 andinfra/README.mdare current.- Restore drill AC-18.2 (
docs/validation/restore-drill.md, M10; the one Missing row of the AC-coverage audit). - 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).
- Owner runs the Pi bundle on the Kvitt Pi (§3 item 2); the checklist is
infra/README.md"Pi: what the owner does" (nine steps; NPM at step 6 is done, the router at step 7 is a check only, D-124). - Plans (SPEC §11.2 v0.23), 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 and its NPM rework, 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): landed. - Next: the owner runs the bundle on the Kvitt Pi when he moves over (D-100; the image must carry the H-1 fix, which every image since
725901cdoes); M10 First release after it (keystore per §10.6, D-101); M11 Import history after M9 and M10 (procedure §10.8 "Production run", D-025; each run must reconcile, D-045).
- 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 and its NPM rework, 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)
- Gitea secret: in Gitea, the user menu, Settings, Actions, Secrets, add the secret
KVITT_CICDwith the registry token (write:package). Then the repository, Actions, "Build Kvitt images", "Run workflow". Report: did "Log in to Gitea Container Registry" pass (yes/no), and did the run finish (yes/no; if "Tag and Push Version" failed with a permission error, say so). - Pi: provision the Kvitt Pi and install Docker (
infra/README.md"Pi: what the owner does" steps 1 and 2); fillinfra/pi/.env(HA_HOST,KVITT_PI_HOST,KVITT_PI_SSH_USER,KVITT_TAG); copy the bundle to the Pi; runinstall.sh. Expected: on the phone,https://kvitt.suorra.duckdns.org/healthshows"status":"ok"(yes/no). - Release app pairing, after item 2: run
kvitt-server bootstrapon the Pi (/opt/kvitt/bootstrap.sh) and pair the release app with that code (relayed in chat with the exact screen labels when the time comes).
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? write:repositoryfor the registry secret, only if the workflow's tag push fails (§3 item 1).- 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).
5. Paused (waiting on something outside the repo)
- Nothing.
6. Numbering
- Decisions: D-001 to D-124 used, except D-008 and D-009 (never used); nothing reserved. Next free: D-125.
- Spec version: v0.23 written; next v0.24.
- 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. Both unchanged by v0.23. - 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.