4bcff2cf89
SPEC §10.5 rewritten (10.5.1 Nginx Proxy Manager, 10.5.2 Custom Nginx Configuration, 10.5.3 Fallback: NPM down); Caddy and cert-sync text replaced in §1, §6.1, §7, §8.5, §8.7, §8.9, §10 intro, §10.1 to §10.4, §10.7, §10.10, §11.2 to §11.4, OQ-3, OQ-6, E-7 and §14.7; the Kvitt host name is KVITT_PUBLIC_HOST. D-121: kvitt.publicHost, RELEASE_SERVER_URL and @ReleaseServerUrl (AC-2.6, AC-2.7, §10.6, §13.3 S-1). Decisions D-120 and D-121 with pointers in D-023, D-103 to D-106 and D-117; HANDOFF; prompts/README; a superseded-in-part note on PLAN-M2c-pi; dated D-120 notes under M-2, L-1, L-2, L-8 and I-5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
18 KiB
18 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
mainatd090c6c(the Android host property, D-121, on top of9231a1d, the end of the NPM rework, D-120) plus the SPEC v0.21 docs commit fromagent-docs-v021once it lands.origin/main(Gitea) atd090c6c: everything is pushed (9231a1dtriggered the Actions run of §3 item 1;d090c6ctouches onlyandroid/and triggers none).- 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). - 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. 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 must be fixed before port 8443 is opened. - 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-v021: SPEC v0.21, D-120, D-121, HANDOFF,prompts/README.md, the PLAN-M2c-pi note, dated notes in the security review). - Dev server rebuild (orchestrator/agent).
- D-121 follow-ups:
:dataDataModule.DEFAULT_SERVER_URLis still the old literalhttps://icehack.asuscomm.com:8443(S-22's fallback) and moves toAppBuildInfo.releaseServerUrl;infra/publish-apk.shpasses-Pkvitt.publicHostfromKVITT_PUBLIC_HOST(follow-up agent running). - Gitea Actions: the first run (2026-10-19) got as far as the login (container,
apk add, checkout worked) and failed with "password is empty" (env-based form); the run triggered by the9231a1dpush tests the fixed login step (owner, §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 atd090c6c. The owner created the Gitea secretkvitt_cicdand 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). - 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, installed 2026-10-19: About and the release app opening were confirmed; release pairing is deferred to production (release builds requirehttps, AC-2.8). - 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-v021(orchestrator): SPEC v0.21, D-120, D-121, HANDOFF,prompts/README.md, PLAN-M2c-pi note, security-review notes. - D-121 follow-ups (in flight):
:dataDataModule.DEFAULT_SERVER_URL→AppBuildInfo.releaseServerUrl;infra/publish-apk.shpasses-Pkvitt.publicHost. - Literals left by D-120 and D-121 (agent, no plan yet): the server's
DEFAULT_PUBLIC_URL(https://icehack.asuscomm.com:8443) andDEFAULT_TRUSTED_PROXY(caddy) inconfig.rs,openapi.rs'sPUBLIC_SERVER_URL,contracts/openapi.yaml's server URL and the E-7 and E-12 examples,server/README.md, the fakes'AboutStateURLs; harmless (compose always sets the variables) but they name Home Assistant's host; a contract change needs the §14.8 rule. The pinned-root config's domain (D-082) follows only if that fallback is prepared. install.sh's last line says "Next: README step 7 (NPM proxy host)"; the NPM step is 6 (agent, one-line fix ininfra/pi/install.sh).- 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: deferred by the owner on 2026-10-19 ("when it makes sense to move over to the Pi"); the checklist is the D-120 one (nine steps, NPM at step 6, router at step 7).
- Plans (SPEC §11.2 v0.21), 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 auditand the Android host property: 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).
- 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, the repository, Actions: the run "Build Kvitt images" triggered by the
9231a1dpush: did the step "Log in to Gitea Container Registry" pass (yes/no), and did the whole run succeed (yes/no)? If the step "Tag and Push Version" failed with a permission error: maywrite:repositorybe added tokvitt_cicd(yes/no)? - The DuckDNS name for Kvitt: tell the orchestrator once it exists. An agent then puts it into
infra/pi/.envasKVITT_PUBLIC_HOSTand intoandroid/local.propertiesaskvitt.publicHoston the owner's word (D-120, D-121). - Phone checks: done. About ("… (schema 3)") confirmed and the release app opens confirmed (2026-10-19). Release pairing is deferred to production: release builds require
https(AC-2.8), so the release app pairs only once the Pi runs behind NPM. - Pi checklist, later: deferred by the owner ("when it makes sense to move over to the Pi"). The nine steps of
infra/README.md"Pi: what the owner does" (D-120: NPM proxy host at step 6, router forward 8443 → HA Pi 443 at step 7) are relayed in chat when the owner moves over.
4. Owner questions (asked, unanswered)
- Kvitt's host name (
KVITT_PUBLIC_HOST, a DuckDNS name being set up; §3 item 2). write:repositoryforkvitt_cicd, only if the workflow's tag push fails (§3 item 1).- 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?
5. Paused (waiting on something outside the repo)
- Nothing.
6. Numbering
- Decisions: D-001 to D-121 used, except D-008 and D-009 (never used); nothing reserved. Next free: D-122.
- Spec version: v0.21 written; next v0.22.
- 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.21. - 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.