Files
kvitt/docs/HANDOFF.md
T
jacobh2 b1f4c10a3d SPEC v0.23: D-124 Kvitt on port 443 through NPM; pinned-root template
D-124 (owner decision 2026-10-19; a86ad24, 4852803, 038c603): Kvitt is served
at https://<host> with no explicit port; the router's existing external 443
forward to the Home Assistant Pi serves it, no 8443 rule. Every :8443 in SPEC
replaced; D-120 to D-123 struck through with pointers. The pinned-root config
is generated from its template with kvitt.publicHost (038c603). HANDOFF,
prompts/README.md status and the S-1 TalkBack line follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-19 20:34:44 +02:00

90 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Handoff: state of the work
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
## 1. Where things stand
- `main` at `ad35231` (portless Android fixtures after D-124; on top of D-124's `a86ad24` server and contracts, `4852803` bundle, `038c603` Android with the pinned-root template, SPEC v0.22 `cce3e4e` and `ac17b41`) plus the SPEC v0.23 docs commit from `agent-docs-v023` once it lands. `origin/main` (Gitea) at `038c603`: `ad35231` is not pushed yet.
- **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` (`KVITT_PUBLIC_HOST`) and `android/local.properties` (`kvitt.publicHost`), both 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). From the Mac, `https://kvitt.suorra.duckdns.org/health` presents a valid certificate and returns NPM's 502, as expected while the Kvitt Pi is not running. The router needs no new rule: external TCP 443 already forwards to the Home Assistant Pi (owner confirmed, D-124).
- **Gitea secret diagnosis** (the temporary "Diagnose secrets" step, `dd07ca4`): the user-level secret `babymonitor_cicd` reaches the repository (length 40), `kvitt_cicd` has length 0, so the secret is not stored under that name on the `jacobh2` account. The owner is fixing the name (§3 item 1).
- 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.
- Landed since SPEC v0.22 (recorded in v0.23):
- Port 443 (D-124, owner decision 2026-10-19): Kvitt is served at `https://<host>` with no explicit port, because NPM listens on 443 and selects by host name; the router's existing external-443 forward to the Home Assistant Pi serves Kvitt, no 8443 rule. Server and contracts (`a86ad24`): OpenAPI server `https://{host}`, E-7 and E-12 examples without a port, `tools/contracts-check.py` follows. Bundle (`4852803`): `KVITT_PUBLIC_URL=https://${KVITT_PUBLIC_HOST}`, health checks through NPM on 443, README step 7 "Router: no new rule", the APK-abuse stop-gap is disabling Kvitt's proxy host in NPM. Android (`038c603`): `releaseServerUrl(host)` = `https://<host>` (`RELEASE_SERVER_PORT` removed), the AC-2.6 intent filter without a port (matches every port, so an old `:8443` link still opens S-1 with the server it names), invite-link parsing drops `:443` and `:80`, 28 S-1 goldens on `https://kvitt.example.org`.
- Portless Android fixtures (`ad35231`, D-124): `android/README.md`, the `AppBuildInfo` and `AboutRepositoryImpl` comments, the S-21 and S-22 fake scenarios and the `:data` test fixtures without `:8443`; 30 goldens re-recorded (6 S-21 invite-created, 24 S-22). What is left of `8443` in `android/` is deliberate: tests that an old `:8443` link still opens S-1 and that a typed port survives.
- Pinned-root template (`038c603`, no decision): with `kvitt.tls.pinnedRoot=true` the build generates the pinned-root network security config from `android/app/src/releasePinnedRoot/network_security_config.xml` with the domain `kvitt.publicHost` and stops with a message while `android/app/src/release/res/raw/kvitt_root_ca.crt` is not committed (SPEC §8.5, §10.5.3). The old host name is out of that config and of the S-1 fixture.
- 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` (portless since D-124) 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 (replaced by the existing 443 forward, D-124). The server publishes 8080 on the Kvitt Pi's LAN, `KVITT_TRUSTED_PROXY=${HA_HOST}` (NPM's IPv4, checked by `install.sh`). SPEC §10.5 rewritten (NPM proxy host, the Custom Nginx Configuration with D-117's headers and the `/i/` log exclusion, fallback: no LAN path for release builds). Rehearsed on the Mac: `nginx -t` against NPM v2.16.0 templates, 21 checks of the server behind nginx passed. The Gitea login interpolates the secret in the `run` line (the env form came through empty on act_runner 0.6.1).
- Android host property (`d090c6c`, D-121): `kvitt.publicHost` (Gradle property, else `android/local.properties`, else `kvitt.example.invalid`), `BuildConfig.PUBLIC_HOST`, the manifest placeholder `publicHost`, `RELEASE_SERVER_URL`, `@ReleaseServerUrl` in `:feature:onboarding`. 944 Android tests.
- Landed since SPEC v0.19 (recorded in v0.20):
- APK download rate limit (`96e4cbd`, D-119, security review L-3): E-6 `GET /apk/files/{file_name}` at most 10 requests per minute per client IP in its own window, not counted against the 60 of the other unauthenticated endpoints; a 404 counts; 429 `rate_limited` with `Retry-After`; E-5 and E-4 stay on the 60; unit and integration tests.
- Landed since SPEC v0.18 (recorded in v0.19):
- Android overhaul (`e4e6e5d`, `8229ed6`, `515b60f`, `e08aaf0`, `0714242`, `a92c236`, `74b5fbd`, `c582363`; D-118): shared test support in `:core:design`'s test fixtures (`se.hagstedt.kvitt.design.testing`), `kvitt.android.screenshots` wires Roborazzi and the test libraries; `format/Failures.kt`, `format/Limits.kt`, `savedSplitLabel` in `:core:design`; dead code removed (`KvittMotion` only the seesaw spring, `KvittIcon.Description`, unused dimens and shapes); four catalog entries removed; one `CopyCategoryKeywords` task; one `configureAndroidModule`; S-7's state types in `ExpenseEditorState.kt`. Gate at landing: 941 tests, no golden changed. Release APK 10.46 MB.
- Landed since SPEC v0.17 (recorded in v0.18):
- Infra overhaul (`2fac1ec`, `61f92c8`, `fc6126c`, `b647faf`, `e70fe0d`; no decision): `tools/gradle-lock.sh` shared by `gradle-serial.sh`, `clean-builds.sh` and `infra/build-images.sh`; the pre-commit hook runs through `gradle-serial.sh` with JDK 25 on `PATH` (hooks reinstalled in `main` after `e70fe0d`); `install.sh` keeps the deployed `KVITT_TAG`; `--provenance=false` on buildx pushes, the workflow token through env; `RCLONE_REMOTE` dropped from the dev env.
- Server gap pass (`8660b7f`, D-113): `kvitt-server invite-partner` (0 ok, 2 household full, 3 no admin), so a scripted partner needs no second admin device; `push sent` info line; CLI tests (`invite-partner`, `migrate`, `healthcheck`, SIGTERM drain); `tools/contracts-check.py` 134 checks.
- Infra security (`7fb86f5`, D-117): Caddy log redaction of `/i/<code>` and query strings, CSP and three more response headers, Caddy non-root (uid 10002, read-only, caps dropped), `/certs` 0750, dev stack mounts only the FCM key, `/opt/kvitt/deployed-digests` with a WARNING on change, `kvitt_cicd` with `write:package` only, the plain root key (old variant B) not acceptable.
- M12f (`dc7a296`, `5b2ed37`, `2bd6b58`, `c366fbd`, `a0259ab`; D-109 to D-112, D-116): S-16 lost version read-only, S-22 "<version> (schema <n>)" from `/health`, `SettlementRepository` returns null instead of throwing, E-17 retry after each successful sync, invite links dropped while paired, private notifications on the lock screen, `localeFilters` en/sv, `:data` tests (429, D-040, WorkManager via `work-testing`), the home/account screenshot harness fix (stale `sv_dark` goldens re-recorded), 19 unused string keys removed (`strings.csv` 562 rows). Gate at landing: 941 tests. Release APK 10.0 MB.
- Validation reports (SPEC §11.4): `docs/validation/ac-coverage.md` (taken at `956908a`, `ed1b130` before the rewrite): 270 rows, 260 Done, 5 Partial, 1 Missing, 4 device-only; feature complete in code, not yet against §4's acceptance rule; its S-16 and S-22 mismatches are fixed by M12f. `docs/validation/security-review.md` (taken at `18c527f`): Critical 0, High 1, Medium 3, Low 10, Info 8; L-6 kept (D-116), L-7 fixed (D-116), L-1, L-2, L-5, L-9, M-2 fixed (D-117), L-8 fixed (`e70fe0d`); H-1 (`725901c`, D-115), M-3 (`baa5806`) and L-4 (`b985ce9`, D-114) fixed by the server overhaul; L-10 left as the spec says, decision pending (§4). Owner answers of 2026-10-19: M-1 no (backups stay unencrypted, rclone keeps the `drive` scope; accepted risk in SPEC §8.1 and §8.7), L-3 accepted and fixed (`96e4cbd`, D-119), the 16 MP and 512-character limits (D-114) confirmed; M-2's remaining question (the Home Assistant SSH key form) is moot with the NPM rework (D-120) and closed; D-120 also moves L-1 and the headers to NPM, makes L-2 moot, reverses the L-8 fix and replaces I-5's check with a conntrack check (dated notes in the review, SPEC §11.4). H-1 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-v023`: SPEC v0.23, D-124, HANDOFF, `prompts/README.md`).
- Gitea Actions: the login step interpolates the secret in the `run` line (`4f69ae0`); the diagnosis found `kvitt_cicd` empty (§1); the owner is storing it under the right name (§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 `038c603`. The owner created a Gitea secret meant as `kvitt_cicd` (not found under that name, §1) 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`, 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).
- 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-v023` (orchestrator): SPEC v0.23, D-124, HANDOFF, `prompts/README.md`, the S-1 TalkBack line of `docs/ux/design-spec.md`.
- Closed by D-124, `038c603` and `ad35231`: the server's and the bundle's `:8443`, `RELEASE_SERVER_PORT`, the intent filter's port, the pinned-root config's old host name and the stale "pinned Caddy root" comment, `OnboardingStates.kt`'s `GOLDEN_SERVER`, the `:data` comments, the `FakeScenarios` and `SettingsScenarios` fixture URLs, `android/README.md`.
- Push `ad35231` and the v0.23 docs commit to `origin` (orchestrator).
- Remove the temporary "Diagnose secrets" workflow step (`dd07ca4`) once a run passes the login (agent, after §3 item 1).
- Possible firewall rule on the Kvitt Pi limiting access to port 8080 to the HA Pi (`HA_HOST`); not done, not decided (owner).
- `PLAN-M2c-pi.md` is superseded where it names Caddy and cert-sync (note at its top); SPEC §10 and `infra/README.md` are current.
- Restore drill AC-18.2 (`docs/validation/restore-drill.md`, M10; the one Missing row of the AC-coverage audit).
- AC-18.1, AC-20.1 and AC-16.3 shown on the Pi and the phones (M2c-pi run, M10).
- A `KvittWorkerFactory.createWorker` test (Android agent, no plan yet).
- Security review L-6: an S-1 design state if the owner wants a confirm step for an unusual server (OQ-6, default not in v1; owner, §4).
- Security review L-10: `forbidden` or `not_found` for the other user's device id (owner decision, §4).
- State-class naming: `*Ui`, `*UiState` and `*State` are mixed across features; needs a decision if a convention is wanted (owner; no plan).
- Unverified risk: the workflow pushes a git tag; whether that re-triggers the path-filtered workflow is not known (watch the next runs).
- Owner runs the Pi bundle on the Kvitt Pi (§3 item 2); the checklist is `infra/README.md` "Pi: what the owner does" (nine steps; NPM at step 6 is done, the router at step 7 is a check only, D-124).
- Plans (SPEC §11.2 v0.23), 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), port 443 (D-124): landed.
- Next: the owner runs the bundle on the Kvitt Pi when he moves over (D-100; the image must carry the H-1 fix, which every image since `725901c` does); 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 secret: in Gitea, the user menu, Settings, Actions, Secrets, add the secret `KVITT_CICD` with the registry token (`write:package`). Then the repository, Actions, "Build Kvitt images", "Run workflow". Report: did "Log in to Gitea Container Registry" pass (yes/no), and did the run finish (yes/no; if "Tag and Push Version" failed with a permission error, say so).
2. Pi: provision the Kvitt Pi and install Docker (`infra/README.md` "Pi: what the owner does" steps 1 and 2); fill `infra/pi/.env` (`HA_HOST`, `KVITT_PI_HOST`, `KVITT_PI_SSH_USER`, `KVITT_TAG`); copy the bundle to the Pi; run `install.sh`. Expected: on the phone, `https://kvitt.suorra.duckdns.org/health` shows `"status":"ok"` (yes/no).
3. Release app pairing, after item 2: run `kvitt-server bootstrap` on the Pi (`/opt/kvitt/bootstrap.sh`) and pair the release app with that code (relayed in chat with the exact screen labels when the time comes).
## 4. Owner questions (asked, unanswered)
- OQ-6 (SPEC §12): a confirm step on S-1 for an unusual server; default "not in v1".
- Security review L-10: should a member get `not_found` instead of `forbidden` for a device id of the other user (hides whose it is)? As built and as the spec says: `forbidden`.
- State-class naming (`*Ui`, `*UiState`, `*State` mixed): fix one convention, or leave as built?
- `write:repository` for the registry secret, 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); Kvitt on port 443 through the existing router forward (D-124).
## 5. Paused (waiting on something outside the repo)
- Nothing.
## 6. Numbering
- Decisions: D-001 to D-124 used, except D-008 and D-009 (never used); nothing reserved. Next free: **D-125**.
- Spec version: v0.23 written; next **v0.24**.
- 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.23.
- Sync protocol **1**, push payload **1**.
## 7. How the work is run
See CLAUDE.md › Orchestration. HUMAN steps in reports are relayed in chat by the orchestrator; the owner does not read them in files.