SPEC v0.25: D-126 app gate in NPM; infra README step 4a

Outside the home LAN NPM closes every request without X-Kvitt-App: <key>
(return 444); the LAN check reads $realip_remote_addr (NPM's X-Real-IP
realip makes $remote_addr forgeable) and Let's Encrypt's challenge passes
on the normalised $uri. Block rehearsed in NPM v2.16.0's openresty.
README: step 4a (key, keyed APK, gate, acceptance test), health checks,
stale CARGO_BUILD_JOBS text and owner decisions fixed. HANDOFF as of
2026-10-21.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-21 18:59:45 +02:00
parent 34e5cad02a
commit 2acfc34a0e
4 changed files with 162 additions and 59 deletions
+23 -18
View File
@@ -3,13 +3,17 @@
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
## 1. Where things stand
- `main` at `28fa71b` (the compose deploy hint in the workflow and `infra/build-images.sh`, after `6e5981d`, the self-contained Pi compose project, D-125); `origin/main` (Gitea) at `6e5981d`, so `28fa71b` is not pushed yet; plus the SPEC v0.24 docs commit from `agent-docs-v024b` once it lands. Landed since SPEC v0.23 (`b1f4c10`): `3820f9c`, `200c99f`, `6e5981d`, `28fa71b` (below).
- State as of 2026-10-21. `main` at `34e5cad` (the Android side of D-126: `X-Kvitt-App` on every request), after `d9e189a` (SPEC v0.24, D-125); `origin/main` (Gitea) at `d9e189a`, so `34e5cad` is not pushed yet; plus the SPEC v0.25 docs commit from `agent-gate-docs` once it lands. Landed since SPEC v0.24: `34e5cad` (below).
- **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` (port 443, D-124). It is in `infra/pi/.env.example` and `infra/pi/.env` (`KVITT_PUBLIC_HOST`) and `android/local.properties` (`kvitt.publicHost`, git-ignored). SPEC §10.1 and §10.5.1 point here.
- **NPM proxy host** for `kvitt.suorra.duckdns.org` is Online with a Let's Encrypt certificate (owner, 2026-10-19). The router needs no new rule: external TCP 443 already forwards to the Home Assistant Pi (owner confirmed, D-124).
- **Images**: `gitea.lan/jacobh2/kvitt-server` and `gitea.lan/jacobh2/kvitt-backup`, tags `v1.0.0` (run 69) and `v1.0.1` (the sparse-checkout push; the tag the Pi uses), plus `latest`; the packages are public, so the Pi pulls without `docker login`. Gitea Actions on the owner's Pi runner (act_runner 0.6.1, Alpine job container): the first failures were an empty secret (the user-level secret was misnamed on the owner's side, fixed 2026-10-19; a user-level secret of the repository owner reaches the repository); run 69 succeeded: login ok, `v1.0.0`, `kvitt-backup` 5 min 20 s, `kvitt-server` 2 h 42 min on the Pi 3 (first build, `CARGO_BUILD_JOBS=1`; later runs reuse the builder's cargo cache); the tag push works with the `write:package` token. SPEC §10.3.
- **Kvitt Pi** (D-125): Kvitt runs from `/home/jacobhagstedt/kvitt` as the owner's user `jacobhagstedt`, uid and gid 1000:1000 (`KVITT_UID`/`KVITT_GID`); Docker and the `gitea.lan` registry trust are already on the Pi; nothing outside that folder is installed or changed. The owner is installing now (2026-10-20, §3 item 1).
- **Kvitt Pi** (D-125): Kvitt runs from `/home/jacobhagstedt/kvitt` as the owner's user `jacobhagstedt`, uid and gid 1000:1000 (`KVITT_UID`/`KVITT_GID`); Docker and the `gitea.lan` registry trust were already on the Pi; nothing outside that folder is installed or changed. **The stack is running** (owner, 2026-10-21): image `v1.0.1`, `server` and `backup` both healthy after a `chown` of `data/` (it had been created as root); compose's `mem_limit` warning (the Pi kernel has no memory cgroup, so the 160 MiB and 96 MiB limits are not enforced) is accepted. `/health` through NPM from outside answered `"status":"ok"` (public health OK).
- **App gate** (D-126, owner decision 2026-10-21, in progress): outside the home LAN `192.168.1.0/24` NPM closes (`return 444`) every request without `X-Kvitt-App: <key>`, every route included; the key is the build property `kvitt.appKey` (`android/local.properties`, git-ignored), generated by the owner with `openssl rand -hex 32` and pasted into NPM's block; the server and `contracts/openapi.yaml` are unchanged. Android side landed (`34e5cad`); the docs (SPEC v0.25, `infra/README.md` step 4a with the block and the acceptance test) are `agent-gate-docs`. Not yet in NPM: `android/local.properties` has no `kvitt.appKey` yet (checked 2026-10-21), so no keyed APK exists.
- **First release APK**: `apk/kvitt-123.apk` (10.0 MB, git-ignored by `*.apk`) handed to the owner from the main checkout for sideloading on both phones; it was built before any `kvitt.appKey` existed, so it sends no `X-Kvitt-App`: it works only until the gate is pasted (on the home Wi-Fi it keeps working afterwards). The phones need a keyed APK before the gate goes in (§3).
- 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, `cce3e4e` SPEC v0.22, `a86ad24` server and contracts on port 443, `4852803` bundle on port 443 (D-124), `038c603` Android on port 443 and the pinned-root template, `ad35231` portless Android fixtures, `b1f4c10` SPEC v0.23, `3820f9c` sparse-checkout workflow, `200c99f` `.env` without informational keys, `6e5981d` self-contained Pi compose project (D-125), `28fa71b` compose deploy hint in the workflow and `infra/build-images.sh`.
- Landed since SPEC v0.24 (recorded in v0.25):
- App key header (`34e5cad`, D-126): `kvitt.appKey` (`-Pkvitt.appKey`, else `android/local.properties`, else empty = no header; 16 to 128 printable ASCII characters without white space, else the build stops giving the length only), `BuildConfig.APP_KEY`, `AppBuildInfo.appKey` (`:data`, `toString` hides it); `X-Kvitt-App` in `KvittApi`'s common headers and `PictureAuth`; 952 Android tests.
- Landed since SPEC v0.23 (recorded in v0.24):
- Sparse-checkout workflow (`3820f9c`, no decision): cone-mode `sparse-checkout` of `server` and `infra/backup` (the two build contexts), so the action fetches with `--filter=blob:none` and no PNG is checked out; `fetch-depth: 0` stays because `fetch-tags` with a shallow fetch only brings tags on the tip; the temporary "Diagnose secrets" step removed. Its push built `v1.0.1`.
- `.env` without the informational keys (`200c99f`, no decision; revises D-103): `KVITT_PI_HOST` and `KVITT_PI_SSH_USER` dropped, read by nothing on the Pi; the README's `scp`, `ssh` and NPM steps name `<kvitt-pi-address>`.
@@ -40,22 +44,22 @@ 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 is fixed (`725901c`).
- 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-v024b`: SPEC v0.24, D-125, HANDOFF, `prompts/README.md`, notes in `PLAN-M11.md` and `PLAN-M2c-pi.md`).
- The owner installing Kvitt on the Kvitt Pi (§3 item 1).
- This docs pass (`agent-gate-docs`: SPEC v0.25, D-126, `infra/README.md` step 4a and the gate block, its stale `CARGO_BUILD_JOBS` text and owner decisions fixed, this file).
- The owner: first backup, bootstrap and release pairing; then the app key and the gate (§3).
- 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 `6e5981d`. The Gitea secret `kvitt_cicd` (a user-level secret of `jacobh2`, renamed by the owner 2026-10-19) works; the Drive token is `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`).
- 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 952 tests at `34e5cad`; 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`, rebuilt with the host `kvitt.suorra.duckdns.org` and no port (D-124) and installed 2026-10-19; release pairing waits for the Kvitt Pi (release builds require `https`, AC-2.8; §3 item 3).
- 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`, rebuilt with the host `kvitt.suorra.duckdns.org` and no port (D-124) and installed 2026-10-19; release pairing against the Kvitt Pi is next (§3 item 1, with `apk/kvitt-123.apk`).
- 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-v024b` (orchestrator): SPEC v0.24, D-125, HANDOFF, `prompts/README.md`, the plan notes.
- Land `agent-gate-docs` (orchestrator): SPEC v0.25, D-126, `infra/README.md`, HANDOFF. Push `34e5cad` and it to `origin` (neither touches `server/**`, `infra/backup/**` or `.gitea/**`, so no workflow run).
- Closed by `28fa71b`: the `/opt/kvitt` leftovers in `.gitea/workflows/build-images.yaml` and `infra/build-images.sh` (compose deploy hint, `docker image inspect`). Still stale in the workflow header (agent, next infra pass): "NOT YET RUN" and "First-build duration on the Pi: unknown" (now 2 h 42 min).
- Push `28fa71b` and the v0.24 docs commit to `origin` (orchestrator). `28fa71b` touches `.gitea/workflows/**`, so the push starts a workflow run (next tag `v1.0.2`, same image content; the server build should reuse the builder's cargo cache).
- `infra/README.md` "Gitea Actions workflow" is stale in two places (agent, next infra pass): `CARGO_BUILD_JOBS` "takes effect only once `server/Dockerfile` declares it" (it does since `b4d9467`) and "the first build's duration on the Pi is unknown"; its "Owner decisions (open)" 1 (`write:repository`, moot: the tag push works) and 2 (host name, answered) are answered.
- Pushed: `28fa71b` and the v0.24 docs commit (`origin/main` at `d9e189a`).
- Closed by `agent-gate-docs`: `infra/README.md`'s stale `CARGO_BUILD_JOBS` text and its "Owner decisions (open)" (now answered).
- `infra/backup/Dockerfile`: stale comment "on the Pi the container runs as uid 10001" (since D-125 the owner's uid); left as is, because editing it triggers a 5-minute image build (agent, with the next change there).
- Historical `:8443` and `icehack.asuscomm.com` mentions in `docs/ux/` and `docs/validation/` are left as the record of their time (no action unless the owner wants them rewritten).
- Restore drill AC-18.2 (`docs/validation/restore-drill.md`, M10; the one Missing row of the AC-coverage audit); `infra/RESTORE.md` is the runbook since D-125.
@@ -69,27 +73,28 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
- `PLAN-M2c-pi.md` is superseded where it names Caddy, cert-sync, scripts or `/opt/kvitt` (note at its top, D-120 and D-125); SPEC §10 and `infra/README.md` are current. `PLAN-M11.md` has a note that `/opt/kvitt/import` is now `~/kvitt/import`.
- Plans (SPEC §11.2 v0.24), 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 with its NPM rework and the self-contained compose project (D-125), 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), port 443 (D-124): landed.
- Next: the owner installs Kvitt on the Kvitt Pi (§3; image `v1.0.1`, which carries the H-1 fix like every image since `725901c`); M10 First release after it (keystore per §10.6, D-101); M11 Import history after M9 and M10 (procedure SPEC §10.8 "Production run", now in `~/kvitt/import/`, D-025, D-125; each run must reconcile, D-045).
- Next: the owner pairs the release app against the Kvitt Pi (stack running, `v1.0.1`), then the app key and gate (D-126, §3); M10 First release after it (keystore per §10.6, D-101); M11 Import history after M9 and M10 (procedure SPEC §10.8 "Production run", now in `~/kvitt/import/`, D-025, D-125; each run must reconcile, D-045).
## 3. HUMAN steps pending (relay in chat)
1. Pi install (D-125; `infra/README.md` "Pi: what the owner does", steps 1 to 3). On the Mac: `cp infra/pi/.env.example infra/pi/.env`, set `HA_HOST` (NPM's LAN IPv4 address) and `KVITT_TAG=v1.0.1`, check `KVITT_UID`/`KVITT_GID` are 1000; `mkdir -p infra/pi/secrets && cp infra/secrets/fcm-service-account.json infra/secrets/rclone.conf infra/pi/secrets/`; `scp -r infra/pi/compose.yaml infra/pi/.env infra/pi/secrets jacobhagstedt@<kvitt-pi-address>:kvitt/`; `rm -r infra/pi/secrets`. On the Pi: `cd ~/kvitt`, `chmod 700 secrets && chmod 600 secrets/*`, `mkdir -p data/apk backups import`, `docker compose pull && docker compose up -d`, `docker compose ps`, `curl -s localhost:8080/health`. Expected: `server` and `backup` both `(healthy)` within a minute; `curl` prints `{"status":"ok",...,"db_ok":true}` (yes/no).
2. From outside: on the phone with Wi-Fi off, open `https://kvitt.suorra.duckdns.org/health` in Chrome. Expected: `"status":"ok"` without a certificate warning (yes/no).
3. First backup, bootstrap, release pairing. On the Pi in `~/kvitt`: `docker compose exec backup /backup.sh` ends with `backup: ok <stamp>` (yes/no); then `docker compose exec server kvitt-server bootstrap` prints the admin invite code, link and expiry; pair the release app `se.hagstedt.kvitt` with it (the orchestrator relays the exact screen labels then).
4. Partner phone, after the owner's release app is paired: `docker compose exec server kvitt-server invite-partner` on the Pi; the partner installs the release APK and pairs with that invite (relayed with screen labels when the time comes).
Done 2026-10-21: the Pi install (stack running, `v1.0.1`, both containers healthy) and the public health check through NPM.
1. First backup, bootstrap, release pairing. On the Pi in `~/kvitt`: `docker compose exec backup /backup.sh` ends with `backup: ok <stamp>` (yes/no); then `docker compose exec server kvitt-server bootstrap` prints the admin invite code, link and expiry; sideload `apk/kvitt-123.apk` and pair the release app `se.hagstedt.kvitt` with it (the orchestrator relays the exact screen labels then).
2. Partner phone, after the owner's release app is paired: `docker compose exec server kvitt-server invite-partner` on the Pi; sideload the release APK on the partner's phone and pair with that invite (relayed with screen labels when the time comes).
3. App key (D-126; `infra/README.md` step 4a items 1 to 3), after `agent-gate-docs` lands: generate `kvitt.appKey` in `android/local.properties` on the Mac (an agent may run the one-line `openssl` command; the value is never printed); build and publish a keyed APK (`infra/publish-apk.sh`, copy to `data/apk/`); both phones on it (update banner, or sideload).
4. Gate (step 4a items 4 and 5), only after both phones run the keyed APK: NPM, Hosts, Proxy Hosts, Kvitt row, Edit, Advanced: paste the step 4 block with the key in place of `KVITT_APP_KEY`, Save (row Online, yes/no). Acceptance test: on home Wi-Fi Chrome opens `https://kvitt.suorra.duckdns.org/i/TEST` and the invite page shows (yes/no); with Wi-Fi off the same URL gives a connection error (yes/no); with Wi-Fi off Kvitt's About screen shows the server version (yes/no).
## 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?
- Answered 2026-10-20: Kvitt on the Pi is a self-contained compose project in `~/kvitt` with nothing installed elsewhere (D-125). Answered 2026-10-19: Kvitt's host name is `kvitt.suorra.duckdns.org` (§1); Kvitt on port 443 through the existing router forward (D-124). Moot: `write:repository` for the registry secret (the tag push works with `write:package`).
- Answered 2026-10-21: auth on everything, the key is the app itself, the landing page on the LAN only is OK (D-126). Answered 2026-10-20: Kvitt on the Pi is a self-contained compose project in `~/kvitt` with nothing installed elsewhere (D-125). Answered 2026-10-19: Kvitt's host name is `kvitt.suorra.duckdns.org` (§1); Kvitt on port 443 through the existing router forward (D-124). Moot: `write:repository` for the registry secret (the tag push works with `write:package`).
## 5. Paused (waiting on something outside the repo)
- Nothing.
## 6. Numbering
- Decisions: D-001 to D-125 used, except D-008 and D-009 (never used); nothing reserved. Next free: **D-126**.
- Spec version: v0.24 written; next **v0.25**.
- 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.24.
- Decisions: D-001 to D-126 used, except D-008 and D-009 (never used); nothing reserved. Next free: **D-127**.
- Spec version: v0.25 written; next **v0.26**.
- 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.25.
- Sync protocol **1**, push payload **1**.
## 7. How the work is run