SPEC v0.32: web plans W1 to W5 (D-144 to D-154), APK install failures (D-155)
Build Kvitt images / build (push) Failing after 14m44s

D-144 to D-154 record the web feature plans W1 to W5 (934eee7, a65ab71,
f9a9bfb, 1aecf51, 3ebe57d): preset account and payment in the query
string, Home empty state, the CSV format, AC-8.6 in the browser and
search in the URL, the expense editor's banner and controls, live
settle-up errors, analytics drill-down routes and chart accessibility,
W-18's route, the partner intro and About's fallback; D-150 unused.
D-155 records d686ee0: new AC-16.7, UpdateFailure in §14.2.4,
design-spec §3.6, S-17 and S-22. SPEC §11.2, §11.3, §15.3, §15.6 and
§15.7 follow the code; HANDOFF gets the state, the owner's browser
checks and the web follow-ups for a W0b plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-21 21:52:13 +02:00
parent 83f52937cf
commit c13be3bbfd
4 changed files with 94 additions and 49 deletions
+37 -14
View File
@@ -3,16 +3,24 @@
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
## 1. Where things stand
- State as of 2026-10-21 late. `main` at `1c9856b` (W6 Android Insights on S-11, D-141 to D-143), pushed to `origin/main` (Gitea), after `415f9f5` (CLAUDE.md: web client, three gates), `2ae65a5` (W0 web foundation), `3fbd393` (SPEC v0.29) and `c0f4345` (update-check fix, D-140). The Android gate is green again on `main` (W6's E-34/E-35 DTOs fixed the three `:data` example tests red since `50a0d67`). This docs pass (`agent-docs-v31`) records D-140 as SPEC v0.30 and D-141 to D-143 as v0.31. Running: web W1 to W5 (D-144 to D-154 reserved for them). Landed after `1c9856b`, not yet recorded in the spec: `d686ee0`, the APK install-failure fix (D-155; two `strings.csv` keys `update_install_failed`, `update_install_blocked`, so the file has 590 rows against SPEC §13.4's 588; the web dictionaries are regenerated with them in this pass). The push of `1c9856b` started the Pi image build with the web client and E-34/E-35 (in progress on the Pi); then the owner deploys it (§3 step 4), and after the Pi runs the new image the release APK with Insights is published (`infra/publish-apk.sh --deploy`).
- State as of 2026-10-21, night. `main` at `83f5293` (web fix: deterministic feature strings, `suggest_category` vectors, raw bodies in the fake server), after the five web feature plans W1 `934eee7`, W3 `f9a9bfb`, W5 `3ebe57d`, W2 `a65ab71`, W4 `1aecf51` (D-144 to D-154; D-150 unused) and `d268dd0` (`.gitignore` anchors `export/`); before them `aaa09d9` (SPEC v0.30, v0.31), `d686ee0` (APK install failures visible, D-155) and `1c9856b` (W6). All five web plans are landed and pushed (orchestrator). The web gate is green on `main` (59 test files, 532 tests at `1aecf51`; 59 files, 598 tests at `83f5293` with the `suggest_category` vectors, run by this docs pass). This docs pass (`agent-docs-v32`) records D-144 to D-155 as SPEC v0.32. The Pi image build with the complete web app is in progress on the Pi; the owner deploys it (§3 step 4), then checks the web client in a browser (§3 step 5). Release APK `kvitt-140` deployed 2026-10-21 (`infra/publish-apk.sh --deploy`): install-failure messages (D-155) and S-11 Insights, which need the new server image (E-34, E-35).
- **Kvitt host name** (owner decision 2026-10-19): `kvitt.suorra.duckdns.org`, a subdomain of the owner's DuckDNS family domain `suorra.duckdns.org`; NPM issues its Let's Encrypt certificate; `KVITT_PUBLIC_HOST=kvitt.suorra.duckdns.org`, so `KVITT_PUBLIC_URL=https://kvitt.suorra.duckdns.org` (port 443, D-124). It is in `infra/pi/.env.example` and `infra/pi/.env` (`KVITT_PUBLIC_HOST`) and `android/local.properties` (`kvitt.publicHost`, git-ignored). SPEC §10.1 and §10.5.1 point here.
- **NPM proxy host** for `kvitt.suorra.duckdns.org` is Online with a Let's Encrypt certificate (owner, 2026-10-19). The router needs no new rule: external TCP 443 already forwards to the Home Assistant Pi (owner confirmed, D-124).
- **Images**: `gitea.lan/jacobh2/kvitt-server` and `gitea.lan/jacobh2/kvitt-backup`, tags `v1.0.0` (run 69) and `v1.0.1` (the sparse-checkout push; the tag the Pi uses), plus `latest`; the packages are public, so the Pi pulls without `docker login`. Gitea Actions on the owner's Pi runner (act_runner 0.6.1, Alpine job container): the first failures were an empty secret (the user-level secret was misnamed on the owner's side, fixed 2026-10-19; a user-level secret of the repository owner reaches the repository); run 69 succeeded: login ok, `v1.0.0`, `kvitt-backup` 5 min 20 s, `kvitt-server` 2 h 42 min on the Pi 3 (first build, `CARGO_BUILD_JOBS=1`; later runs reuse the builder's cargo cache); the tag push works with the `write:package` token. SPEC §10.3.
- **Kvitt Pi** (D-125): Kvitt runs from `/home/jacobhagstedt/kvitt` as the owner's user `jacobhagstedt`, uid and gid 1000:1000 (`KVITT_UID`/`KVITT_GID`); Docker and the `gitea.lan` registry trust were already on the Pi; nothing outside that folder is installed or changed. **The stack is running** (owner, 2026-10-21): image `v1.0.1`, `server` and `backup` both healthy after a `chown` of `data/` (it had been created as root); compose's `mem_limit` warning (the Pi kernel has no memory cgroup, so the 160 MiB and 96 MiB limits are not enforced) is accepted. `/health` through NPM from outside answered `"status":"ok"` (public health OK). **Live** (2026-10-21): both phones are paired against it and the Splitwise import (M11) is done.
- **App gate** (D-126, owner decision 2026-10-21): outside the home LAN `192.168.1.0/24` NPM closes (`return 444`) every request without `X-Kvitt-App: <key>`, every route included; the key is the build property `kvitt.appKey` (`android/local.properties`, git-ignored), generated by the owner with `openssl rand -hex 32` and pasted into NPM's block (`infra/npm/kvitt.conf`, `7521c01`); the server and `contracts/openapi.yaml` are unchanged. Android side `34e5cad`, docs `2acfc34` (SPEC v0.25, `infra/README.md` step 4a). `android/local.properties` has a `kvitt.appKey` and `infra/pi/out/apk/` holds `kvitt-125.apk` (checked 2026-10-21); whether both phones run a keyed APK and the gate is in NPM is not recorded here (orchestrator confirms with the owner, §3).
- **Web client programme** (owner interview 2026-10-21, SPEC v0.28, D-129 to D-134, SPEC §15, FR-21): a React web client in `web/`, served by `kvitt-server` under `/app/` from the server image, with the app's features except the phone-only ones, plus search, CSV export, keyboard shortcuts and a language choice; login with an invite code (device `Web · <browser>`); the NPM gate also passes `/app/`, `POST /invites/accept` and bearer-token requests except `/health`, `/setup/`, `/i/`, `/apk/` (new block in `infra/npm/kvitt.conf`, README step 4, SPEC §10.5.2; checked with nginx 1.31.7, 17 requests); advanced analytics on the server (`/analytics/*`, D-133, forecast = recurring templates due + 3-month average of non-recurring spend). Plans `PLAN-W0.md` to `PLAN-W7.md`, prompts `prompts/W0.md` to `prompts/W7.md`. W7 landed `50a0d67` (D-135 to D-138, SPEC v0.29: `/app/` from `KVITT_WEB_DIR`, E-34 `GET /analytics/trends`, E-35 `GET /analytics/forecast`, rules SPEC §14.5.7, the Node stage and the repository-root build context); W0 landed `2ae65a5` (`web/**`: Vite, React 19, TypeScript strict, sync-pull ledger, invite-code login, ported money rules passing the vectors, tokens, i18n generated from `docs/ux/strings.csv` and `web-strings.csv`); W1 to W5 are running in parallel, each in its own `web/src/features/<name>/`; W6 (Android Insights) landed `1c9856b` and shows on the phones once the image with W7 and W0 is deployed and a release APK with W6 is published. The web gate is `npm run check` in `web/` (third gate, in `CLAUDE.md` since `415f9f5`).
- **Web client programme** (owner interview 2026-10-21, SPEC v0.28, D-129 to D-134, SPEC §15, FR-21): a React web client in `web/`, served by `kvitt-server` under `/app/` from the server image, with the app's features except the phone-only ones, plus search, CSV export, keyboard shortcuts and a language choice; login with an invite code (device `Web · <browser>`); the NPM gate also passes `/app/`, `POST /invites/accept` and bearer-token requests except `/health`, `/setup/`, `/i/`, `/apk/` (new block in `infra/npm/kvitt.conf`, README step 4, SPEC §10.5.2; checked with nginx 1.31.7, 17 requests); advanced analytics on the server (`/analytics/*`, D-133, forecast = recurring templates due + 3-month average of non-recurring spend). Plans `PLAN-W0.md` to `PLAN-W7.md`, prompts `prompts/W0.md` to `prompts/W7.md`. W7 landed `50a0d67` (D-135 to D-138, SPEC v0.29: `/app/` from `KVITT_WEB_DIR`, E-34 `GET /analytics/trends`, E-35 `GET /analytics/forecast`, rules SPEC §14.5.7, the Node stage and the repository-root build context); W0 landed `2ae65a5` (`web/**`: Vite, React 19, TypeScript strict, sync-pull ledger, invite-code login, ported money rules passing the vectors, tokens, i18n generated from `docs/ux/strings.csv` and `web-strings.csv`); W1 to W5 landed 2026-10-21 (`934eee7`, `a65ab71`, `f9a9bfb`, `1aecf51`, `3ebe57d`; D-144 to D-154, SPEC v0.32), each in its own `web/src/features/<name>/`, W2 with the shared editor fields in `web/src/components/editor/`; loose ends for a W0b plan in §2 "Web follow-ups"; W6 (Android Insights) landed `1c9856b` and shows on the phones once the image with W7 and W0 is deployed and a release APK with W6 is published. The web gate is `npm run check` in `web/` (third gate, in `CLAUDE.md` since `415f9f5`).
- **First release APK**: `apk/kvitt-123.apk` (10.0 MB, git-ignored by `*.apk`) handed to the owner from the main checkout for sideloading on both phones (both paired, 2026-10-21); it was built before any `kvitt.appKey` existed, so it sends no `X-Kvitt-App`: it works only until the gate is pasted (on the home Wi-Fi it keeps working afterwards). The phones need a keyed APK before the gate goes in (§3).
- History rewrite and first push, 2026-10-18 (owner authorised): superseded PNG versions were dropped from the whole history (no LFS), the current goldens and UX mock-ups restored in `18c527f`; pack 45 MiB; `origin/main` pushed at `18c527f`. A bundle of the pre-rewrite history (`kvitt-pre-rewrite.bundle`) is kept outside the repository, in the orchestrator's scratchpad. Every commit from M2b on got a new hash; the oxipng pass (`7c453d8` before the rewrite) no longer exists as a commit (it only touched PNGs). SPEC, `docs/decisions.md`, this file and `prompts/README.md` cite the rewritten hashes since v0.18 (`7c453d8` stays as the oxipng pass's name; `docs/validation/ac-coverage.md` keeps its pre-rewrite hashes verbatim); since v0.19 the plans cite rewritten hashes too (eight pre-rewrite hashes in `PLAN-*.md` replaced by the commit with the same subject); `docs/spikes/pi-image-arm64.md` and the READMEs had none stale. For any other old hash, find the commit by its subject with `git log --oneline --grep`.
- History (rewritten hashes): `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `c277ee3` M2b, `06f48ef` M2a, `6dce4c7` SPEC v0.4, `7869ec4` M3, `8f0e156` M5, `853c038` SPEC v0.5, `15056dc` M4, `d9dd584` SPEC v0.6, `50339ae` M9, `86e08a1` SPEC v0.7, `aa765bb` M12a, `d39eb2c` SPEC v0.8, `b09f322` M12c, `6eaccec` M12b, `0006bd7` UX Kalas categories and tokens, `6da6b1b` UX sprint U2, `e676812` SPEC v0.9, `557af7e` M12d, `7730a31` M6, `9ee67d4` SPEC v0.10, `a41f240` and `6453932` app wiring before M8, `1ea752c` M7a, `ea7d494` M7e, `7380e50` M7d, `0e1d99d` M7b, `0b140aa` M7c, `c4ab35f` M7f, `b24c8b5` M6b, `f4dc10c` SPEC v0.11, `3dda414` M7g, `cb832a7` and `3d9eea8` M8, `6159e18` SPEC v0.12, `8c60715` M12e, `b7b7483` M12e launcher icon, `51d6ff8` SPEC v0.13, `a312758` Vico removal, `f56dcbe` FCM `fid`, `3e6c337` Pi image arm64 check, `e2dba84` SPEC v0.14, `328c466` M8b, `6eb3f85` `FcmMessageBody.fid`, `bfde12c` SPEC v0.15, `75b549f` M8c goldens, `a0c25b0` M8c licences row, `c30a49e` R8 release build, `bacbd18` M8d, `bfc3acb` SPEC v0.16, `2476667` M2c-pi bundle, `a59abeb` M8e, `b4d9467` Dockerfile `CARGO_BUILD_JOBS`, `956908a` M8f, `1328afe` chips cleanup, `400eac9` SPEC v0.17, `18c527f` goldens restored after the rewrite; then `2fac1ec` to `e70fe0d` infra overhaul, `8660b7f` server gap pass, `7fb86f5` infra security, `dc7a296` to `a0259ab` M12f, `8103dae` to `b985ce9` server overhaul, `48fe119` SPEC v0.18, `e4e6e5d` to `c582363` Android overhaul, `3ef2c15` SPEC v0.19, `96e4cbd` APK download rate limit (D-119), `481889a` SPEC v0.20, `f1abb0d` `cargo audit` in the server gate, `4f69ae0` Gitea login in the run line, `d05f13a` Pi stack behind NPM, `62f5279` and `9231a1d` infra README for NPM (D-120), `d090c6c` Android host property (D-121), `4bcff2c` SPEC v0.21, `dd07ca4` temporary workflow diagnostic step, `4a4e45f` S-22's release server and `4b1ae0c` `publish-apk.sh` host (D-122), `c9abd24` and `e381e37` server host defaults and OpenAPI `{host}` (D-123), `ac17b41` `install.sh` step number, `cce3e4e` SPEC v0.22, `a86ad24` server and contracts on port 443, `4852803` bundle on port 443 (D-124), `038c603` Android on port 443 and the pinned-root template, `ad35231` portless Android fixtures, `b1f4c10` SPEC v0.23, `3820f9c` sparse-checkout workflow, `200c99f` `.env` without informational keys, `6e5981d` self-contained Pi compose project (D-125), `28fa71b` compose deploy hint in the workflow and `infra/build-images.sh`.
- Landed since SPEC v0.31 (recorded in v0.32):
- W1 web Home and accounts (`934eee7`, D-144, D-145): W-2 with the balance hero and seesaw, account cards, the archived section (open state per page session) and the "Du och <partner> är med" empty card; W-3 with month groups and the overflow menu; W-4 at `/app/accounts/new` and `/app/accounts/:id/edit`; `?account=<id>` links into W-5 and W-7.
- W2 web expenses, search and CSV export (`a65ab71`, D-146 to D-148): W-5 with the §9.11 suggestion, the rejection banner and the discard dialog; W-6 with the audit timeline; W-12 with filters in the URL; the CSV export (`;`, BOM, decimal comma); the shared editor fields `web/src/components/editor/`; `vite.config.ts` `server.fs.allow` for `contracts/category-keywords.csv`.
- W3 web settle up (`f9a9bfb`, D-144, D-149): W-7 with live allocation errors, `?account=` and `?settlement=`; W-8 with allocations and timeline.
- W4 web analytics (`1aecf51`, D-151, D-152): W-9 stacked bars, W-10 drill-down with the D-128 line chart (bucket route `/app/analytics/bucket/<key>`, filter in the query string), W-11 Insights on E-34/E-35 with 3, 6 or 12 months; "Visa som tabell"; `resolveRange` and `spendSeries` ported with their vectors.
- W5 web settings (`3ebe57d`, D-153, D-154): W-13 hub, W-14 profile with the partner's first-login intro, W-15 categories, W-16 recurring, W-17 devices, W-18 invites at `/app/settings/invite`, W-19 about with the `GET /me` header fallback.
- Web fix (`83f5293`, no decision): Vitest's module cache kept a stale `catalog.ts` glob result in the main checkout (raw keys such as `web_accounts_load_failed` on screen); the glob lost its type argument and `catalog.test.ts` compares the loaded feature catalogs with the files; `web_accounts_load_failed` became the shared `web_ledger_load_failed` in `web-strings.csv`; `suggest_category` moved to PORTED in `lib/vectors.test.ts`; the fake server keeps non-JSON bodies raw.
- APK install failures (`d686ee0`, D-155): installer status reaches `UpdateRepository` by session id; blocked → "Kunde inte installera uppdateringen" with the security-settings text, aborted → `Available`, other failures → the installer's message; "Försök igen" downloads again; new AC-16.7; `strings.csv` 590 rows. Release `kvitt-140` deployed 2026-10-21.
- Landed since SPEC v0.29 (recorded in v0.30 and v0.31):
- Update-check fix (`c0f4345`, D-140, v0.30): S-22 "Check for update" calls `UpdateRepository.checkNow()`, which always asks `GET /apk/latest` and restarts the 6-hour window; the automatic checks keep the throttle (AC-16.2, AC-16.6).
- W6 Android Insights (`1c9856b`, D-141 to D-143, v0.31): `InsightsRepository` (E-34 trends, E-35 forecast, online only, names from Room); an "Insikter" section below S-11's local analytics following the measure toggle, 6 whole months, household and per-category line charts with the server's average and moving average, a forecast card per account ("Återkommande + snitt = prognos"); offline and error banners over the last data; 40 `s11_insights_*` goldens; Android gate green (791 tests in the feature run). `strings.csv` 588 rows (16 `analytics_insights_*` keys); the web dictionaries regenerated.
@@ -57,23 +65,23 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
- Validation reports (SPEC §11.4): `docs/validation/ac-coverage.md` (taken at `956908a`, `ed1b130` before the rewrite): 270 rows, 260 Done, 5 Partial, 1 Missing, 4 device-only; feature complete in code, not yet against §4's acceptance rule; its S-16 and S-22 mismatches are fixed by M12f. `docs/validation/security-review.md` (taken at `18c527f`): Critical 0, High 1, Medium 3, Low 10, Info 8; L-6 kept (D-116), L-7 fixed (D-116), L-1, L-2, L-5, L-9, M-2 fixed (D-117), L-8 fixed (`e70fe0d`); H-1 (`725901c`, D-115), M-3 (`baa5806`) and L-4 (`b985ce9`, D-114) fixed by the server overhaul; L-10 left as the spec says, decision pending (§4). Owner answers of 2026-10-19: M-1 no (backups stay unencrypted, rclone keeps the `drive` scope; accepted risk in SPEC §8.1 and §8.7), L-3 accepted and fixed (`96e4cbd`, D-119), the 16 MP and 512-character limits (D-114) confirmed; M-2's remaining question (the Home Assistant SSH key form) is moot with the NPM rework (D-120) and closed; D-120 also moves L-1 and the headers to NPM, makes L-2 moot, reverses the L-8 fix and replaces I-5's check with a conntrack check (dated notes in the review, SPEC §11.4). H-1 is fixed (`725901c`).
- Server overhaul (landed on `main` `8103dae` to `b985ce9`): shared helpers and one test harness, clippy pedantic wins; H-1 rate limiter (`725901c`, D-115), pictures at most 16 MP and 64 MB decoded (`baa5806`, D-114), `fcm_token` at most 512 characters (`b985ce9`, D-114); shared row inserts, invite creation and CLI helpers, `tests/common/` harness, synchronous `tasks::start`; 268 server tests; all crates at their latest versions. Recorded in SPEC v0.18.
- In flight:
- This docs pass (`agent-docs-v31`: SPEC v0.30 and v0.31, D-140 to D-143, `strings.csv`, the web dictionaries, design-spec S-11 and §3.10, this file).
- Web W1 to W5 (`web/src/features/**`, D-144 to D-154 reserved); the Pi image build with the web client and E-34/E-35.
- Record D-155 (`d686ee0`, install failures visible on S-17 and S-22) in the next spec version, v0.32, with `strings.csv` at 590 rows (docs agent).
- This docs pass (`agent-docs-v32`: SPEC v0.32, D-144 to D-155, design-spec §3.6, S-17, S-22, this file).
- The Pi image build with the complete web app (in progress on the Pi), then the owner's deploy and browser checks (§3 steps 4 and 5).
- Earlier outcomes (details in SPEC §11.2, §11.3 and `docs/decisions.md`): M8 end-to-end test passed on the owner's S26 against the dev server on 2026-10-18 (pairing, scripted partner, offline expense synced, push by `fid` delivered); M8b to M8f UI fixes; the M2c-pi bundle `infra/pi/` (D-103 to D-106, owner checklist in `infra/README.md` "Pi: what the owner does"); release builds R8-shrunk and signed (alias `kvitt`, RSA 4096, v2).
- Compatibility: an APK from M12e on cannot pair or list devices against a pre-M12e server, so the server is rebuilt and redeployed before a new APK is installed (SPEC §14.8). The dev server already runs schema 3.
- Remote: `origin` on Gitea, SSH key works; pushed at `7521c01`. The Gitea secret `kvitt_cicd` (a user-level secret of `jacobh2`, renamed by the owner 2026-10-19) works; the Drive token is `infra/secrets/rclone.conf` (`[gdrive]`; folder id in `.env` as `RCLONE_FOLDER_ID`). Unused on the Mac since D-120: `infra/secrets/ha_ssh_key` and `ha_ssh_key.pub` (git-ignored; may be deleted).
- Both gates exist: Android `tools/gradle-serial.sh check` (from `android/`), server `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test` (from `server/`, `cargo audit` since `f1abb0d`). Both green on `main` as far as reported (Android green at `1c9856b`, which fixed the three `:data` example tests red since `50a0d67`; server 299 tests at `50a0d67`). The third gate (W0, D-131): `npm run check` in `web/` (`tsc --noEmit`, `eslint .`, `vitest run`, `vite build`, generated-types freshness); a change to `contracts/openapi.yaml` regenerates `web/src/api/schema.d.ts` in the same landing.
- Both gates exist: Android `tools/gradle-serial.sh check` (from `android/`), server `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test` (from `server/`, `cargo audit` since `f1abb0d`). Both green on `main` as far as reported (Android green at `1c9856b`, which fixed the three `:data` example tests red since `50a0d67`; server 299 tests at `50a0d67`). The third gate (W0, D-131): `npm run check` in `web/` (generated files, `tsc --noEmit`, `eslint .`, Prettier, `vitest run`, `vite build`), green on `main` with W1 to W5 (59 files, 598 tests at `83f5293`); a change to `contracts/openapi.yaml` regenerates `web/src/api/schema.d.ts` in the same landing.
- Importer: the dry run of 2026-10-18 reconciled on both real files (Vardag 1746 rows, Hemma 37 rows, 0 unmapped). The production import on the Kvitt Pi is done (owner, 2026-10-21). The CSV files stay in `export/` (git-ignored).
- Development stack (M2c-dev): running on the Mac at `http://192.168.1.210:8080`, database at schema 3, data kept; backups `infra/dev/backups/kvitt-20261018T093143Z.sqlite.gz` and `kvitt-20261018T130443Z.sqlite.gz` (git-ignored). On this Podman setup run `infra/dev/up.sh --down`, then `infra/dev/up.sh`, to swap in a rebuilt image. Since `7fb86f5` `up.sh` copies only `fcm-service-account.json` into `infra/dev/secrets/` (git-ignored). The owner's phone is paired with it; a scripted partner exists (tokens held by the orchestrator outside the repo). Firebase project `kvitt-1337` with apps `se.hagstedt.kvitt` and `se.hagstedt.kvitt.debug`; service account `kvitt-fcm`.
- Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug`, paired against the dev server; the release app `se.hagstedt.kvitt`, rebuilt with the host `kvitt.suorra.duckdns.org` and no port (D-124) and installed 2026-10-19; paired against the Kvitt Pi 2026-10-21. The partner's phone is paired too.
- Disk: the MacBook had about 72 GB free during the v0.20 docs pass (2026-10-19); run `tools/clean-builds.sh` when it drops under about 5 GB.
## 2. Open work (one line each, with who owns it: agent or owner)
- Land `agent-docs-v31` (orchestrator): SPEC v0.30 and v0.31, D-140 to D-143, `strings.csv`, `web/src/i18n/*.json`, design-spec, HANDOFF; gate `python3 tools/contracts-check.py` (the web gate for the regenerated dictionaries).
- Pi image with the web client and E-34/E-35 (built from the `1c9856b` push): watch the workflow run, then the owner deploys it (§3 step 4), then HUMAN step 3; then publish a release APK with W6 (`infra/publish-apk.sh --deploy`) so the phones show Insights.
- Land `agent-docs-v32` (orchestrator): SPEC v0.32, D-144 to D-155, design-spec, HANDOFF; gates `python3 tools/contracts-check.py` and `npm run check` in `web/`.
- Pi image with the complete web app (built from the push of the web plans): watch the workflow run, then the owner deploys it (§3 step 4) and checks the web client (§3 step 5); HUMAN step 3 if the NPM block is not yet current. Release APK `kvitt-140` (W6 Insights, D-155) is already deployed; its Insights need this image.
- W0b web cleanup plan (agent, no plan yet): the "Web follow-ups" below.
- Closed by `28fa71b`: the `/opt/kvitt` leftovers in `.gitea/workflows/build-images.yaml` and `infra/build-images.sh` (compose deploy hint, `docker image inspect`). Still stale in the workflow header (agent, next infra pass): "NOT YET RUN" and "First-build duration on the Pi: unknown" (now 2 h 42 min).
- Pushed: through `1c9856b` (W6 Android Insights).
- Pushed: through the five web plans (orchestrator, 2026-10-21).
- Closed by `2acfc34` (`agent-gate-docs`): `infra/README.md`'s stale `CARGO_BUILD_JOBS` text and its "Owner decisions (open)" (now answered).
- `infra/backup/Dockerfile`: stale comment "on the Pi the container runs as uid 10001" (since D-125 the owner's uid); left as is, because editing it triggers a 5-minute image build (agent, with the next change there).
- Historical `:8443` and `icehack.asuscomm.com` mentions in `docs/ux/` and `docs/validation/` are left as the record of their time (no action unless the owner wants them rewritten).
@@ -88,15 +96,30 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
- `PLAN-M2c-pi.md` is superseded where it names Caddy, cert-sync, scripts or `/opt/kvitt` (note at its top, D-120 and D-125); SPEC §10 and `infra/README.md` are current. `PLAN-M11.md` has a note that `/opt/kvitt/import` is now `~/kvitt/import`.
- Plans (SPEC §11.2 v0.24), in dependency order; prompt `prompts/<id>.md`:
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7g, M8 (phone test passed), M8b to M8f, M9, M12a to M12f, U1, U2, R8 release build, M2c-pi bundle with its NPM rework and the self-contained compose project (D-125), server gap pass, infra overhaul, infra security, the server overhaul, the Android overhaul, the APK download rate limit, `cargo audit`, the Android host property and its follow-ups (D-122), the server host defaults (D-123), port 443 (D-124), pull-to-refresh (D-127), the editor keyboard fix, the S-12 line chart (D-128): landed.
- Web client programme (D-134, SPEC §11.2 rows W0 to W7): W0 (`2ae65a5`), W6 (`1c9856b`) and W7 (`50a0d67`) landed; W1 to W5 running in parallel (briefs from `prompts/W1.md` to `prompts/W5.md`); their decisions D-144 to D-154 are recorded by a docs agent when they land.
- Web client programme (D-134, SPEC §11.2 rows W0 to W7): all landed: W0 (`2ae65a5`), W1 (`934eee7`), W2 (`a65ab71`), W3 (`f9a9bfb`), W4 (`1aecf51`), W5 (`3ebe57d`), W6 (`1c9856b`), W7 (`50a0d67`); D-144 to D-154 recorded in v0.32. Next for the web: W0b (follow-ups below).
- Next: the app key and gate if not yet done (D-126, §3); M10 First release (keystore per §10.6, D-101; the reports `first-release.md` and `restore-drill.md`). Done 2026-10-21: release pairing of both phones against the Kvitt Pi and M11 Import history (SPEC §10.8 "Production run").
### Web follow-ups (for a W0b plan; agent)
- Shared pieces built two or three times in feature folders, to move into `web/src/components/` or `web/src/lib/`: an account tile (`features/accounts/AccountTile`, `features/analytics/components/AccountTile`); the loading and error cards (`PageStates` in `accounts`, `expenses`, `analytics`); `useToday` (`features/accounts/useToday.ts`, `features/analytics/shared.ts`); `categoryName`; the audit `Timeline` and its line builders (`features/expenses/Timeline.tsx`, `timelineLines.ts`, `features/settle/timeline.ts`); `apiErrorKey` and the rejection texts (`features/expenses/messages.ts`, `features/settle/messages.ts`); W1's `BalanceHero`, `Seesaw`, `AccountBalanceHeader`, `OverflowMenu` (D-145).
- `components/AmountField`: reads `,` as the decimal point in English too (should follow the page language, as `lib/money.ts`); needs `hideLabel` and `placeholder` props.
- `lib/ids.ts`: type the `random` parameter as `(bytes: Uint8Array<ArrayBuffer>) => void`.
- Vitest cache `web/node_modules/.vitest-cache`: its stale `catalog.ts` result is fixed at the root by `83f5293` (glob without a type argument, `catalog.test.ts` compares with the files); still decide whether to disable the cache or key it on the feature i18n files.
- `vite.config.ts`: the dev proxy lacks `/analytics` (W-11 under `npm run dev`); Recharts in its own chunk (the vendor chunk is 515 kB).
- `app/routes.ts`: `bucket` in `DrillDownKind` (D-151); builders for `?account=<id>` and `?settlement=<id>` (D-144), replacing `features/accounts/links.ts` and `features/settle/links.ts`.
- `lib/vectors.test.ts`: `spend_series` and `resolve_range` are still listed as not ported (their vectors run in `features/analytics/domain/analytics.vectors.test.ts`); move them to `PORTED` against the analytics port (`suggest_category` done in `83f5293`).
- W5's recurring editor (`features/settings/recurring/fields.tsx`) uses its own fields; swap them for `components/editor` (D-148).
- `editor_paid_by_toggle` says "Dubbeltryck" (TalkBack wording) on the web; a `web_` variant for a click.
- `web/package.json` `kvitt.versionCode` is fixed at 1 (`X-Kvitt-App-Version`, W-19); PLAN-W0 wanted the commit count at build time.
- `/app/settings/invites` is kept as an alias of `/app/settings/invite` (D-153); drop it if nothing links there.
- Closed by `83f5293`: the fake test server keeps non-JSON bodies raw.
## 3. HUMAN steps pending (relay in chat)
Done 2026-10-21: the Pi install (stack running, `v1.0.1`, both containers healthy), the public health check through NPM, bootstrap, release pairing of both phones, the Splitwise import.
1. App key (D-126; `infra/README.md` step 4a items 1 to 3; the key exists on the Mac and the Pi serves the keyed `kvitt-133.apk` since the first `--deploy` run, D-139; whether both phones run it is unconfirmed): generate `kvitt.appKey` in `android/local.properties` on the Mac (an agent may run the one-line `openssl` command; the value is never printed); build and publish a keyed APK (`infra/publish-apk.sh`, copy to `data/apk/`); both phones on it (update banner, or sideload).
2. Gate (step 4a items 4 and 5), only after both phones run the keyed APK: NPM, Hosts, Proxy Hosts, Kvitt row, Edit, Advanced: paste the step 4 block with the key in place of `KVITT_APP_KEY`, Save (row Online, yes/no). Acceptance test: on home Wi-Fi Chrome opens `https://kvitt.suorra.duckdns.org/i/TEST` and the invite page shows (yes/no); with Wi-Fi off the same URL gives a connection error (yes/no); with Wi-Fi off Kvitt's About screen shows the server version (yes/no).
3. Web client gate lines (D-130, SPEC v0.28): when the NPM block is pasted (item 2, or again if the D-126 block is already in NPM), paste the current `infra/npm/kvitt.conf` content (step 4), which also lets the web client through and sets its Content-Security-Policy; Save (row Online, yes/no). After the first image with the web client is deployed (W7, §10.10), acceptance test with Wi-Fi off on the phone: `https://kvitt.suorra.duckdns.org/app/` shows the Kvitt login page (yes/no) and `https://kvitt.suorra.duckdns.org/health` still gives a connection error (yes/no) (step 4a item 5 d and e).
4. Deploy the new image (web client and E-34/E-35, SPEC §10.10), once the Gitea workflow run for `1c9856b` has succeeded: on the Kvitt Pi in `~/kvitt`, set `KVITT_TAG` in `.env` to the tag the run printed, then `docker compose pull && docker compose up -d`; `server` and `backup` healthy (yes/no). Then the orchestrator publishes the release APK with Insights.
4. Deploy the new image (the complete web app and E-34/E-35, SPEC §10.10), once the Gitea workflow run (in progress on the Pi) has succeeded: on the Kvitt Pi in `~/kvitt`, set `KVITT_TAG` in `.env` to the tag the run printed, then `docker compose pull && docker compose up -d`; `server` and `backup` healthy (yes/no). The release APK `kvitt-140` is already deployed; S-11's Insights work once this image runs.
5. Web client in a browser (after step 4): on the phone, Inställningar › Para ihop en enhet › För mig, create a code; on the Mac open `https://kvitt.suorra.duckdns.org/app/`, "Logga in i Kvitt", type the code, log in: Home shows (yes/no). Then, each yes/no: Home and one account open; add, edit and delete an expense; Sök, export a CSV and open it in Numbers (columns and amounts right); Gör upp: record a payment and delete it; Statistik, open a category row (line chart), Insikter; each Settings page opens; on the phone Inställningar › Enheter shows `Web · <browser>`, "Ta bort åtkomst" on it, and the browser shows the login page with the removed-device message.
## 4. Owner questions (asked, unanswered)
- OQ-6 (SPEC §12): a confirm step on S-1 for an unusual server; default "not in v1".
@@ -108,9 +131,9 @@ Done 2026-10-21: the Pi install (stack running, `v1.0.1`, both containers health
- Nothing.
## 6. Numbering
- Decisions: D-001 to D-143 used, except D-008 and D-009 (never used). Reserved: **D-144 to D-154** for the web feature agents W1 to W5, **D-155** for the APK install-failure fix. Next free after those: **D-156**.
- Spec version: v0.31 written (v0.30 D-140, v0.31 D-141 to D-143). Next **v0.32**.
- Server schema version **3** (migration 0003); next **4**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); next **2**. Unchanged by v0.27 to v0.31 (W7 and W6 needed no migration).
- Decisions: D-001 to D-155 used, except D-008, D-009 and D-150 (never used). Next free: **D-156**.
- Spec version: v0.32 written (D-144 to D-155). Next **v0.33**.
- Server schema version **3** (migration 0003); next **4**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); next **2**. Unchanged by v0.27 to v0.32 (no plan since needed a migration).
- Sync protocol **1**, push payload **1**.
## 7. How the work is run
+12
View File
@@ -148,3 +148,15 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
| D-141 | 2026-10-21 | Insights UI contract. `:core:model` (`model/Insights.kt`) `InsightsRepository` with `trends(months: 1..24, measure)` and `forecast(measure)`, both returning `OnlineResult`: `Offline` without a request when offline (§9.9), `Failed("network")` when the server cannot be reached, else the §14.3.2 code. Types `SpendTrends`, `SpendTrend`, `TrendPoint`, `CategoryTrend` (`category: Category?`), `SpendForecast`, `AccountForecast` (`account: Account?`), `ForecastMonth`, named after the E-34/E-35 fields (§14.5.7). Category and account names resolved from Room, archived and deleted rows included; an unknown id gives null. Nothing is stored. `:data`: `api/AnalyticsDtos.kt`, two `KvittApi` calls, `repo/InsightsRepositoryImpl.kt`, bound in `DataModule`; `ExampleRoundTripTest` covers the E-34 and E-35 examples (fixing the gate red since `50a0d67`). Fakes: `FakeInsightsRepository`, `analytics/InsightsScenarios.kt`, scenario flag `insightsFailure`. Landed `1c9856b`. | The insights are server results (D-133), so the contract mirrors the wire objects and adds only what the phone knows better: names, colours and archived state from its own copy. A null for an unknown id lets the UI skip rows the phone has not synced yet instead of failing. | SPEC §14.2.4, §14.8; `android/core/model/src/main/kotlin/se/hagstedt/kvitt/model/Insights.kt`, `android/data/src/main/kotlin/se/hagstedt/kvitt/data/api/AnalyticsDtos.kt`, `android/data/src/main/kotlin/se/hagstedt/kvitt/data/repo/InsightsRepositoryImpl.kt`, `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/FakeInsightsRepository.kt` |
| D-142 | 2026-10-21 | Insights placement and loading. An "Insights" ("Insikter") section below S-11's local analytics (after "Per konto", or after the empty state), with the caption "Senaste 6 hela månaderna · Hushållet" ("· Min del" for my share). It follows S-11's measure toggle and always shows 6 whole months (E-34's default), whatever S-11's range. Both reads run in parallel and load when S-11 opens, when the measure changes to one not yet loaded, when the phone comes online, and on "Försök igen". The data is kept in memory per measure while S-11 is on the back stack, never stored. Offline shows the "Kräver anslutning" banner with "Insikterna räknas fram på servern. Anslut till internet för att se dem."; a failure shows an error banner ("Kunde inte nå servern" for `network`, else the code's text) with "Försök igen"; the last data stays visible under either banner; before the first load skeleton cards read "Hämtar insikter". S-11's offline analytics are unchanged (AC-14.1). Landed `1c9856b`. | The owner wanted the server's analytics in the app too (D-133); placing them under the local charts keeps S-11 fully usable offline, and a fixed 6-month window matches the server's default and S-11's default range without a second range control. | SPEC §13.3 S-11, AC-14.1, §15.8; `docs/ux/design-spec.md` S-11 "Insights"; `android/feature/analytics/src/main/kotlin/se/hagstedt/kvitt/feature/analytics/InsightsViewModel.kt`, `InsightsModel.kt` |
| D-143 | 2026-10-21 | Insights display rules. "Utgifter per månad": the household (or my-share) trend on D-128's `SpendLineChart` in `primary`, with the server's average (dashed) and moving average, omitted when the average is 0 or there is one point; under it "Snitt X i månaden", the last whole month against the average in D-128's words, and "Hittills i <månad>: X". "Kategorier per månad": the open category's chart in its category colour over a radio list of the categories in the server's order (tile, name, "Snitt X i månaden", change "+N %" / "−N %" / "0 %"); the open one defaults to the first, the largest total; category ids the phone does not hold are skipped. With no spending at all, one card "Inga utgifter de senaste 6 hela månaderna" replaces both charts. "Prognos" with its caption, then one card per account in the server's order (tile, muted when archived, name, "Hittills i <månad>: X", "Arkiverat" badge), its two months as rows "<Månad>" / "Återkommande X + snitt Y" with the forecast amount on the right (recurring + average = forecast); an account with nothing this month and zero forecasts shows one line "No recurring expenses and no recent spending" ("Inga återkommande utgifter och inga utgifter på sistone"). Goldens `s11_insights_<state>` for 13 states (loaded, loaded_scrolled, category, my_share, loading, offline, offline_kept, unreachable, unreachable_empty, empty, long, long_scrolled, screen) in three variants plus `s11_insights_long_sv_light_200`. Landed `1c9856b`. | Reusing D-128's chart and caption words keeps S-11's Insights and S-12 reading alike; showing the forecast's two parts makes the number explainable without a help text. | SPEC §13.3 S-11, §13.4; `docs/ux/design-spec.md` S-11 "Insights", §3.10; `docs/ux/strings.csv` `analytics_insights_*`; `android/feature/analytics/src/main/kotlin/se/hagstedt/kvitt/feature/analytics/InsightsSection.kt` |
| D-144 | 2026-10-21 | Preset account and payment on the web travel in the query string. W-5 New expense takes `/app/expenses/new?account=<id>` and W-7 Settle up `/app/settle?account=<id>` (S-5's "New expense" and "Settle this account"); W-7 also takes `/app/settle?settlement=<id>` to edit a saved payment (W-8 "Edit", AC-11.6). Link builders: `newExpenseFor`, `settleFor` in `web/src/features/accounts/links.ts` and `settleAccountPath`, `settlementEditPath` in `web/src/features/settle/links.ts`, until W0 adds builders for them to `web/src/app/routes.ts` (follow-up). On W-5 an id that is not an active account falls back to the AC-8.6 default. Landed `934eee7` (W1), `f9a9bfb` (W3). | §15.3 leaves query parameters to the owning plan; a query parameter keeps one route per page, survives a reload and needs no change in `src/app/`. | SPEC §15.6 W-3, W-5, W-7, W-8; `web/src/features/accounts/links.ts`, `web/src/features/settle/links.ts` |
| D-145 | 2026-10-21 | Web Home empty state and archived section. When the partner has joined and no account exists, W-2 shows the S-4-home-empty card ("Du och <partner> är med", `home_empty_welcome`, with both avatars and `home_empty_subtitle`) in place of the balance header, then the empty state with "Nytt konto"; while there is no partner it shows S-3's content. The "Arkiverade (n)" section is collapsed by default; its open state is kept in memory for the page session (navigation keeps it, a reload collapses it), never stored. W1 built `home/BalanceHero`, `home/Seesaw`, `accounts/AccountBalanceHeader`, `accounts/OverflowMenu` and `accounts/AccountTile` inside its own folders, because W0 has no shared versions yet (follow-up W0b). Landed `934eee7`. | A balance header over no accounts would show a meaningless 0 kr; S-4 has this card for the same moment. Keeping the archived state in memory matches S-4 (survives navigation, not a cold start) and adds no stored key. | SPEC §15.6 W-2; `web/src/features/home/HomePage.tsx` |
| D-146 | 2026-10-21 | Web CSV export format (W-12, the choice §15.7 left to W2). UTF-8 with a byte-order mark; `;` between fields; CRLF after every row; RFC 4180 quoting (a field holding `;`, `"`, CR or LF, or starting or ending with a space, is quoted, `"` doubled); amounts in kronor with a decimal comma, two decimals, no grouping and no sign ("1234,50", in every page language); dates ISO `YYYY-MM-DD`. Header row in the page language; columns date, description, category, account, payer, amount, one share column per household user ("Andel <namn>", users in avatar-slot order), notes; one row per expense in the list's order. A text cell starting with `=`, `+`, `-`, `@`, TAB or CR gets a leading `'` (CSV injection). File name `kvitt-utgifter-<YYYY-MM-DD>.csv` (Swedish) or `kvitt-expenses-<YYYY-MM-DD>.csv` (English), built in the browser as a `Blob` download. Landed `a65ab71`. | Excel and Numbers with a Swedish locale read the BOM as UTF-8, split on `;` and take "1234,50" as a number; a fixed decimal comma keeps the file the same whatever the page language. Formula neutralising follows OWASP. | SPEC §15.7, AC-21.10; `web/src/features/export/csv.ts`, `download.ts` |
| D-147 | 2026-10-21 | AC-8.6 and search on the web. AC-8.6's "on this phone" means this browser: the last-used account and the last category per account are kept in `localStorage` (`kvitt.editor.lastAccount`, `kvitt.editor.lastCategory`, per-viewer conveniences of §15.5). W-12's query and filters live in the URL query string (`q`, `min` and `max` in öre, `from`, `to`, `account`, `category`, `payer`), so a search can be bookmarked and survives a reload. Landed `a65ab71`. | The web has no device database; `localStorage` is the browser's equivalent of the phone's preferences. A search in the URL is the web's natural "remembered filter". | SPEC AC-8.6, §15.5, §15.7; `web/src/features/expenses/model.ts`, `web/src/features/search/filter.ts` |
| D-148 | 2026-10-21 | Web expense editor behaviour (W-5). A rejected save shows an error banner above the form with S-16's text for the §14.4.6 reason (`home_sync_reject_*`; `clock_skew` names the computer's clock, `web_expenses_reject_clock_skew`) and, when the server returned its row, "Visa nuvarande version" (`home_sync_lost_show_current`, opens W-6); that row goes into the snapshot and the form keeps what was typed. A failed request shows "Kunde inte nå servern" or the code's `error_*` text the same way. "Utgiften sparades" (`snackbar_expense_saved`) shows after an edit too (S-7 shows it after an add only). The discard dialog covers `Esc` and in-app navigation (a router blocker). Differences from design-spec S-7: no amount keypad (the browser keyboard), a native select for the account, a native date input with an "Idag"/"Igår" hint, two percent fields plus presets instead of the ratio slider, no compact summary row. The shared editor fields of design-spec §3.7 live in `web/src/components/editor/` (exported from `index.ts`: `AccountDot`, `CategoryPicker`, `ConfirmDialog`, `DateControl`, `DateField`, `NotesField`, `OptionRow`, `OptionRows`, `PayerRow`, `SelectControl`, `SelectField`, `SplitBar`, `SplitPreview`, `SplitSection`, and the helpers of `categories.ts` and `split.ts`), for W2 and W5's recurring editor. The §9.11 keyword table is imported raw from `contracts/category-keywords.csv` at build time; `web/vite.config.ts` got `server.fs.allow: ['.', '../contracts']` for it. Landed `a65ab71`. | The web keeps no pending rows (§15.5), so a rejection is reported where it happened, with the phone's words; showing the server's version without losing the typed values lets the user redo the change. The desktop controls replace phone-only gestures without changing the rules. | SPEC §15.5, §15.6 W-5; `web/src/features/expenses/ExpenseEditorPage.tsx`, `messages.ts`, `web/src/components/editor/index.ts`, `web/vite.config.ts` |
| D-149 | 2026-10-21 | W-7 allocation errors as you type. Once the amount is a valid number, the §14.5.3 tokens a user can fix in the rows (`allocation_not_positive`, `no_allocations`, `too_many_allocations`) show under the allocation rows while typing, not only after "Bekräfta"; `allocation_sum_mismatch` stays the remaining indicator's job and an empty amount the field's. The hint "Skriv ett belopp så föreslår Kvitt en fördelning" (`settle_empty_hint`) shows only while the amount is empty and there are no rows. Landed `f9a9bfb`. | design-spec S-9 puts invalid allocations inline under the rows; on a desktop page the rows and the Confirm button are visible together, so waiting for Confirm only delays the message. | SPEC §15.6 W-7; `web/src/features/settle/SettlePage.tsx` |
| D-150 | 2026-10-21 | Unused (reserved for the web feature plans W1 to W5; none needed it). | | |
| D-151 | 2026-10-21 | Web analytics drill-down routes and filter. Clicking a bar of W-9 (AC-14.8, a whole bucket) opens `/app/analytics/bucket/<key>` (`bucketHref`), beside `/app/analytics/:groupKind/:groupId` for a breakdown row; the filter travels in the query string: `range` (the preset), `from` and `to` (custom range only), `interval` (month or week), `measure`, `group` and `bucket` (a group within one bucket). W-10's own period and interval changes replace that query string (no new history entry). Landed `1aecf51`. | The drill-down must survive a reload and be linkable like every web page; reusing the `:groupKind` route with the kind `bucket` keeps W0's route table unchanged (`DrillDownKind` in `routes.ts` gains `bucket` in W0b). Replacing the query keeps Back going to W-9 rather than through every period tried. | SPEC §15.6 W-9, W-10; `web/src/features/analytics/shared.ts`, `web/src/features/analytics/domain/rules.ts` (`filterToSearch`, `filterFromSearch`) |
| D-152 | 2026-10-21 | Web chart accessibility and Insights months. The chart SVGs (`StackedBarChart`, `SpendLineChart`) and their legends are hidden from screen readers; the month or week labels under the bars are keyboard stops that open the bucket drill-down; "Visa som tabell" (`web_analytics_show_table`) under each chart shows its numbers as a table. W-11 Insights offers 3, 6 or 12 months (default 6, E-34's default) through `web_insights_months`, and follows the measure. Landed `1aecf51`. | A Recharts SVG is not readable by a screen reader; a table carries the same numbers, and real buttons make every bar reachable by keyboard (AC-21.11). 3, 6 and 12 cover the useful windows of E-34 (1 to 24) without a free field. | SPEC §15.6 W-9, W-10, W-11, AC-21.11; `web/src/features/analytics/components/ChartTable.tsx`, `web/src/features/analytics/domain/insights.ts` (`INSIGHT_MONTHS`) |
| D-153 | 2026-10-21 | W-18's route is `/app/settings/invite` (W0's `paths.settingsInvite`), not `/app/settings/invites` as §15.6 said; W5 also routes `/app/settings/invites` to the same page so the old address works. Sub-routes of W-15 and W-16: `/app/settings/categories/new`, `/app/settings/categories/:id`, `/app/settings/recurring/new`, `/app/settings/recurring/:id`. Landed `3ebe57d`. | W0's route table was fixed before W5 started and the singular matches the S-21 screen; the spec follows the code. | SPEC §15.6 W-15, W-16, W-18; `web/src/features/settings/index.ts` |
| D-154 | 2026-10-21 | Partner first login and About on the web. The S-2 step after a `partner` login (AC-2.10) is an intro on W-14 Profile with "Fortsätt"; this browser remembers it in `localStorage` under `kvitt.profileStepDone`, holding the device id, so a later login on the same browser as a new device shows it again. W-19 About reads E-1 `/health`; when it does not answer (from outside the LAN, §15.2), it reads the `X-Kvitt-Server-Version` header of a fresh `GET /me` and shows the version without the schema (D-110's fallback). Landed `3ebe57d`. | AC-2.10 asks for the step once; the device id ties "once" to this login rather than to the browser forever. A fresh request gives a current header instead of a cached one. | SPEC AC-2.10, §15.2, §15.6 W-14, W-19; `web/src/features/settings/ProfilePage.tsx`, `AboutPage.tsx` |
| D-155 | 2026-10-21 | A failed or declined update install is no longer silent. The `PackageInstaller` session's final status reaches `UpdateRepository` by session id (`InstallResultReceiver` hands it to the waiting `AndroidApkInstaller` call; `ApkInstaller.install` returns a `PendingInstall`). `STATUS_FAILURE_BLOCKED` gives `UpdateState.Failed(info, UpdateFailure.Blocked)`: "Kunde inte installera uppdateringen" (`update_install_failed`) with the security-settings text (`update_install_blocked`); `STATUS_FAILURE_ABORTED` (the user declined the system dialog) goes back to `Available` without an error; every other failure gives `Failed(info, UpdateFailure.Install(detail))` with the installer's own status message as the body. `UpdateState.Failed` gains `reason: UpdateFailure = UpdateFailure.Download` (`sealed interface UpdateFailure { Download; Blocked; Install(detail: String?) }`, `:core:model` `Sync.kt`), so existing fakes are unchanged. The cached APK is deleted once the installer answers, so "Försök igen" downloads again. Goldens `S-17-install-blocked`, `S-17-install-failed`, `s22_about_install_blocked` new, `update_banner` re-recorded. Landed `d686ee0`. | A refused or failed install left the banner as it was, so a tap on "Uppdatera" seemed to do nothing. Showing the reason tells the user which setting to change or what the installer said; a declined dialog is the user's choice, not an error. | SPEC AC-16.7, §14.2.4; `docs/ux/design-spec.md` §3.6 `UpdateBanner`, S-17; `docs/ux/strings.csv`; `android/core/model/src/main/kotlin/se/hagstedt/kvitt/model/Sync.kt`, `android/data/src/main/kotlin/se/hagstedt/kvitt/data/update/Updater.kt`, `android/core/design/src/main/kotlin/se/hagstedt/kvitt/design/component/Sync.kt` |
+6 -3
View File
@@ -508,7 +508,7 @@ Every default category has its own colour; `teal` is shared with the archived ex
**`Banner(kind: Lantern | Error | Warning | Neutral, icon, title, body, actions)`**: radius 24, padding 14 / 16 / 10, gap 6; icon 24 (filled) + title (title-small) + body (body-medium); actions right-aligned, text buttons in the banner's content colour. Lantern = `tertiary-container`, Error = `error-container`, Warning = `warning-container`, Neutral = `surface-container-high`. `role` status (alert for Error).
**`UpdateBanner(state: UpdateState, onUpdate, onLater, onRetry)`** (S-4, S-22): `Available`: Lantern, `system_update`, "Version 1.2 finns", "Nyheter och rättningar · 8,4 MB" (size from `UpdateInfo.sizeBytes`), actions "Senare" (text) and "Uppdatera" (tertiary filled, `download`); offline: "Uppdatera" disabled and the body reads "Kräver anslutning". `Downloading`: Lantern, `downloading`, "Laddar ner… 64 %", `LinearWavyProgressIndicator` in `tertiary` (determinate, track `tertiary` at 28 %), no actions; TalkBack progress "Laddar ner version 1.2, 64 procent". `Failed`: Error, `error`, "Nedladdningen misslyckades. Försök igen." with "Senare" and "Försök igen" (error filled, `refresh`). `NeedsInstallPermission`: Available look; "Uppdatera" opens the S-17 dialog.
**`UpdateBanner(state: UpdateState, onUpdate, onLater, onRetry)`** (S-4, S-22): `Available`: Lantern, `system_update`, "Version 1.2 finns", "Nyheter och rättningar · 8,4 MB" (size from `UpdateInfo.sizeBytes`), actions "Senare" (text) and "Uppdatera" (tertiary filled, `download`); offline: "Uppdatera" disabled and the body reads "Kräver anslutning". `Downloading`: Lantern, `downloading`, "Laddar ner… 64 %", `LinearWavyProgressIndicator` in `tertiary` (determinate, track `tertiary` at 28 %), no actions; TalkBack progress "Laddar ner version 1.2, 64 procent". `Failed`: Error, `error`, "Senare" and "Försök igen" (error filled, `refresh`; downloads again); title and body follow `UpdateFailure` (D-155): `Download` "Nedladdningen misslyckades. Försök igen." (`update_failed`), no body; `Blocked` "Kunde inte installera uppdateringen" (`update_install_failed`) with the body "Installationen blockerades av telefonens säkerhetsinställningar (Auto Blocker eller Installera okända appar). Ändra inställningen och försök igen." (`update_install_blocked`); `Install(detail)` the same title with the installer's own message as the body (none when it gave none). A declined system install dialog is not a failure: the banner returns to `Available`. `NeedsInstallPermission`: Available look; "Uppdatera" opens the S-17 dialog.
**`NeedsConnectionBanner(body: String)`**: Warning banner, `cloud_off`, title "Kräver anslutning", screen-specific body. **`NeedsConnectionHint`**: supporting line under a control: `cloud_off` 16 + "Kräver anslutning" in body-small weight 600 `on-surface-variant`, padding 4 / 16.
@@ -1126,7 +1126,9 @@ Not a screen: the `UpdateBanner` on S-4 (and S-22) and one dialog.
|---|---|---|
| `S-17-update` | variant first-time dialog | the dialog over S-4 with the `Available` banner |
| `S-17-update-progress` | loading (downloading) | banner `Downloading` 64 % |
| `S-17-update-failed` | error | banner `Failed` |
| `S-17-update-failed` | error | banner `Failed` (download) |
| `S-17-install-blocked` | error | banner `Failed(Blocked)` with the security-settings body (D-155) |
| `S-17-install-failed` | error | banner `Failed(Install(detail))` with the installer's message (D-155) |
**Interactions**: "Uppdatera" → `install()`; after the system setting, returning to the app continues; verified download → system installer (out of the app's hands). "Senare" hides the banner until the next cold start. Offline: "Uppdatera" disabled with "Kräver anslutning". Debug builds never show it (AC-16.5).
@@ -1207,8 +1209,9 @@ Not a screen: the `UpdateBanner` on S-4 (and S-22) and one dialog.
| Frame | State |
|---|---|
| `S-22-about` | loaded |
| `s22_about_install_blocked` | the `UpdateBanner` `Failed(Blocked)` variant above the facts (D-155) |
**Interactions**: "Sök efter uppdatering" → `check()` with progress in the button; result: the banner appears, or snackbar "Du har den senaste versionen"; network → "Kunde inte nå servern".
**Interactions**: "Sök efter uppdatering" → `checkNow()` (D-140; skips the 6-hour throttle) with progress in the button; result: the banner appears, or snackbar "Du har den senaste versionen"; network → "Kunde inte nå servern".
### Notifications (`:app` builds them, copy in `:core:design`)