Files
kvitt/docs/HANDOFF.md
T
jacobh2 cce3e4e9d9 SPEC v0.22: D-122 S-22's release server and publish-apk host; D-123 no built-in server host; Kvitt host name
D-122 (4a4e45f, 4b1ae0c): AppBuildInfo.releaseServerUrl, S-22 before pairing, publish-apk.sh reads
KVITT_PUBLIC_HOST from infra/pi/.env. D-123 (c9abd24, e381e37): KVITT_PUBLIC_URL required,
KVITT_TRUSTED_PROXY defaults to none, OpenAPI server https://{host}:8443, E-7/E-12 examples on
kvitt.example.invalid. The owner's host name kvitt.suorra.duckdns.org recorded in HANDOFF (SPEC §10.1,
§10.5.1 point there). HANDOFF and prompts/README.md status at ac17b41.

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

20 KiB
Raw Permalink Blame History

Handoff: state of the work

Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.

1. Where things stand

  • main at ac17b41 (the install.sh step-number fix, on top of D-123 c9abd24/e381e37, D-122 4a4e45f/4b1ae0c, the temporary workflow diagnostic dd07ca4 and SPEC v0.21 4bcff2c) plus the SPEC v0.22 docs commit from agent-docs-v022 once it lands. origin/main (Gitea) at ac17b41: everything is pushed.
  • Kvitt host name (owner decision 2026-10-19): kvitt.suorra.duckdns.org, a subdomain of the owner's DuckDNS family domain suorra.duckdns.org; NPM issues its Let's Encrypt certificate; KVITT_PUBLIC_HOST=kvitt.suorra.duckdns.org, so KVITT_PUBLIC_URL=https://kvitt.suorra.duckdns.org:8443. An agent is writing it into infra/pi/.env (KVITT_PUBLIC_HOST) and android/local.properties (kvitt.publicHost), both git-ignored. SPEC §10.1 and §10.5.1 point here.
  • History rewrite and first push, 2026-10-18 (owner authorised): superseded PNG versions were dropped from the whole history (no LFS), the current goldens and UX mock-ups restored in 18c527f; pack 45 MiB; origin/main pushed at 18c527f. A bundle of the pre-rewrite history (kvitt-pre-rewrite.bundle) is kept outside the repository, in the orchestrator's scratchpad. Every commit from M2b on got a new hash; the oxipng pass (7c453d8 before the rewrite) no longer exists as a commit (it only touched PNGs). SPEC, docs/decisions.md, this file and prompts/README.md cite the rewritten hashes since v0.18 (7c453d8 stays as the oxipng pass's name; docs/validation/ac-coverage.md keeps its pre-rewrite hashes verbatim); since v0.19 the plans cite rewritten hashes too (eight pre-rewrite hashes in PLAN-*.md replaced by the commit with the same subject); docs/spikes/pi-image-arm64.md and the READMEs had none stale. For any other old hash, find the commit by its subject with git log --oneline --grep.
  • History (rewritten hashes): 5e1be38 plans and prompts, c885aba M2c-dev, 76ee957 SPEC v0.3, c277ee3 M2b, 06f48ef M2a, 6dce4c7 SPEC v0.4, 7869ec4 M3, 8f0e156 M5, 853c038 SPEC v0.5, 15056dc M4, d9dd584 SPEC v0.6, 50339ae M9, 86e08a1 SPEC v0.7, aa765bb M12a, d39eb2c SPEC v0.8, b09f322 M12c, 6eaccec M12b, 0006bd7 UX Kalas categories and tokens, 6da6b1b UX sprint U2, e676812 SPEC v0.9, 557af7e M12d, 7730a31 M6, 9ee67d4 SPEC v0.10, a41f240 and 6453932 app wiring before M8, 1ea752c M7a, ea7d494 M7e, 7380e50 M7d, 0e1d99d M7b, 0b140aa M7c, c4ab35f M7f, b24c8b5 M6b, f4dc10c SPEC v0.11, 3dda414 M7g, cb832a7 and 3d9eea8 M8, 6159e18 SPEC v0.12, 8c60715 M12e, b7b7483 M12e launcher icon, 51d6ff8 SPEC v0.13, a312758 Vico removal, f56dcbe FCM fid, 3e6c337 Pi image arm64 check, e2dba84 SPEC v0.14, 328c466 M8b, 6eb3f85 FcmMessageBody.fid, bfde12c SPEC v0.15, 75b549f M8c goldens, a0c25b0 M8c licences row, c30a49e R8 release build, bacbd18 M8d, bfc3acb SPEC v0.16, 2476667 M2c-pi bundle, a59abeb M8e, b4d9467 Dockerfile CARGO_BUILD_JOBS, 956908a M8f, 1328afe chips cleanup, 400eac9 SPEC v0.17, 18c527f goldens restored after the rewrite; then 2fac1ec to e70fe0d infra overhaul, 8660b7f server gap pass, 7fb86f5 infra security, dc7a296 to a0259ab M12f, 8103dae to b985ce9 server overhaul, 48fe119 SPEC v0.18, e4e6e5d to c582363 Android overhaul, 3ef2c15 SPEC v0.19, 96e4cbd APK download rate limit (D-119), 481889a SPEC v0.20, f1abb0d cargo audit in the server gate, 4f69ae0 Gitea login in the run line, d05f13a Pi stack behind NPM, 62f5279 and 9231a1d infra README for NPM (D-120), d090c6c Android host property (D-121), 4bcff2c SPEC v0.21, dd07ca4 temporary workflow diagnostic step, 4a4e45f S-22's release server and 4b1ae0c publish-apk.sh host (D-122), c9abd24 and e381e37 server host defaults and OpenAPI {host} (D-123), ac17b41 install.sh step number.
  • Landed since SPEC v0.21 (recorded in v0.22):
    • D-121 follow-ups (4a4e45f, 4b1ae0c; D-122): AppBuildInfo.releaseServerUrl from RELEASE_SERVER_URL; S-22 shows the paired server, before pairing the build's release server; DataModule.DEFAULT_SERVER_URL removed; fakes and tests on https://kvitt.example.org:8443 (30 S-21/S-22 goldens re-recorded); infra/publish-apk.sh reads KVITT_PUBLIC_HOST from infra/pi/.env (refuses missing, CHANGE_ME, a URL, the old name) and passes -Pkvitt.publicHost.
    • Server host defaults (c9abd24, e381e37; D-123): KVITT_PUBLIC_URL required (every command exits 1 without it, before opening the database); KVITT_TRUSTED_PROXY defaults to none; OpenAPI server https://{host}:8443 with variable host (default kvitt.example.invalid); E-7 and E-12 examples on kvitt.example.invalid; tools/contracts-check.py follows. 276 server tests.
    • install.sh names the NPM proxy host as README step 6 (ac17b41, no decision).
    • Temporary "Diagnose secrets" step in .gitea/workflows/build-images.yaml (dd07ca4, no decision), to be removed once the login failure's cause is known.
  • Landed since SPEC v0.20 (recorded in v0.21):
    • cargo audit in the server gate (f1abb0d, no decision): the gate is cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test; the pre-commit hook runs cargo audit --no-fetch only when server/Cargo.lock or a server Cargo.toml is staged. First run: 0 advisories over 319 crates.
    • NPM rework (4f69ae0, d05f13a, 62f5279, 9231a1d; D-120): TLS terminates in the owner's Nginx Proxy Manager on the Home Assistant Pi; Caddy, cert-sync, the Caddyfile, the HA SSH key and the router forward to the Kvitt Pi are gone. Kvitt gets its own host name KVITT_PUBLIC_HOST (owner-chosen; a DuckDNS name is being set up), because icehack.asuscomm.com is NPM's proxy host for Home Assistant. Router external 8443 → HA Pi 443. The server publishes 8080 on the Kvitt Pi's LAN, KVITT_TRUSTED_PROXY=${HA_HOST} (NPM's IPv4, checked by install.sh). SPEC §10.5 rewritten (NPM proxy host, the Custom Nginx Configuration with D-117's headers and the /i/ log exclusion, fallback: no LAN path for release builds). Rehearsed on the Mac: nginx -t against NPM v2.16.0 templates, 21 checks of the server behind nginx passed. The Gitea login interpolates the secret in the run line (the env form came through empty on act_runner 0.6.1).
    • Android host property (d090c6c, D-121): kvitt.publicHost (Gradle property, else android/local.properties, else kvitt.example.invalid), BuildConfig.PUBLIC_HOST, the manifest placeholder publicHost, RELEASE_SERVER_URL, @ReleaseServerUrl in :feature:onboarding. 944 Android tests.
  • Landed since SPEC v0.19 (recorded in v0.20):
    • APK download rate limit (96e4cbd, D-119, security review L-3): E-6 GET /apk/files/{file_name} at most 10 requests per minute per client IP in its own window, not counted against the 60 of the other unauthenticated endpoints; a 404 counts; 429 rate_limited with Retry-After; E-5 and E-4 stay on the 60; unit and integration tests.
  • Landed since SPEC v0.18 (recorded in v0.19):
    • Android overhaul (e4e6e5d, 8229ed6, 515b60f, e08aaf0, 0714242, a92c236, 74b5fbd, c582363; D-118): shared test support in :core:design's test fixtures (se.hagstedt.kvitt.design.testing), kvitt.android.screenshots wires Roborazzi and the test libraries; format/Failures.kt, format/Limits.kt, savedSplitLabel in :core:design; dead code removed (KvittMotion only the seesaw spring, KvittIcon.Description, unused dimens and shapes); four catalog entries removed; one CopyCategoryKeywords task; one configureAndroidModule; S-7's state types in ExpenseEditorState.kt. Gate at landing: 941 tests, no golden changed. Release APK 10.46 MB.
  • Landed since SPEC v0.17 (recorded in v0.18):
    • Infra overhaul (2fac1ec, 61f92c8, fc6126c, b647faf, e70fe0d; no decision): tools/gradle-lock.sh shared by gradle-serial.sh, clean-builds.sh and infra/build-images.sh; the pre-commit hook runs through gradle-serial.sh with JDK 25 on PATH (hooks reinstalled in main after e70fe0d); install.sh keeps the deployed KVITT_TAG; --provenance=false on buildx pushes, the workflow token through env; RCLONE_REMOTE dropped from the dev env.
    • Server gap pass (8660b7f, D-113): kvitt-server invite-partner (0 ok, 2 household full, 3 no admin), so a scripted partner needs no second admin device; push sent info line; CLI tests (invite-partner, migrate, healthcheck, SIGTERM drain); tools/contracts-check.py 134 checks.
    • Infra security (7fb86f5, D-117): Caddy log redaction of /i/<code> and query strings, CSP and three more response headers, Caddy non-root (uid 10002, read-only, caps dropped), /certs 0750, dev stack mounts only the FCM key, /opt/kvitt/deployed-digests with a WARNING on change, kvitt_cicd with write:package only, the plain root key (old variant B) not acceptable.
    • M12f (dc7a296, 5b2ed37, 2bd6b58, c366fbd, a0259ab; D-109 to D-112, D-116): S-16 lost version read-only, S-22 " (schema )" from /health, SettlementRepository returns null instead of throwing, E-17 retry after each successful sync, invite links dropped while paired, private notifications on the lock screen, localeFilters en/sv, :data tests (429, D-040, WorkManager via work-testing), the home/account screenshot harness fix (stale sv_dark goldens re-recorded), 19 unused string keys removed (strings.csv 562 rows). Gate at landing: 941 tests. Release APK 10.0 MB.
  • Validation reports (SPEC §11.4): docs/validation/ac-coverage.md (taken at 956908a, ed1b130 before the rewrite): 270 rows, 260 Done, 5 Partial, 1 Missing, 4 device-only; feature complete in code, not yet against §4's acceptance rule; its S-16 and S-22 mismatches are fixed by M12f. docs/validation/security-review.md (taken at 18c527f): Critical 0, High 1, Medium 3, Low 10, Info 8; L-6 kept (D-116), L-7 fixed (D-116), L-1, L-2, L-5, L-9, M-2 fixed (D-117), L-8 fixed (e70fe0d); H-1 (725901c, D-115), M-3 (baa5806) and L-4 (b985ce9, D-114) fixed by the server overhaul; L-10 left as the spec says, decision pending (§4). Owner answers of 2026-10-19: M-1 no (backups stay unencrypted, rclone keeps the drive scope; accepted risk in SPEC §8.1 and §8.7), L-3 accepted and fixed (96e4cbd, D-119), the 16 MP and 512-character limits (D-114) confirmed; M-2's remaining question (the Home Assistant SSH key form) is moot with the NPM rework (D-120) and closed; D-120 also moves L-1 and the headers to NPM, makes L-2 moot, reverses the L-8 fix and replaces I-5's check with a conntrack check (dated notes in the review, SPEC §11.4). H-1 must be fixed before port 8443 is opened.
  • Server overhaul (landed on main 8103dae to b985ce9): shared helpers and one test harness, clippy pedantic wins; H-1 rate limiter (725901c, D-115), pictures at most 16 MP and 64 MB decoded (baa5806, D-114), fcm_token at most 512 characters (b985ce9, D-114); shared row inserts, invite creation and CLI helpers, tests/common/ harness, synchronous tasks::start; 268 server tests; all crates at their latest versions. Recorded in SPEC v0.18.
  • In flight:
    • This docs pass (agent-docs-v022: SPEC v0.22, D-122, D-123, HANDOFF, prompts/README.md).
    • The host name into infra/pi/.env and android/local.properties (agent, git-ignored files only).
    • Gitea Actions: the login failed with "password is empty" in the env form, and the run-line form is in place (4f69ae0); dd07ca4 added a temporary "Diagnose secrets" step to find the cause (owner, §3 item 1).
  • Earlier outcomes (details in SPEC §11.2, §11.3 and docs/decisions.md): M8 end-to-end test passed on the owner's S26 against the dev server on 2026-10-18 (pairing, scripted partner, offline expense synced, push by fid delivered); M8b to M8f UI fixes; the M2c-pi bundle infra/pi/ (D-103 to D-106, owner checklist in infra/README.md "Pi: what the owner does"); release builds R8-shrunk and signed (alias kvitt, RSA 4096, v2).
  • Compatibility: an APK from M12e on cannot pair or list devices against a pre-M12e server, so the server is rebuilt and redeployed before a new APK is installed (SPEC §14.8). The dev server already runs schema 3.
  • Remote: origin on Gitea, SSH key works; pushed at ac17b41. The owner created the Gitea secret kvitt_cicd and the Drive token (infra/secrets/rclone.conf, [gdrive]; folder id in .env as RCLONE_FOLDER_ID). Unused on the Mac since D-120: infra/secrets/ha_ssh_key and ha_ssh_key.pub (git-ignored; may be deleted).
  • Both gates exist: Android tools/gradle-serial.sh check (from android/), server cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test (from server/, cargo audit since f1abb0d). Both green on main as far as reported (Android 944 tests at d090c6c, one more test in 4a4e45f; server 276 tests at c9abd24).
  • 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; backups infra/dev/backups/kvitt-20261018T093143Z.sqlite.gz and kvitt-20261018T130443Z.sqlite.gz (git-ignored). On this Podman setup run infra/dev/up.sh --down, then infra/dev/up.sh, to swap in a rebuilt image. Since 7fb86f5 up.sh copies only fcm-service-account.json into infra/dev/secrets/ (git-ignored). The owner's phone is paired with it; a scripted partner exists (tokens held by the orchestrator outside the repo). Firebase project kvitt-1337 with apps se.hagstedt.kvitt and se.hagstedt.kvitt.debug; service account kvitt-fcm.
  • Installed on the owner's phone (SM-S942B): the debug app se.hagstedt.kvitt.debug, paired against the dev server; the release app se.hagstedt.kvitt, installed 2026-10-19: About and the release app opening were confirmed; release pairing is deferred to production (release builds require https, AC-2.8).
  • Disk: the MacBook had about 72 GB free during the v0.20 docs pass (2026-10-19); run tools/clean-builds.sh when it drops under about 5 GB.

2. Open work (one line each, with who owns it: agent or owner)

  • Land agent-docs-v022 (orchestrator): SPEC v0.22, D-122, D-123, HANDOFF, prompts/README.md.
  • Closed by D-122, D-123 and ac17b41: the server's DEFAULT_PUBLIC_URL and DEFAULT_TRUSTED_PROXY, openapi.rs and contracts/openapi.yaml server URL, the E-7 and E-12 examples, server/README.md, the fakes' AboutState URLs, DataModule.DEFAULT_SERVER_URL, publish-apk.sh's host, install.sh's step number.
  • Old host name still in the Android tree (agent, no plan yet): the pinned-root config's domain (android/app/src/releasePinnedRoot/res/xml/network_security_config.xml, D-082; follows kvitt.publicHost only if that fallback is prepared) and the stale comment "pinned Caddy root … icehack.asuscomm.com" at android/app/build.gradle.kts:35; OnboardingStates.kt's GOLDEN_SERVER fixture (old name, test fixture only, a golden change if replaced).
  • Remove the temporary "Diagnose secrets" workflow step (dd07ca4) once the login failure's cause is known (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.md is superseded where it names Caddy and cert-sync (note at its top); SPEC §10 and infra/README.md are 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.createWorker test (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: forbidden or not_found for the other user's device id (owner decision, §4).
  • State-class naming: *Ui, *UiState and *State are mixed across features; needs a decision if a convention is wanted (owner; no plan).
  • Unverified risk: the 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 when ready (§3 item 3); the checklist is the D-120 one (nine steps, NPM at step 6, router at step 7). The NPM proxy host and the router forward come first (§3 item 2).
  • Plans (SPEC §11.2 v0.22), 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): landed.
    • Next: the owner runs the bundle on the Kvitt Pi when he moves over (D-100, deferred; a server image with the H-1 fix before the router forward); 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).

3. HUMAN steps pending (relay in chat)

  1. Gitea, the repository, Actions, the latest run of "Build Kvitt images": open the step "Diagnose secrets" and paste its output lines in the chat (it prints only the lengths of the secrets and the job token, never their values, plus the repository, actor, event, ref and server URL). Also: did "Log in to Gitea Container Registry" pass (yes/no)? If "Tag and Push Version" failed with a permission error: may write:repository be added to kvitt_cicd (yes/no)?
  2. NPM proxy host for kvitt.suorra.duckdns.org, per infra/README.md "Pi: what the owner does" step 6 (the owner said he will set it up now; Domain Names kvitt.suorra.duckdns.org, Forward Hostname / IP the Kvitt Pi's LAN address, Forward Port 8080, SSL tab with a new Let's Encrypt certificate, the Advanced tab block of SPEC §10.5.2). First check that kvitt.suorra.duckdns.org resolves to the home's public address (the same as icehack.asuscomm.com). Expected: the Let's Encrypt request succeeds and NPM lists the proxy host with its certificate. Then the router (step 7): forward external TCP 8443 to the Home Assistant Pi's TCP 443. /health through NPM answers only once the Kvitt Pi runs (item 3).
  3. Pi bundle, when ready: fill the remaining CHANGE_ME values in infra/pi/.env (the Kvitt Pi's address KVITT_PI_HOST, HA_HOST, KVITT_TAG; KVITT_PUBLIC_HOST is being filled by an agent), copy the bundle to the Kvitt Pi and run the nine steps of infra/README.md "Pi: what the owner does", relayed in chat when the owner moves over.
  4. Release app pairing, after item 3: release builds require https (AC-2.8), so the release app pairs only once the Pi runs behind NPM; a release APK built by infra/publish-apk.sh now targets https://kvitt.suorra.duckdns.org:8443.

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_found instead of forbidden for a device id of the other user (hides whose it is)? As built and as the spec says: forbidden.
  • State-class naming (*Ui, *UiState, *State mixed): fix one convention, or leave as built?
  • write:repository for kvitt_cicd, 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).

5. Paused (waiting on something outside the repo)

  • Nothing.

6. Numbering

  • Decisions: D-001 to D-123 used, except D-008 and D-009 (never used); nothing reserved. Next free: D-124.
  • Spec version: v0.22 written; next v0.23.
  • 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.22.
  • 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.