cce3e4e9d9
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 atac17b41. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
86 lines
20 KiB
Markdown
86 lines
20 KiB
Markdown
# 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 "<version> (schema <n>)" 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.
|