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 atac17b41. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
+29
-24
@@ -3,9 +3,15 @@
|
||||
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
|
||||
|
||||
## 1. Where things stand
|
||||
- `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).
|
||||
- `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).
|
||||
- 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).
|
||||
@@ -22,24 +28,23 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
|
||||
- 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-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).
|
||||
- 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 `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`).
|
||||
- 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-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`).
|
||||
- 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).
|
||||
@@ -49,31 +54,31 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
|
||||
- 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: 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.
|
||||
- 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 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.
|
||||
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)
|
||||
- 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?
|
||||
- `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-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.
|
||||
- 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
|
||||
|
||||
+4
-2
@@ -124,5 +124,7 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
|
||||
| D-117 | 2026-10-18 | Infra security fixes, landed `7fb86f5`. (1) ~~Caddy's access log and error log (`log default`) filter `request>uri` with ``regexp `^(/i/)[^?]+\|([?]).+` `${1}${2}REDACTED` `` (`wrap json`), so invite codes and the `?from=` name never reach a log; `Authorization` stays redacted by default.~~ (revised by D-120: NPM's `location ^~ /i/` without access or error log; NPM logs no request headers) (2) ~~Deferred~~ Response headers `Content-Security-Policy "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"`, `X-Content-Type-Options nosniff`, `X-Frame-Options DENY`, `Referrer-Policy no-referrer` (revised by D-120: set at NPM's server level with the server's copies hidden). (3) ~~The `caddy` container runs as `10002:10002` with `group_add` 1000, `read_only`, `cap_drop: [ALL]` plus `cap_add: [NET_BIND_SERVICE]` (the image's binary has that file capability and cannot be exec'd without it), `no-new-privileges`, tmpfs `/config` and `/tmp`; `install.sh` sets `/certs` 0750 1000:1000 and `caddy_data` to 10002 (extends D-105).~~ (removed by D-120) (4) The development stack mounts `infra/dev/secrets/` (git-ignored), into which `up.sh` copies only `fcm-service-account.json`. (5) `deploy.sh` and `install.sh` print each pulled image's digest and append `<UTC time> <KVITT_TAG> <image reference> <digest>` to `/opt/kvitt/deployed-digests`, with a WARNING when a recorded reference changed digest; the workflow prints the pushed digests. (6) `kvitt_cicd` needs only `write:package` (the tag push uses the checkout credentials). (7) ~~The plain root key (variant B) for cert-sync is no longer acceptable: variant A (restricted) or a dedicated non-root user.~~ (moot since D-120: no Home Assistant SSH key) (8) ~~One-off check after the router forward that Caddy's `remote_ip` shows public addresses.~~ (revised by D-120: the conntrack check of SPEC §10.10) | Security review at `18c527f`: L-1, L-2, L-5, L-9, M-2, I-5. | SPEC §8.7, §8.9, §10 intro, §10.2, §10.3, §10.5.1, §10.5.2, §10.10; `infra/pi/`, `infra/dev/`, `.gitea/workflows/build-images.yaml`, `infra/README.md` |
|
||||
| D-118 | 2026-10-18 | Shared test support lives in `:core:design`'s test fixtures (`src/testFixtures`, package `se.hagstedt.kvitt.design.testing`: `Variant`, `TestClock`, `TestEnvironment`, `ScreenSnapshots` with capture modes `Root`/`Screen`/`ScreenWhenIdle`, `MainDispatcherRule`, `withMain`), consumed as `testImplementation(testFixtures(project(":core:design")))`; not a module, so the §6.2 graph is unchanged; the architecture check rejects test fixtures outside test configurations. The convention plugin `kvitt.android.screenshots` wires Roborazzi and the test libraries; feature build scripts carry no screenshot block. Shared failure texts (`format/Failures.kt`: `FAILURE_NETWORK`, `commonFailureText`), limits (`format/Limits.kt`: `BP_TOTAL`, `DISPLAY_NAME_MAX`, `DESCRIPTION_MAX`, `NOTES_MAX`, `FIRST_DATE`, `DAYS_AHEAD_MAX`) and `savedSplitLabel` live in `:core:design`. `KvittMotion` holds only the seesaw spring; `KvittIcon.Description` and unused dimens/shapes removed; catalog entries `roborazzi-junit-rule`, `roborazzi-compose`, `androidx-hilt-navigation-compose`, `androidx-lifecycle-viewmodel-compose` removed; one `CopyCategoryKeywords` task shared by `:data` and `:core:fakes`. 941 tests, no golden changed, release APK 10.46 MB. Landed by the Android overhaul (`e4e6e5d` to `c582363`). | Eight modules each had a copy of the same screenshot harness, and M12f had to fix the same capture bug in two of them; test fixtures rather than a `:core:testing` module let `:core:design`'s own tests use them, add no module to the §6.2 graph and are treated by lint as test sources. The failure-text map, the limits (`BP_TOTAL` in five copies) and the saved-split label were duplicated across features; the removed tokens, icon and catalog entries were never referenced. | SPEC §6.2 (`:core:design` row, Screenshot tests), §11.1, §11.3; `docs/ux/design-spec.md` §2.11 and the icon table; `android/README.md` |
|
||||
| D-119 | 2026-10-19 | APK download rate limit. E-6 `GET /apk/files/{file_name}` is limited to 10 requests per minute per client IP in its own fixed window (`APK_DOWNLOAD_LIMIT` in `src/api/rate_limit.rs`), separate from and not counted against the 60 per minute of the other unauthenticated endpoints; the client IP as in D-032 (peer, or the last `X-Forwarded-For` entry from the trusted proxy); refusals are 429 `rate_limited` with `Retry-After`; every request counts, a 404 included; in memory, pruned like the other windows. E-5 `GET /apk/latest` and E-4 `GET /i/{code}` stay on the 60 per minute. Landed `96e4cbd` (unit test and an integration test in `tests/core_api/rate_limit.rs`). Owner: "APK download limit assumed is OK". | Security review L-3: each E-6 answer can be a ~10 MB APK, so 60 per minute per IP let one client pull about 600 MB a minute from the Pi; E-5 and E-4 answers are small and the phone checks E-5 at most every 6 hours (AC-16.2). | SPEC §8.6, §10.6, §14.3.4 (E-6), §11.4 |
|
||||
| D-120 | 2026-10-19 | TLS terminates in the owner's Nginx Proxy Manager (NPM) on the Home Assistant Pi instead of Caddy and cert-sync, which are removed (`infra/cert-sync/**`, `infra/pi/Caddyfile`, images, volumes `certs` and `caddy_data`, secrets `ha_ssh_key` and `ha_known_hosts`, `.env` keys `HA_PORT`, `HA_USER`, `HA_CERT_DIR`). (1) Kvitt gets its own public host name `KVITT_PUBLIC_HOST` (owner-chosen, recorded in HANDOFF; a DuckDNS name is being set up), because `icehack.asuscomm.com` is NPM's proxy host for Home Assistant and NPM selects by name. `KVITT_PUBLIC_URL=https://${KVITT_PUBLIC_HOST}:8443`. A path prefix on another name is not supported. (2) The router forwards external TCP 8443 to the Home Assistant Pi's 443 (NPM); nothing is forwarded to the Kvitt Pi. NPM issues the name's Let's Encrypt certificate via HTTP-01 on port 80. (3) The Kvitt Pi runs `server` (publishes 8080 on all LAN interfaces, hardening unchanged) and `backup` (unchanged, D-105). NPM forwards plain HTTP over the LAN (accepted by the owner). (4) `KVITT_TRUSTED_PROXY=${HA_HOST}`, NPM's LAN IPv4 address, checked by `install.sh`; the server is unchanged and takes the last `X-Forwarded-For` entry from that peer. (5) The NPM proxy-host settings and the Custom Nginx Configuration of §10.5.2 carry over D-117's headers and log redaction: headers at server level with the server's copies hidden; its own `location /` without `add_header` (NPM's default adds `X-Served-By`, which makes nginx drop server-level headers); `location ^~ /i/` with `access_log off` and `error_log /dev/null crit` because NPM's access log writes `$request_uri` with the query string; `client_max_body_size 6m`; `proxy_read_timeout 60s`; HSTS on (NPM's 2 years). (6) Health checks: `http://localhost:8080/health` on the Pi, then through NPM (warning only); `install.sh` removes the old stack's leftovers. (7) Gitea login: the secret is interpolated in the `run` line (`printf '%s' "${{ secrets.kvitt_cicd }}" \| docker login …`), the owner's proven form, because the env-based form came through empty on act_runner 0.6.1 with a user-level secret; this reverses L-8's 'never expanded inside a shell line'; act_runner masks the value. Revises D-103 (bundle files), D-104 (images, Caddy pin, cert-sync packages, trigger path), D-106 (cert-sync part), D-117 parts 1, 2, 3, 7, 8, D-023's cert-sync secrets. Verified on the Mac 2026-10-19: nginx -t against NPM v2.16.0 templates; rehearsal server-behind-nginx 21 checks passed (headers once each, HSTS, http→https 301, 413 at 7 MB, rate limit per forwarded client, forged X-Forwarded-For contained, invite code/name/`/i/` absent from nginx logs including on 502). | Owner 2026-10-19: NPM already manages Let's Encrypt; the HA name is taken; 8443 → NPM 443 confirmed. | SPEC §8.5, §8.7, §8.9, §10 intro, §10.1, §10.2, §10.3, §10.4, §10.5, §10.7, §10.10, §11.2, §14.7 |
|
||||
| D-121 | 2026-10-19 | The Android app's public host is a build property. `kvitt.publicHost` is read from the Gradle property (`-Pkvitt.publicHost=…`), else the key `kvitt.publicHost` in `android/local.properties` (owner's, git-ignored), else the placeholder `kvitt.example.invalid` (RFC 2606; never resolves, so a release built without it cannot reach a real host). The build refuses a value that is not a host name. `:app` exposes it as `BuildConfig.PUBLIC_HOST`, as the manifest placeholder `publicHost` (the AC-2.6 `https` intent filter, port 8443, `/i/`, `autoVerify="false"`) and as `RELEASE_SERVER_URL` = `https://<host>:8443` (`link/ReleaseServer.kt`). `:feature:onboarding` receives that URL through the Hilt qualifier `@ReleaseServerUrl` for S-1's prefill and its non-default-server check. Invite-link parsing still accepts a server named by the link (AC-2.6). Tests and goldens use fixed fixture hosts. `infra/publish-apk.sh` passes `-Pkvitt.publicHost` from `KVITT_PUBLIC_HOST` (follow-up agent running). The pinned-root config's domain (D-082) is still the old literal and must follow the property if that fallback is ever prepared. Landed `d090c6c`. | D-120 gave Kvitt its own host name; it was hard-coded in three places. | SPEC AC-2.6, AC-2.7, §8.5, §10.5.3, §10.6, §13.3 S-1, §12 OQ-6; `android/README.md` |
|
||||
| D-120 | 2026-10-19 | TLS terminates in the owner's Nginx Proxy Manager (NPM) on the Home Assistant Pi instead of Caddy and cert-sync, which are removed (`infra/cert-sync/**`, `infra/pi/Caddyfile`, images, volumes `certs` and `caddy_data`, secrets `ha_ssh_key` and `ha_known_hosts`, `.env` keys `HA_PORT`, `HA_USER`, `HA_CERT_DIR`). (1) Kvitt gets its own public host name `KVITT_PUBLIC_HOST` (owner-chosen, recorded in HANDOFF; ~~a DuckDNS name is being set up~~ the owner chose `kvitt.suorra.duckdns.org` on 2026-10-19), because `icehack.asuscomm.com` is NPM's proxy host for Home Assistant and NPM selects by name. `KVITT_PUBLIC_URL=https://${KVITT_PUBLIC_HOST}:8443`. A path prefix on another name is not supported. (2) The router forwards external TCP 8443 to the Home Assistant Pi's 443 (NPM); nothing is forwarded to the Kvitt Pi. NPM issues the name's Let's Encrypt certificate via HTTP-01 on port 80. (3) The Kvitt Pi runs `server` (publishes 8080 on all LAN interfaces, hardening unchanged) and `backup` (unchanged, D-105). NPM forwards plain HTTP over the LAN (accepted by the owner). (4) `KVITT_TRUSTED_PROXY=${HA_HOST}`, NPM's LAN IPv4 address, checked by `install.sh`; the server is unchanged and takes the last `X-Forwarded-For` entry from that peer (the server's built-in defaults, the OpenAPI server URL and the E-7/E-12 examples that still named the old host: revised by D-123). (5) The NPM proxy-host settings and the Custom Nginx Configuration of §10.5.2 carry over D-117's headers and log redaction: headers at server level with the server's copies hidden; its own `location /` without `add_header` (NPM's default adds `X-Served-By`, which makes nginx drop server-level headers); `location ^~ /i/` with `access_log off` and `error_log /dev/null crit` because NPM's access log writes `$request_uri` with the query string; `client_max_body_size 6m`; `proxy_read_timeout 60s`; HSTS on (NPM's 2 years). (6) Health checks: `http://localhost:8080/health` on the Pi, then through NPM (warning only); `install.sh` removes the old stack's leftovers. (7) Gitea login: the secret is interpolated in the `run` line (`printf '%s' "${{ secrets.kvitt_cicd }}" \| docker login …`), the owner's proven form, because the env-based form came through empty on act_runner 0.6.1 with a user-level secret; this reverses L-8's 'never expanded inside a shell line'; act_runner masks the value. Revises D-103 (bundle files), D-104 (images, Caddy pin, cert-sync packages, trigger path), D-106 (cert-sync part), D-117 parts 1, 2, 3, 7, 8, D-023's cert-sync secrets. Verified on the Mac 2026-10-19: nginx -t against NPM v2.16.0 templates; rehearsal server-behind-nginx 21 checks passed (headers once each, HSTS, http→https 301, 413 at 7 MB, rate limit per forwarded client, forged X-Forwarded-For contained, invite code/name/`/i/` absent from nginx logs including on 502). | Owner 2026-10-19: NPM already manages Let's Encrypt; the HA name is taken; 8443 → NPM 443 confirmed. | SPEC §8.5, §8.7, §8.9, §10 intro, §10.1, §10.2, §10.3, §10.4, §10.5, §10.7, §10.10, §11.2, §14.7 |
|
||||
| D-121 | 2026-10-19 | The Android app's public host is a build property. `kvitt.publicHost` is read from the Gradle property (`-Pkvitt.publicHost=…`), else the key `kvitt.publicHost` in `android/local.properties` (owner's, git-ignored), else the placeholder `kvitt.example.invalid` (RFC 2606; never resolves, so a release built without it cannot reach a real host). The build refuses a value that is not a host name. `:app` exposes it as `BuildConfig.PUBLIC_HOST`, as the manifest placeholder `publicHost` (the AC-2.6 `https` intent filter, port 8443, `/i/`, `autoVerify="false"`) and as `RELEASE_SERVER_URL` = `https://<host>:8443` (`link/ReleaseServer.kt`). `:feature:onboarding` receives that URL through the Hilt qualifier `@ReleaseServerUrl` for S-1's prefill and its non-default-server check. Invite-link parsing still accepts a server named by the link (AC-2.6). Tests and goldens use fixed fixture hosts. `infra/publish-apk.sh` passes `-Pkvitt.publicHost` from `KVITT_PUBLIC_HOST` (~~follow-up agent running~~ landed `4b1ae0c`, D-122; S-22's fallback server also D-122). The pinned-root config's domain (D-082) is still the old literal and must follow the property if that fallback is ever prepared. Landed `d090c6c`. | D-120 gave Kvitt its own host name; it was hard-coded in three places. | SPEC AC-2.6, AC-2.7, §8.5, §10.5.3, §10.6, §13.3 S-1, §12 OQ-6; `android/README.md` |
|
||||
| D-122 | 2026-10-19 | S-22's server before pairing comes from the build (extends D-121). `AppBuildInfo` (defined in `:data` `di/DataModule.kt`, provided by `:app` in `di/AppBuildInfoModule.kt`, consumed by `:data`) gains `releaseServerUrl: String`, filled from `RELEASE_SERVER_URL` = `https://<kvitt.publicHost>:8443`: `data class AppBuildInfo(val versionCode: Int, val versionName: String, val debug: Boolean, val releaseServerUrl: String)`. S-22's server row shows the paired server's base URL; while none is stored it shows `AppBuildInfo.releaseServerUrl` (`AboutRepositoryImpl`'s `releaseServerUrl`, was `defaultServerUrl`); `DataModule.DEFAULT_SERVER_URL` (`https://icehack.asuscomm.com:8443`) is removed, so `:data` names no host. Fakes and tests use `https://kvitt.example.org:8443` (`FakeScenarios`, `SettingsScenarios`, `RepositoriesTest`; 30 S-21/S-22 goldens re-recorded with that fixture host, only the host changed); new test `about_unpaired_showsTheReleaseServerOfTheBuild`. `infra/publish-apk.sh` reads `KVITT_PUBLIC_HOST` from `infra/pi/.env` without sourcing it (refuses a missing file or key, `CHANGE_ME`, a value that is not a host name such as a URL, or the old name `icehack.asuscomm.com`, the checks of `install.sh`), passes `-Pkvitt.publicHost` to `:app:assembleRelease` and prints the release server in its header and summary. Landed `4a4e45f` (Android) and `4b1ae0c` (`publish-apk.sh`, `infra/README.md`). | D-121 left S-22's unpaired fallback on Home Assistant's host and `publish-apk.sh` without the host, so a release could be built for the placeholder. | SPEC §6.2, §10.6, §13.3 S-22, §14.2.4 |
|
||||
| D-123 | 2026-10-19 | The server has no built-in Kvitt host (revises D-120's leftovers). (1) `KVITT_PUBLIC_URL` has no default (`DEFAULT_PUBLIC_URL` removed from `config.rs`): unset or empty, every `kvitt-server` command exits 1 with a configuration error (`configuration: KVITT_PUBLIC_URL is not set …`) before opening the database; the Pi compose sets `https://${KVITT_PUBLIC_HOST}:8443`, the dev stack `http://<Mac LAN address>:<port>` (`infra/dev/up.sh`; compose fallback `http://localhost:8080`). (2) `KVITT_TRUSTED_PROXY` defaults to `none` (`NO_TRUSTED_PROXY`, ignoring case and surrounding white space): no peer trusted and the value never looked up; empty or unresolvable also means no proxy (D-022 unchanged); compose sets `HA_HOST`. (3) The OpenAPI server is `https://{host}:8443` with server variable `host`, default `kvitt.example.invalid` (RFC 6761 reserved), description 'the owner-chosen Kvitt host name, SPEC §10.5', in `contracts/openapi.yaml` and the generated document (`openapi.rs` `PUBLIC_SERVER_URL`, `HOST_VARIABLE_DEFAULT`); operations unchanged (representational for §14.8). (4) Examples E-7 `public_url` and E-12 `link` use `https://kvitt.example.invalid:8443`. `tools/contracts-check.py` expects the `{host}` URL (`e381e37`). Tests: CLI `every_command_refuses_to_start_without_a_public_url` (`serve`, `bootstrap`, `migrate`; no database created), config `public_url_is_required`, rate limiter `the_default_none_trusts_no_peer`, `an_unresolvable_name_trusts_no_peer`, `an_ip_literal_is_trusted`; 276 server tests. `server/README.md` updated. Landed `c9abd24`, `e381e37`. | Since D-120 the old name `icehack.asuscomm.com` is Home Assistant's; a wrong built-in host is worse than none, and `caddy` named a container that no longer exists. | SPEC §10.4, §11.3, §14.3, §14.3.4 (E-7, E-12) |
|
||||
|
||||
Reference in New Issue
Block a user