SPEC v0.34: L-10 implemented (D-156), W0b recorded, web version code in the image build
Build Kvitt images / build (push) Has been cancelled

- AC-3.2, E-16 (§14.3.4), §11.4: the "to implement" notes removed; D-156 says
  implemented in 07c36d2; security review L-10 note: fixed in 07c36d2. ctutils
  0.4.4 (RUSTSEC-2026-0385) in the v0.34 row. Server tests 300, contracts-check 136.
- §15.3: W0b as built (5305f37): shared pieces, no cross-feature imports or web_
  keys, per-locale amount fields, Recharts chunk, Vitest cache off, version code.
  Web gate 609 tests.
- server/Dockerfile: ARG KVITT_VERSION_CODE=1 in the Node stage, passed to
  npm run build; the Gitea workflow computes git rev-list --count HEAD and passes
  it as a build argument (§10.3).
- HANDOFF: main 5305f37 pushed 2026-10-24; next image build, deploy, laptop login,
  browser checks; Web follow-ups closed.

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-24 11:29:42 +02:00
parent 5305f37038
commit 5d948f9e52
6 changed files with 38 additions and 37 deletions
+18 -29
View File
@@ -3,14 +3,14 @@
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
## 1. Where things stand
- State as of 2026-10-24. `main` at `c13be3b` (SPEC v0.32: web plans W1 to W5, D-144 to D-154, APK install failures, D-155), pushed to `origin` 2026-10-24. Release 140 (`kvitt-140`, deployed 2026-10-21) runs on both phones (verified 2026-10-24). Waiting for the Pi image with the complete web app (the Gitea workflow run in progress on the Pi runner); then the owner deploys it (§3 step 3), logs in from the laptop browser (the web client's first use, D-156) and runs the browser checks (§3 step 4). This docs pass (`agent-docs-v33`) records the owner's answers and observations of 2026-10-24 as D-156 and the small-edits working rule as D-157 (SPEC v0.33). Before it: the five web plans W1 `934eee7`, W3 `f9a9bfb`, W5 `3ebe57d`, W2 `a65ab71`, W4 `1aecf51`, the web fix `83f5293`, `d686ee0` (D-155) and `1c9856b` (W6); the web gate is green on `main` (59 files, 598 tests at `83f5293`).
- State as of 2026-10-24. `main` at `5305f37` (W0b web shared follow-ups), pushed to `origin` 2026-10-24; before it `07c36d2` (L-10, D-156) and `593c727` (SPEC v0.33, D-156, D-157). Release 140 (`kvitt-140`, deployed 2026-10-21) runs on both phones (verified 2026-10-24). Next: the Pi image build of the pushed `main` with the complete web app (the Gitea workflow on the Pi runner; this pass's `server/Dockerfile` and workflow change triggers another run when pushed), then the owner deploys it (§3 step 3), logs in from the laptop browser (the web client's first use, D-156) and runs the browser checks (§3 step 4). This docs pass (`agent-docs-v34`) records L-10 as implemented and W0b (SPEC v0.34, no new decision) and passes the web's version code into the image build (`ARG KVITT_VERSION_CODE` in `server/Dockerfile`, `--build-arg KVITT_VERSION_CODE=<git rev-list --count HEAD>` in the workflow). Gates on `main`: server 300 tests, web 609 tests, contracts-check 136.
- **Verified 2026-10-24** (owner, D-156): Samsung Auto Blocker was the cause of the silent update failure; turning it off let the update install, and the owner turned it back on, so every in-app update on the Samsung phones needs it off (the D-155 blocked text names it; SPEC AC-16.3, `infra/README.md`). The editor keyboard fix (S-19, S-15, S-6, `05ec3f3`) works on the device. The first backup on the Kvitt Pi ran and a snapshot pair is in the Drive folder (AC-18.1). The runner Pi's disk reached 11 of 15 GB during image builds; the owner runs `docker builder prune -af` between builds (SPEC §10.3).
- **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 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 and the lines stay (D-156, SPEC §10.2). `/health` through NPM from outside answered `"status":"ok"` (public health OK). **Live** (2026-10-21): both phones are paired against it and the Splitwise import (M11) is done.
- **App gate** (D-126, owner decision 2026-10-21): 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 (`infra/npm/kvitt.conf`, `7521c01`); the server and `contracts/openapi.yaml` are unchanged. Android side `34e5cad`, docs `2acfc34` (SPEC v0.25, `infra/README.md` step 4a). `android/local.properties` has a `kvitt.appKey` and `infra/pi/out/apk/` holds `kvitt-125.apk` (checked 2026-10-21); both phones run release 140, built with the key like every APK since `kvitt-133` (2026-10-24); whether the gate is in NPM is not recorded here (orchestrator confirms with the owner, §3).
- **Web client programme** (owner interview 2026-10-21, SPEC v0.28, D-129 to D-134, SPEC §15, FR-21): a React web client in `web/`, served by `kvitt-server` under `/app/` from the server image, with the app's features except the phone-only ones, plus search, CSV export, keyboard shortcuts and a language choice; login with an invite code (device `Web · <browser>`); the NPM gate also passes `/app/`, `POST /invites/accept` and bearer-token requests except `/health`, `/setup/`, `/i/`, `/apk/` (new block in `infra/npm/kvitt.conf`, README step 4, SPEC §10.5.2; checked with nginx 1.31.7, 17 requests); advanced analytics on the server (`/analytics/*`, D-133, forecast = recurring templates due + 3-month average of non-recurring spend). Plans `PLAN-W0.md` to `PLAN-W7.md`, prompts `prompts/W0.md` to `prompts/W7.md`. W7 landed `50a0d67` (D-135 to D-138, SPEC v0.29: `/app/` from `KVITT_WEB_DIR`, E-34 `GET /analytics/trends`, E-35 `GET /analytics/forecast`, rules SPEC §14.5.7, the Node stage and the repository-root build context); W0 landed `2ae65a5` (`web/**`: Vite, React 19, TypeScript strict, sync-pull ledger, invite-code login, ported money rules passing the vectors, tokens, i18n generated from `docs/ux/strings.csv` and `web-strings.csv`); W1 to W5 landed 2026-10-21 (`934eee7`, `a65ab71`, `f9a9bfb`, `1aecf51`, `3ebe57d`; D-144 to D-154, SPEC v0.32), each in its own `web/src/features/<name>/`, W2 with the shared editor fields in `web/src/components/editor/`; loose ends for a W0b plan in §2 "Web follow-ups"; W6 (Android Insights) landed `1c9856b` and shows on the phones once the image with W7 and W0 is deployed and a release APK with W6 is published. The web gate is `npm run check` in `web/` (third gate, in `CLAUDE.md` since `415f9f5`).
- **Web client programme** (owner interview 2026-10-21, SPEC v0.28, D-129 to D-134, SPEC §15, FR-21): a React web client in `web/`, served by `kvitt-server` under `/app/` from the server image, with the app's features except the phone-only ones, plus search, CSV export, keyboard shortcuts and a language choice; login with an invite code (device `Web · <browser>`); the NPM gate also passes `/app/`, `POST /invites/accept` and bearer-token requests except `/health`, `/setup/`, `/i/`, `/apk/` (new block in `infra/npm/kvitt.conf`, README step 4, SPEC §10.5.2; checked with nginx 1.31.7, 17 requests); advanced analytics on the server (`/analytics/*`, D-133, forecast = recurring templates due + 3-month average of non-recurring spend). Plans `PLAN-W0.md` to `PLAN-W7.md`, prompts `prompts/W0.md` to `prompts/W7.md`. W7 landed `50a0d67` (D-135 to D-138, SPEC v0.29: `/app/` from `KVITT_WEB_DIR`, E-34 `GET /analytics/trends`, E-35 `GET /analytics/forecast`, rules SPEC §14.5.7, the Node stage and the repository-root build context); W0 landed `2ae65a5` (`web/**`: Vite, React 19, TypeScript strict, sync-pull ledger, invite-code login, ported money rules passing the vectors, tokens, i18n generated from `docs/ux/strings.csv` and `web-strings.csv`); W1 to W5 landed 2026-10-21 (`934eee7`, `a65ab71`, `f9a9bfb`, `1aecf51`, `3ebe57d`; D-144 to D-154, SPEC v0.32), each in its own `web/src/features/<name>/`, W2 with the shared editor fields in `web/src/components/editor/`; its loose ends done by W0b (`5305f37`, §2 "Web follow-ups"); W6 (Android Insights) landed `1c9856b` and shows on the phones once the image with W7 and W0 is deployed and a release APK with W6 is published. The web gate is `npm run check` in `web/` (third gate, in `CLAUDE.md` since `415f9f5`).
- **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 (both paired, 2026-10-21); 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). Both phones have since moved to the keyed release 140 (2026-10-24), so the gate can go in (§3 step 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, `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`.
@@ -63,13 +63,11 @@ 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 answered 2026-10-24: `not_found` for the other user's device (D-156; to implement, §2). 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`).
- 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 answered 2026-10-24: `not_found` for the other user's device (D-156), fixed in `07c36d2`. 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-v33`: SPEC v0.33, D-156, D-157, `infra/README.md`, this file).
- The Pi image build with the complete web app (in progress on the Pi runner), then the owner's deploy and browser checks (§3 steps 3 and 4).
- `CLAUDE.md` small-edits rule (D-157), edited in the main checkout by a small agent.
- W0b web cleanup (agent in `web/`, the follow-ups below).
- This docs pass (`agent-docs-v34`: SPEC v0.34, `docs/decisions.md` D-156, the security review's L-10 note, `server/Dockerfile`, `.gitea/workflows/build-images.yaml`, this file).
- The Pi image build of the pushed `main`, then the owner's deploy and browser checks (§3 steps 3 and 4).
- 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 `c13be3b` (2026-10-24). 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).
@@ -80,11 +78,11 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
- 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-v33` (orchestrator): SPEC v0.33, D-156, D-157, `infra/README.md`, HANDOFF; gate `python3 tools/contracts-check.py`.
- Pi image with the complete web app (built from the push of the web plans): watch the workflow run, then the owner deploys it (§3 step 3) and checks the web client from the laptop browser (§3 step 4); HUMAN step 2 if the NPM block is not yet current. Release 140 (W6 Insights, D-155) runs on both phones; its Insights need this image.
- W0b web cleanup (agent, in progress in `web/`): the "Web follow-ups" below.
- Land `agent-docs-v34` (orchestrator): SPEC v0.34, D-156 implemented, L-10 note, the version-code ARG in `server/Dockerfile` and the workflow, HANDOFF; gate `python3 tools/contracts-check.py`.
- `infra/build-images.sh` (agent, next infra pass): pass `--build-arg KVITT_VERSION_CODE=$(git rev-list --count HEAD)` to the server build as the workflow does (v0.34); without it the Mac-built image's web reports version code 1.
- Pi image with the complete web app (built from the push of `5305f37`, or of `agent-docs-v34` once landed and pushed): watch the workflow run, then the owner deploys it (§3 step 3) and checks the web client from the laptop browser (§3 step 4); HUMAN step 2 if the NPM block is not yet current. Release 140 (W6 Insights, D-155) runs on both phones; its Insights need this image.
- 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).
- Pushed: `c13be3b` (orchestrator, 2026-10-24).
- Pushed: `5305f37` (orchestrator, 2026-10-24).
- Closed by `2acfc34` (`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).
@@ -93,35 +91,26 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
- Image-build location (owner, after the current run's result, D-156): stay on the Pi 3 runner or move to the Mac fallback `infra/build-images.sh`.
- AC-20.1 shown on the Pi (M10). AC-18.1 and AC-16.3 observed 2026-10-24 (D-156).
- A `KvittWorkerFactory.createWorker` test (Android agent, no plan yet).
- Security review L-10 (server agent, no plan yet; D-156): `revoke_device` answers `not_found` instead of `forbidden` when a member names the other user's device; drop 403 from E-16 in `contracts/openapi.yaml` and the handler's utoipa annotation together (contract change: spec bump and decision, §14.8); `web/src/api/schema.d.ts` regenerated.
- Closed by `07c36d2`: security review L-10 (E-16 `not_found` for the other user's device, 403 dropped from `contracts/openapi.yaml`; also `ctutils` 0.4.4 for RUSTSEC-2026-0385).
- 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).
- `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), pull-to-refresh (D-127), the editor keyboard fix, the S-12 line chart (D-128): landed.
- Web client programme (D-134, SPEC §11.2 rows W0 to W7): all landed: W0 (`2ae65a5`), W1 (`934eee7`), W2 (`a65ab71`), W3 (`f9a9bfb`), W4 (`1aecf51`), W5 (`3ebe57d`), W6 (`1c9856b`), W7 (`50a0d67`); D-144 to D-154 recorded in v0.32. Next for the web: W0b (follow-ups below).
- Web client programme (D-134, SPEC §11.2 rows W0 to W7): all landed: W0 (`2ae65a5`), W1 (`934eee7`), W2 (`a65ab71`), W3 (`f9a9bfb`), W4 (`1aecf51`), W5 (`3ebe57d`), W6 (`1c9856b`), W7 (`50a0d67`); D-144 to D-154 recorded in v0.32; W0b follow-ups `5305f37` (v0.34).
- Next: the app key and gate if not yet done (D-126, §3); M10 First release (keystore per §10.6, D-101; the reports `first-release.md` and `restore-drill.md`). Done 2026-10-21: release pairing of both phones against the Kvitt Pi and M11 Import history (SPEC §10.8 "Production run").
### Web follow-ups (for a W0b plan; agent)
- Shared pieces built two or three times in feature folders, to move into `web/src/components/` or `web/src/lib/`: an account tile (`features/accounts/AccountTile`, `features/analytics/components/AccountTile`); the loading and error cards (`PageStates` in `accounts`, `expenses`, `analytics`); `useToday` (`features/accounts/useToday.ts`, `features/analytics/shared.ts`); `categoryName`; the audit `Timeline` and its line builders (`features/expenses/Timeline.tsx`, `timelineLines.ts`, `features/settle/timeline.ts`); `apiErrorKey` and the rejection texts (`features/expenses/messages.ts`, `features/settle/messages.ts`); W1's `BalanceHero`, `Seesaw`, `AccountBalanceHeader`, `OverflowMenu` (D-145).
- `components/AmountField`: reads `,` as the decimal point in English too (should follow the page language, as `lib/money.ts`); needs `hideLabel` and `placeholder` props.
- `lib/ids.ts`: type the `random` parameter as `(bytes: Uint8Array<ArrayBuffer>) => void`.
- Vitest cache `web/node_modules/.vitest-cache`: its stale `catalog.ts` result is fixed at the root by `83f5293` (glob without a type argument, `catalog.test.ts` compares with the files); still decide whether to disable the cache or key it on the feature i18n files.
- `vite.config.ts`: the dev proxy lacks `/analytics` (W-11 under `npm run dev`); Recharts in its own chunk (the vendor chunk is 515 kB).
- `app/routes.ts`: `bucket` in `DrillDownKind` (D-151); builders for `?account=<id>` and `?settlement=<id>` (D-144), replacing `features/accounts/links.ts` and `features/settle/links.ts`.
- `lib/vectors.test.ts`: `spend_series` and `resolve_range` are still listed as not ported (their vectors run in `features/analytics/domain/analytics.vectors.test.ts`); move them to `PORTED` against the analytics port (`suggest_category` done in `83f5293`).
- W5's recurring editor (`features/settings/recurring/fields.tsx`) uses its own fields; swap them for `components/editor` (D-148).
- `editor_paid_by_toggle` says "Dubbeltryck" (TalkBack wording) on the web; a `web_` variant for a click.
- `web/package.json` `kvitt.versionCode` is fixed at 1 (`X-Kvitt-App-Version`, W-19); PLAN-W0 wanted the commit count at build time.
- `/app/settings/invites` is kept as an alias of `/app/settings/invite` (D-153); drop it if nothing links there.
- Closed by `83f5293`: the fake test server keeps non-JSON bodies raw.
### Web follow-ups
- Done in W0b (`5305f37`, SPEC §15.3 v0.34): shared pieces once in `components`, `lib`, `api`, `app`; `AmountField` per page language with `hideLabel` and `placeholder`; `ids.ts` typing; Vitest cache off; `/analytics` in the dev proxy and Recharts in its own chunk (vendor 516 → 132 kB); `routes.ts` builders (`links.ts` files deleted); `spend_series` and `resolve_range` in `vectors.test.ts`; W5's recurring editor on `components/editor`; a click wording for the paid-by toggle; the version code from the commit count. Testing Library's wait limit is 5 s (`web/src/test/setup.ts`, load on the Mac during parallel agents).
- Kept: `/app/settings/invites` as an alias of `/app/settings/invite` (the spec names it, D-153).
- The image's version code: `ARG KVITT_VERSION_CODE` in `server/Dockerfile` and the workflow's build argument (`agent-docs-v34`); `infra/build-images.sh` still to follow (§2).
## 3. HUMAN steps pending (relay in chat)
Done 2026-10-21: the Pi install (stack running, `v1.0.1`, both containers healthy), the public health check through NPM, bootstrap, release pairing of both phones, the Splitwise import. Done 2026-10-24: both phones on release 140 (keyed, so the old step 1 is done), the first backup on Drive (AC-18.1).
Standing note for every in-app update on the Samsung phones: turn Auto Blocker off before tapping "Uppdatera", back on afterwards (D-156). Between two workflow builds on the runner Pi: `docker builder prune -af`.
1. Gate (step 4a items 4 and 5), both phones run the keyed APK (release 140): 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).
2. Web client gate lines (D-130, SPEC v0.28): when the NPM block is pasted (item 1, or again if the D-126 block is already in NPM), paste the current `infra/npm/kvitt.conf` content (step 4), which also lets the web client through and sets its Content-Security-Policy; Save (row Online, yes/no). After the first image with the web client is deployed (W7, §10.10), acceptance test with Wi-Fi off on the phone: `https://kvitt.suorra.duckdns.org/app/` shows the Kvitt login page (yes/no) and `https://kvitt.suorra.duckdns.org/health` still gives a connection error (yes/no) (step 4a item 5 d and e).
3. Deploy the new image (the complete web app and E-34/E-35, SPEC §10.10), once the Gitea workflow run (in progress on the Pi) has succeeded: on the Kvitt Pi in `~/kvitt`, set `KVITT_TAG` in `.env` to the tag the run printed, then `docker compose pull && docker compose up -d`; `server` and `backup` healthy (yes/no). Release 140 runs on both phones; S-11's Insights work once this image runs.
3. Deploy the new image (the complete web app and E-34/E-35, SPEC §10.10), once the Gitea workflow run of the pushed `main` has succeeded: on the Kvitt Pi in `~/kvitt`, set `KVITT_TAG` in `.env` to the tag the run printed, then `docker compose pull && docker compose up -d`; `server` and `backup` healthy (yes/no). Release 140 runs on both phones; S-11's Insights work once this image runs.
4. Web client in a browser on the laptop (after step 3, D-156): on the phone, Inställningar › Para ihop en enhet › För mig, create a code; on the laptop open `https://kvitt.suorra.duckdns.org/app/`, "Logga in i Kvitt", type the code, log in: Home shows (yes/no). Then, each yes/no: Home and one account open; add, edit and delete an expense; Sök, export a CSV and open it in Numbers (columns and amounts right); Gör upp: record a payment and delete it; Statistik, open a category row (line chart), Insikter; each Settings page opens; on the phone Inställningar › Enheter shows `Web · <browser>`, "Ta bort åtkomst" on it, and the browser shows the login page with the removed-device message.
## 4. Owner questions (asked, unanswered)
@@ -135,8 +124,8 @@ Standing note for every in-app update on the Samsung phones: turn Auto Blocker o
## 6. Numbering
- Decisions: D-001 to D-157 used, except D-008, D-009 and D-150 (never used). Next free: **D-158**.
- Spec version: v0.33 written (D-156, D-157). Next **v0.34**.
- 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**. Unchanged by v0.27 to v0.33 (no plan since needed a migration).
- Spec version: v0.34 written (L-10 implemented, W0b; no new decision). Next **v0.35**.
- 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**. Unchanged by v0.27 to v0.34 (no plan since needed a migration).
- Sync protocol **1**, push payload **1**.
## 7. How the work is run
+1 -1
View File
@@ -160,5 +160,5 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| D-153 | 2026-10-21 | W-18's route is `/app/settings/invite` (W0's `paths.settingsInvite`), not `/app/settings/invites` as §15.6 said; W5 also routes `/app/settings/invites` to the same page so the old address works. Sub-routes of W-15 and W-16: `/app/settings/categories/new`, `/app/settings/categories/:id`, `/app/settings/recurring/new`, `/app/settings/recurring/:id`. Landed `3ebe57d`. | W0's route table was fixed before W5 started and the singular matches the S-21 screen; the spec follows the code. | SPEC §15.6 W-15, W-16, W-18; `web/src/features/settings/index.ts` |
| D-154 | 2026-10-21 | Partner first login and About on the web. The S-2 step after a `partner` login (AC-2.10) is an intro on W-14 Profile with "Fortsätt"; this browser remembers it in `localStorage` under `kvitt.profileStepDone`, holding the device id, so a later login on the same browser as a new device shows it again. W-19 About reads E-1 `/health`; when it does not answer (from outside the LAN, §15.2), it reads the `X-Kvitt-Server-Version` header of a fresh `GET /me` and shows the version without the schema (D-110's fallback). Landed `3ebe57d`. | AC-2.10 asks for the step once; the device id ties "once" to this login rather than to the browser forever. A fresh request gives a current header instead of a cached one. | SPEC AC-2.10, §15.2, §15.6 W-14, W-19; `web/src/features/settings/ProfilePage.tsx`, `AboutPage.tsx` |
| D-155 | 2026-10-21 | A failed or declined update install is no longer silent. The `PackageInstaller` session's final status reaches `UpdateRepository` by session id (`InstallResultReceiver` hands it to the waiting `AndroidApkInstaller` call; `ApkInstaller.install` returns a `PendingInstall`). `STATUS_FAILURE_BLOCKED` gives `UpdateState.Failed(info, UpdateFailure.Blocked)`: "Kunde inte installera uppdateringen" (`update_install_failed`) with the security-settings text (`update_install_blocked`); `STATUS_FAILURE_ABORTED` (the user declined the system dialog) goes back to `Available` without an error; every other failure gives `Failed(info, UpdateFailure.Install(detail))` with the installer's own status message as the body. `UpdateState.Failed` gains `reason: UpdateFailure = UpdateFailure.Download` (`sealed interface UpdateFailure { Download; Blocked; Install(detail: String?) }`, `:core:model` `Sync.kt`), so existing fakes are unchanged. The cached APK is deleted once the installer answers, so "Försök igen" downloads again. Goldens `S-17-install-blocked`, `S-17-install-failed`, `s22_about_install_blocked` new, `update_banner` re-recorded. Landed `d686ee0`. | A refused or failed install left the banner as it was, so a tap on "Uppdatera" seemed to do nothing. Showing the reason tells the user which setting to change or what the installer said; a declined dialog is the user's choice, not an error. | SPEC AC-16.7, §14.2.4; `docs/ux/design-spec.md` §3.6 `UpdateBanner`, S-17; `docs/ux/strings.csv`; `android/core/model/src/main/kotlin/se/hagstedt/kvitt/model/Sync.kt`, `android/data/src/main/kotlin/se/hagstedt/kvitt/data/update/Updater.kt`, `android/core/design/src/main/kotlin/se/hagstedt/kvitt/design/component/Sync.kt` |
| D-156 | 2026-10-24 | Owner answers and phone observations of 2026-10-24. (1) OQ-6 (security review L-6): no confirmation step on S-1 for an invite link that prefills an unusual server; the default stands (S-1 shows the server row expanded, D-116). (2) Security review L-10: a member who names a device of the other user gets `not_found`, the same as an unknown id, not `forbidden` (default accepted); **to implement**: `revoke_device` in `server/crates/kvitt-server/src/api/devices.rs` answers `forbidden` today, and `contracts/openapi.yaml` lists 403 for E-16, so the change drops both (contract change, §14.8). (3) A host firewall rule limiting the Kvitt Pi's port 8080 to the Home Assistant Pi (`HA_HOST`): not now, deferred (open). (4) `mem_limit` in `infra/pi/compose.yaml` stays; the Pi kernel without the memory cgroup ignores it with a warning, accepted. (5) Images keep being built by the Gitea workflow on the Pi 3 runner for now; the owner waits for the result of the run in progress before deciding otherwise; `infra/build-images.sh` on the Mac stays the fallback. The runner Pi's disk reached 11 of 15 GB during builds; the owner runs `docker builder prune -af` between builds. (6) Verified on the phones: Samsung Auto Blocker was the cause of the silent update failure (turning it off let the update install; the owner turned it back on), so the in-app install needs it off each time, and the D-155 blocked text names it; the editor keyboard fix (S-19, S-15, S-6, `05ec3f3`) confirmed on the device; both phones run release 140. (7) The first backup on the Kvitt Pi ran and a snapshot pair is in the Drive folder (AC-18.1, verified by the owner). (8) The web client is first used from a browser on the owner's laptop; the phones keep the app. | The owner answered the open questions of HANDOFF §4 and SPEC §12 and reported what the phones and the Pi showed; L-10's leak is minor between two trusted users, but `not_found` hides whose a device is at no cost. Auto Blocker is a Samsung setting the app cannot change, so the spec and the owner's checklist name it. | SPEC AC-3.2, §8.4, E-16 (§14.3.4), AC-16.3, AC-18.1, §10.2, §10.3, §11.4, §12 OQ-6, §15.2; `infra/README.md` "Pi: what the owner does"; `docs/HANDOFF.md` |
| D-156 | 2026-10-24 | Owner answers and phone observations of 2026-10-24. (1) OQ-6 (security review L-6): no confirmation step on S-1 for an invite link that prefills an unusual server; the default stands (S-1 shows the server row expanded, D-116). (2) Security review L-10: a member who names a device of the other user gets `not_found`, the same as an unknown id, not `forbidden` (default accepted); implemented in `07c36d2` (SPEC v0.34): `revoke_device` in `server/crates/kvitt-server/src/api/devices.rs` answers `not_found` with the unknown-id body, and `contracts/openapi.yaml` drops E-16's 403 (contract change, §14.8). (3) A host firewall rule limiting the Kvitt Pi's port 8080 to the Home Assistant Pi (`HA_HOST`): not now, deferred (open). (4) `mem_limit` in `infra/pi/compose.yaml` stays; the Pi kernel without the memory cgroup ignores it with a warning, accepted. (5) Images keep being built by the Gitea workflow on the Pi 3 runner for now; the owner waits for the result of the run in progress before deciding otherwise; `infra/build-images.sh` on the Mac stays the fallback. The runner Pi's disk reached 11 of 15 GB during builds; the owner runs `docker builder prune -af` between builds. (6) Verified on the phones: Samsung Auto Blocker was the cause of the silent update failure (turning it off let the update install; the owner turned it back on), so the in-app install needs it off each time, and the D-155 blocked text names it; the editor keyboard fix (S-19, S-15, S-6, `05ec3f3`) confirmed on the device; both phones run release 140. (7) The first backup on the Kvitt Pi ran and a snapshot pair is in the Drive folder (AC-18.1, verified by the owner). (8) The web client is first used from a browser on the owner's laptop; the phones keep the app. | The owner answered the open questions of HANDOFF §4 and SPEC §12 and reported what the phones and the Pi showed; L-10's leak is minor between two trusted users, but `not_found` hides whose a device is at no cost. Auto Blocker is a Samsung setting the app cannot change, so the spec and the owner's checklist name it. | SPEC AC-3.2, §8.4, E-16 (§14.3.4), AC-16.3, AC-18.1, §10.2, §10.3, §11.4, §12 OQ-6, §15.2; `infra/README.md` "Pi: what the owner does"; `docs/HANDOFF.md` |
| D-157 | 2026-10-24 | Small edits are made by a Haiku or Sonnet agent directly in the main checkout, not in a new worktree, and the orchestrator commits them on `main`. Owner's words: "be smart about it: Spin up smaller agents (like haiku or sonnet) for these super small things, and do not do it in a new worktree, instead simply do it directly here." Real work stays with Opus agents in their own worktrees. Refines the working rules that every change goes through an Opus agent in a worktree and that Haiku or Sonnet are only for owner-requested read-only scans. | A worktree, a full brief and an Opus agent cost far more than a one-line fix is worth. | `CLAUDE.md` › Orchestration (edited separately) |
+2
View File
@@ -97,6 +97,8 @@ Location: `server/crates/kvitt-server/src/api/devices.rs:57-63` (404 for unknown
Issue: trivial information leak between the two trusted users; listed for completeness.
Fix (server): return 404 in both cases, or leave as is.
> **Note 2026-10-24 (D-156, SPEC v0.34):** fixed in `07c36d2`. A member naming the other user's device gets 404 `not_found` with the same body as an unknown id; the admin still revokes any device except the current one; `contracts/openapi.yaml` no longer lists 403 for E-16.
### Info
**I-1.** `X-Kvitt-Server-Version` on every response and `GET /health` (`server_version`, `schema_version`, `db_ok`) are visible to unauthenticated scanners (`src/router.rs:125-131`, `src/api/health.rs`). Spec-mandated; the version is not a secret. Accept.