SPEC v0.24: D-125 self-contained Pi compose project; workflow results
Build Kvitt images / build (push) Successful in 5m45s
Build Kvitt images / build (push) Successful in 5m45s
D-125 (owner decision 2026-10-20, landed6e5981d): the Kvitt Pi runs Kvitt from /home/jacobhagstedt/kvitt, a self-contained compose project with relative bind mounts, both containers as the owner's uid/gid, the seven .env keys compose reads, no scripts and nothing under /opt; operations are plain docker compose commands. Revises D-103, D-105, D-117 point 5 and D-120 point 6. SPEC §10.2, §10.6, §10.8 to §10.10, §11.2 and §14.7 rewritten; KVITT_PI_HOST placeholders become <kvitt-pi-address>. §10.3 records the Gitea workflow's first results (run 69: v1.0.0, backup 5 min 20 s, server 2 h 42 min; v1.0.1 from the sparse-checkout push) and the sparse checkout of3820f9c. HANDOFF, prompts/README.md and notes in PLAN-M11.md and PLAN-M2c-pi.md follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
+35
-28
@@ -3,12 +3,18 @@
|
||||
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).
|
||||
- `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).
|
||||
- **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).
|
||||
- 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.
|
||||
- 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.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>`.
|
||||
- Self-contained Pi compose project (`6e5981d`, D-125, owner decision 2026-10-20: 'It should be a self contained docker compose thing', 'do not install anything in any other place'): `/home/jacobhagstedt/kvitt` holds `compose.yaml`, `.env`, `secrets/` and the bind-mounted `data/`, `backups/`, `import/`; both containers as `KVITT_UID`/`KVITT_GID` (1000 on the Pi), read-only root, 16 MB tmpfs `/tmp`, each mounting only its own secret file (`create_host_path: false`); `.env` exactly the seven keys compose reads, `RCLONE_REMOTE` fixed to `gdrive:`; `install.sh`, `deploy.sh`, `bootstrap.sh`, `backup-now.sh`, `install-apk.sh`, `lib.sh` removed, operations are plain `docker compose` commands (`infra/README.md` "Pi: what the owner does", seven steps, and `infra/RESTORE.md`); no digest record (`docker image inspect` instead); APKs by `scp` into `data/apk/`. Rehearsed with Podman as uid 501: healthy, `/health` 200, bootstrap, APK served, backup in three modes, restore steps.
|
||||
- Compose deploy hint (`28fa71b`, no decision): the workflow's header and digest-step comment and `infra/build-images.sh`'s closing line name `cd ~/kvitt`, `KVITT_TAG` in `.env`, `docker compose pull && docker compose up -d` and `docker image inspect` instead of `/opt/kvitt/deploy.sh` and `/opt/kvitt/deployed-digests`.
|
||||
- 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.
|
||||
@@ -17,7 +23,7 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
|
||||
- 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.
|
||||
- Temporary "Diagnose secrets" step in `.gitea/workflows/build-images.yaml` (`dd07ca4`, no decision), to be removed once the login failure's cause is known (removed in `3820f9c`).
|
||||
- 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).
|
||||
@@ -34,11 +40,11 @@ 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-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).
|
||||
- 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).
|
||||
- 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).
|
||||
- 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`).
|
||||
- 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`.
|
||||
@@ -46,43 +52,44 @@ 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-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).
|
||||
- Land `agent-docs-v024b` (orchestrator): SPEC v0.24, D-125, HANDOFF, `prompts/README.md`, the plan notes.
|
||||
- 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.
|
||||
- `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.
|
||||
- Possible firewall rule on the Kvitt Pi limiting port 8080 to the HA Pi (`HA_HOST`); not done, not decided (owner). D-125 makes no system changes, so it would be the owner's own step.
|
||||
- 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).
|
||||
- `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).
|
||||
|
||||
## 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).
|
||||
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).
|
||||
|
||||
## 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).
|
||||
- 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-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.
|
||||
- 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.
|
||||
- Sync protocol **1**, push payload **1**.
|
||||
|
||||
## 7. How the work is run
|
||||
|
||||
+5
-4
@@ -107,9 +107,9 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
|
||||
| D-100 | 2026-10-18 | No SSH from the Mac to the Pis. M2c-pi produces a self-contained bundle `infra/pi/` (compose file, Caddyfile, env template, install, deploy, bootstrap and backup scripts, a README checklist) that the owner copies to the Kvitt Pi and runs there. The OQ-3 values (Kvitt Pi address, Home Assistant Pi address, SSH port and user, certificate directory, SSH add-on) become placeholders the owner fills in the bundle's `.env`; OQ-3 is answered by this. Where the spec has a Mac script act on the Pi (`infra/deploy.sh`, `infra/publish-apk.sh`), the step becomes a bundle script the owner runs; M2c-pi fixes the layout inside `infra/pi/`, the script names and how images and APKs reach the Pi. M2c-pi runs as the last step before M10, after the app is proven on the phone against the development server. Agents never need allowances for the Kvitt Pi, the Home Assistant Pi, Gitea or Google Drive. Revises D-023 and the M2c-pi plan; D-001 stands. Fulfilled by D-103 to D-106 (bundle landed `2476667`). | Owner decision 2026-10-18: no agent access to the home servers; the Pi is set up once, by hand, from a checklist. | SPEC §10 intro, §10.1, §10.10, §11.1, §11.2 (M2c-pi, M10), §11.3, §12 (OQ-3); PLAN-M2c-pi |
|
||||
| D-101 | 2026-10-18 | The release keystore `kvitt-release.jks` (created with the D-024 `keytool` command) lives at the repository root of the main checkout, git-ignored by the `*.jks` pattern of `.gitignore` and never committed; `android/local.properties` references it by absolute path in `kvitt.release.storeFile` with the other three `kvitt.release.*` keys. The owner keeps a copy of the file and its password in their password manager. Revises D-024 ("outside the repository"). | Owner decision 2026-10-18; the `*.jks` ignore rule already keeps the file out of git. | SPEC §10.6, §11.2 (M10), §11.3; PLAN-M10 |
|
||||
| D-102 | 2026-10-18 | S-7 editor options as fixed rows. Below the category quick row, account, date, payer and split are four full-width rows in a fixed order (Konto, Datum, Betalt av, Delning) instead of a wrapping chip row, so no option moves when a value changes. Rows form a segmented group (outer corners 24, inner 8, gap 3) in surface-container-high (secondary-container while the split section is open), each at least 48 dp and tappable in full. A row has a leading element in a 24 dp box, a label (label-large, muted) in a column as wide as the widest label, the value (body-large, one line, ellipsis) and the trailing affordance at the right edge (expand_more/expand_less, swap_horiz for the payer). At ≥ 1.5× font scale the label sits over the value, which takes up to two lines, and every row keeps the two-line height. TalkBack sentences are unchanged. S-9 and S-19 keep their chips (S-19 follows in M8e, D-107). Also: connected buttons get 8 dp side padding so 'Household' fits half of S-11's filter bar at 1x. Landed by M8d (`bacbd18`): `EditorOptionRows` in `:core:design`, `KvittDimens.connectedButtonSide`. | Owner finding on S-7, 2026-10-18: the chips moved when a value changed. | SPEC §13.3 (S-7), §13.4; `docs/ux/design-spec.md` §3.7, §3.10 |
|
||||
| D-103 | 2026-10-18 | The M2c-pi bundle `infra/pi/`: `compose.yaml` (was `infra/compose.yaml`), `.env.example` (was `infra/env.example`), ~~`Caddyfile`~~ (removed by D-120), and the on-Pi scripts `install.sh` (idempotent: creates `/opt/kvitt/{secrets,import,apk}`, installs the secrets with their modes, adds `gitea.lan` to `insecure-registries`, sets the volume owners, starts the stack), `deploy.sh <tag> [--skip-backup]` (backs up before every deploy, because the Pi cannot tell whether a tag carries a migration; keeps `/opt/kvitt/previous-tag` for rollback), `bootstrap.sh`, `backup-now.sh`, `install-apk.sh`, helpers in `lib.sh`. APK path: `infra/publish-apk.sh` on the Mac writes `infra/pi/out/apk/` (`version_code` = `git rev-list --count HEAD`, `version_name` = `YYYY.MM.DD-<short sha>` in Europe/Stockholm, passed as `-Pkvitt.versionCode` / `-Pkvitt.versionName`; copies `mapping.txt` as `kvitt-<version_code>.mapping.txt`; refuses a dirty tree and an unsigned APK); the owner `scp`s it to `/opt/kvitt/apk/`; `install-apk.sh` copies it into the `data` volume, writing `latest.json` last and keeping the newest 5; mappings stay outside the volume. `.env` keys grow by `KVITT_REGISTRY_USER`, `KVITT_REGISTRY_TOKEN` (read by `install.sh` only) and the informational `KVITT_PI_HOST`, `KVITT_PI_SSH_USER`. The §11.2 gate becomes `docker compose -f infra/pi/compose.yaml config`. Landed `2476667`. Fulfils D-100. | M2c-pi report: D-100 left the layout, script names and the image and APK paths to M2c-pi. | SPEC §10 intro, §10.2, §10.3, §10.6, §10.7, §10.10, §11.2 (M2c-pi), §11.3, §14.7; `infra/README.md` |
|
||||
| D-103 | 2026-10-18 | The M2c-pi bundle `infra/pi/`: `compose.yaml` (was `infra/compose.yaml`), `.env.example` (was `infra/env.example`), ~~`Caddyfile`~~ (removed by D-120), ~~and the on-Pi scripts `install.sh` (idempotent: creates `/opt/kvitt/{secrets,import,apk}`, installs the secrets with their modes, adds `gitea.lan` to `insecure-registries`, sets the volume owners, starts the stack), `deploy.sh <tag> [--skip-backup]` (backs up before every deploy, because the Pi cannot tell whether a tag carries a migration; keeps `/opt/kvitt/previous-tag` for rollback), `bootstrap.sh`, `backup-now.sh`, `install-apk.sh`, helpers in `lib.sh`~~ (revised by D-125: no scripts and no `/opt`; plain `docker compose` commands in `/home/jacobhagstedt/kvitt`; the backup before every deploy stays). APK path: `infra/publish-apk.sh` on the Mac writes `infra/pi/out/apk/` (`version_code` = `git rev-list --count HEAD`, `version_name` = `YYYY.MM.DD-<short sha>` in Europe/Stockholm, passed as `-Pkvitt.versionCode` / `-Pkvitt.versionName`; copies `mapping.txt` as `kvitt-<version_code>.mapping.txt`; refuses a dirty tree and an unsigned APK); ~~the owner `scp`s it to `/opt/kvitt/apk/`; `install-apk.sh` copies it into the `data` volume, writing `latest.json` last and keeping the newest 5~~ (revised by D-125: the owner `scp`s the APK, then `latest.json`, into `~/kvitt/data/apk/`; older APKs removed by hand); mappings stay outside the volume. ~~`.env` keys grow by `KVITT_REGISTRY_USER`, `KVITT_REGISTRY_TOKEN` (read by `install.sh` only) and the informational `KVITT_PI_HOST`, `KVITT_PI_SSH_USER`.~~ (revised 2026-10-20: the informational keys dropped in `200c99f`, read by nothing on the Pi; the registry keys dropped by D-125, whose `.env` holds exactly the seven keys compose reads) The §11.2 gate becomes `docker compose -f infra/pi/compose.yaml config`. Landed `2476667`. Fulfils D-100. | M2c-pi report: D-100 left the layout, script names and the image and APK paths to M2c-pi. | SPEC §10 intro, §10.2, §10.3, §10.6, §10.7, §10.10, §11.2 (M2c-pi), §11.3, §14.7; `infra/README.md` (revised by D-125) |
|
||||
| D-104 | 2026-10-18 | Images `${KVITT_REGISTRY}/kvitt-server`, ~~`kvitt-cert-sync`~~ (removed by D-120), `kvitt-backup` with `KVITT_REGISTRY=gitea.lan/jacobh2` (was `${KVITT_REGISTRY}/kvitt/<name>`). `.gitea/workflows/build-images.yaml` follows the owner's working example: runner `host` (a Raspberry Pi 3, 1 GB; builds run on the runner itself), Alpine `apk add git docker-cli docker-cli-buildx`, a plain-HTTP buildx builder created once and reused, secret `kvitt_cicd`; triggers on `server/**`, ~~`infra/cert-sync/**`~~ (dropped by D-120), `infra/backup/**`, `.gitea/workflows/**` and `workflow_dispatch`; tags `v1.MINOR.PATCH` (the next patch after the newest tag) plus `latest`, the git tag pushed afterwards; `--build-arg CARGO_BUILD_JOBS=1` (honoured by `server/Dockerfile` since `b4d9467`; first build duration unknown). `infra/build-images.sh` on the Mac is the fallback (default tag the short SHA; builds locally and prints the push commands when `infra/secrets/registry.env` is absent). Both sidecars schedule with supercronic instead of busybox crond (crond runs no job as non-root, tested); ~~the cert-sync image adds `openssl`~~. Pinned versions (2026-10-18): ~~`caddy:2.11.7@sha256:685ae3d7c34d9b5f8caeddc5d14051db7bbd88ff1f5a2fa0c16bd8ca328fb7af`~~, `alpine:3.24.2@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6`, ~~openssh-client-default 10.3_p1, curl 8.22.0, openssl 3.5.9~~, supercronic 0.2.49 (revised by D-120: no Caddy pin and no cert-sync image or packages; only the backup sidecar uses supercronic); Docker Engine from Docker's Debian arm64 apt repository (docs.docker.com: the Raspberry Pi OS page is 32-bit only), `docker-ce` 5:29.8.2. | M2c-pi report: the owner's example workflow pushes under the owner path with flat image names; busybox crond did not run jobs as a non-root user. | SPEC §10.1, §10.2, §10.3, §12 (OQ-1); `.gitea/workflows/build-images.yaml`, `infra/README.md` "Versions" |
|
||||
| D-105 | 2026-10-18 | `backup` runs as uid 10001 on the Pi too, so it can open the WAL database (`sqlite3 .backup` needs the `-shm` file the server owns); `/opt/kvitt/secrets/rclone.conf` mode 0600 owned 10001:10001. ~~`cert-sync` stays uid 1000 with `ha_ssh_key`, `ha_known_hosts` 0600 owned by 1000.~~ (cert-sync removed by D-120) `backup.sh` uses a temporary copy of the read-only `rclone.conf` so rclone can save refreshed tokens. `install.sh` chowns `/data`, `/data/apk`, `/backups` to 10001 ~~and `/certs` to 1000~~ once. Reconciles SPEC §10.2; revises D-023 for `rclone.conf`. | M2c-pi report: uid 1000 could not back up the live database without loosening the server's files. | SPEC §10.2, §10.9; `infra/README.md` "Backup uid" |
|
||||
| D-105 | 2026-10-18 | `backup` runs as ~~uid 10001~~ the server's uid on the Pi too (revised by D-125: on the Pi both containers run as the owner's uid, `KVITT_UID`/`KVITT_GID`; the development stack keeps 10001), so it can open the WAL database (`sqlite3 .backup` needs the `-shm` file the server owns); ~~`/opt/kvitt/secrets/rclone.conf` mode 0600 owned 10001:10001~~ (revised by D-125: `~/kvitt/secrets/rclone.conf`, the owner's, mode 0600). ~~`cert-sync` stays uid 1000 with `ha_ssh_key`, `ha_known_hosts` 0600 owned by 1000.~~ (cert-sync removed by D-120) `backup.sh` uses a temporary copy of the read-only `rclone.conf` so rclone can save refreshed tokens. ~~`install.sh` chowns `/data`, `/data/apk`, `/backups` to 10001 and `/certs` to 1000 once.~~ (`/certs` removed by D-120; revised by D-125: no `install.sh`, the owner creates `data/apk`, `backups` and `import` before the first start) Reconciles SPEC §10.2; revises D-023 for `rclone.conf`. | M2c-pi report: uid 1000 could not back up the live database without loosening the server's files. | SPEC §10.2, §10.9; `infra/README.md` "Backup uid" |
|
||||
| D-106 | 2026-10-18 | ~~`sync.sh` always requests the `tar -C <dir> -cf - fullchain.pem privkey.pem` stream, so both `authorized_keys` variants (A restricted, B plain) work unchanged; it refuses a key not matching its certificate and a certificate expiring before the installed one; an unreachable Caddy admin API at first start is not a failure (Caddy loads the files when it starts).~~ (revised by D-120: cert-sync removed) `backup.sh` uploads every pending `kvitt-*` / `pictures-*` file including leftovers of failed runs, confirms each by name and size with `rclone lsf`, deletes the local copies only after all are confirmed, and writes `last-success` only after that. | M2c-pi report: the add-on's support for `authorized_keys` options is unknown; rehearsal of renew, refuse and failed-upload cases on the Mac. | SPEC §10.5.1, §10.9 |
|
||||
| D-107 | 2026-10-18 | S-19's options as fixed rows, extending D-102. The recurring template editor replaces its wrapping chips row (Konto · Betalt av · Delning) with `EditorOptionRows`: three full-width rows in the fixed order Konto, Betalt av, Delning, with no Datum row. They look and behave as in S-7: label column, value on one line with an ellipsis, label over value with a two-line value at ≥ 1.5× font scale, and Delning in secondary-container while the split section is open. A row's place and size never depend on its value. The schedule fields stay in the "Schema" section. TalkBack sentences are unchanged. A Robolectric layout test asserts identical row bounds for short and long values at 100 % and 200 %. The chip composables stay in `:core:design` for S-9 (`EditorChipRow`, `DateChip`); `AccountChip`, `PayerChip`, `SplitChip` were removed in `1328afe`. Landed by M8e (`a59abeb`). | Owner finding (D-102): chips moved when a value changed; S-19 had the same chips row. | SPEC §13.3 (S-19); `docs/ux/design-spec.md` §3.7, S-19 |
|
||||
| D-108 | 2026-10-18 | Navigation is instant. Every destination change (pushed screens, bottom-destination switches, S-7 and S-9 and the other editors, onboarding and the session gate) has no enter, exit, pop or size transition (`EnterTransition.None` / `ExitTransition.None`, no `SizeTransform`, set once on the app's `NavHost`, `NavTransitions` in `:app`). Replaces the Navigation bullet of design-spec §2.11. Motion inside a screen (ripples, state layers, expanding sections, FAB extend/collapse, bottom-sheet open, the seesaw and bunting) is unchanged. Not a contract change. Landed by M8f (`956908a`). | Owner finding on the S26, 2026-10-18: the default 700 ms fades made the app feel slow. | SPEC §13.2; `docs/ux/design-spec.md` §2.11 |
|
||||
@@ -121,11 +121,12 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
|
||||
| D-114 | 2026-10-18 | Upload and token limits. A picture upload (E-9, AC-4.4) must also be at most 16 000 000 pixels (width × height, for example 4000 × 4000) and decode to at most 64 MB (16 MP × 4 bytes, so a 16-bit image is refused above 8 MP), checked from the image header before decoding with the decoder limits set as well (`MAX_PIXELS`, `MAX_DECODE_ALLOC` in `src/api/pictures.rs`), refused with `image_invalid`; the 8000 px per side and 5 MiB limits stay. `fcm_token` at E-17 is 1 to 512 characters after trimming (`FCM_TOKEN_MAX`); longer is `invalid_request` with field `fcm_token`, like the empty string (D-034); a Firebase Installation ID has 22 characters. Landed by the server overhaul (`baa5806`, `b985ce9`). | Security review M-3: 8000 × 8000 decodes to 256 MB RGBA, above the 160 MiB container. L-4: a device could store a token up to the 1 MiB body limit, sent to FCM in every push. | SPEC §4 (AC-4.4), §9.6, §14.3.4 (E-17) |
|
||||
| D-115 | 2026-10-18 | Rate-limit token acceptance, revising D-032: a bearer token is accepted only when a response to it is 2xx; a request with a token not yet accepted counts against its client IP's unauthenticated limit before it runs and is refused with `rate_limited` once the IP is over; a 2xx gives that count back; a 401 for an accepted token counts against the IP and withdraws the acceptance; token entries are pruned once their window expires, accepted or not; every 401 counts against the IP exactly once. Landed by the server overhaul (`725901c`), with unit tests per rule and an integration test of random tokens without the protocol header (60 × 426, then 429, the paired device unaffected). | Security review H-1: a random token without `X-Kvitt-Protocol` (426) became accepted for good, bypassed the IP limit and was never pruned, so a stranger could grow the in-memory map without bound. | SPEC §8.6 |
|
||||
| D-116 | 2026-10-18 | Notifications are posted with `VISIBILITY_PRIVATE` and a public version carrying the generic "Kvitt" / "Open Kvitt to see it" (`notif_generic_title`, `notif_generic_body`), so a secure lock screen shows no description, amount, account or name (new AC-13.9; `NotificationPrivacyTest`). L-6 (a link can prefill any server on S-1) stays as built: S-1 opens its server row expanded when the link's server is not the release default, release builds require `https://`, and a paired phone ignores links (D-084); a confirmation step would need a design state and is not in v1 (OQ-6). Landed by M12f (`2bd6b58`). | Security review L-7 and L-6. | SPEC §4 (AC-2.6, AC-2.8, AC-13.9), §12 (OQ-6) |
|
||||
| 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-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;~~ (revised by D-125: no digest record on the Pi; the owner compares `docker image inspect` with the workflow's digests) 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~~ 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`~~ (revised by D-124: `https://${KVITT_PUBLIC_HOST}`, port 443). A path prefix on another name is not supported. (2) The router forwards external TCP ~~8443~~ 443 (revised by D-124: the existing forward for Home Assistant, no new rule) 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-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`~~ (revised by D-124: `https://${KVITT_PUBLIC_HOST}`, port 443). A path prefix on another name is not supported. (2) The router forwards external TCP ~~8443~~ 443 (revised by D-124: the existing forward for Home Assistant, no new rule) 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` (no check since D-125); 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.~~ (revised by D-125: no scripts; the owner runs the same two checks by hand, SPEC §10.5.1) (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~~ no port (revised by D-124), `/i/`, `autoVerify="false"`) and as `RELEASE_SERVER_URL` = ~~`https://<host>:8443`~~ (revised by D-124: `https://<host>`) (`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`~~ (revised by D-124: `https://<kvitt.publicHost>`): `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`~~ (revised by D-124: `https://${KVITT_PUBLIC_HOST}`), 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`~~ (revised by D-124: `https://{host}`) 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`~~ (revised by D-124: `https://kvitt.example.invalid`). `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) |
|
||||
| D-124 | 2026-10-19 | Kvitt is served on port 443 (`https://<host>`, no explicit port) because NPM listens on 443 and selects the proxy host by host name; the router's existing external-443 forward to the Home Assistant Pi serves Kvitt, and there is no 8443 rule (revises D-120 point 2 and the `:8443` of D-120 to D-123). (1) Server and contracts (`a86ad24`): the OpenAPI server is `https://{host}` (`contracts/openapi.yaml`, `openapi.rs` `PUBLIC_SERVER_URL`); examples E-7 `public_url` and E-12 `link` use `https://kvitt.example.invalid` without a port; `tools/contracts-check.py` expects the portless header; the server's tests, configuration message and README name no port. (2) Bundle (`4852803`): `KVITT_PUBLIC_URL=https://${KVITT_PUBLIC_HOST}` in `infra/pi/compose.yaml`; the health checks through NPM use `https://<KVITT_PUBLIC_HOST>/health` on 443 (`infra/pi/lib.sh`); `infra/publish-apk.sh` prints the release server without a port; `infra/README.md` step 7 is "Router: no new rule" (verify the existing TCP 443 rule to `<HA_HOST>`); the APK-abuse stop-gap is now "disable Kvitt's proxy host in NPM", because the 443 forward also serves Home Assistant. (3) Android (`038c603`): `releaseServerUrl(host)` = `https://<host>` (`RELEASE_SERVER_PORT` removed); the AC-2.6 `https` intent filter carries no `android:port`, which matches every port of the host, so an old `:8443` link still opens S-1 with the server it names; invite-link parsing drops an explicit default port (`:443` for `https`, `:80` for `http`); the S-1 fixture `GOLDEN_SERVER` is `https://kvitt.example.org` (28 S-1 goldens re-recorded, only the server row changed). | Owner decision 2026-10-19: NPM serves every proxy host on 443 and picks Kvitt's by name, so a second external port adds a router rule and nothing else; the owner confirmed that external 443 already forwards to the Home Assistant Pi. | SPEC §1, AC-1.1, AC-2.6, §6.1, §7, §8.1, §8.5, §10.1, §10.2, §10.4, §10.5, §10.5.3, §10.6, §10.10, §11.2, §11.3, §14.2.4, §14.3, E-7, E-12 |
|
||||
| D-125 | 2026-10-20 | The Pi runs Kvitt from `/home/jacobhagstedt/kvitt`, a self-contained compose project: `compose.yaml`, `.env`, `secrets/`, relative bind mounts `./data`, `./backups`, `./import`; containers run as the owner's uid/gid (`KVITT_UID`/`KVITT_GID`, 1000 on the Pi); no install script, no `/opt`, no Docker daemon or system changes (registry trust and Docker are prerequisites); operations are plain `docker compose` commands. Details: `.env` holds exactly the seven keys compose reads (`KVITT_REGISTRY`, `KVITT_TAG`, `KVITT_PUBLIC_HOST`, `HA_HOST`, `KVITT_UID`, `KVITT_GID`, `RCLONE_FOLDER_ID`); `RCLONE_REMOTE` fixed to `gdrive:` in compose; private packages → one-time `docker login gitea.lan`; each container mounts only its own secret file read-only (`create_host_path: false`); read-only root, 16 MB tmpfs `/tmp`, backup `HOME=/tmp`; secret files the owner's, mode 0600. Equivalents of the removed scripts (`install.sh`, `deploy.sh`, `bootstrap.sh`, `backup-now.sh`, `install-apk.sh`, `lib.sh`): backup `docker compose exec backup /backup.sh`; bootstrap `docker compose exec server kvitt-server bootstrap`; partner invite `docker compose exec server kvitt-server invite-partner`; deploy = backup, set `KVITT_TAG`, `pull`, `up -d`; rollback = the previous tag; APK = `scp` the APK, then `latest.json`, into `data/apk/` (old APKs removed by hand); digests via `docker image inspect`. Restore moves files on the host; only rclone and sqlite3 run in the backup container. Compose default network. The development stack is unchanged (uid 10001). Revises D-103 (scripts, `/opt/kvitt`, `.env` keys), D-105 (uid and `rclone.conf` owner on the Pi), D-117 point 5 (the digest record is dropped), D-120 point 6 (health checks via scripts dropped). Landed `6e5981d`. Rehearsed locally with Podman as uid 501: both containers healthy, `/health` 200, bootstrap, APK served, backup in three modes (no folder id, a local remote, a failing remote), the restore steps. | Owner decision 2026-10-20: 'It should be a self contained docker compose thing', 'do not install anything in any other place'. | SPEC §8.5, §8.9, §10 intro, §10.1, §10.2, §10.3, §10.4, §10.5, §10.5.1, §10.6, §10.7, §10.8, §10.9, §10.10, §11.2, §11.3, §12 (OQ-3), §14.7; `infra/pi/compose.yaml`, `infra/pi/.env.example`, `infra/README.md` "Pi: what the owner does", `infra/RESTORE.md`, `PLAN-M11.md` and `PLAN-M2c-pi.md` notes |
|
||||
|
||||
Reference in New Issue
Block a user