SPEC v0.21: D-120 Nginx Proxy Manager instead of Caddy and cert-sync; D-121 Android host property

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
This commit is contained in:
2026-10-19 20:04:09 +02:00
parent d090c6c850
commit 4bcff2cf89
6 changed files with 191 additions and 147 deletions
+34 -30
View File
@@ -3,9 +3,13 @@
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
## 1. Where things stand
- `main` at `96e4cbd` (the APK download rate limit, D-119, landed after the SPEC v0.19 docs commit `3ef2c15`) plus the SPEC v0.20 docs commit from `agent-docs-v020` once it lands. `origin/main` (Gitea) at `96e4cbd`: everything up to the rate limit is pushed.
- `main` at `d090c6c` (the Android host property, D-121, on top of `9231a1d`, the end of the NPM rework, D-120) plus the SPEC v0.21 docs commit from `agent-docs-v021` once it lands. `origin/main` (Gitea) at `d090c6c`: everything is pushed (`9231a1d` triggered the Actions run of §3 item 1; `d090c6c` touches only `android/` 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/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).
- 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).
- 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):
@@ -15,50 +19,50 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
- 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. H-1 must be fixed before port 8443 is opened.
- 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:
- NPM rework (D-120 reserved, agent running): the owner runs Nginx Proxy Manager on the Home Assistant Pi, and NPM renews the Let's Encrypt certificate for `icehack.asuscomm.com`. Caddy and cert-sync are replaced by an NPM proxy host that forwards to the Kvitt Pi's server on LAN port 8080, so TLS terminates in NPM, the Kvitt Pi's compose stack loses `caddy` and `cert-sync`, and the `infra/pi/` bundle and its owner checklist change. Security review M-2 (the HA SSH key form for cert-sync) becomes moot and is closed. SPEC §10.5 and the other Caddy and cert-sync text are rewritten when D-120 lands (a docs pass records it; next free decision D-121). Open assumptions are §3 item 3.
- This docs pass (`agent-docs-v020`: SPEC v0.20, D-119, HANDOFF, `prompts/README.md`).
- `cargo-audit` in the server gate: the orchestrator is installing `cargo-audit` on the Mac; an agent adds it to the gate after the install.
- 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: `:data` `DataModule.DEFAULT_SERVER_URL` is still the old literal `https://icehack.asuscomm.com:8443` (S-22's fallback) and moves to `AppBuildInfo.releaseServerUrl`; `infra/publish-apk.sh` passes `-Pkvitt.publicHost` from `KVITT_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 the `9231a1d` push tests the fixed login step (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 `96e4cbd`. 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`); the cert-sync key `infra/secrets/ha_ssh_key` exists (all git-ignored). The first push triggered the Gitea Actions workflow; its result is the owner's §3 item 1.
- Both gates exist: Android `tools/gradle-serial.sh check` (from `android/`), server `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test` (from `server/`). Both green on `main` as far as reported.
- Remote: `origin` on Gitea, SSH key works; pushed at `d090c6c`. 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`).
- 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, with the build whose checks are §3 item 2; the release app `se.hagstedt.kvitt`, installed 2026-10-19, not yet paired (its checks are §3 item 2 as well).
- 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-v020` (orchestrator): SPEC v0.20, D-119, HANDOFF, `prompts/README.md`.
- 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): `:data` `DataModule.DEFAULT_SERVER_URL` → `AppBuildInfo.releaseServerUrl`; `infra/publish-apk.sh` passes `-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`) and `DEFAULT_TRUSTED_PROXY` (`caddy`) in `config.rs`, `openapi.rs`'s `PUBLIC_SERVER_URL`, `contracts/openapi.yaml`'s server URL and the E-7 and E-12 examples, `server/README.md`, the fakes' `AboutState` URLs; 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 in `infra/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.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).
- Tooling: `cargo-audit` in the server gate (agent, after the orchestrator's install on the Mac, in flight).
- 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 first runs).
- Land the NPM rework (D-120) when its agent reports, then a docs pass records D-120 and rewrites SPEC §10.5 and the Caddy and cert-sync text (orchestrator).
- The owner's answers to the phone checks of §3 item 2 (debug app checks; release app pairing with a device invite the orchestrator provides).
- 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 bundle and its checklist are rewritten by D-120 first; record the router-forward answer in `infra/README.md` then (the cert-sync key-form question is closed).
- Plans (SPEC §11.2 v0.20), 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, server gap pass, infra overhaul, infra security, the server overhaul and the Android overhaul: landed.
- Next: a server image with the H-1 fix before the router forward; the NPM rework (D-120) reshapes the Pi bundle; the owner runs it on the Kvitt Pi when he moves over (D-100, deferred); 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).
- 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 audit` and 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).
## 3. HUMAN steps pending (relay in chat)
1. Gitea, the repository, Actions: did the run "Build Kvitt images" triggered by the first push succeed (yes/no), and how long did it take? Parked by the owner ("come back to it"). If the run's tag push failed with `kvitt_cicd` limited to `write:package`: may `write:repository` be added (yes/no)?
2. Phone checks, answers pending (yes/no each):
1. Debug app (`se.hagstedt.kvitt.debug`): screen switches are instant, with no fade or slide; Inställningar, Om Kvitt shows the server version as "… (schema 3)"; the recurring editor shows Konto, Betalt av, Delning as three fixed rows.
2. Release app (`se.hagstedt.kvitt`, installed 2026-10-19): it opens to "Para ihop" without a crash; then pair it with the device invite the orchestrator provides; Home shows "Test"; add one expense and watch it sync.
3. NPM questions (for D-120; the agent proceeds on the assumptions): is `icehack.asuscomm.com` already a proxy host in Nginx Proxy Manager for another service (assumed free)? Does the router forward external 8443 to NPM's 443 or to a dedicated NPM port (assumed 443)?
4. Pi checklist, later: deferred by the owner ("when it makes sense to move over to the Pi"). The steps of `infra/README.md` "Pi: what the owner does" are rewritten by D-120 (no cert-sync key, the router forward goes to NPM) and relayed in chat when the owner moves over.
1. Gitea, the repository, Actions: the run "Build Kvitt images" triggered by the `9231a1d` push: 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: may `write:repository` be added to `kvitt_cicd` (yes/no)?
2. The DuckDNS name for Kvitt: tell the orchestrator once it exists. An agent then puts it into `infra/pi/.env` as `KVITT_PUBLIC_HOST` and into `android/local.properties` as `kvitt.publicHost` on the owner's word (D-120, D-121).
3. 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.
4. 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)
- §3 item 1: the workflow result and duration (parked), and `write:repository` if the tag push failed.
- §3 item 2: the debug and release app checks.
- §3 item 3: the two NPM questions (assumed: `icehack.asuscomm.com` free in NPM; external 8443 to NPM 443).
- Kvitt's host name (`KVITT_PUBLIC_HOST`, a DuckDNS name being set up; §3 item 2).
- `write:repository` for `kvitt_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_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?
@@ -67,9 +71,9 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
- Nothing.
## 6. Numbering
- Decisions: D-001 to D-119 used, except D-008 and D-009 (never used); D-120 reserved by the running NPM agent. Next free: **D-121**.
- Spec version: v0.20 written; next **v0.21**.
- 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.20.
- 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