SPEC v0.15: D-097 chart corners, D-100 Pi bundle, D-101 keystore; M8 phone test, M8b, owner answers
- D-097: chart bars top 4 dp, bottom 2 dp (M8b f0575d3); design-spec §2.9, §3.10 - D-100: no SSH from the Mac to the Pis; M2c-pi delivers infra/pi/ for the owner to run; OQ-3 answered by .env placeholders; revises D-023, keeps D-001 - D-101: kvitt-release.jks at the repository root, git-ignored; revises D-024 - M8 end-to-end test result and M8b fixes in §11.2/§11.3; OQ-5 answered; 128 px picture size not needed (§14.2.4) - HANDOFF and prompts/README rewritten for the current state and numbering Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Kvitt: specification
|
||||
|
||||
Version **0.14** (2026-10-18). Schema version **3**. Sync protocol version **1**. Push payload version **1**.
|
||||
Version **0.15** (2026-10-18). Schema version **3**. Sync protocol version **1**. Push payload version **1**.
|
||||
|
||||
This file is the single source of truth for Kvitt. It is written for implementation agents that have no other context. If code, a plan or `CLAUDE.md` disagrees with this file, this file is right. Words in `code` are literal identifiers, paths, enum values or error codes and must be used exactly as written. Anything not decided here is listed in §12 as `TODO(spec)`; do not invent a value for it.
|
||||
|
||||
@@ -406,7 +406,7 @@ Local-only phone tables: `sync_state` and `sync_rejections` (§14.2).
|
||||
| Background | WorkManager (periodic 15 min sync, expedited sync) | Survives process death, respects Doze |
|
||||
| Dates | kotlinx-datetime | Allowed in pure modules (no `java.time`) |
|
||||
| Images | Coil (Compose) with an auth header interceptor | Disk cache for offline avatars |
|
||||
| Charts | Compose `Canvas` in `:core:design`, no chart library (D-063). Vico 3.3.1 cannot give the per-bar TalkBack nodes and tap targets, 2 dp segment gaps, 10/4 corner radii on the whole bar and an axis gutter growing at 200 % that `docs/ux/design-spec.md` §3.10 requires. Vico was removed from the version catalog on 2026-10-18 (`2b4e21c`); it was never used | The three charts are small; the design spec's accessibility rules need per-bar control |
|
||||
| Charts | Compose `Canvas` in `:core:design`, no chart library (D-063). Vico 3.3.1 cannot give the per-bar TalkBack nodes and tap targets, 2 dp segment gaps, corner radii on the whole bar (10/4 at M6; top 4, bottom 2 dp since D-097) and an axis gutter growing at 200 % that `docs/ux/design-spec.md` §3.10 requires. Vico was removed from the version catalog on 2026-10-18 (`2b4e21c`); it was never used | The three charts are small; the design spec's accessibility rules need per-bar control |
|
||||
| Push | Firebase Cloud Messaging (firebase-messaging 26.0.0 through the Firebase BoM 35.0.2, google-services plugin 4.5.0; registration by Firebase Installation ID, D-083), only inside `:adapter:fcm` | Free tier; vendor isolated per `CLAUDE.md` |
|
||||
| Secrets | Android Keystore: an AES-256-GCM key (non-exportable) encrypts the device token; ciphertext in a private DataStore file | `EncryptedSharedPreferences` is deprecated |
|
||||
| Build | Gradle with version catalog `android/gradle/libs.versions.toml`, Gradle daemon on JDK 25 (pinned), bytecode target Java 17. With AGP 9, `check` runs only the debug unit-test variant. `versionCode` and `versionName` come from the Gradle properties `kvitt.versionCode` and `kvitt.versionName`, defaulting to `1` and `dev` (§10.6) | Existing pin |
|
||||
@@ -859,17 +859,19 @@ Evidence (D-049): a chronological replay of the household's 1 775 Splitwise expe
|
||||
|
||||
All infrastructure files live in `infra/` (ownership §14.7). Secrets live in `infra/secrets/` on the Mac (git-ignored) and in `/opt/kvitt/secrets/` on the Pi; neither is ever committed.
|
||||
|
||||
**Development first (D-001).** During development the server runs locally on the owner's M4 Mac (Docker compose from `infra/dev/compose.yaml`, or plain `cargo run` in `server/`), and debug builds pair against it over the home Wi-Fi (debug builds accept `http://` to private LAN addresses, AC-2.8). The development stack (built by M2c-dev, landed `c885aba`, D-022) has the `server` service and the `backup` job runnable on demand without the Drive upload; it has no Caddy and no TLS (plain `http://`, which debug builds accept on private LAN addresses). Its data is a bind-mounted folder on the Mac (`infra/dev/data/`, git-ignored, and `infra/dev/backups/` for local snapshots); it mounts the whole `infra/secrets/` folder read-only at `/secrets` instead of a single key file (a missing `fcm-service-account.json` means push is disabled, §10.4); its env file is `infra/dev/.env.example`; its `server` healthcheck is `kvitt-server healthcheck`; its `backup` runs as uid 10001 so that it can open the WAL files the server owns. The Pi's env file stays `infra/env.example`, written by M2c-pi. The development stack's database was migrated to schema 2 (migration 0002, §14.1.1) on 2026-10-18 after a backup, and to schema 3 (migration 0003, §14.1.2) on 2026-10-18 when it was rebuilt from `90505a8`, again after a backup. On the Mac's 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`. Firebase (§10.7) is set up during development because push is needed then. Everything in this section that concerns the Pi (the Pi hosts and router of §10.1, §10.2, the registry and Pi deploy of §10.3, §10.5 TLS, publishing to the Pi in §10.6, the Drive upload of §10.9) is built by milestone M2c-pi, which runs after M8 and before M10.
|
||||
**Development first (D-001).** During development the server runs locally on the owner's M4 Mac (Docker compose from `infra/dev/compose.yaml`, or plain `cargo run` in `server/`), and debug builds pair against it over the home Wi-Fi (debug builds accept `http://` to private LAN addresses, AC-2.8). The development stack (built by M2c-dev, landed `c885aba`, D-022) has the `server` service and the `backup` job runnable on demand without the Drive upload; it has no Caddy and no TLS (plain `http://`, which debug builds accept on private LAN addresses). Its data is a bind-mounted folder on the Mac (`infra/dev/data/`, git-ignored, and `infra/dev/backups/` for local snapshots); it mounts the whole `infra/secrets/` folder read-only at `/secrets` instead of a single key file (a missing `fcm-service-account.json` means push is disabled, §10.4); its env file is `infra/dev/.env.example`; its `server` healthcheck is `kvitt-server healthcheck`; its `backup` runs as uid 10001 so that it can open the WAL files the server owns. The Pi's env template is part of the M2c-pi bundle (D-100). The development stack's database was migrated to schema 2 (migration 0002, §14.1.1) on 2026-10-18 after a backup, and to schema 3 (migration 0003, §14.1.2) on 2026-10-18 when it was rebuilt from `90505a8`, again after a backup. On the Mac's 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`. Firebase (§10.7) is set up during development because push is needed then. Everything in this section that concerns the Pi (the Pi hosts and router of §10.1, §10.2, the registry and Pi deploy of §10.3, §10.5 TLS, publishing to the Pi in §10.6, the Drive upload of §10.9) is built by milestone M2c-pi, which runs after M8 and before M10.
|
||||
|
||||
**No SSH from the Mac to the Pis (D-100, revises D-023).** M2c-pi delivers a self-contained bundle in `infra/pi/`: the compose file, the Caddyfile, an env template, install, deploy, bootstrap and backup scripts and a README checklist. The owner copies the bundle to the Kvitt Pi, fills the placeholders in its `.env` (the OQ-3 values among them) and runs the scripts there. Agents never connect to the Kvitt Pi, the Home Assistant Pi, Gitea or Google Drive and need no allowance for them; they check the bundle on the Mac (`docker compose config` with the env template, `shellcheck`). Where §10.2 to §10.10 have a script on the Mac act on the Pi (for example `infra/deploy.sh` and `infra/publish-apk.sh` over SSH), that step becomes a bundle script the owner runs on the Pi; 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. D-001 stands: development stays on the Mac.
|
||||
|
||||
### 10.1 Hosts and network
|
||||
|
||||
| Host | Role | Facts |
|
||||
|---|---|---|
|
||||
| Kvitt Pi | Runs the compose stack | Raspberry Pi 3, 1 GB RAM, 64-bit Raspberry Pi OS Lite (Debian bookworm or later), Docker Engine with the compose plugin, static LAN address. LAN address and SSH user: `TODO(spec)` OQ-3 |
|
||||
| Home Assistant Pi | Owns ports 80/443 and the Let's Encrypt certificate (certbot) for `icehack.asuscomm.com` | Reached by cert-sync over SSH. Address, SSH user and certificate paths: `TODO(spec)` OQ-3 |
|
||||
| Kvitt Pi | Runs the compose stack | Raspberry Pi 3, 1 GB RAM, 64-bit Raspberry Pi OS Lite (Debian bookworm or later), Docker Engine with the compose plugin, static LAN address. LAN address: a placeholder in the bundle's `.env`, filled by the owner (OQ-3, D-100); the Mac does not reach it over SSH |
|
||||
| Home Assistant Pi | Owns ports 80/443 and the Let's Encrypt certificate (certbot) for `icehack.asuscomm.com` | Reached by cert-sync (on the Kvitt Pi) over SSH. Address, SSH port and user and certificate directory: placeholders in the bundle's `.env` (`HA_HOST`, `HA_PORT`, `HA_USER`, `HA_CERT_DIR`), filled by the owner (OQ-3, D-100) |
|
||||
| Owner's M4 Mac | Builds images and APKs; runs the development server (D-001) | Apple Silicon, so `linux/arm64` builds run natively in Docker |
|
||||
| Router (ASUS) | `icehack.asuscomm.com` DDNS | Forwards external TCP 8443 to Kvitt Pi TCP 8443 (HUMAN step) |
|
||||
| Container registry | Holds Kvitt images | A Gitea instance on another Pi at `http://gitea.lan`: plain HTTP, reachable only on the home Wi-Fi (D-001). If Gitea requires credentials for push or pull, the owner supplies them during M2c-pi (HUMAN) and they live in `infra/secrets/` |
|
||||
| Container registry | Holds Kvitt images | A Gitea instance on another Pi at `http://gitea.lan`: plain HTTP, reachable only on the home Wi-Fi (D-001). If Gitea requires credentials for push or pull, the owner enters them where the bundle's checklist says (HUMAN, D-100); agents never hold them |
|
||||
|
||||
The Pi never builds anything. It only pulls images and runs them.
|
||||
|
||||
@@ -982,13 +984,13 @@ Used only if the certificate sync cannot be made to work. Documented in `infra/R
|
||||
### 10.6 APK hosting and release
|
||||
|
||||
* `version_code` = `git rev-list --count HEAD` on the commit being released; `version_name` = `YYYY.MM.DD-<short sha>` of that commit. Both are passed to Gradle as properties `kvitt.versionCode` and `kvitt.versionName` by the release script; debug builds use `1` and `dev`.
|
||||
* The release keystore is created once by the owner (HUMAN, §11, D-024) on the Mac, in a directory outside the repository, with exactly:
|
||||
* The release keystore is created once on the Mac (by the owner, or by an agent the owner allows; §11, D-024, D-101) at the root of the main checkout as `kvitt-release.jks`, with exactly:
|
||||
|
||||
```
|
||||
keytool -genkeypair -v -keystore kvitt-release.jks -alias kvitt -keyalg RSA -keysize 4096 -validity 10000
|
||||
```
|
||||
|
||||
The file is never inside the repository or a worktree. `android/local.properties` holds its absolute path and secrets in the keys `kvitt.release.storeFile`, `kvitt.release.storePassword`, `kvitt.release.keyAlias` (`kvitt`), `kvitt.release.keyPassword`. Losing it means the phones must uninstall and reinstall (data comes back by sync). The owner keeps a copy in their password manager. Without these keys in `local.properties`, `:app:assembleRelease` builds an unsigned APK (M8).
|
||||
The file is git-ignored by the `*.jks` pattern and never committed (D-101, revises D-024's "outside the repository"). `android/local.properties` holds its absolute path and secrets in the keys `kvitt.release.storeFile`, `kvitt.release.storePassword`, `kvitt.release.keyAlias` (`kvitt`), `kvitt.release.keyPassword`. Losing it means the phones must uninstall and reinstall (data comes back by sync). The owner keeps a copy of the file and its password in their password manager. Without these keys in `local.properties`, `:app:assembleRelease` builds an unsigned APK (M8).
|
||||
* `infra/publish-apk.sh` on the Mac: builds `:app:assembleRelease` through `tools/gradle-serial.sh`, computes SHA-256 and size, copies the APK to the Pi's `data` volume as `apk/kvitt-<version_code>.apk`, writes `apk/latest.json` atomically (write temp, rename):
|
||||
|
||||
```json
|
||||
@@ -1109,6 +1111,7 @@ Restore (documented step by step in `infra/RESTORE.md`):
|
||||
* Disk: SQLite is a few MB; pictures under 1 MB; APKs about 20 MB each, 5 kept.
|
||||
* Upgrades: build, push, `infra/deploy.sh <tag>`. Migrations run on start; a failed migration exits non-zero, the container restarts in a loop, and the previous tag is redeployed with `infra/deploy.sh <previous tag>` (migrations are forward-only, so the infra plan's runbook says to restore the pre-upgrade backup if a migration had partially applied; sqlx runs each migration in a transaction, so this should not happen).
|
||||
* Before every deploy that includes a migration, `infra/deploy.sh` triggers a backup run and waits for `last-success` to update.
|
||||
* Who runs what (D-100): every command on the Pi in this section is run by the owner from the M2c-pi bundle's scripts and README checklist; `infra/deploy.sh` here stands for the bundle's deploy script. Agents prepare and check the bundle on the Mac and never connect to the Pi.
|
||||
* Memory check: `docker stats --no-stream` after the release milestone; server RSS must be under 100 MB with both phones syncing.
|
||||
|
||||
---
|
||||
@@ -1144,9 +1147,12 @@ M7a..M7f (landed) ─► M6b core:design fixes (landed 3a1bd14) ─┬─► M8
|
||||
|
||||
After M8 (v0.13):
|
||||
M8, M7g (landed) ─► M12e contract pass (landed 1470a2f and 90505a8; D-086 to D-095, server schema 3)
|
||||
|
||||
After the first phone test (v0.15):
|
||||
M8 phone test (2026-10-18) ─► M8b UI fixes (landed f0575d3, D-097) ─► M2c-pi bundle (D-100), last, after the app is proven ─► M10
|
||||
```
|
||||
|
||||
After M1, M2a and M2b land, these run in parallel without touching each other's files (§14.7): M3, M4 (lands after M3, D-015), M5, M2c-dev, M6 (once U2 is approved and the §9.7 colour change from U2's proposal has landed in this spec), M7a to M7f (once M6 lands), M9 (once M4 lands). M2c-pi runs after M8 and before M10 (D-001). Group M12 "Category contract v2" (D-049, D-054): M12a first; then M12b and M12c in parallel; M6 and M7c wait for M12c (all three landed: `6b60ef7`, `2e2ebe7`, `e499107`). M12d (D-059) ran in parallel with M6 (landed `bdba9d3` and `3080804`); M7a to M7f ran in parallel and landed on 2026-10-18 (`b9b061a`, `31760f9`, `1e42f76`, `082b8dc`, `6ac711d`, `a54eb7c`), followed by M6b, the `:core:design` fix pass (`3a1bd14`). M8 and M7g (feature cleanups) ran in parallel (disjoint paths: `android/app/**` and `android/adapter/fcm/**` against `android/feature/**`) and landed on 2026-10-18 (`34a2590`; `369e516` and `00a14a3`). The contract pass M12e (D-086 to D-095) ran after them and landed on 2026-10-18 (`1470a2f` contract pass, `90505a8` launcher icon).
|
||||
After M1, M2a and M2b land, these run in parallel without touching each other's files (§14.7): M3, M4 (lands after M3, D-015), M5, M2c-dev, M6 (once U2 is approved and the §9.7 colour change from U2's proposal has landed in this spec), M7a to M7f (once M6 lands), M9 (once M4 lands). M2c-pi runs after M8 and before M10 (D-001), as the last step once the app is proven on the phone against the development server (D-100). Group M12 "Category contract v2" (D-049, D-054): M12a first; then M12b and M12c in parallel; M6 and M7c wait for M12c (all three landed: `6b60ef7`, `2e2ebe7`, `e499107`). M12d (D-059) ran in parallel with M6 (landed `bdba9d3` and `3080804`); M7a to M7f ran in parallel and landed on 2026-10-18 (`b9b061a`, `31760f9`, `1e42f76`, `082b8dc`, `6ac711d`, `a54eb7c`), followed by M6b, the `:core:design` fix pass (`3a1bd14`). M8 and M7g (feature cleanups) ran in parallel (disjoint paths: `android/app/**` and `android/adapter/fcm/**` against `android/feature/**`) and landed on 2026-10-18 (`34a2590`; `369e516` and `00a14a3`). The contract pass M12e (D-086 to D-095) ran after them and landed on 2026-10-18 (`1470a2f` contract pass, `90505a8` launcher icon).
|
||||
|
||||
### 11.2 Milestone table
|
||||
|
||||
@@ -1159,7 +1165,7 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
|
||||
| M2a | Server skeleton and `kvitt-domain` | Cargo workspace, three crates, config, DB pool, migration `0001_init.sql` byte-identical to `contracts/schema/server-v1.sql`, error type with every §14.3.2 code, auth extractor (§8.2), all routes of §7 registered with handlers returning 501 `not_implemented`, utoipa document equal to `contracts/openapi.yaml`, `GET /health` and `GET /setup/status` implemented, `kvitt-domain` passing all vectors plus `next_occurrence_date`, `server/Dockerfile`; shell files in M3, M4 and M9 paths (D-004); the seams of §6.3 (D-015) | Server gate (includes the OpenAPI match test, the migration identity test, the vector tests) | none | M1 |
|
||||
| M2b | Android skeleton and `:domain` | Gradle project in `android/` with every module of §6.2, version catalog, architecture check, `:core:model` types and repository interfaces of §14.2.4, `:domain` passing all vectors (including analytics), `:core:fakes` with the empty scenario (§6.2, D-018) for each repository, the Room 3 entities of §14.2.1 and §14.2.2 and `KvittDatabase` version 1 in `:data` with the exported schema (D-012), screenshot tests with Roborazzi (D-010), module shells for every module with one empty entry composable per feature (D-004), the feature convention of §6.2 in `build-logic` (D-017), `:app` launching to an empty themed screen, `:app` string files with the app name only in both languages, lint `MissingTranslation` as an error in every module | Android gate | Plug the phone in once to confirm `installDebug` works | M1 |
|
||||
| M2c-dev | Infra for development (D-001, D-022) | Landed `c885aba`. `infra/dev/compose.yaml` (`server` built from `server/Dockerfile`, no Caddy, no TLS, bind-mounted data, `infra/secrets/` mounted read-only at `/secrets`, `backup` runnable on demand), `infra/dev/.env.example`, the dev scripts in `infra/dev/` (build, up, logs, backup, `copy-google-services.sh`), `infra/README.md` (dev stack and Firebase), `infra/backup/{Dockerfile, backup.sh}` (Drive upload skipped when `RCLONE_FOLDER_ID` is empty), Firebase project and apps through the REST API (§10.7), service-account key and `google-services.json` in `infra/secrets/` | `docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example config` succeeds; `shellcheck $(find infra -name '*.sh')` clean; on the Mac (with the M2a image): `GET /health` answers 200 from the Mac; one local backup run produces a snapshot that passes `PRAGMA integrity_check` | `gcloud auth login`; on the phone on the home Wi-Fi, open the dev server's `/health` and answer yes/no that it shows `"status":"ok"` | M0 (uses the M2a image for the health check when available) |
|
||||
| M2c-pi | Infra on the Pi (D-001) | `infra/` Pi compose stack, Caddyfile, cert-sync image, build, deploy and publish scripts, `infra/README.md` (Pi provisioning, router forwarding, trust of the `gitea.lan` registry), `infra/RESTORE.md`, Drive backups to folder id `1jDp1dGE5R3H2DBcXCqIPN1iWzQnPkdlV`, Gitea Actions image build from the owner's example workflow in `.gitea/workflows/`, the `/opt/kvitt/import` bind mount (§10.2) | `docker compose -f infra/compose.yaml config` succeeds; `shellcheck $(find infra -name '*.sh')` clean; on the Pi: cert-sync fetched a valid certificate, `https://icehack.asuscomm.com:8443/health` answers from the phone on mobile data; one backup file appears in the Drive folder | Answer OQ-3; grant at the start of the sprint the allowances to act on the Kvitt Pi, the Home Assistant Pi, Gitea and Google Drive (D-023); set up key-based SSH from the Mac to the Kvitt Pi; provide the example Gitea Actions workflow and the Gitea credentials; install Docker on the Pi if missing; add `gitea.lan` to `insecure-registries` on the Pi; set the router forward 8443 → Kvitt Pi; add the cert-sync public key to the Home Assistant SSH add-on; `rclone authorize "drive"` | M2c-dev, M8 |
|
||||
| M2c-pi | Infra on the Pi (D-001, D-100) | A self-contained bundle in `infra/pi/` that the owner copies to the Kvitt Pi and runs (D-100): compose stack, Caddyfile, env template with the OQ-3 values as placeholders, cert-sync image, install, deploy, bootstrap and backup scripts, a README checklist for the owner (Pi provisioning, router forwarding, trust of the `gitea.lan` registry, the cert-sync key on the Home Assistant SSH add-on, `rclone authorize "drive"`), `infra/RESTORE.md`, Drive backups to folder id `1jDp1dGE5R3H2DBcXCqIPN1iWzQnPkdlV`, Gitea Actions image build from the owner's example workflow in `.gitea/workflows/`, the `/opt/kvitt/import` bind mount (§10.2) | On the Mac: the bundle's compose file passes `docker compose config` with the env template; `shellcheck $(find infra -name '*.sh')` clean. On the Pi, run by the owner from the checklist: cert-sync fetched a valid certificate, `https://icehack.asuscomm.com:8443/health` answers from the phone on mobile data; one backup file appears in the Drive folder | Copy the bundle to the Kvitt Pi and fill the placeholders in its `.env` (OQ-3); run the README checklist: install Docker if missing, add `gitea.lan` to `insecure-registries`, set the router forward 8443 → Kvitt Pi, add the cert-sync public key to the Home Assistant SSH add-on, `rclone authorize "drive"`, provide the example Gitea Actions workflow and the Gitea credentials; answer yes/no per check. No allowances for agents on the Pis, Home Assistant, Gitea or Drive (D-100) | M2c-dev, M8; runs last, after the app is proven (D-100) |
|
||||
| M3 | Server core API | Landed `503cee5` (D-030 to D-034). E-3 to E-30 and E-33 implemented (§14.3; E-2 is M2a's, M3 tests it), invite landing page, pictures, rate limiting (`src/api/rate_limit.rs`), `bootstrap`, `pair-device`, the recurring generation function of §6.3 (D-015) | Server gate; integration tests per endpoint and per error code listed for it; every generation case of §9.5 (catch-up, clamping, split resolution, idempotency, resume without backfill, `next_occurrence_date`) | none | M2a |
|
||||
| M4 | Server sync, audit, push, scheduler | Landed `bd6f10a` (D-035 to D-039). E-31, E-32, the real `AuditWriter` (writer and diff) and `PushQueue` (FCM sender), `src/tasks/**` starting them, `sync_mutations` pruning daily at 04:00 (D-021), scheduler, `rotate-epoch` | Server gate; integration tests for every sync rule of §14.4 and the scheduler cases of §9.5 (run at start, daily 06:00 on DST days, catch-up through M3's generation function); FCM sender tested against a local stub HTTP server | none | M2a; lands after M3 (D-015) |
|
||||
| M5 | Android data layer | Landed `c28c52b` (D-026 to D-029, D-040 to D-043). DAOs on M2b's Room 3 entities and `KvittDatabase` v1 (§14.2, D-012; any change to the entities or the exported schema is a contract change, §14.8), Ktor client and DTOs, token store, sync engine and workers, real repositories, APK updater | Android gate; DTO round-trip tests over every file in `contracts/examples/`; sync engine tests with Ktor `MockEngine` covering §14.4 including the 14-day offline case (AC-15.6) | none | M1, M2b |
|
||||
@@ -1176,10 +1182,10 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
|
||||
| M7e | Feature: analytics | Landed `6ac711d` 2026-10-18 (D-069 to D-071). `:feature:analytics` screens S-11, S-12 | as M7a | none | M6, M12d |
|
||||
| M7f | Feature: settings | Landed `a54eb7c` 2026-10-18 (D-071 to D-073). `:feature:settings` screens S-13, S-14, S-15, S-19, S-20, S-21, S-22 | as M7a | none | M6, M12d |
|
||||
| M7g | Feature cleanups | Landed `34a2590` 2026-10-18. Cleanups in `android/feature/**` after M6b; scope in its brief | as M7a | none | M7a to M7f, M6b |
|
||||
| M8 | App integration | Landed `369e516` and `00a14a3` 2026-10-18 (D-081 to D-085). `:app` navigation and deep links (§13.2, D-071; each top-level screen draws its own bar), session gate (S-1 for Unpaired and Unrecognised, S-2, S-18 for Revoked, back on S-18 closes the app; Home shows S-3 while `partner == null`), deletion of the debug fakes module (both variants bind `:data`, D-081), `:adapter:fcm` with registration by Firebase Installation ID (D-083), push work and notification building (AC-13.5, D-085), invite links ignored while paired (D-084), `ProvideHousehold` as `LocalKvittHousehold`, `LocalKvittClock` from `:data`'s Clock, `LocalAvatarPictures` handing the `User` to `:data`'s Coil mapper, notification permission after the first successful sync after pairing (AC-2.9), update check in release only, revoke wipe, network security configs (§8.5, D-082), manifest hardening (§8.8), release signing and version properties (§10.6; unsigned without keystore keys), `google-services.json` copied with `infra/dev/copy-google-services.sh` | Android gate (green at landing: BUILD SUCCESSFUL, 710 actionable tasks); on the owner's phone against the development server on the Mac (D-001): pair, add an expense offline, reconnect, see it synced, receive a push (not yet run at landing: phone not attached; HANDOFF §3) | Tap through the pairing, offline and push test as relayed by the orchestrator | M3, M4, M5, M7a-M7f, M2c-dev (Firebase, development server) |
|
||||
| M8 | App integration | Landed `369e516` and `00a14a3` 2026-10-18 (D-081 to D-085). `:app` navigation and deep links (§13.2, D-071; each top-level screen draws its own bar), session gate (S-1 for Unpaired and Unrecognised, S-2, S-18 for Revoked, back on S-18 closes the app; Home shows S-3 while `partner == null`), deletion of the debug fakes module (both variants bind `:data`, D-081), `:adapter:fcm` with registration by Firebase Installation ID (D-083), push work and notification building (AC-13.5, D-085), invite links ignored while paired (D-084), `ProvideHousehold` as `LocalKvittHousehold`, `LocalKvittClock` from `:data`'s Clock, `LocalAvatarPictures` handing the `User` to `:data`'s Coil mapper, notification permission after the first successful sync after pairing (AC-2.9), update check in release only, revoke wipe, network security configs (§8.5, D-082), manifest hardening (§8.8), release signing and version properties (§10.6; unsigned without keystore keys), `google-services.json` copied with `infra/dev/copy-google-services.sh` | Android gate (green at landing: BUILD SUCCESSFUL, 710 actionable tasks); on the owner's phone against the development server on the Mac (D-001): pair, add an expense offline, reconnect, see it synced, receive a push (run on 2026-10-18 on the owner's S26 against the development server: pairing with the bootstrap code OK; a scripted partner joined through a second admin device paired with `kvitt-server pair-device`; Home showed the account; the offline expense showed the pending marker and synced after reconnect, pending 0, row on the server; the device registered `fcm_token` and `app_version_name`; a push by `fid` arrived and its tap worked). Follow-up M8b (landed `f0575d3`, D-097) fixed what the test showed: the invite-code field, the quick-row badge, the status-bar inset of `KvittTopAppBar`, the chart corners; `FcmMessageBody.fid` in `:data` followed (`09f474f`, D-096) | Tap through the pairing, offline and push test as relayed by the orchestrator | M3, M4, M5, M7a-M7f, M2c-dev (Firebase, development server) |
|
||||
| M12e | Contract pass | Landed `1470a2f` and `90505a8` 2026-10-18 (D-086 to D-095). The §11.3 contract requests of M5 to M8 taken together on both sides: E-3 `invite_kind` and `Pairing` (D-086); the admin's name in E-12 links, passed on by E-4 (D-087); `SyncSummary.lastAttemptFailed` (D-088); `Rejection.actor` (D-089); `X-Kvitt-App-Version-Name`, `devices.app_version_name` by migration 0003, server schema 3 (D-090); `NewRecurringTemplate` (D-091); `UpdateCheck` (D-092); `SettleState.untouched` (D-093); the D-027 re-stamp in `:data` (D-094); `PushEvent` in `:core:model`, snackbar hosts in six screens, snackbar resource ids from S-7 and S-8, the adaptive launcher icon (D-095). Goldens re-recorded: S-20 devices (version name); new `s1_pairing_link_admin` and `s22_about_check_failed` | Both gates (green at landing: Android BUILD SUCCESSFUL, 710 actionable tasks, 906 tests; server 253 tests, the OpenAPI equality, vector and migration byte-identity tests included) | none (the development server must be rebuilt before the new APK is installed, §14.8) | M8, M7g, SPEC v0.12 |
|
||||
| M9 | Splitwise importer | Landed `ab298ab` (D-044 to D-046). `kvitt-import` per §10.8 with tests over synthetic CSV fixtures (`crates/kvitt-import/tests/import.rs`), reconciliation against the totals rows, the committed category table | Server gate | Provide the CSV files (kept outside git) when OQ-2 is answered | M2a, M4 |
|
||||
| M10 | First release | Release build signed, published, both phones paired, push working both ways, nightly backup on Drive, restore drill report, memory check | `docs/validation/first-release.md` and `docs/validation/restore-drill.md` complete; server RSS under 100 MB | Create the release keystore with the `keytool` command of §10.6, outside the repository; install the APK on both phones from the landing page; allow "Install unknown apps"; partner pairs with the invite; confirm notifications arrive | M2c-pi, M3, M4, M8 |
|
||||
| M10 | First release | Release build signed, published, both phones paired, push working both ways, nightly backup on Drive, restore drill report, memory check | `docs/validation/first-release.md` and `docs/validation/restore-drill.md` complete; server RSS under 100 MB | Create the release keystore with the `keytool` command of §10.6 at the repository root, or allow an agent to (D-101), and keep a copy and the password in the password manager; run the Pi steps from the M2c-pi bundle (D-100); install the APK on both phones from the landing page; allow "Install unknown apps"; partner pairs with the invite; confirm notifications arrive | M2c-pi, M3, M4, M8 |
|
||||
| M11 | Import history | Rehearsed on 2026-10-18: the real run reconciled on both files in a throwaway database (Vardag 1746 rows, Hemma 37 rows; 21 rows unmapped before D-044). Dry run of 2026-10-18 with the M12b mapping (D-053): Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped; both reconciled. Real import run on the Pi per §10.8 "Production run" (D-025), `docs/validation/splitwise-import.md` with per-account totals compared to Splitwise's balances (aggregates only) | Imported balances equal Splitwise's "Total balance" rows per group | Provide the CSVs; confirm the balances on the phone match Splitwise | M9, M10 |
|
||||
|
||||
### 11.3 Notes per milestone
|
||||
@@ -1190,20 +1196,20 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
|
||||
* **M5** owns `:data` and builds on the entities, `KvittDatabase` v1 and the exported schema `android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json` that M2b created (D-012); it adds DAOs, the API client, sync and the repositories. Room 3 migrations take an `SQLiteConnection` (§14.2.3).
|
||||
* **M7x** features use `:core:fakes` only; they never depend on `:data`. Each feature agent adds its fake scenarios in `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/<feature>/` and nowhere else in that module. Each fills in its own module shell from M2b, follows the feature convention of §6.2 and takes shared UI from `:core:design` (D-019). All six landed on 2026-10-18 (`b9b061a` M7a, `6ac711d` M7e, `082b8dc` M7d, `31760f9` M7b, `1e42f76` M7c, `a54eb7c` M7f); their readings are D-069 to D-074 and D-080, the shared fixes they asked for went into M6b (`3a1bd14`, D-075 to D-079). M7g (landed `34a2590`) did the feature-side cleanups after M6b.
|
||||
* **M6b** (landed `3a1bd14`) is the `:core:design` fix pass after the M7 wave, in M6's paths (§14.7); see the §11.2 row.
|
||||
* **M12e** (landed `1470a2f` and `90505a8` on 2026-10-18, D-086 to D-095) took the contract requests that M5, M6, M7 and M8 had collected here, as one contract change on both sides (§14.8): the invite kind from `pair()` (D-086), the admin's name in invite links (D-087), `SyncSummary.lastAttemptFailed` (D-088), `Rejection.actor` (D-089), the device version name with server schema 3 (D-090), `NewRecurringTemplate` without an id (D-091), `UpdateCheck` (D-092), `SettleState.untouched` (D-093), the D-027 re-stamp in `:data` (D-094), and `PushEvent` in `:core:model`, the six screen-hosted snackbar hosts, snackbar resource ids from S-7 and S-8 and the launcher icon (D-095). The 128 px picture size was left unchanged by assumption: `:data` keeps fetching E-11 at 512 and no size parameter was added, since v1 does not need one (the owner's yes/no stays open, HANDOFF §3). Room stays at schema 1 (the phone stores no devices). A phone running the M12e APK needs a server at schema 3 (§14.8 "Compatibility").
|
||||
* **M12e** (landed `1470a2f` and `90505a8` on 2026-10-18, D-086 to D-095) took the contract requests that M5, M6, M7 and M8 had collected here, as one contract change on both sides (§14.8): the invite kind from `pair()` (D-086), the admin's name in invite links (D-087), `SyncSummary.lastAttemptFailed` (D-088), `Rejection.actor` (D-089), the device version name with server schema 3 (D-090), `NewRecurringTemplate` without an id (D-091), `UpdateCheck` (D-092), `SettleState.untouched` (D-093), the D-027 re-stamp in `:data` (D-094), and `PushEvent` in `:core:model`, the six screen-hosted snackbar hosts, snackbar resource ids from S-7 and S-8 and the launcher icon (D-095). The 128 px picture size was left unchanged by assumption: `:data` keeps fetching E-11 at 512 and no size parameter was added, since v1 does not need one (the owner confirmed on 2026-10-18 that it is not needed: single 512 size, §14.2.4). Room stays at schema 1 (the phone stores no devices). A phone running the M12e APK needs a server at schema 3 (§14.8 "Compatibility").
|
||||
* **Remaining contract requests** (none is a contract change in v0.14; it needs the §14.8 rule when taken):
|
||||
* S-22's "Licenser för öppen källkod" does nothing: no licences screen and no strings for one exist (S-22 and the §6.2 strings rule); open point OQ-5 for the owner, default "not in v1".
|
||||
* None open. S-22's "Licenser för öppen källkod" did nothing (no licences screen and no strings for one); the owner answered OQ-5 on 2026-10-18: no licences screen in v1, the button is hidden (implemented by M8c).
|
||||
|
||||
The `fid` request was taken on 2026-10-18 (`c694f78`, D-096): the server sends the installation id in FCM HTTP v1 `message.fid` (§14.6).
|
||||
* **M2c-dev / M2c-pi** (D-001): M2c-dev builds only what development needs on the Mac and runs now; M2c-pi builds everything that concerns the Pi (registry trust, TLS, router, cert-sync, Pi deploy, APK publishing to the Pi, Drive upload, Gitea Actions) and runs after M8, before M10.
|
||||
* **M2c-dev / M2c-pi** (D-001, D-100): M2c-dev builds only what development needs on the Mac (landed); M2c-pi builds everything that concerns the Pi (registry trust, TLS, router, cert-sync, Pi deploy, APK publishing to the Pi, Drive upload, Gitea Actions) as the `infra/pi/` bundle the owner runs, and runs last, after M8 and the phone test, before M10.
|
||||
* **M4** (landed `bd6f10a`) added `into_make_service_with_connect_info::<SocketAddr>()` to `serve` so the per-IP rate limits of §8.6 see the peer address (D-032). Its tests are three binaries, `tests/sync_api/`, `tests/push_fcm/` and `tests/scheduler_daily/` (§14.7); the scheduler lives in `src/tasks/scheduler.rs`.
|
||||
* **M8** (landed `369e516` and `00a14a3`, D-081 to D-085) is the only plan that edits `:app` after M2b. As built: both variants bind `:data` through `di/AppDataModule.kt`, which includes `DataBindingsModule`; the debug fakes module is deleted (D-081, revising D-041). The session gate shows S-1 for Unpaired and Unrecognised, S-2, and S-18 for Revoked (back on S-18 closes the app); Home shows S-3 while `partner == null`; each top-level screen draws its own bar (D-071). M6's `:app` requests are met: `ProvideHousehold` is implemented as `LocalKvittHousehold`, `LocalKvittClock` is provided from `:data`'s Clock, and `LocalAvatarPictures` passes the `User` model to `:data`'s Coil mapper. M5's requests are met: `AppBuildInfo` from `BuildConfig`; WorkManager with `KvittWorkerFactory` and the default initializer disabled; `SyncTriggers.onAppStart` on process start and `onForeground` on later returns; push handled by an expedited WorkManager job calling `syncForPush` (D-085); `UpdateRepository.check` on every start and resume, in release builds only (AC-16.5); Coil's singleton is `:data`'s `ImageLoader`. The notification permission is asked after the first successful sync after pairing (AC-2.9). Network security configs per §8.5 (D-082); the merged release manifest exports only `MainActivity` and permission-guarded library receivers (§8.8); `:app:assembleRelease` builds unsigned when `local.properties` has no keystore keys (§10.6); FCM registration by Firebase Installation ID (D-083); invite links ignored while paired (D-084). Not yet done at landing (phone not attached): the end-to-end pairing, offline and push test on the owner's phone and the design-spec §2.7 typeface check (HANDOFF §3). Its contract requests were taken by M12e (D-086 to D-095); the `fid` field followed in `c694f78` (D-096).
|
||||
* **M10**: the orchestrator relays every HUMAN step in chat. The keystore is created with the command of §10.6 (D-024).
|
||||
* **M2c-pi**: the allowances to act on the Kvitt Pi, the Home Assistant Pi, Gitea and Google Drive are requested from the owner at the start of that sprint (D-023).
|
||||
* **M8** (landed `369e516` and `00a14a3`, D-081 to D-085) is the only plan that edits `:app` after M2b. As built: both variants bind `:data` through `di/AppDataModule.kt`, which includes `DataBindingsModule`; the debug fakes module is deleted (D-081, revising D-041). The session gate shows S-1 for Unpaired and Unrecognised, S-2, and S-18 for Revoked (back on S-18 closes the app); Home shows S-3 while `partner == null`; each top-level screen draws its own bar (D-071). M6's `:app` requests are met: `ProvideHousehold` is implemented as `LocalKvittHousehold`, `LocalKvittClock` is provided from `:data`'s Clock, and `LocalAvatarPictures` passes the `User` model to `:data`'s Coil mapper. M5's requests are met: `AppBuildInfo` from `BuildConfig`; WorkManager with `KvittWorkerFactory` and the default initializer disabled; `SyncTriggers.onAppStart` on process start and `onForeground` on later returns; push handled by an expedited WorkManager job calling `syncForPush` (D-085); `UpdateRepository.check` on every start and resume, in release builds only (AC-16.5); Coil's singleton is `:data`'s `ImageLoader`. The notification permission is asked after the first successful sync after pairing (AC-2.9). Network security configs per §8.5 (D-082); the merged release manifest exports only `MainActivity` and permission-guarded library receivers (§8.8); `:app:assembleRelease` builds unsigned when `local.properties` has no keystore keys (§10.6); FCM registration by Firebase Installation ID (D-083); invite links ignored while paired (D-084). The end-to-end test ran on 2026-10-18 on the owner's S26 against the development server: 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; an expense added offline 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. **M8b** (landed `f0575d3`) fixed what the owner saw: the invite-code field holds the raw code and draws the hyphens as a `VisualTransformation` with an exact offset mapping (cursor bug fixed, unit test `InviteCodeFieldTest`); the quick-row and grid selected badge is drawn above the ripple clip (it was cut off); `KvittTopAppBar` pads for the status-bar inset through a `windowInsets` parameter and `KvittScaffold` follows; chart bar corners top 4, bottom 2 dp (D-097); 221 goldens re-recorded. `FcmMessageBody.fid` in `:data` followed in `09f474f` (D-096). Still unverified on the phone: the status-bar fix (next install) and the design-spec §2.7 typeface check (the owner looked and had no complaint). Its contract requests were taken by M12e (D-086 to D-095); the `fid` field followed in `c694f78` (D-096).
|
||||
* **M10**: the orchestrator relays every HUMAN step in chat. The keystore is created with the command of §10.6 at the repository root and referenced from `android/local.properties` (D-024, D-101).
|
||||
* **M2c-pi** (D-100, revises D-023): no SSH from the Mac to the Pis and no allowances for agents on the Kvitt Pi, the Home Assistant Pi, Gitea or Google Drive. The agent writes and checks the `infra/pi/` bundle on the Mac; the owner copies it to the Kvitt Pi, fills the `.env` placeholders (OQ-3) and runs the README checklist, answering yes/no per check. It runs as the last step before M10, after the app is proven on the phone.
|
||||
* **M9** (landed `ab298ab`) reuses the server's own push validation (`kvitt_server::api::validate`, `rows`) and the `SqlAuditWriter`, so imported rows pass the same checks as synced ones (§14.4.4). Its tests are one binary, `crates/kvitt-import/tests/import.rs`, over synthetic fixtures only; one test checks the §10.8 category table against `data/splitwise_categories.csv`.
|
||||
* **M12a, M12b, M12c** (D-049 to D-054) carry the owner's UX-session decisions into the contracts and the landed code: M3, M5 and M9 were built with ten defaults, so this is a contract change against `main`, not only the spec. M12a (landed `6b60ef7`) wrote `contracts/`, the checker and `:domain`'s `suggestCategory`; M12b (landed `2e2ebe7`) and M12c (landed `e499107`) ran in parallel (disjoint paths, §14.7); their outcomes are D-055 to D-058. The Room schema stays at version 1: category rows reach the phone through sync. The colour hex values of `slate` and `amber` are not needed by M12 (`:core:model` holds token names only); they arrive with U2 and M6.
|
||||
* **M12d** (D-059, landed `bdba9d3`) added the two UI-contract members UX-9 found (`ExpenseEditorOptions.frequentCategoryIds`, `CategoryRepository.deletable`) to `:core:model`, `:data` and `:core:fakes`, in parallel with M6 (disjoint paths); its readings are D-060 to D-062 (§14.2.4 "Readings fixed in v0.10"). M7c and M7f consume them; the design spec's fallbacks (`docs/ux/design-spec.md` §3.7 and §6) are no longer needed.
|
||||
* **M6** (landed `3080804`) built `:core:design` with charts on Compose `Canvas` (D-063), the Material 3 Expressive fallbacks of D-064, the formatting tables of D-065 and the golden layout of D-066. Its contract requests for **M8** (no change now): `:app` provides `LocalAvatarPictures` with an `AvatarPictures` that builds the authenticated E-11 Coil request from the user id and `pictureVersion` (§14.3.4); `:app` wraps every screen in `ProvideHousehold(me, partner)`; `:app` provides `LocalKvittClock` (tests and previews pass a fixed clock); `SyncSummary` gains a "last attempt failed" field so `SyncStatusPill` can drop its `failed` parameter (§13.3 S-4); done by M12e: `SyncSummary.lastAttemptFailed`, and `SyncStatusPill`, `syncPillVariant` and `syncLineText` no longer take `failed` (D-088). The design-spec §2.7 open point (that `FontFamily.Default` keeps tabular figures and distinct weights 500/600/700 under every One UI font style on the S26) is checked at the M8 install on the owner's phone; still pending after M8 landed (phone not attached).
|
||||
* **M6** (landed `3080804`) built `:core:design` with charts on Compose `Canvas` (D-063), the Material 3 Expressive fallbacks of D-064, the formatting tables of D-065 and the golden layout of D-066. Its contract requests for **M8** (no change now): `:app` provides `LocalAvatarPictures` with an `AvatarPictures` that builds the authenticated E-11 Coil request from the user id and `pictureVersion` (§14.3.4); `:app` wraps every screen in `ProvideHousehold(me, partner)`; `:app` provides `LocalKvittClock` (tests and previews pass a fixed clock); `SyncSummary` gains a "last attempt failed" field so `SyncStatusPill` can drop its `failed` parameter (§13.3 S-4); done by M12e: `SyncSummary.lastAttemptFailed`, and `SyncStatusPill`, `syncPillVariant` and `syncLineText` no longer take `failed` (D-088). The design-spec §2.7 open point (that `FontFamily.Default` keeps tabular figures and distinct weights 500/600/700 under every One UI font style on the S26) is checked at the M8 install on the owner's phone; on 2026-10-18 the owner looked and had no complaint (not checked under every One UI font style).
|
||||
* **M11** runs the import per §10.8 "Production run" (D-025). The rehearsal on 2026-10-18 ran both real files against a throwaway database and reconciled on both (Vardag 1746 rows, Hemma 37 rows); 21 rows were unmapped before D-044. The dry run of 2026-10-18 with M12b's mapping confirmed it: Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped; both reconciled.
|
||||
|
||||
---
|
||||
@@ -1216,9 +1222,9 @@ Only things that cannot be decided without the owner. Everything else in this sp
|
||||
|---|---|---|---|
|
||||
| OQ-1 | Answered (D-001): the registry is a Gitea instance on another Pi at `http://gitea.lan`, plain HTTP, reachable only on the home Wi-Fi; images are built by Gitea Actions later from the owner's example workflow. | M2c-pi | answered |
|
||||
| OQ-2 | Answered (D-003): two exports, English and Swedish header variants, member columns in either order, ISO dates, point decimal, double-quoted fields, trailing empty line and `Total balance` / `Totalsumma` row, payment category `Payment` / `Betalning`, all rows SEK. Group "hemma" becomes account "Hemma", the general group becomes account "Vardag". Details §10.8 and PLAN-M9. | M9, M11 | answered |
|
||||
| OQ-3 | `TODO(spec)` LAN address and SSH user of the Kvitt Pi; LAN address, SSH port and user of the Home Assistant Pi; the directory holding `fullchain.pem` and `privkey.pem` there (expected `/ssl` on Home Assistant OS); which SSH add-on is used. | M2c-pi | none; M2c-pi blocks on it |
|
||||
| OQ-3 | Answered by placeholders (D-100): the LAN address of the Kvitt Pi; LAN address, SSH port and user of the Home Assistant Pi; the directory holding `fullchain.pem` and `privkey.pem` there (expected `/ssl` on Home Assistant OS); which SSH add-on is used. The owner fills them in the M2c-pi bundle's `.env` on the Pi; no agent needs them. | M2c-pi | answered |
|
||||
| OQ-4 | Answered: the Google Drive backup folder id is `1jDp1dGE5R3H2DBcXCqIPN1iWzQnPkdlV` (§10.9). | M2c-pi | answered |
|
||||
| OQ-5 | Open-source licences on S-22: is a licences screen wanted in v1? "Licenser för öppen källkod" currently does nothing, and no screen or strings exist for it (§11.3 remaining contract requests). | M10 | not in v1 |
|
||||
| OQ-5 | Answered 2026-10-18: no licences screen in v1; S-22's "Licenser för öppen källkod" is hidden (implemented by M8c). | M10 | answered |
|
||||
|
||||
---
|
||||
|
||||
@@ -1319,7 +1325,7 @@ Suggested starting points the UX agent may use, replace or merge (it must delive
|
||||
2. **"Skärgård"**: Material 3 Expressive with a cool sea-and-granite palette, large rounded shapes, bold balance header, playful but quiet.
|
||||
3. **"Kontor"**: dense, utilitarian, data-forward. Neutral greys, strong typographic hierarchy, charts as the hero on Analytics, minimal decoration.
|
||||
|
||||
**Phase 2 (U2): design spec for the chosen direction.** After the owner picks (and comments), in `docs/ux/`. The owner picked direction 3 "Kalas" with the muted palette of `docs/ux/directions/3-kalas/`, typeface per D-020 (D-047). U2 landed `16c4d01`, approved by the owner. `docs/ux/design-spec.md` is the binding design document for `:core:design` (§2 tokens, §3 components) and for every screen (§4, §5); this spec wins over it on rules. `docs/ux/strings.csv` (558 rows at U2, header included; 559 since v0.10 with `ledger_settlement_label`, D-068, and English "Dining" for `category_default_restaurants`, D-067; 578 since v0.11 with the shared keys `common_ok`, `common_undo` and `editor_split_full_on_you` and the module keys of M7b to M7f recorded as built, D-079; keys with the D-017 prefixes; plurals as `key:one` / `key:other`) is the source of every string resource:
|
||||
**Phase 2 (U2): design spec for the chosen direction.** After the owner picks (and comments), in `docs/ux/`. The owner picked direction 3 "Kalas" with the muted palette of `docs/ux/directions/3-kalas/`, typeface per D-020 (D-047). U2 landed `16c4d01`, approved by the owner. `docs/ux/design-spec.md` is the binding design document for `:core:design` (§2 tokens, §3 components) and for every screen (§4, §5); this spec wins over it on rules. Amended after U2: chart bars have top corners 4 dp and bottom corners 2 dp (D-097, design-spec §2.9 and §3.10). `docs/ux/strings.csv` (558 rows at U2, header included; 559 since v0.10 with `ledger_settlement_label`, D-068, and English "Dining" for `category_default_restaurants`, D-067; 578 since v0.11 with the shared keys `common_ok`, `common_undo` and `editor_split_full_on_you` and the module keys of M7b to M7f recorded as built, D-079; keys with the D-017 prefixes; plurals as `key:one` / `key:other`) is the source of every string resource:
|
||||
|
||||
* `design-spec.md`: final tokens (colour roles light and dark, category colour hex values, typography scale, shapes, spacing scale in dp, elevation, motion durations and easing), component inventory (each with anatomy, states, sizes), and for every screen S-1 to S-22: layout description, all states of §13.3, interaction details, TalkBack labels.
|
||||
* `strings.csv`: every user-visible string of the app with columns `key,en,sv,notes`, keys in `snake_case` prefixed by the feature module that holds them (`home_balance_owes_you`, `expense_save`; shared strings for `:core:design` never start with a feature name; the app name is the only `:app` string; §6.2, D-017), placeholders as Android format arguments (`%1$s`), plurals marked in `notes` with their quantities. This becomes `values/strings.xml` and `values-sv/strings.xml` in M6 and M7; feature agents add no strings that are not in this file without a decisions entry. Keys recorded by D-079 (v0.11): shared `common_ok`, `common_undo`, `editor_split_full_on_you` ("All on you" / "Allt på dig"; `ledger_settlement_label` was D-068); module keys as built: `analytics_granularity`, `analytics_measure`, `analytics_group_by`, `analytics_range_apply`; `settle_row_amount`, `settle_detail_delete`, `settle_detail_sync_now`; `expense_detail_sync_now`; `home_archived_expanded`, `home_archived_collapsed`; `settings_category_archived_snackbar`, `settings_category_unarchived_snackbar`, `settings_recurring_summary_none`, `settings_expanded`, `settings_collapsed`, `settings_partner_missing`.
|
||||
@@ -1983,7 +1989,7 @@ Readings fixed in v0.5 (no type changes):
|
||||
|
||||
* **`OnlineResult.Failed("network", null)`.** A REST call that fails on the network (no answer from the server) returns code `network`, the one token outside §14.3.2, so the UI can show "Could not reach the server" (§9.9) rather than the `Offline` state, which means the call was not attempted (D-029).
|
||||
* **No "update required" state.** `HomeState` has no field for a 426. After a 426 `protocol_unsupported` the phone stops syncing until the installed version code changes (§14.3.1, D-043); the update banner shows only when `UpdateState.Available` reports a newer published APK. Accepted.
|
||||
* **Pictures.** `:data` always requests E-11 with size 512; the 128 variant is unused by `:data` so far (M6 may ask for a size parameter).
|
||||
* **Pictures.** `:data` always requests E-11 with size 512 and no size parameter is added to the UI contract: the owner confirmed on 2026-10-18 that the 128 size is not needed on the phone (the server keeps storing both, AC-4.4).
|
||||
* **Choices documented in `:data` and accepted:** `Rejection.summary` (the rejected entity's own text) in `android/data/src/main/kotlin/se/hagstedt/kvitt/data/repo/SettingsRepositories.kt`; `FieldChange` display of `splits`, `allocations` and default category names in `.../data/repo/Timeline.kt`; `RecurringTemplate.draft.date` in `.../data/repo/SettingsRepositories.kt` (template mapping).
|
||||
|
||||
### 14.3 OpenAPI 3.1 outline
|
||||
@@ -2737,3 +2743,4 @@ Rule: a change to any other contract file, or a semantic change to `contracts/op
|
||||
| 0.12 | 2026-10-18 | Outcome of M8 app integration (`369e516` and `00a14a3`, gate green: BUILD SUCCESSFUL, 710 actionable tasks). D-081: both variants bind `:data` through `di/AppDataModule.kt` in `src/main` (includes `DataBindingsModule`); `DebugFakesModule` deleted; fakes only in tests and feature previews; `:app` tests on a plain `Application`; revises D-041 (§6.2, §11.3). D-082: a network security config cannot express IP ranges, so the debug config allows cleartext everywhere and `SessionRepositoryImpl.normaliseServerUrl` enforces the AC-2.8 limit; release config in `src/main`, debug override in `src/debug`, pinned-root config in `src/releasePinnedRoot/res` behind `kvitt.tls.pinnedRoot`; the debug `InsecureBaseConfiguration` lint warning is intended (§8.5, §10.5.3, AC-2.8). D-083: FCM registration by Firebase Installation ID (firebase-messaging 26.0.0, BoM 35.0.2, google-services 4.5.0): `register()` while paired and at each start, `onRegistered(fid)` sends `fcm_token` through E-17, `unregister()` on revoke, `firebase_messaging_installation_id_enabled` meta-data; no server change, `fid` field as a follow-up request (AC-13.7, §6.2, §14.6). D-084: an invite link opened while paired is ignored, never re-pairs (AC-2.6). D-085: a tap on the generic notification, a deletion or `sync_batch` opens Home; unknown type or `v` ≠ 1 only syncs; channel `activity`, notification id = hash of `entity_id`, `sync_batch` id 1; push handled by an expedited WorkManager job calling `syncForPush` (20 s) then building the text from Room (AC-13.6, §14.6). Without a decision id: the session gate (S-1, S-2, S-18; S-3 while `partner == null`) in §13.2; M8's `:app` wiring facts (`LocalKvittHousehold`, `LocalKvittClock`, `LocalAvatarPictures`, permission after the first sync, sync and update triggers, unsigned release without keys, exported components) in §11.2, §11.3, §8.8, §10.6, AC-2.9; M8's contract requests on the §11.3 M12e list (`PushEvent` to `:core:model`, S-22 licences, detail-screen snackbar hosts, S-7/S-8 snackbar strings, launcher icon, `fid`); new OQ-5 (licences screen, default not in v1). §11: M8 and M7g (`34a2590`) landed; M12e started 2026-10-18. The end-to-end pairing, offline and push test and the design-spec §2.7 typeface check on the phone are still to run. |
|
||||
| 0.13 | 2026-10-18 | Outcome of the M12e contract pass (`1470a2f` contract pass, `90505a8` launcher icon; gates green: Android BUILD SUCCESSFUL, 710 actionable tasks, 906 tests; server 253 tests with the OpenAPI equality, vector and migration byte-identity tests). D-086: E-3 returns `invite_kind`; `SessionRepository.pair` returns `OnlineResult<Pairing(user, inviteKind)>`, an unknown kind reads as `device`; S-2 follows only a `partner` pairing; `FakeScenario.pairingInviteKind` (AC-2.4, AC-2.10, §13.2, §13.3 S-2, §14.2.4, E-3). D-087: E-12 link `<public_url>/i/<code>?from=<admin name>`; E-4 takes optional `from` and passes it only into `kvitt://invite?server&code&from`, never shown, an invalid value dropped; `InviteLink`, `AppLink.Invite`, `PairingScreen(initialAdminName)`, `PairingViewModel.prefill` carry it; S-1 shows `onboarding_code_help` with the name; links without a name still parse (AC-2.5, AC-2.6, §13.3 S-1, E-4, E-12). D-088: `SyncSummary.lastAttemptFailed`; `SyncStatusPill`, `syncPillVariant`, `syncLineText` drop `failed`; S-16 failed = online and `lastAttemptFailed` (§13.3 S-4, S-16, §14.2.4, §11.3). D-089: `Rejection.actor` from the stored `current.updated_by_user_id`, partner as fallback; no protocol change (§14.2.4, §14.4.6). D-090: optional request header `X-Kvitt-App-Version-Name` on every operation, stored at E-3 and the hourly last-seen update, blank or over 32 characters ignored; migration 0003 adds `devices.app_version_name`, server schema 3 (new §14.1.2, contract copy with byte-identity test, v2→v3 migration test); `app_version_name` on the Device wire object; `Device.appVersionName`; S-20 shows "Kvitt 1.1"; importer expects schema 3; Room unchanged at 1 (§8.2, §10, §10.8, §14.1, §14.3.1, §14.3.3, E-1, E-3, AC-3.1, §13.3 S-20). D-091: `RecurringRepository.create(NewRecurringTemplate)`, the server mints the id (§13.3 S-19, §14.2.4, E-28). D-092: `UpdateRepository.check(): UpdateCheck` (`UpToDate`, `Available`, `Failed`), 404 is up to date, debug reports `UpToDate`; S-22 up-to-date or could-not-reach-server; revises D-073 (AC-16.6, §13.3 S-22, §14.2.4). D-093: `SettleState.untouched` with nets excluding the edited settlement, empty per account; revises D-074's S-9 reading (§13.3 S-9, §14.2.4). D-094: the D-027 re-stamp as implemented: at the start of a cycle with `clock_skew` set, pending rows stamped later than now are re-stamped to now (§14.4.5). D-095: `PushEvent` in `:core:model`; six screens host the shared snackbar host, no `:app` overlay host; snackbar resource ids from S-7 and S-8; adaptive vector launcher icon (`#9C5238`, `receipt_long` at 48 dp, monochrome layer); revises D-071 (§6.2, §13.2, §13.3 S-7, S-8, §14.2.4, §14.6). Without a decision id: the 128 px picture size left unchanged by assumption (owner question open); the M12e list in §11.3 replaced by its outcome and a "Remaining contract requests" list (OQ-5 licences, `fid`); goldens re-recorded (S-20 devices; new `s1_pairing_link_admin`, `s22_about_check_failed`); compatibility note: the M12e APK needs a schema-3 server, so the server is redeployed first (§14.8); dev stack at schema 3, `up.sh --down` then `up.sh` on Podman (§10). §11: M12e landed (§11.1, §11.2). |
|
||||
| 0.14 | 2026-10-18 | Follow-ups after M12e (`2b4e21c`, `c694f78`, `1e404cc`). D-096: the server sends the stored `fcm_token` (the Firebase Installation ID of D-083) in FCM HTTP v1 `message.fid`, no longer in the deprecated `message.token`; no database or wire change, the column and the E-17 field keep the name `fcm_token`; `contracts/examples/push/*.json` use `fid`; revises D-083's "no server change" (§14.6, AC-13.7, §11.3). Without a decision id: the Pi image's C stage and CA roots and an OAuth plus FCM call from the arm64 image confirmed on the Mac (`docs/spikes/pi-image-arm64.md`; 63.8 MB, cold build 48 s), running on the Pi stays M2c-pi's (§10.3); Vico removed from the version catalog (`2b4e21c`), D-063 pointer (§6.2); §11.3 remaining contract requests: only OQ-5. |
|
||||
| 0.15 | 2026-10-18 | The first phone test, M8b (`f0575d3`) and owner answers. D-097: chart bars top corners 4 dp, bottom 2 dp, replacing 10/4 (§6.2, §13.4, design-spec §2.9, §3.10). D-100: no SSH from the Mac to the Pis; M2c-pi delivers the `infra/pi/` bundle the owner copies to the Kvitt Pi and runs, OQ-3 values as `.env` placeholders, M2c-pi last after the app is proven, no agent allowances on the Pis, Home Assistant, Gitea or Drive; revises D-023, keeps D-001 (§10 intro, §10.1, §10.10, §11.1, §11.2, §11.3, §12). D-101: the release keystore `kvitt-release.jks` at the repository root, git-ignored by `*.jks`, absolute path in `android/local.properties`, copy and password in the owner's password manager; revises D-024 (§10.6, §11.2 M10, §11.3). Without a decision id: the M8 end-to-end test passed on the owner's S26 against the development server (pairing, scripted partner via a second admin device, offline pending and sync, `fcm_token` and `app_version_name` registered, push by `fid` delivered and tapped); M8b fixes (invite-code `VisualTransformation`, quick-row badge above the ripple clip, `KvittTopAppBar` status-bar inset, 221 goldens) and `FcmMessageBody.fid` (`09f474f`) (§11.2 M8, §11.3); OQ-5 answered: licences button hidden in v1 (M8c); the 128 px picture size not needed (§14.2.4); the "boende" keyword keeps suggesting `travel` and the account page split bar stays as designed (no change). |
|
||||
|
||||
+29
-34
@@ -3,53 +3,48 @@
|
||||
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
|
||||
|
||||
## 1. Where things stand
|
||||
- `main` at `1e404cc` (Pi image arm64 check) plus the SPEC v0.14 docs commit from `agent-docs-v014` 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`).
|
||||
- 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).
|
||||
- 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). Not yet run: the end-to-end pairing, offline and push test on the phone and the design-spec §2.7 typeface check (phone was not attached; §3 item 1).
|
||||
- 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 left unchanged by assumption (owner question open, §3 item 5).
|
||||
- `main` at `09f474f` (M12e follow-up: `FcmMessageBody` uses `fid`) plus the SPEC v0.15 docs commit from `agent-docs-v015` 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).
|
||||
- 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`).
|
||||
- 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`).
|
||||
- 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-v014` (this docs change: SPEC v0.14, D-096); an ops agent rebuilding the development stack to `1e404cc` (server sends `fid`).
|
||||
- In flight: M8c (goldens reduction, D-098, D-099: the licences button hidden), M8d (editor rows, D-102), M2c-pi bundle `infra/pi/` (D-103 to D-106), a release-signing check, `agent-docs-v015` (this docs change: SPEC v0.15, D-097, D-100, D-101).
|
||||
- Remote: `origin` on Gitea exists and the SSH key works; the first push waits for the golden reduction (M8c) and a history rewrite (owner decision 2026-10-18: option "fewer goldens", no LFS). Push needs the owner's explicit ask.
|
||||
- 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 (owner's session in `~/orca/workspaces/kvitt/UX-sprint`, branch `UX-sprint`) has landed; nothing is in flight there.
|
||||
- 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 from `main` `90505a8` on 2026-10-18 (M12e server) and being rebuilt to `1e404cc` (`fid` target) by an ops agent, database at schema 3, data kept, bootstrap code unchanged. 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`. Its bootstrap pairing code is held by the orchestrator outside the repo and expires 2026-10-21T12:28Z. No scripted partner device exists yet (it needs the admin token, available only after the owner pairs). 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` built from `c28c52b`, as of the last report; the M8 and M12e builds are not installed yet (phone not attached); the dev server is already at M12e, so the M12e debug build can be installed.
|
||||
- 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 and the M8c and M8d work.
|
||||
|
||||
## 2. Open work (one line each, with who owns it: agent or owner)
|
||||
- Land `agent-docs-v014` (orchestrator): SPEC v0.14, D-096, HANDOFF, `prompts/README.md`.
|
||||
- On-phone test (orchestrator with the owner, §3 item 1): install the debug build from `main` (M12e; the dev server already runs M12e), pair against the dev server, script a partner device with the admin token, run the offline and push checks; then the design-spec §2.7 typeface check (One UI font styles on the S26).
|
||||
- Remaining contract requests (SPEC §11.3, no plan yet): S-22 licences screen (OQ-5, default not in v1).
|
||||
- Plans (SPEC §11.2 v0.14), in dependency order; prompt `prompts/<id>.md`:
|
||||
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7g, M8, M9, M12a, M12b, M12c, M12d, M12e, U1, U2: landed.
|
||||
- Now: the on-phone test (§3 item 1).
|
||||
- Next: M2c-pi Infra on the Pi (blocked on OQ-3; owner: answer OQ-3, grant the Pi, Home Assistant Pi, Gitea and Drive allowances at the start, D-023); M10 First release after M2c-pi (owner: keystore per §10.6, D-024); M11 Import history after M9 and M10 (procedure §10.8 "Production run", D-025; each run must reconcile, D-045).
|
||||
- Land `agent-docs-v015` (orchestrator): SPEC v0.15, D-097, D-100, D-101, HANDOFF, `prompts/README.md`, design-spec §2.9 and §3.10.
|
||||
- Land M8c, M8d, the M2c-pi bundle and the release-signing check when they report (orchestrator), then a docs agent for v0.16 (D-098, D-099, D-102 to D-106).
|
||||
- Install the next debug build and have the owner look (§3 item 1).
|
||||
- First push to Gitea `origin` after M8c and the history rewrite (orchestrator, on the owner's ask).
|
||||
- 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.15), 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, M9, M12a, M12b, M12c, M12d, M12e, U1, U2: landed.
|
||||
- Now: M8c, M8d, M2c-pi bundle, release-signing check.
|
||||
- Next: the owner runs the M2c-pi bundle on the Kvitt Pi, after the app is proven (D-100); M10 First release after M2c-pi (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. M8 pairing, offline and push test on the phone (the owner does it with the orchestrator; dev server at `http://192.168.1.210:8080`; the orchestrator holds the bootstrap code, valid until 2026-10-21T12:28Z):
|
||||
1. Connect the phone to home Wi-Fi, plug it in over USB, open Kvitt (debug).
|
||||
2. On "Para ihop", open "Serveradress" and enter `http://192.168.1.210:8080`.
|
||||
3. Enter the code, enter "Ditt namn", tap "Para ihop".
|
||||
4. Land on "Väntar på din partner"; an agent pairs a scripted partner and creates an account. Yes/no: does Home show the account?
|
||||
5. If Android asks to allow notifications, tap Allow.
|
||||
6. Airplane mode on. Tap "Lägg till utgift", amount, description, "Spara". Yes/no: does the row show "Väntar på synk"?
|
||||
7. Airplane mode off, wait about 10 seconds. Yes/no: marker gone?
|
||||
8. Agent posts an expense as the partner. Yes/no: did a notification appear?
|
||||
2. Look through the M6 component screenshots (`android/core/design/src/test/screenshots/`); approved in advance, nothing blocks on it.
|
||||
3. Answer OQ-3 for M2c-pi (§4).
|
||||
4. Release keystore per SPEC §10.6 for M10, or allow an agent to generate one.
|
||||
5. Yes/no: the keyword "boende" maps to travel. Yes/no: is the 128 px picture size needed? Yes/no: is an open-source licences screen wanted in v1 (OQ-5, default no)?
|
||||
1. Look at the next install on the phone (yes/no each): 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 rows (M8d).
|
||||
2. Keystore (D-101): add the four `kvitt.release.*` lines to `android/local.properties`, or let the agent do it (in flight); keep a copy of `kvitt-release.jks` and its password in the password manager.
|
||||
3. Pi (D-100), after the app is proven: copy the `infra/pi/` bundle to the Kvitt Pi, fill the placeholders in its `.env`, run the README checklist.
|
||||
4. Look through the M6 component screenshots (`android/core/design/src/test/screenshots/`); approved in advance, nothing blocks on it.
|
||||
|
||||
## 4. Owner questions (asked, unanswered)
|
||||
- OQ-3 LAN addresses and SSH users of the Kvitt Pi and the Home Assistant Pi; SSH port of the HA Pi; certificate directory there; which SSH add-on (SPEC §12). Needed by M2c-pi only.
|
||||
- OQ-5 open-source licences screen on S-22 in v1 (SPEC §12; default not in v1).
|
||||
- "boende" keyword → travel (yes/no); 128 px picture size needed (yes/no) (§3 item 5).
|
||||
- None. OQ-3 is answered by placeholders (D-100); OQ-5 answered (hide the button).
|
||||
|
||||
## 5. Paused (waiting on something outside the repo)
|
||||
- M2c-pi: OQ-3.
|
||||
- Nothing.
|
||||
|
||||
## 6. Numbering
|
||||
- Decisions: D-001 to D-096 used (D-008 and D-009 unused). Next free: **D-097**.
|
||||
- Spec version: v0.14 written; next **v0.15**.
|
||||
- Decisions: D-001 to D-097 used plus D-100 and D-101 (D-008 and D-009 unused; D-098, D-099 reserved by M8c, D-102 by M8d, D-103 to D-106 by M2c-pi). Next free: **D-107**.
|
||||
- Spec version: v0.15 written; next **v0.16** (taken by the M8c, M8d and M2c-pi outcomes).
|
||||
- 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**.
|
||||
- Sync protocol **1**, push payload **1**.
|
||||
|
||||
|
||||
+6
-3
@@ -27,8 +27,8 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
|
||||
| D-020 | 2026-10-17 | No bundled font: the app uses the Material 3 default typeface. No licence step. | PLAN-M6 open point (b); avoids a font licence clearance. | SPEC §6.2, §11.2, §13.4; PLAN-M2b, PLAN-M6, PLAN-U1, PLAN-U2 |
|
||||
| D-021 | 2026-10-17 | `sync_mutations` pruning runs daily at 04:00 Europe/Stockholm (DST-aware), deleting rows whose `received_at` is older than 90 days. | PLAN-M4 open point (c); the spec said only "daily". | SPEC §14.1 (rules text, not the DDL), §14.4.9, §11.2; PLAN-M4 |
|
||||
| D-022 | 2026-10-17 | Development infrastructure (M2c-dev, landed `c885aba`). No new CLI installs: Firebase project and Android apps through the Firebase Management REST API with a `gcloud` access token, no Firebase CLI; `rclone` belongs to M2c-pi. The agent may write git-ignored secrets, `google-services.json` included, under `infra/secrets/`; M8 copies it into `android/app/` with `infra/dev/copy-google-services.sh`. Phone health check against the dev server is a HUMAN step. Gate: `docker compose -f infra/dev/compose.yaml --env-file infra/dev/.env.example config`; `shellcheck $(find infra -name '*.sh')`. As built: dev stack without Caddy or TLS, bind-mounted data folder, the whole `infra/secrets/` mounted read-only at `/secrets` (a missing key file means push disabled), env file `infra/dev/.env.example` (the Pi's stays `infra/env.example` for M2c-pi), `kvitt-server healthcheck` used by compose, backups with `sqlite3 .backup` (§10.9). The server must start when `KVITT_TRUSTED_PROXY` does not resolve. Firebase on `kvitt-1337` with apps `se.hagstedt.kvitt` and `se.hagstedt.kvitt.debug`; service account `kvitt-fcm@kvitt-1337.iam.gserviceaccount.com` with `roles/firebasecloudmessaging.admin` (`roles/firebasemessaging.admin` does not exist). | Owner's tools already on the Mac; M2c-dev report. | SPEC §10 intro, §10.4, §10.7, §11.2, §11.3, §14.7; PLAN-M2c-dev, PLAN-M8 |
|
||||
| D-023 | 2026-10-17 | M2c-pi: key-based SSH to the Kvitt Pi and the Gitea credentials are HUMAN steps; the allowances to act on the Kvitt Pi, the Home Assistant Pi, Gitea and Google Drive are requested from the owner at the start of that sprint; the Gitea Actions workflow lives at `.gitea/workflows/`, owned by M2c-pi; secret files on the Pi (`ha_ssh_key`, `ha_known_hosts`, `rclone.conf`) mode 0600 owned by uid 1000, the uid of the `cert-sync` and `backup` containers; adding `gitea.lan` to `insecure-registries` is its own HUMAN step. | PLAN-M2c-pi open points (a) to (c). | SPEC §10.2, §10.3, §11.2, §11.3, §14.7; PLAN-M2c-pi |
|
||||
| D-024 | 2026-10-17 | Release keystore: `keytool -genkeypair -v -keystore kvitt-release.jks -alias kvitt -keyalg RSA -keysize 4096 -validity 10000`, created by the owner and kept outside the repository (was `android/keystore/release.jks`); `local.properties` holds its absolute path. | §10.6 did not fix the key parameters (PLAN-M10 H-1); a keystore outside every checkout cannot be committed or deleted with a worktree. | SPEC §10.6, §11.2, §11.3; PLAN-M10 |
|
||||
| D-023 | 2026-10-17 | M2c-pi: ~~key-based SSH to the Kvitt Pi and the Gitea credentials are HUMAN steps; the allowances to act on the Kvitt Pi, the Home Assistant Pi, Gitea and Google Drive are requested from the owner at the start of that sprint;~~ (revised by D-100: no SSH from the Mac to the Pis and no agent allowances; the owner runs the `infra/pi/` bundle; the Gitea credentials stay a HUMAN step); the Gitea Actions workflow lives at `.gitea/workflows/`, owned by M2c-pi; secret files on the Pi (`ha_ssh_key`, `ha_known_hosts`, `rclone.conf`) mode 0600 owned by uid 1000, the uid of the `cert-sync` and `backup` containers; adding `gitea.lan` to `insecure-registries` is its own HUMAN step. | PLAN-M2c-pi open points (a) to (c). | SPEC §10.2, §10.3, §11.2, §11.3, §14.7; PLAN-M2c-pi |
|
||||
| D-024 | 2026-10-17 | Release keystore: `keytool -genkeypair -v -keystore kvitt-release.jks -alias kvitt -keyalg RSA -keysize 4096 -validity 10000`, created by the owner ~~and kept outside the repository~~ (was `android/keystore/release.jks`; revised by D-101: at the repository root, git-ignored by `*.jks`); `local.properties` holds its absolute path. | §10.6 did not fix the key parameters (PLAN-M10 H-1); a keystore outside every checkout cannot be committed or deleted with a worktree. | SPEC §10.6, §11.2, §11.3; PLAN-M10 |
|
||||
| D-025 | 2026-10-17 | Production Splitwise import (M11): a backup first; user ids resolved by display name from the database; CSVs copied to the Pi into `/opt/kvitt/import/`, bind-mounted read-only into `server` as `/import`; the import runs as `docker compose run --rm --entrypoint kvitt-import server ...` with the server stopped (`--entrypoint` because the image's entrypoint is `kvitt-server`); CSVs deleted afterwards. The category table stays at `server/crates/kvitt-import/data/splitwise_categories.csv`. | PLAN-M11 open points (a) to (c); PLAN-M9 table location. | SPEC §10.2, §10.8, §11.2, §11.3; PLAN-M2c-pi, PLAN-M9, PLAN-M11 |
|
||||
| D-026 | 2026-10-17 | A 409 `cursor_ahead` on pull is treated as epoch recovery: cursor 0, epoch cleared, every local expense and settlement marked pending with a new `pending_mutation_id`, cycle restarted. | M5 report (`c28c52b`): a cursor ahead of the server means the server lost rows, as after a restore without `rotate-epoch`. | SPEC §14.4.2 |
|
||||
| D-027 | 2026-10-17 | A `clock_skew` rejection gives the row a new `pending_mutation_id` and the row stays pending (the server stored the rejection under the old id and would replay it). Orchestrator addition: on the first sync attempt after a `clock_skew` rejection, the phone re-stamps `updated_at` of each such pending row to its current clock before pushing, so a corrected clock heals the row. ~~The re-stamp is not in `:data` at `c28c52b`: contract request for M8 or a small `:data` follow-up (HANDOFF open work).~~ (Revised by D-094: implemented by M12e as a re-stamp of pending rows stamped later than now, at the start of a cycle with `clock_skew` set.) | M5 report; without the re-stamp a row stamped by a wrong clock would stay rejected after the clock is fixed. | SPEC §14.4.5 |
|
||||
@@ -67,7 +67,7 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
|
||||
| D-060 | 2026-10-18 | `ExpenseEditorOptions.frequentCategoryIds` holds only categories actually used in each active account, so a list may have fewer than five entries or be empty. Filling up to five from `sort_order` and placing the selected category fifth stays in the editor (UX-9, `docs/ux/design-spec.md` §3.7). Not a contract change. As landed by M12d (`bdba9d3`). | M12d report: D-059's "at most 5, unused ones left out" left open whether the repository or the editor fills the row; keeping the fill in the editor keeps the repository a plain usage count. | SPEC §14.2.4 ("Readings fixed in v0.10"), §11.2, §11.3 |
|
||||
| D-061 | 2026-10-18 | `frequentCategoryIds` has a key for every active account (an empty list when nothing was used there) and no key for an archived account. After use count (descending) and `sort_order` (ascending), ties fall back to the category id (ascending), so the order is identical in Room and in the fakes. | M12d report: without a last tie-break two categories with equal count and `sort_order` (custom categories can share one) could order differently in `:data` and `:core:fakes`. | SPEC §14.2.4 |
|
||||
| D-062 | 2026-10-18 | `CategoryRepository.deletable(id)` returns false for an unknown or already deleted id. Archived categories follow the same AC-7.6 rule as active ones. In `:core:fakes`, `delete` and `deletable` share one rule function. | M12d report: D-059 did not cover unknown, deleted or archived ids; one rule function keeps the fake from offering a delete it then refuses. | SPEC §14.2.4, AC-7.6 |
|
||||
| D-063 | 2026-10-18 | Charts (S-11) are drawn with Compose `Canvas` in `:core:design`, not Vico. No chart dependency is added; ~~Vico stays in `android/gradle/libs.versions.toml` unused and can be removed in a later cleanup.~~ (Vico removed from the catalog on 2026-10-18, `2b4e21c`; SPEC v0.14.) Takes the fallback §6.2 already allowed. As landed by M6 (`3080804`). | M6 report: Vico 3.3.1 cannot give design-spec §3.10's one focusable TalkBack node per bar with its own sentence and tap target, 2 dp gaps between segments, the 10/4 corner radii on the whole bar (not per segment) and a y-axis gutter that grows at 200 % font scale. | SPEC §6.2 (Charts, module table), §11.2, §11.3 |
|
||||
| D-063 | 2026-10-18 | Charts (S-11) are drawn with Compose `Canvas` in `:core:design`, not Vico. No chart dependency is added; ~~Vico stays in `android/gradle/libs.versions.toml` unused and can be removed in a later cleanup.~~ (Vico removed from the catalog on 2026-10-18, `2b4e21c`; SPEC v0.14.) Takes the fallback §6.2 already allowed. As landed by M6 (`3080804`). | M6 report: Vico 3.3.1 cannot give design-spec §3.10's one focusable TalkBack node per bar with its own sentence and tap target, 2 dp gaps between segments, the 10/4 corner radii on the whole bar (not per segment; 4/2 since D-097) and a y-axis gutter that grows at 200 % font scale. | SPEC §6.2 (Charts, module table), §11.2, §11.3 |
|
||||
| D-064 | 2026-10-18 | Material 3 Expressive against material3 1.4.0: `ShortNavigationBar` is public and used. Not usable, with the fallback taken: `MaterialShapes` (Cookie4, Clover4Leaf, Sunny, Cookie9) → rounded 30 %, rounded 16 dp, circle, circle; `ButtonGroup` → `SingleChoiceSegmentedButtonRow` styled per the design spec; `LoadingIndicator` → `CircularProgressIndicator`; `LinearWavyProgressIndicator` → `LinearProgressIndicator`; medium extended FAB → `ExtendedFloatingActionButton` 56 dp high, radius 20; Expressive button sizes → `Button` with `heightIn`; segmented list items → Kvitt's own `SegmentedGroup`; Expressive slider → Kvitt's own split bar with a knob; `MotionScheme.expressive()` and the `MaterialTheme(motionScheme)` overload are internal → Material components keep the default motion and Kvitt components use the design-spec §2.11 springs in `KvittMotion`; `Shapes.largeIncreased` and the other increased shapes are internal → held in `KvittShapes`. Fulfils design-spec §6 open point 5. | M6 report: the design spec names Expressive APIs that material3 1.4.0 (pinned since M2b) does not expose publicly; the spec's rule is a fallback without a design change. | SPEC §6.2 (UI); `docs/ux/design-spec.md` §3.1, §6 |
|
||||
| D-065 | 2026-10-18 | Formatting tables are code: Swedish and English month and weekday names are tables inside `DateFormatter`, not platform locale data, so JVM tests and phone output are identical. Chart axis labels have no dots ("maj, jun, … okt"); list dates keep the Swedish dot ("3 okt.", "25 sep."). File sizes are SI megabytes (10^6 bytes) with one decimal, rounded half up, U+00A0 before "MB" ("8,4 MB", "8.4 MB"). | M6 report: platform locale data differs between the JVM used by tests and Android, and §9.8 gave no file-size format for the update banner. | SPEC §9.8 |
|
||||
| D-066 | 2026-10-18 | Golden layout: one Roborazzi golden per component sheet × light/dark × sv/en (`<sheet>_<lang>_<theme>.png`), plus a 200 % font-scale variant for long states; the content is composed once and language, theme and font scale are switched through a configuration context, so Compose's non-linear font scaling still applies. Feature modules (M7a to M7f) follow the same layout. | M6 report: one layout keeps the goldens reviewable and the feature agents' screenshot tests consistent. | SPEC §6.2 (Screenshot tests) |
|
||||
@@ -101,3 +101,6 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
|
||||
| D-094 | 2026-10-18 | The D-027 re-stamp as implemented: when `sync_state.clock_skew` is set at the start of a cycle, every pending expense and settlement whose `updated_at` (or a tombstone's `deleted_at`) is later than the phone's clock is re-stamped to now, in one Room transaction before pushing; rows stamped no later than now are untouched. Room schema unchanged. Revises D-027. | M5 report (D-027, §11.3 list): the re-stamp was not in `:data`. | SPEC §14.4.5 step 3 |
|
||||
| D-095 | 2026-10-18 | `PushEvent` lives in `:core:model` (`model/Push.kt`); `:adapter:fcm` keeps `PushListener` and `PushMapping`. Six screens host the shared `snackbarHostState` in their own scaffold (Home, AccountDetail, ExpenseDetail, SettlementDetail, DrillDown, SyncStatus); `:app` has no overlay host. `ExpenseEditorScreen.onSaved(ExpenseId, @StringRes Int?)`, `onDeleted()` without argument on S-7 and S-8; `:app` shows `snackbar_expense_deleted`. Launcher icon: adaptive, vectors only, `mipmap-anydpi/ic_launcher.xml`, background `#9C5238` (light primary), foreground and monochrome `receipt_long` mark at 48 dp, manifest `icon` and `roundIcon`; lint `MissingApplicationIcon` cleared. Revises D-071. | M8 report (§11.3 list): `PushEvent` was in the adapter, the overlay host could cover a FAB or the bottom bar, S-7 and S-8 handed over resolved strings, the app had no launcher icon. | SPEC §6.2, §13.2, §13.3 (S-7, S-8), §14.2.4, §14.6, §11.3 |
|
||||
| D-096 | 2026-10-18 | The server sends the device's stored `fcm_token` (the Firebase Installation ID of D-083) in the FCM HTTP v1 `message.fid` field, no longer in the deprecated `message.token`. No database or wire change: the `devices.fcm_token` column and the E-17 field keep the name `fcm_token`. `contracts/examples/push/*.json` use `fid` (a contract-file change, hence this entry and SPEC v0.14). Landed `c694f78`. Revises D-083's "no server change". | The FCM HTTP v1 reference (projects.messages, last updated 2026-09-08, read 2026-10-18) deprecates `token` with "Use fid instead" and defines `fid` as the Firebase Installation ID target; `token`, `fid`, `topic` and `condition` are mutually exclusive. Live probe from the arm64 image (`docs/spikes/pi-image-arm64.md`): FCM answered "unregistered" for a made-up id, so the field is accepted. | SPEC §14.6, AC-13.7, §11.3; `server/crates/kvitt-server/src/push/fcm.rs`, `contracts/examples/push/` |
|
||||
| D-097 | 2026-10-18 | S-11 chart bars have top corners 4 dp and bottom corners 2 dp on the whole bar, replacing top 10, bottom 4. The 2 dp gaps between segments and the clip of the whole bar (not per segment) are unchanged. Assumption recorded: the owner's "4 outer / 2 inner" means top / bottom. Landed by M8b (`f0575d3`) in `KvittShapes` and `Charts.kt`, goldens re-recorded. | First phone test: the owner found the bar tops too round. | SPEC §6.2 (Charts), §13.4; `docs/ux/design-spec.md` §2.9 (shapes table), §3.10 |
|
||||
| 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. | 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 |
|
||||
|
||||
@@ -260,7 +260,7 @@ M3 shape scale: extraSmall 4, small 8, medium 12, large 16, largeIncreased 20, e
|
||||
| Segmented-group inner corners | small 8 |
|
||||
| Allocation amount field | 14 |
|
||||
| Icon grid cells (S-15) | large 16 |
|
||||
| Chart bars | top 10, bottom 4 |
|
||||
| Chart bars | top 4, bottom 2 |
|
||||
| Category tile | `MaterialShapes.Cookie4Sided` → fallback `RoundedCornerShape(30%)` |
|
||||
| Account tile | `MaterialShapes.Clover4Leaf` → fallback `RoundedCornerShape(16.dp)` |
|
||||
| Large avatars (≥ 40 dp outside rows: hero, settings card, S-2, S-9, S-10, S-14), app mark, dialog icon tile, sync status tile | `MaterialShapes.Sunny` → fallback `CircleShape` |
|
||||
@@ -586,7 +586,7 @@ TalkBack: "Konto: Vardag. Dubbeltryck för att byta konto"; "Datum: idag"; "Beta
|
||||
|
||||
**`StackedBarChart(buckets, groups, onBucket)`**
|
||||
* Plot 170 dp high, y-axis gutter 44 dp on the left: gridlines (1 dp `chart-grid`) at 3–4 round values with labels in whole kronor (10 / 12 sp, `chart-axis-label`, sv "15 000", en "15,000"); x labels under each bar (11 / 14 sp: "maj", "jun" or the week number "41" with the caption "Vecka" under the axis).
|
||||
* One bar per bucket (every bucket of the range, empty ones as zero, AC-14.7), gap 10, top corners 10, bottom 4; segments stacked in legend order with 2 dp gaps; segment colour per group (category colour or account chart colour).
|
||||
* One bar per bucket (every bucket of the range, empty ones as zero, AC-14.7), gap 10, top corners 4, bottom 2; segments stacked in legend order with 2 dp gaps; segment colour per group (category colour or account chart colour).
|
||||
* More buckets than fit at a minimum bar width of 16 dp: horizontal scroll, scrolled to the newest bucket on first show. At 200 %, x labels are drawn for every second bucket when they would overlap.
|
||||
* Tap a bar → `onBucket(key)` (S-12). Each bar is a focusable semantics node: "September, 41 484 kronor, Resor största kategori".
|
||||
* Chart summary (TalkBack): "Utgifter över tid, <range>: högst i september med 41 484 kronor, lägst i maj med 24 710 kronor" (`analytics_summary_over_time`).
|
||||
|
||||
+5
-5
@@ -2,7 +2,7 @@
|
||||
|
||||
One ready-to-paste start prompt per plan (`PLAN-<id>.md` at the repository root), plus `ORCHESTRATOR-prompt.md`. Before pasting, the orchestrator fills `{{NEXT_DECISION}}`, `{{SPEC_VERSION}}`, `{{SCHEMA_VERSION}}` (disjoint ranges for agents that run in parallel, CLAUDE.md › Numbering) and `{{ORCHESTRATOR_NOTES}}` (answers to the plan's open points, phone assignment, overrides). `U2.md` also takes `{{OWNER_PICK_AND_COMMENTS}}`. U1 and U2 run in the owner's own Claude session in `~/orca/workspaces/kvitt/UX-sprint` on branch `UX-sprint`. The numbers are for proposed entries in agents' reports: implementation agents never edit `SPEC.md` or `docs/decisions.md`; a docs agent records what the orchestrator accepts (CLAUDE.md › Orchestration).
|
||||
|
||||
Dependencies follow SPEC §11.1 and §11.2 (v0.14). Status as of 2026-10-18: M0 done, M1 landed (`c6df2ab`), M2c-dev landed (`c885aba`), M2a landed (`fdf2093`), M2b landed (`fad6fc9`), M3 landed (`503cee5`), M5 landed (`c28c52b`), M4 landed (`bd6f10a`), M9 landed (`ab298ab`); SPEC v0.7 landed (`e8ee518`); M12a landed (`6b60ef7`); SPEC v0.8 landed (`7fb9f6f`); M12c landed (`e499107`); M12b landed (`2e2ebe7`); U1 and U2 landed (`16c4d01`, owner approved); SPEC v0.9 landed (`45eecd1`); M12d landed (`bdba9d3`); M6 landed (`3080804`); SPEC v0.10 landed (`7bd0aa1`); M7a landed (`b9b061a`), M7e (`6ac711d`), M7d (`082b8dc`), M7b (`31760f9`), M7c (`1e42f76`), M7f (`a54eb7c`); M6b `:core:design` fixes landed (`3a1bd14`); SPEC v0.11 landed (`d504004`, the M7 and M6b outcomes, D-069 to D-080); M7g feature cleanups landed (`34a2590`, no prompt file; brief by the orchestrator); M8 landed (`369e516`, `00a14a3`), its on-phone test still to run; SPEC v0.12 records the M8 outcomes (D-081 to D-085); the M12e contract pass (no prompt file; brief by the orchestrator) landed (`1470a2f`, `90505a8`); SPEC v0.13 records its outcomes (D-086 to D-095, server schema 3) and landed (`03b7184`); the Vico removal (`2b4e21c`), the FCM `fid` target (`c694f78`) and the Pi image arm64 check (`1e404cc`) landed; SPEC v0.14 records them (D-096). The plans' open points are resolved in SPEC v0.3 and recorded in each plan's §4; v0.4 records the M2a and M2b decisions (D-005 to D-014); v0.5 the M3 and M5 decisions (D-026 to D-034, D-040 to D-043); v0.6 the M4 decisions (D-035 to D-039); v0.7 the M9 decisions (D-044 to D-046); v0.8 the owner's UX-session decisions (D-047 to D-054) and the milestone group M12; v0.9 D-055 to D-059 and M12d; v0.10 D-060 to D-068; v0.11 D-069 to D-080; v0.12 D-081 to D-085; v0.13 D-086 to D-095; v0.14 D-096.
|
||||
Dependencies follow SPEC §11.1 and §11.2 (v0.15). Status as of 2026-10-18: M0 done, M1 landed (`c6df2ab`), M2c-dev landed (`c885aba`), M2a landed (`fdf2093`), M2b landed (`fad6fc9`), M3 landed (`503cee5`), M5 landed (`c28c52b`), M4 landed (`bd6f10a`), M9 landed (`ab298ab`); SPEC v0.7 landed (`e8ee518`); M12a landed (`6b60ef7`); SPEC v0.8 landed (`7fb9f6f`); M12c landed (`e499107`); M12b landed (`2e2ebe7`); U1 and U2 landed (`16c4d01`, owner approved); SPEC v0.9 landed (`45eecd1`); M12d landed (`bdba9d3`); M6 landed (`3080804`); SPEC v0.10 landed (`7bd0aa1`); M7a landed (`b9b061a`), M7e (`6ac711d`), M7d (`082b8dc`), M7b (`31760f9`), M7c (`1e42f76`), M7f (`a54eb7c`); M6b `:core:design` fixes landed (`3a1bd14`); SPEC v0.11 landed (`d504004`, the M7 and M6b outcomes, D-069 to D-080); M7g feature cleanups landed (`34a2590`, no prompt file; brief by the orchestrator); M8 landed (`369e516`, `00a14a3`), its on-phone test still to run; SPEC v0.12 records the M8 outcomes (D-081 to D-085); the M12e contract pass (no prompt file; brief by the orchestrator) landed (`1470a2f`, `90505a8`); SPEC v0.13 records its outcomes (D-086 to D-095, server schema 3) and landed (`03b7184`); the Vico removal (`2b4e21c`), the FCM `fid` target (`c694f78`) and the Pi image arm64 check (`1e404cc`) landed; SPEC v0.14 records them (D-096); the M8 phone test ran on 2026-10-18 and M8b UI fixes landed (`f0575d3`, D-097) with the `fid` follow-up (`09f474f`); SPEC v0.15 records them and D-100 (M2c-pi as an owner-run bundle) and D-101 (keystore at the repository root). The plans' open points are resolved in SPEC v0.3 and recorded in each plan's §4; v0.4 records the M2a and M2b decisions (D-005 to D-014); v0.5 the M3 and M5 decisions (D-026 to D-034, D-040 to D-043); v0.6 the M4 decisions (D-035 to D-039); v0.7 the M9 decisions (D-044 to D-046); v0.8 the owner's UX-session decisions (D-047 to D-054) and the milestone group M12; v0.9 D-055 to D-059 and M12d; v0.10 D-060 to D-068; v0.11 D-069 to D-080; v0.12 D-081 to D-085; v0.13 D-086 to D-095; v0.14 D-096; v0.15 D-097, D-100, D-101.
|
||||
|
||||
## Dependency order and waves
|
||||
|
||||
@@ -33,9 +33,9 @@ Plans in the same wave can run in parallel (they own disjoint paths, SPEC §14.7
|
||||
| 3 | M9 Splitwise importer | `M9.md` | M2a, M4 | done (`ab298ab`) |
|
||||
| 3 | M6b Design-system fixes after M7 | none (orchestrator brief) | M7a to M7f | done (`3a1bd14`) |
|
||||
| 4 | M7g Feature cleanups | none (orchestrator brief) | M7a to M7f, M6b | done (`34a2590`) |
|
||||
| 4 | M8 App integration | `M8.md` | M3, M4, M5, M7a to M7f, M2c-dev | done (`369e516`, `00a14a3`); on-phone test outstanding |
|
||||
| 4 | M8 App integration | `M8.md` | M3, M4, M5, M7a to M7f, M2c-dev | done (`369e516`, `00a14a3`); on-phone test run 2026-10-18; M8b fixes `f0575d3` |
|
||||
| 4 | M12e Contract pass | none (orchestrator brief) | M8, SPEC v0.12 (§11.3 list) | done (`1470a2f`, `90505a8`) |
|
||||
| 5 | M2c-pi Infra on the Pi | `M2c-pi.md` | M2c-dev, M8; OQ-3 answered | none needed |
|
||||
| 5 | M2c-pi Infra on the Pi (bundle `infra/pi/`, D-100) | `M2c-pi.md` | M2c-dev, M8; runs last, after the app is proven | none needed |
|
||||
| 6 | M10 First release | `M10.md` | M2c-pi, M3, M4, M8 | none |
|
||||
| 7 | M11 Import history | `M11.md` | M9, M10 | none |
|
||||
|
||||
@@ -47,5 +47,5 @@ Plans in the same wave can run in parallel (they own disjoint paths, SPEC §14.7
|
||||
- **M8** (landed) wired the navigation of SPEC §13.2 (D-071) and deleted the debug fakes module; both variants bind `:data` (D-081). Its outcomes are D-081 to D-085; its contract requests are on the M12e list in SPEC §11.3.
|
||||
- **M6** landed `3080804` (D-063 to D-066): charts on `Canvas`, Expressive fallbacks against material3 1.4.0, formatting tables in code, one golden per sheet × theme × language (+200 %), which M7a to M7f follow. Its M8 contract requests are in SPEC §11.3.
|
||||
- **M12** (D-054): done; the importer test `category_table_matches_the_spec` is green again since M12b (`2e2ebe7`). M12d (D-059) landed `bdba9d3` (D-060 to D-062); M7a to M7f run on top of both.
|
||||
- **Phone:** M2b (one `installDebug`), M5 (only if instrumented tests), M8 and M10 use the phone; only one at a time, named in the notes. M8's on-phone test is run by the orchestrator with the owner; M12e did not use the phone; its APK needs a schema-3 server (SPEC §14.8), and the dev server already runs it.
|
||||
- **Outside the repository:** M2c-dev (D-022, done), M2c-pi (the Kvitt Pi, the Home Assistant Pi, Gitea, Drive: allowances requested at the start of that sprint, D-023), M8 (dev server on the Mac), M10 and M11 (production) need the owner's allowance in the notes.
|
||||
- **Phone:** M2b (one `installDebug`), M5 (only if instrumented tests), M8 and M10 use the phone; only one at a time, named in the notes. M8's on-phone test was run by the orchestrator with the owner on 2026-10-18; M12e did not use the phone; its APK needs a schema-3 server (SPEC §14.8), and the dev server already runs it.
|
||||
- **Outside the repository:** M2c-dev (D-022, done), M2c-pi needs none: it writes the `infra/pi/` bundle and the owner runs it on the Pi (D-100, revises D-023); M8 (dev server on the Mac), M10 and M11 (production) need the owner's allowance in the notes.
|
||||
|
||||
Reference in New Issue
Block a user