# Handoff: state of the work Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this. ## 1. Where things stand - `main` at `4755fe0` (chips cleanup) plus the SPEC v0.17 docs commit from `agent-docs-v017` once it lands. History: `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `fad6fc9` M2b, `fdf2093` M2a, `040bf41` SPEC v0.4, `503cee5` M3, `c28c52b` M5, `982451d` SPEC v0.5, `bd6f10a` M4, `23a2f1e` SPEC v0.6, `ab298ab` M9, `e8ee518` SPEC v0.7, `6b60ef7` M12a, `7fb9f6f` SPEC v0.8, `e499107` M12c, `2e2ebe7` M12b, `d452fa6` UX Kalas categories and tokens, `16c4d01` UX sprint U2, `45eecd1` SPEC v0.9, `bdba9d3` M12d, `3080804` M6, `7bd0aa1` SPEC v0.10, `72f21b2` app debug fakes bindings (temporary, deleted by M8), `e6ad858` app release wiring of `:data` (pre-M8), `b9b061a` M7a, `6ac711d` M7e, `082b8dc` M7d, `31760f9` M7b, `1e42f76` M7c, `a54eb7c` M7f, `3a1bd14` M6b, `d504004` SPEC v0.11, `34a2590` M7g, `369e516` and `00a14a3` M8, `cf8ef45` SPEC v0.12, `1470a2f` M12e contract pass, `90505a8` M12e launcher icon, `03b7184` SPEC v0.13, `2b4e21c` Vico catalog entries removed (D-063), `c694f78` server FCM target in `fid` (D-096), `1e404cc` Pi image arm64 check (`docs/spikes/pi-image-arm64.md`), `fd60170` SPEC v0.14, `f0575d3` M8b UI fixes from the first phone test, `09f474f` `FcmMessageBody.fid` in `:data` (D-096 follow-up), `e7d963a` SPEC v0.15, `b481fab` M8c goldens at 1x in three variants (D-098), `64f5706` M8c S-22 licences row removed (D-099), `7c453d8` every PNG through oxipng, `57f984f` R8 release build, `3c68594` M8d editor rows (D-102), `3a3720b` SPEC v0.16, `5e027df` M2c-pi Pi bundle (D-103 to D-106), `15397e4` M8e S-19 option rows (D-107), `965bb5d` server Dockerfile `CARGO_BUILD_JOBS` argument, `ed1b130` M8f instant navigation (D-108), `4755fe0` unused editor chips removed. - Landed: M0 spec, M1 contracts and vectors (`c6df2ab`), M2a (`fdf2093`), M2b (`fad6fc9`), M2c-dev (`c885aba`), M3 (`503cee5`), M4 (`bd6f10a`), M5 (`c28c52b`), M9 (`ab298ab`), M12a (`6b60ef7`), M12b (`2e2ebe7`), M12c (`e499107`), U1 and U2 (`16c4d01`, owner approved), M12d (`bdba9d3`), M6 (`3080804`), M7a to M7f, M6b (`3a1bd14`), SPEC v0.11 (`d504004`, D-069 to D-080), M7g feature cleanups (`34a2590`), M8 app integration (`369e516` + `00a14a3`, landed 2026-10-18, gate green: BUILD SUCCESSFUL, 710 actionable tasks), SPEC v0.12 (`cf8ef45`, D-081 to D-085), M12e contract pass (`1470a2f` + `90505a8`, landed 2026-10-18; gates green: Android BUILD SUCCESSFUL, 710 actionable tasks, 906 tests; cargo 253 tests, OpenAPI equality, vectors and migration byte-identity green), SPEC v0.13 (`03b7184`, D-086 to D-095), Vico removal (`2b4e21c`), FCM `fid` target (`c694f78`, D-096), Pi image arm64 check (`1e404cc`: C stage, CA roots and an OAuth plus FCM call from the arm64 image confirmed on the Mac; running on the Pi stays M2c-pi's), SPEC v0.14 (`fd60170`, D-096), M8 on-phone test (2026-10-18, passed), M8b (`f0575d3`, D-097), `FcmMessageBody.fid` (`09f474f`), SPEC v0.15 (`e7d963a`, D-097, D-100, D-101); landed today after v0.15: M8c (`b481fab`, `64f5706`, `7c453d8`; D-098, D-099), the R8 release build (`57f984f`), M8d (`3c68594`, D-102); SPEC v0.16 (`3a3720b`); then the M2c-pi bundle (`5e027df`, D-103 to D-106), M8e (`15397e4`, D-107), the Dockerfile `CARGO_BUILD_JOBS` argument (`965bb5d`), M8f (`ed1b130`, D-108) and the chips cleanup (`4755fe0`). - M2c-pi bundle outcome (SPEC v0.17): `infra/pi/` with `compose.yaml`, `.env.example`, `Caddyfile`, `install.sh`, `deploy.sh [--skip-backup]` (backup before every deploy, `/opt/kvitt/previous-tag`), `bootstrap.sh`, `backup-now.sh`, `install-apk.sh` (D-103); images `gitea.lan/jacobh2/kvitt-server|kvitt-cert-sync|kvitt-backup`, Gitea workflow `.gitea/workflows/build-images.yaml` (runner `host`, secret `kvitt_cicd`, tags `v1.0.N` + `latest`, `CARGO_BUILD_JOBS=1`), `infra/build-images.sh` as Mac fallback, supercronic instead of crond (D-104); `backup` uid 10001, `rclone.conf` owned 10001 (D-105); cert-sync tar stream for both key variants, backup confirm-then-delete (D-106). Gate at landing: shellcheck clean on 18 scripts, compose config valid, cert-sync arm64 image builds; rehearsed on the Mac with Podman (cert-sync install, renew, skip, refuse; Caddy reload; `Authorization` redacted; TLS 1.1 refused; backup upload, `lsf` confirmation, delete after confirm, failed-upload retry; healthcheck red at 26 h; APK prune keeps 5). Not verifiable from the Mac: the add-on's `authorized_keys` options, the Gitea workflow run, pulls from `gitea.lan`, the router forward, Drive, real Docker compose. - M8e, M8f and cleanup outcome (SPEC v0.17): S-19's account, payer and split are three fixed `EditorOptionRows` (D-107); navigation is instant, no `NavHost` transitions (D-108); `AccountChip`, `PayerChip`, `SplitChip` removed (`4755fe0`), S-9 keeps `DateChip` in `EditorChipRow`. - M8 outcome (recorded in SPEC v0.12 as D-081 to D-085): both variants bind `:data` through `di/AppDataModule.kt`, debug fakes module gone (D-081, revises D-041); debug network security config allows cleartext everywhere, the AC-2.8 limit is enforced in `SessionRepositoryImpl.normaliseServerUrl`, pinned-root config behind `kvitt.tls.pinnedRoot` (D-082); FCM registration by Firebase Installation ID, firebase-messaging 26.0.0, no server change (D-083); invite links ignored while paired (D-084); generic-notification, deletion and `sync_batch` taps open Home, push in an expedited WorkManager job (D-085). - M8 end-to-end test (2026-10-18, dev server, owner's S26): pairing with the bootstrap code OK; a scripted partner joined through a second admin device paired with `kvitt-server pair-device` (the CLI has no partner-invite subcommand: small ops request for a later server plan); Home showed the account; the offline expense showed the pending marker and synced after reconnect (pending 0, the server has the row); the owner's device registered `fcm_token` and `app_version_name`; a push by `fid` was delivered and the notification tap worked. Still unverified on the phone: the status-bar fix (next install) and the design-spec §2.7 typeface check (the owner looked, no complaint). - M8b outcome (SPEC v0.15): invite-code field holds the raw code, hyphens as a `VisualTransformation` (cursor bug fixed, unit test); quick-row selected badge drawn above the ripple clip; `KvittTopAppBar` pads for the status-bar inset (`windowInsets` parameter), `KvittScaffold` follows; chart bars top 4, bottom 2 dp (D-097); 221 goldens re-recorded. `FcmMessageBody.fid` in `:data` followed (`09f474f`). - M8c, R8 and M8d outcome (SPEC v0.16): goldens `_sv_light`, `_sv_dark`, `_en_light` plus `_sv_light_200`, at 1x (`w411dp-hdp-mdpi`), oxipng-compressed: 1143 PNGs / 131 MB to 865 / 26 MB, mock-ups 36.8 to 26.0 MB (D-098, revises D-066); S-22 has no licences row, `settings_about_licences` gone (D-099, OQ-5 closed); release APK R8-shrunk, about 11 MB (was 43 MB), `mapping.txt` to be kept beside the APK by the publish script (SPEC §10.6); release signing verified (alias `kvitt`, RSA 4096, v2); S-7 options as four fixed rows `EditorOptionRows`, connected buttons 8 dp side padding (D-102). - Owner answers 2026-10-18: "boende" keeps suggesting `travel` (no change); the 128 px picture size is not needed; OQ-5 "no, hide the button" (implemented by M8c); the account page split bar stays as designed; M2c-pi becomes an owner-run bundle (D-100); the keystore lives at the repository root (D-101); the Gitea push waits for fewer goldens and a history rewrite, no LFS. - M12e outcome (recorded in SPEC v0.13 as D-086 to D-095): E-3 `invite_kind` and `Pairing`, S-2 only after a `partner` pairing (D-086); admin name in E-12 links via `?from=`, passed on by E-4, S-1 help line names the admin (D-087); `SyncSummary.lastAttemptFailed` (D-088); `Rejection.actor` (D-089); `X-Kvitt-App-Version-Name`, migration 0003, server schema 3, S-20 "Kvitt 1.1" (D-090); `NewRecurringTemplate` (D-091); `UpdateCheck` (D-092, revises D-073); `SettleState.untouched` (D-093, revises D-074); D-027 re-stamp in `:data` (D-094); `PushEvent` in `:core:model`, six screen-hosted snackbar hosts, snackbar resource ids, adaptive vector launcher icon (D-095, revises D-071). 128 px picture size: the owner confirmed it is not needed (single 512 size, SPEC §14.2.4). - Compatibility: the M12e APK cannot pair or list devices against a pre-M12e server (DTOs without defaults, by design), so the server is rebuilt and redeployed before the new APK is installed (SPEC §14.8). Old phones on the new server work. - In flight: `agent-docs-v017` (this docs change: SPEC v0.17, D-103 to D-108), an AC-coverage audit (its report to be committed later as `docs/validation/ac-coverage.md`). Then a one-off history rewrite (drop the superseded PNG blobs; no clone exists) and the first push to `origin` (owner authorised, no confirmation needed), which also triggers the Gitea Actions workflow. - Remote: `origin` on Gitea exists and the SSH key works; the golden reduction (M8c) has landed; the history rewrite and the first push follow this docs pass (owner authorised 2026-10-18, no further confirmation). The owner created the Gitea secret `kvitt_cicd` and the Drive token (`infra/secrets/rclone-drive-token.json`, turned into `infra/secrets/rclone.conf` with `[gdrive]` `type`, `scope`, `token`; the folder id stays `RCLONE_FOLDER_ID` in `.env`; both git-ignored); the cert-sync key `infra/secrets/ha_ssh_key` exists. - Both gates exist: Android `tools/gradle-serial.sh check` (from `android/`), server `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo test` (from `server/`). Both green on `main` as far as reported. - The UX sprint branch `UX-sprint` and its worktree `~/orca/workspaces/kvitt/UX-sprint` were removed on 2026-10-18 (fully merged). - Importer: the dry run of 2026-10-18 with M12b's mapping reconciled on both real files (Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped). The CSV files stay in `export/` (git-ignored). - Development stack (M2c-dev): running on the Mac, reachable at `http://192.168.1.210:8080`; rebuilt on 2026-10-18 with the M12e server and the `fid` target (a push by `fid` arrived in the M8 test), database at schema 3, data kept. Backups `infra/dev/backups/kvitt-20261018T093143Z.sqlite.gz` (before schema 2) and `infra/dev/backups/kvitt-20261018T130443Z.sqlite.gz` (before schema 3) exist (git-ignored). On this Podman setup `infra/dev/up.sh` alone does not swap in a rebuilt image: run `infra/dev/up.sh --down`, then `infra/dev/up.sh`. The owner's phone paired with the bootstrap code on 2026-10-18; a scripted partner exists, joined through a second admin device paired with `kvitt-server pair-device` (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`; `infra/secrets/fcm-service-account.json` and `infra/secrets/google-services.json` in the main checkout (git-ignored). - Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug`, paired against the dev server for the M8 test on 2026-10-18; the next install carries M8b to M8f. ## 2. Open work (one line each, with who owns it: agent or owner) - Land `agent-docs-v017` (orchestrator): SPEC v0.17, D-103 to D-108, HANDOFF, `prompts/README.md`, design-spec §2.11, §3.7 and S-19. - AC-coverage audit (agent, running): report to `docs/validation/ac-coverage.md`, committed later. - History rewrite (drop superseded PNG blobs; no clone exists), then the first push to Gitea `origin` (orchestrator; owner authorised, no confirmation needed); the push triggers the Gitea Actions workflow (§3 item 1). - Install the next debug build and have the owner look (§3 item 2); build a signed release for the smoke taps (§3 item 3). - Owner runs the M2c-pi bundle on the Kvitt Pi (§3 item 4); record the add-on key variant (A/B) in `infra/README.md` and the router-forward answer. - Cleanup (agents of the owning modules, no plan yet): 20 unused strings R8 found (`editor_next`, `home_notification_rationale`, `settle_empty_settled`, `home_sync_status_tag_*`, …) could be deleted by their modules; optional `localeFilters` en+sv. - Ops request (no plan yet): a `kvitt-server` subcommand for a partner invite, so a scripted partner needs no second admin device. - Plans (SPEC §11.2 v0.17), in dependency order; prompt `prompts/.md`: - M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7g, M8 (phone test passed), M8b to M8f, M9, M12a to M12e, U1, U2, R8 release build, M2c-pi bundle: landed. - Next: the owner runs the M2c-pi bundle on the Kvitt Pi (D-100); M10 First release after it (keystore per §10.6, D-101); M11 Import history after M9 and M10 (procedure §10.8 "Production run", D-025; each run must reconcile, D-045). ## 3. HUMAN steps pending (relay in chat) 1. After the first push: tell me whether the Gitea Actions run ("Build Kvitt images") succeeded and how long it took. 2. Next install on the phone (yes/no each): screen changes are instant, with no fade or slide; plus the earlier checks: the title bar sits below the status bar; the quick-row selected badge is whole; the invite-code field cursor stays after the last character; the expense editor shows Konto, Datum, Betalt av, Delning as four rows that do not move when a value changes; the recurring editor shows Konto, Betalt av, Delning as three such rows; "Household" fits its half of the Analytics filter bar. 3. Release build smoke taps: the app opens; S-1 or Home renders; Settings, About shows the version; add one expense and watch it sync. 4. Pi, after the app is proven: the nine steps of `infra/README.md` "Pi: what the owner does": 1. Provision: Raspberry Pi Imager, Raspberry Pi OS Lite (64-bit), user, SSH key, time zone; static DHCP lease in the router. 2. Docker Engine from Docker's Debian arm64 repository; `docker run --rm hello-world` prints "Hello from Docker!". 3. Home Assistant, Settings, Add-ons, the SSH add-on, Configuration, `authorized_keys`: add the cert-sync key as variant A (restricted `tar` command); if the add-on refuses it, variant B (plain key). Question: which variant was accepted, A or B? 4. On the Mac: `infra/pi/.env` from `.env.example`, every `CHANGE_ME` replaced; secrets staged into `infra/pi/secrets/`. 5. `scp` the bundle to the Pi, remove `infra/pi/secrets` on the Mac; on the Pi `ssh-keyscan` the Home Assistant host into `secrets/ha_known_hosts` (at least 1 line). 6. `./install.sh` on the Pi; expected end "Kvitt is running."; then remove `~/kvitt-pi/secrets`. 7. Router, WAN, Virtual Server / Port Forwarding: TCP 8443 to the Kvitt Pi's 8443; on the phone with Wi-Fi off, `https://icehack.asuscomm.com:8443/health` shows `"status":"ok"` without a certificate warning. Question: yes/no? 8. `/opt/kvitt/backup-now.sh` ends with "last-success: "; `/opt/kvitt/bootstrap.sh` prints the admin invite code (first install only). 9. New versions: `/opt/kvitt/deploy.sh v1.0.N` after the Gitea workflow pushed that tag. ## 4. Owner questions (asked, unanswered) - None. OQ-3 is answered by placeholders (D-100); OQ-5 answered and implemented (D-099). ## 5. Paused (waiting on something outside the repo) - Nothing. ## 6. Numbering - Decisions: D-001 to D-108 used except D-008 and D-009. Next free: **D-109**. - Spec version: v0.17 written; next **v0.18**. - Server schema version **3** (migration 0003, landed with M12e); next **4**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); next **2**. Both unchanged by v0.17. - Sync protocol **1**, push payload **1**. ## 7. How the work is run See CLAUDE.md › Orchestration. HUMAN steps in reports are relayed in chat by the orchestrator; the owner does not read them in files.