SPEC v0.39: K2's report recorded (D-191 to D-200), card design-spec S-23 to S-27
- SPEC: §11.2 K2 row landed a8f91ee with its gate, §11.3, §12 OQ-9, §13.2 the destination enum as
built, §13.3 S-5, S-13, S-23 to S-27 as built and K2's four added keys, §14.2.1 (D-192, D-193),
§14.2.2 (card_providers, D-194), §14.2.3 (Room 2 landed), §14.2.4 readings fixed in v0.39
(D-191 to D-196, OQ-9), change log v0.39.
- docs/decisions.md: D-191 to D-200.
- docs/ux/design-spec.md: S-23 to S-27 from K2's goldens, §3.11 four destinations and the
label shrink-to-fit (D-197), §3.12 card artwork, credit_card and card icons, the card states of
S-5, S-7, S-8, S-13, cards_ prefix.
- docs/HANDOFF.md: K2 and K3 landed, Android gate green, PLAN-K2 §8 steps pending, numbering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
+15
-11
@@ -3,8 +3,8 @@
|
||||
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
|
||||
|
||||
## 1. Where things stand
|
||||
- State as of 2026-10-24. `main` at `671c00d` (**K1 landed**: card purchases on the server, schema 4, E-36 to E-44), after `ae73f58` (SPEC v0.37, D-211), `8b620c8` (the K0 report, SPEC v0.36), `8fa9198` (`infra/build-images.sh` Podman detection), `f15fa23` (`infra/build-images.sh` passes `KVITT_VERSION_CODE`), `4339d01` (the owner's SJ Prio card artwork), `b5114ad` (SPEC v0.35, card purchases), `2ce317f` (the K0 card test app), `5d948f9` (SPEC v0.34) and `5305f37` (W0b web shared follow-ups); before them `07c36d2` (L-10, D-156) and `593c727` (SPEC v0.33, D-156, D-157). This pass (`agent-docs-k1`): SPEC v0.38, K1's report as D-181 to D-188; `main` with this pass is pushed by the orchestrator after it lands. **Deployed and verified 2026-10-24** (owner, D-211): image `v1.0.3` (git tag `v1.0.3` = `8fa9198`, built on the Mac) runs on the Kvitt Pi, `server` and `backup` healthy; the web client is live at `https://kvitt.suorra.duckdns.org/app/`, paired from the laptop browser with a device invite, and the owner reports "All seems to work" for Home and an account, add, edit and delete an expense, CSV export, record and delete a payment, analytics with category drill-down and Insights, the settings pages, revoking the browser from the phone, and the Android Insights section on S-11 against the new server. W0 to W7 are landed and verified (SPEC §11.2, §15). Release 140 (`kvitt-140`, deployed 2026-10-21) runs on both phones. Gates on `main` at `671c00d`: server 356 tests, web 609 tests (`match_card_purchase` listed in `NOT_PORTED` until K3), contracts-check 157 (17 `match_card_purchase` cases). **The Android gate on `main` is red by design until K2 lands**: `:domain` `VectorFilesTest` and `:data` `ExampleRoundTripTest` pin the contract's vector and example sets, which K1 extended (contract-first landing; K1 was committed with the pre-commit hook bypassed for that reason). Until K2 lands, a commit on `main` that runs the hook needs the same bypass, decided by the orchestrator.
|
||||
- **Card purchases programme** (owner request and answers 2026-10-24, SPEC v0.35, D-158 to D-176, FR-22, §9.12): each user connects their own SJ Prio Mastercard (SEB Kort) through Enable Banking's restricted production mode with BankID; the server alone holds the key and sessions, fetches purchases up to 4 times a day in the background plus "Uppdatera", and only the card holder sees them (not in `GET /sync`, audit, push or analytics); a fourth bottom destination "Kortköp" (Översikt, Kortköp, Statistik, Inställningar, revising D-048), "Anslut kort" in Settings with the SJ Prio artwork, "Skapa utgift" prefilled into S-7, one-tap links to matching expenses (same amount, ±3 days), "Från kortköp" on account detail; server schema 4 (migration 0004), Room 2, endpoints E-36 to E-44, screens S-23 to S-27, web W-21 to W-23. Plans `PLAN-K0.md` to `PLAN-K3.md`, prompts `prompts/K0.md` to `prompts/K3.md`, written by `agent-a3b7abf183f883261`, landed `b5114ad`. **K0 done** (2026-10-24): the test app (`2ce317f`) ran with the owner on his card; report [`docs/spikes/K0-enable-banking.md`](spikes/K0-enable-banking.md); its answers are SPEC v0.36 (D-177 to D-180). Main findings: the card returns at most 100 transactions per response and never a continuation key, so history is fetched in windows of at most 30 days, split in half at 100 rows (D-178); merchant text from `remittance_information`, the date `booking_date` else `value_date`, everything in SEK, `entry_reference` on pending rows too (D-179); consent 180 days; background fetch accepted; at least 150 days of history right after BankID. **Owner's assumptions** (2026-10-24, "let's just assume it all works now given your test", D-177), verified by K1's owner steps (PLAN-K1 §8 steps 5 to 7): the partner's card links to the owner's application (fallback: her own application, D-175), a pending purchase keeps its `entry_reference` when booked (fallback: CP-8's amount-and-date rule), the session is reused the next day without BankID. The Enable Banking application exists (restricted production, redirect URL `https://kvitt.suorra.duckdns.org/cards/callback`, the owner's card linked); its key file lives only in the main checkout's git-ignored `docs/spikes/k0/.local/<application id>.pem` and reaches the Pi through K1's owner steps (`~/kvitt/secrets/cards/enable-banking.pem`, D-175), never downloaded again. The artwork is committed for Android and the web, cropped to the card (D-180). **K1 landed** `671c00d` (2026-10-24): migration 0004 (byte-identical contract and server copies), `src/cards/` with the registry, the Enable Banking adapter, the windowed fetch, the poller and the callback page, E-36 to E-44 with four new error codes and a 502 response, examples, `match_card_purchase.json` (17 cases) with its Python reference, `tests/cards_api/` (34 tests against a local Enable Banking stub; the real API is never called), the Pi compose mount of `secrets/cards/` and the NPM `/cards/callback` block; its report is SPEC v0.38 (D-181 to D-188). **Running: K2** (Android, its text into v0.39) and **K3** (web, v0.40), decisions from D-191 to D-210 in disjoint ranges; K2 lands next, K3 after K2. The key is staged at `infra/secrets/cards/enable-banking.pem` in the main checkout (git-ignored); the application id is known to the orchestrator (not written here, CP-13).
|
||||
- State as of 2026-10-24. `main` at `faeb752` (**K3 landed**: card purchases on the web, W-21 to W-23; its report goes into SPEC v0.40, next docs pass), after `a8f91ee` (**K2 landed**: card purchases on Android, Room 2, S-23 to S-27; recorded in SPEC v0.39 by this pass), `07df30d` (SPEC v0.38, K1's report), `671c00d` (**K1 landed**: card purchases on the server, schema 4, E-36 to E-44), `ae73f58` (SPEC v0.37, D-211), `8b620c8` (the K0 report, SPEC v0.36), `8fa9198` (`infra/build-images.sh` Podman detection), `f15fa23` (`infra/build-images.sh` passes `KVITT_VERSION_CODE`), `4339d01` (the owner's SJ Prio card artwork), `b5114ad` (SPEC v0.35, card purchases), `2ce317f` (the K0 card test app), `5d948f9` (SPEC v0.34) and `5305f37` (W0b web shared follow-ups); before them `07c36d2` (L-10, D-156) and `593c727` (SPEC v0.33, D-156, D-157). This pass (`agent-docs-k2`): SPEC v0.39, K2's report as D-191 to D-200, the design-spec sections S-23 to S-27; `main` with this pass is pushed by the orchestrator after it lands. **Deployed and verified 2026-10-24** (owner, D-211): image `v1.0.3` (git tag `v1.0.3` = `8fa9198`, built on the Mac) runs on the Kvitt Pi, `server` and `backup` healthy; the web client is live at `https://kvitt.suorra.duckdns.org/app/`, paired from the laptop browser with a device invite, and the owner reports "All seems to work" for Home and an account, add, edit and delete an expense, CSV export, record and delete a payment, analytics with category drill-down and Insights, the settings pages, revoking the browser from the phone, and the Android Insights section on S-11 against the new server. W0 to W7 are landed and verified (SPEC §11.2, §15). Release 140 (`kvitt-140`, deployed 2026-10-21) runs on both phones. Gates: server 356 tests at `671c00d`; contracts-check 157 (17 `match_card_purchase` cases); Android green again at `a8f91ee` (`BUILD SUCCESSFUL`, 791 actionable tasks, 1154 tests, 0 failures; the card examples E-36 to E-44 round-trip, `VectorFilesTest` and `ExampleRoundTripTest` pass), so the pre-commit hook needs no bypass any more; the web gate at `faeb752` is K3's to report (v0.40).
|
||||
- **Card purchases programme** (owner request and answers 2026-10-24, SPEC v0.35, D-158 to D-176, FR-22, §9.12): each user connects their own SJ Prio Mastercard (SEB Kort) through Enable Banking's restricted production mode with BankID; the server alone holds the key and sessions, fetches purchases up to 4 times a day in the background plus "Uppdatera", and only the card holder sees them (not in `GET /sync`, audit, push or analytics); a fourth bottom destination "Kortköp" (Översikt, Kortköp, Statistik, Inställningar, revising D-048), "Anslut kort" in Settings with the SJ Prio artwork, "Skapa utgift" prefilled into S-7, one-tap links to matching expenses (same amount, ±3 days), "Från kortköp" on account detail; server schema 4 (migration 0004), Room 2, endpoints E-36 to E-44, screens S-23 to S-27, web W-21 to W-23. Plans `PLAN-K0.md` to `PLAN-K3.md`, prompts `prompts/K0.md` to `prompts/K3.md`, written by `agent-a3b7abf183f883261`, landed `b5114ad`. **K0 done** (2026-10-24): the test app (`2ce317f`) ran with the owner on his card; report [`docs/spikes/K0-enable-banking.md`](spikes/K0-enable-banking.md); its answers are SPEC v0.36 (D-177 to D-180). Main findings: the card returns at most 100 transactions per response and never a continuation key, so history is fetched in windows of at most 30 days, split in half at 100 rows (D-178); merchant text from `remittance_information`, the date `booking_date` else `value_date`, everything in SEK, `entry_reference` on pending rows too (D-179); consent 180 days; background fetch accepted; at least 150 days of history right after BankID. **Owner's assumptions** (2026-10-24, "let's just assume it all works now given your test", D-177), verified by K1's owner steps (PLAN-K1 §8 steps 5 to 7): the partner's card links to the owner's application (fallback: her own application, D-175), a pending purchase keeps its `entry_reference` when booked (fallback: CP-8's amount-and-date rule), the session is reused the next day without BankID. The Enable Banking application exists (restricted production, redirect URL `https://kvitt.suorra.duckdns.org/cards/callback`, the owner's card linked); its key file lives only in the main checkout's git-ignored `docs/spikes/k0/.local/<application id>.pem` and reaches the Pi through K1's owner steps (`~/kvitt/secrets/cards/enable-banking.pem`, D-175), never downloaded again. The artwork is committed for Android and the web, cropped to the card (D-180). **K1 landed** `671c00d` (2026-10-24): migration 0004 (byte-identical contract and server copies), `src/cards/` with the registry, the Enable Banking adapter, the windowed fetch, the poller and the callback page, E-36 to E-44 with four new error codes and a 502 response, examples, `match_card_purchase.json` (17 cases) with its Python reference, `tests/cards_api/` (34 tests against a local Enable Banking stub; the real API is never called), the Pi compose mount of `secrets/cards/` and the NPM `/cards/callback` block; its report is SPEC v0.38 (D-181 to D-188). **K2 landed** `a8f91ee` (2026-10-24): Room version 2 (`MIGRATION_1_2` with its test, `2.json`), the card cycle after each household cycle (D-191 to D-196), `:feature:cards` S-23 to S-27 with 135 goldens, S-5 "Från kortköp", S-7 prefill and link, S-8 card line, S-13 "Anslut kort", the fourth bottom destination "Kortköp" (labels shrink to fit, D-197), 77 `strings.csv` rows, new catalog entry `androidx.browser` 1.10.0; recorded in SPEC v0.39 (D-191 to D-200, OQ-9). The phone was not attached, so nothing of K2 has run on a device yet; the orchestrator builds and installs the release APK with the card screens after landing. **K3 landed** `faeb752` (web W-21 to W-23); its report is recorded next (v0.40, decisions from D-201 to D-210). The key is staged at `infra/secrets/cards/enable-banking.pem` in the main checkout (git-ignored); the application id is known to the orchestrator (not written here, CP-13).
|
||||
- **Verified 2026-10-24** (owner, D-156): Samsung Auto Blocker was the cause of the silent update failure; turning it off let the update install, and the owner turned it back on, so every in-app update on the Samsung phones needs it off (the D-155 blocked text names it; SPEC AC-16.3, `infra/README.md`). The editor keyboard fix (S-19, S-15, S-6, `05ec3f3`) works on the device. The first backup on the Kvitt Pi ran and a snapshot pair is in the Drive folder (AC-18.1). The runner Pi's disk reached 11 of 15 GB during image builds; the owner runs `docker builder prune -af` between builds (SPEC §10.3).
|
||||
- **Kvitt host name** (owner decision 2026-10-19): `kvitt.suorra.duckdns.org`, a subdomain of the owner's DuckDNS family domain `suorra.duckdns.org`; NPM issues its Let's Encrypt certificate; `KVITT_PUBLIC_HOST=kvitt.suorra.duckdns.org`, so `KVITT_PUBLIC_URL=https://kvitt.suorra.duckdns.org` (port 443, D-124). It is in `infra/pi/.env.example` and `infra/pi/.env` (`KVITT_PUBLIC_HOST`) and `android/local.properties` (`kvitt.publicHost`, git-ignored). SPEC §10.1 and §10.5.1 point here.
|
||||
- **NPM proxy host** for `kvitt.suorra.duckdns.org` is Online with a Let's Encrypt certificate (owner, 2026-10-19). The router needs no new rule: external TCP 443 already forwards to the Home Assistant Pi (owner confirmed, D-124).
|
||||
@@ -67,21 +67,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 answered 2026-10-24: `not_found` for the other user's device (D-156), fixed in `07c36d2`. Owner answers of 2026-10-19: M-1 no (backups stay unencrypted, rclone keeps the `drive` scope; accepted risk in SPEC §8.1 and §8.7), L-3 accepted and fixed (`96e4cbd`, D-119), the 16 MP and 512-character limits (D-114) confirmed; M-2's remaining question (the Home Assistant SSH key form) is moot with the NPM rework (D-120) and closed; D-120 also moves L-1 and the headers to NPM, makes L-2 moot, reverses the L-8 fix and replaces I-5's check with a conntrack check (dated notes in the review, SPEC §11.4). H-1 is fixed (`725901c`).
|
||||
- Server overhaul (landed on `main` `8103dae` to `b985ce9`): shared helpers and one test harness, clippy pedantic wins; H-1 rate limiter (`725901c`, D-115), pictures at most 16 MP and 64 MB decoded (`baa5806`, D-114), `fcm_token` at most 512 characters (`b985ce9`, D-114); shared row inserts, invite creation and CLI helpers, `tests/common/` harness, synchronous `tasks::start`; 268 server tests; all crates at their latest versions. Recorded in SPEC v0.18.
|
||||
- In flight:
|
||||
- This docs pass (`agent-docs-k1`): SPEC v0.38, `docs/decisions.md` D-181 to D-188 (D-189, D-190 unused), `PLAN-K2.md`, `PLAN-K3.md`, `prompts/README.md` (v0.39 and v0.40), this file.
|
||||
- K2 (Android) and K3 (web), Opus agents in their own worktrees.
|
||||
- This docs pass (`agent-docs-k2`): SPEC v0.39, `docs/decisions.md` D-191 to D-200, `docs/ux/design-spec.md` (S-23 to S-27, §3.11, §3.12, S-5, S-7, S-8, S-13), this file.
|
||||
- Next: the docs pass for K3's report (SPEC v0.40, D-201 to D-210).
|
||||
- Earlier outcomes (details in SPEC §11.2, §11.3 and `docs/decisions.md`): M8 end-to-end test passed on the owner's S26 against the dev server on 2026-10-18 (pairing, scripted partner, offline expense synced, push by `fid` delivered); M8b to M8f UI fixes; the M2c-pi bundle `infra/pi/` (D-103 to D-106, owner checklist in `infra/README.md` "Pi: what the owner does"); release builds R8-shrunk and signed (alias `kvitt`, RSA 4096, v2).
|
||||
- Compatibility: an APK from M12e on cannot pair or list devices against a pre-M12e server, so the server is rebuilt and redeployed before a new APK is installed (SPEC §14.8). The dev server already runs schema 3.
|
||||
- Remote: `origin` on Gitea, SSH key works; pushed at `c13be3b` (2026-10-24). The Gitea secret `kvitt_cicd` (a user-level secret of `jacobh2`, renamed by the owner 2026-10-19) works; the Drive token is `infra/secrets/rclone.conf` (`[gdrive]`; folder id in `.env` as `RCLONE_FOLDER_ID`). Unused on the Mac since D-120: `infra/secrets/ha_ssh_key` and `ha_ssh_key.pub` (git-ignored; may be deleted).
|
||||
- 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.
|
||||
- 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 `a8f91ee`, 1154 tests; server 356 tests at `671c00d`). 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)
|
||||
- K1 landed `671c00d` (recorded in v0.38, D-181 to D-188). K2 and K3 running (agents; decisions from D-191 to D-210 in disjoint ranges, spec text into v0.39 for K2 and v0.40 for K3): K2 lands next and turns the Android gate green again, K3 after K2. Next: build the image with K1 (`infra/build-images.sh v1.0.N`, orchestrator), then K1's owner steps 1 to 4 (§3). After a client with the card screens is deployed, K1's owner steps 5 to 7 verify K0's three assumptions (D-177).
|
||||
- K1 (`671c00d`, v0.38), K2 (`a8f91ee`, v0.39) and K3 (`faeb752`, report pending for v0.40) landed. The image with K1 is built as `v1.0.4` (git tag on `671c00d`); its deploy on the Pi is K1's owner steps 1 to 4 (§3; the orchestrator records the owner's report). Then the release APK with the card screens (orchestrator), PLAN-K2 §8 steps 1 to 7 and PLAN-K1 §8 steps 5 to 7 (§3), which verify K0's three assumptions (D-177). Docs agent next: K3's report as SPEC v0.40.
|
||||
- OQ-9 (owner, §4): `CardPurchase.linked` cannot tell "unlinked" from "linked to an expense not yet on the phone"; a `linkedExpenseId` field would be a contract change.
|
||||
- K2's dependency report (agent, next Android pass): datastore 1.2.1 → 1.3.0, kotlinx-serialization 1.11.0 → 1.12.0, roborazzi 1.76.0 → 1.77.0, firebaseBom 35.0.2 → 35.1.0 (each one minor behind); Gradle 9.8.1 → 9.8.2 (patch).
|
||||
- K0 test app session in `docs/spikes/k0/.local/state.json` (owner): keep it or press "9 Koppla från" in the test app (PLAN-K0 §6 step 7); K1 does not use it. The key file stays in `.local/` either way.
|
||||
- Card design-spec sections S-23 to S-27 for `docs/ux/design-spec.md` (docs agent, from K2's report).
|
||||
- Closed by this pass: the card design-spec sections S-23 to S-27 (`docs/ux/design-spec.md`, written from K2's goldens).
|
||||
- Closed by `f15fa23` and `8fa9198`: `infra/build-images.sh` passes `--build-arg KVITT_VERSION_CODE` to the server build as the workflow does (v0.34), and detects Podman without a pipe.
|
||||
- Closed 2026-10-24 (D-211): image builds moved to the Mac (`infra/build-images.sh v1.0.N`, the workflow the fallback); `v1.0.3` deployed on the Kvitt Pi; the web client checked from the laptop browser and S-11's Insights on the phone.
|
||||
- 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).
|
||||
@@ -114,10 +116,12 @@ Standing note for every in-app update on the Samsung phones: turn Auto Blocker o
|
||||
1. Gate (step 4a items 4 and 5), both phones run the keyed APK (release 140): NPM, Hosts, Proxy Hosts, Kvitt row, Edit, Advanced: paste the step 4 block with the key in place of `KVITT_APP_KEY`, Save (row Online, yes/no). Acceptance test: on home Wi-Fi Chrome opens `https://kvitt.suorra.duckdns.org/i/TEST` and the invite page shows (yes/no); with Wi-Fi off the same URL gives a connection error (yes/no); with Wi-Fi off Kvitt's About screen shows the server version (yes/no).
|
||||
2. Web client gate lines (D-130, SPEC v0.28): when the NPM block is pasted (item 1, or again if the D-126 block is already in NPM), paste the current `infra/npm/kvitt.conf` content (step 4), which also lets the web client through and sets its Content-Security-Policy; Save (row Online, yes/no). The image with the web client is deployed (`v1.0.3`, D-211); acceptance test with Wi-Fi off on the phone: `https://kvitt.suorra.duckdns.org/app/` shows the Kvitt login page (yes/no) and `https://kvitt.suorra.duckdns.org/health` still gives a connection error (yes/no) (step 4a item 5 d and e).
|
||||
3. Card purchases: K0's owner steps are done (2026-10-24: Enable Banking account and Terms of Service, the application, the key in `docs/spikes/k0/.local/`, the owner's card linked, the test app run); the artwork is committed (D-180). **K1's owner steps 1 to 4 are pending** (PLAN-K1 §8), relayed by the orchestrator in chat after the image with K1 is built: step 1 is done by the orchestrator (the key staged at `infra/secrets/cards/enable-banking.pem` in the main checkout, git-ignored, mode 0600); step 2 on the Pi in `~/kvitt`: `mkdir -p secrets/cards && chmod 700 secrets/cards` before anything else (the new `compose.yaml` refuses to start without that folder, even with no card configured, D-188), `scp` the key, `chmod 600 secrets/cards/*`, copy the new `compose.yaml`, set `KVITT_EB_APP_ID=` in `.env` (the orchestrator gives the id in chat) and leave `KVITT_EB_MEMBER_APP_ID=` empty; step 3 paste the current `infra/npm/kvitt.conf` into NPM (row Online, yes/no); step 4 backup, `KVITT_TAG`, `docker compose pull && docker compose up -d` (`server` healthy, `/health` shows `"schema_version":4`, yes/no). The partner's card, the next-day check and pending to booked (PLAN-K1 §8 steps 5 to 7) follow once a client with the card screens is deployed.
|
||||
4. Card purchases on the phone (PLAN-K2 §8, all pending; after K1's image runs on the Pi and the release APK with the card screens is installed, Auto Blocker off for the update and back on afterwards): 1 Inställningar › Anslut kort › SJ Prio Mastercard › 30 dagar › "Anslut med BankID", sign in BankID, back in Kvitt "Kortet är anslutet" and the Kortköp tab lists the purchases (yes/no each); 2 Kortköp › a purchase › "Skapa utgift" › "Spara": the expense is in its account and the purchase under "Kopplade" (yes/no); 3 on a purchase the partner already entered, the row's "Koppla till …" moves it to "Kopplade" (yes/no); 4 flight mode on, "Ignorera" a purchase, flight mode off: still ignored after a sync (yes/no); 5 an account › menu › "Från kortköp" › a purchase › "Spara": the expense is in that account (yes/no); 6 Inställningar › Anslut kort › SJ Prio Mastercard › "Koppla från" › "Koppla från": Kortköp shows "Anslut ditt kort", and after reconnecting the linked purchases are under "Kopplade" (yes/no); 7 on the partner's phone after she connects her card (PLAN-K1 §8 step 5): Kortköp shows only her purchases and none of the owner's (yes/no).
|
||||
|
||||
## 4. Owner questions (asked, unanswered)
|
||||
- OQ-7 (SPEC §12): a notification for new card purchases? Default: none in this version (D-171).
|
||||
- OQ-8 (SPEC §12): turning several card purchases into expenses at once? Default: not in this version (D-171).
|
||||
- OQ-9 (SPEC §12): a purchase linked to an expense the phone has not pulled yet shows under "Kopplade" without its line and offers "Skapa utgift" in its sheet until the next sync; add a `linkedExpenseId` to tell it apart (a UI-contract change)? Default: not now. Note only, no question: S-25's "timed out" relies on the server answering `expired` (15 minutes), the phone does not check the clock.
|
||||
- Firewall rule limiting the Kvitt Pi's port 8080 to the HA Pi: deferred (2026-10-24, D-156); ask again only if the owner raises it.
|
||||
- State-class naming (`*Ui`, `*UiState`, `*State` mixed): fix one convention, or leave as built?
|
||||
- Answered 2026-10-24 (D-211): image builds move to the Mac (`infra/build-images.sh`), the Pi workflow is the fallback. Answered 2026-10-24 (D-156, D-157): OQ-6 no confirm step on S-1 for an unusual server; L-10 `not_found` for the other user's device; `mem_limit` kept, the warning accepted; image builds stay on the Pi for now; the web client is first used from the laptop browser; small edits by a Haiku or Sonnet agent in the main checkout, committed on `main` by the orchestrator (D-157). Answered 2026-10-21: auth on everything, the key is the app itself, the landing page on the LAN only is OK (D-126). Answered 2026-10-20: Kvitt on the Pi is a self-contained compose project in `~/kvitt` with nothing installed elsewhere (D-125). Answered 2026-10-19: Kvitt's host name is `kvitt.suorra.duckdns.org` (§1); Kvitt on port 443 through the existing router forward (D-124). Moot: `write:repository` for the registry secret (the tag push works with `write:package`).
|
||||
@@ -126,10 +130,10 @@ Standing note for every in-app update on the Samsung phones: turn Auto Blocker o
|
||||
- Nothing.
|
||||
|
||||
## 6. Numbering
|
||||
- Decisions: D-001 to D-190 and D-211 used, except D-008, D-009, D-150, D-189 and D-190 (never used); D-158 to D-176 are the card purchases (v0.35), D-177 to D-180 K0's results and the artwork (v0.36), D-211 the Mac image builds and the verified `v1.0.3` deploy (v0.37), D-181 to D-188 K1's report (v0.38). **D-191 to D-210 are reserved for K2 and K3**; next free after them: **D-212**.
|
||||
- Spec version: v0.38 written (K1's report, D-181 to D-188). Next **v0.39** (K2's text), then **v0.40** (K3's); `PLAN-K2.md`, `PLAN-K3.md`, `prompts/README.md` and SPEC §11.1 and §11.3 name them.
|
||||
- Server schema version **4** in code (migration 0004, K1 `671c00d`); the Kvitt Pi and the dev stack still run schema 3 until their next deploy. Room schema version **1** in code; **2** specified (§14.2.3) for K2.
|
||||
- New ids in v0.35: FR-22 (AC-22.1 to AC-22.15), US-18, US-19, CP-1 to CP-14 (§9.12), S-23 to S-27, W-21 to W-23, E-36 to E-44, OQ-7, OQ-8; plans K0 to K3.
|
||||
- Decisions: D-001 to D-200 and D-211 used, except D-008, D-009, D-150, D-189 and D-190 (never used); D-158 to D-176 are the card purchases (v0.35), D-177 to D-180 K0's results and the artwork (v0.36), D-211 the Mac image builds and the verified `v1.0.3` deploy (v0.37), D-181 to D-188 K1's report (v0.38), D-191 to D-200 K2's report (v0.39). **D-201 to D-210 are reserved for K3's report**; next free after them: **D-212**.
|
||||
- Spec version: v0.39 written (K2's report, D-191 to D-200). Next **v0.40** (K3's report).
|
||||
- Server schema version **4** in code (migration 0004, K1 `671c00d`); the Kvitt Pi and the dev stack still run schema 3 until their next deploy. Room schema version **2** in code (K2 `a8f91ee`, `MIGRATION_1_2`).
|
||||
- New ids in v0.35: FR-22 (AC-22.1 to AC-22.15), US-18, US-19, CP-1 to CP-14 (§9.12), S-23 to S-27, W-21 to W-23, E-36 to E-44, OQ-7, OQ-8; plans K0 to K3. New in v0.39: OQ-9.
|
||||
- Sync protocol **1**, push payload **1**.
|
||||
|
||||
## 7. How the work is run
|
||||
|
||||
@@ -195,4 +195,14 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
|
||||
| D-188 | 2026-10-24 | Placement and infra (K1): the card wire types live in `src/wire/card.rs`, the card row helpers in `src/cards/mod.rs`; the rate limiter puts the client IP it determined (§8.5, §8.6) in a request extension (`ClientIp`), from which the user-present fetch takes `Psu-Ip-Address`; the Pi's `compose.yaml` mounts `./secrets/cards` with `create_host_path: false`, so `docker compose up` refuses to start until `secrets/cards/` exists, even with no card configured (the owner creates it, mode 0700, before deploying K1's image). | One wire module per area as before; one source for the client IP; a missing folder must not become a root-owned directory Docker creates (D-117). | SPEC §9.12 CP-5, §10.2, §14.7; `src/api/rate_limit.rs`, `infra/pi/compose.yaml` |
|
||||
| D-189 | 2026-10-24 | Unused (reserved for K1; K1 needed D-181 to D-188). | | |
|
||||
| D-190 | 2026-10-24 | Unused (reserved for K1; K1 needed D-181 to D-188). | | |
|
||||
| D-191 | 2026-10-24 | The phone's card cycle (K2; refines §14.2.4 `:data`): it runs after each successful household sync cycle and after `CardRepository.refresh`, one cycle at a time, and its own outcome (success, failure, no card support) never changes the household cycle's outcome or `SyncSummary`. A 404 `not_found` from E-40 means "no card support" (a server before schema 4): the card tables are left as they are and the next household cycle tries again, so the phone picks up card support as soon as the server is updated, with no flag stored. | A card failure must not show as a failed sync of the household data; a stored "no support" flag would need a rule for when to clear it, and one extra request per cycle costs nothing. | SPEC §14.2.4 `:data`; `android/data/src/main/kotlin/se/hagstedt/kvitt/data/cards/CardSync.kt`, `sync/SyncEngine.kt` |
|
||||
| D-192 | 2026-10-24 | "Take the server's state at the next pull" made concrete (K2; refines §14.2.1): when E-44 answers `expense_already_linked` or `validation_failed`, the phone clears `pending` on that row, resets its `card_version` to 0 and moves the card cursor (`card_sync_state.cursor`) back to just before the row's version (`card_version − 1`, at least 0) when it is ahead, so the next E-40 pull returns the unchanged server row and overwrites the local one. | The server row did not change, so a plain pull from the current cursor would never send it again and the phone would keep its refused link or ignore state. | SPEC §14.2.1; `CardSync.takeServerState` |
|
||||
| D-193 | 2026-10-24 | Links to expenses that are gone (K2; refines CP-9 and §14.2.1): before E-44 the card cycle clears a pending link whose expense is not on the phone or is deleted there (deleted locally, or removed after a rejection), and sends the row without it; a local expense delete and a sync rejection that deletes an expense from the phone (`LocalRows.deleteExpense`) both clear every link to it at once. | E-44 with an expense the server will never have would stay pending for ever (`not_found` with `field` `expense_id` keeps it pending, §14.2.1); the purchase belongs back under "Att hantera". | SPEC §9.12 CP-9, §14.2.1; `CardSync.push`, `data/sync/LocalRows.kt`, `ExpenseRepositoryImpl.delete` |
|
||||
| D-194 | 2026-10-24 | Card catalogue cache (K2; refines §14.2.2): the last E-36 answer is kept as JSON in the per-phone DataStore `prefs.preferences_pb`, key `card_providers`, written on every successful E-36; S-23, S-25 and S-26 name the card and draw its artwork from it, offline too. The phone asks E-36 again after a connect or an "Uppdatera" when the cache is empty or the connected provider is not in it. E-36 itself stays online only (S-24). | `CardsState.provider` must name the card offline, and the catalogue is not part of the card pull. | SPEC §14.2.2, §14.2.4; `data/repo/PhonePrefs.kt`, `CardRepositoryImpl.ensureCatalogue` |
|
||||
| D-195 | 2026-10-24 | Following a server fetch (K2; refines CP-2 and CP-5): after E-38 reports `connected` (the first fetch starts on the server) or E-42 answers `fetching` true, the phone pulls E-40 every 3 seconds, at most 40 times (2 minutes), and stops as soon as the connection's `last_fetch_at` or `last_fetch_error` differs from its value when following began; one follower at a time. While an "Uppdatera" of this phone runs, another `refresh` returns at once without a request. | Without it the first fetch's purchases would only arrive with the next sync trigger; the fetch takes up to 6 bank requests (D-178). | SPEC §9.12 CP-2, CP-5, §14.2.4; `CardRepositoryImpl.followFetch` (`FOLLOW_DELAY_MILLIS`, `FOLLOW_ROUNDS`) |
|
||||
| D-196 | 2026-10-24 | The return link on Android (K2; refines CP-2): `kvitt://cards/connected?attempt=<id>` (attempt id 1 to 64 ASCII letters, digits or `-`, else ignored) is held by `AppViewModel.cardReturn`. If S-25 is open it takes the id and polls that attempt at once; otherwise, once the session is paired and not in onboarding, `AppViewModel` asks E-38 once (which writes a connected connection and follows the first fetch, D-195) and opens S-23. | The bank page may come back after S-25 was closed or the process was killed; S-23 is where the result shows. | SPEC §9.12 CP-2, §14.2.4; `android/app/.../AppViewModel.kt`, `link/AppLinks.kt`, `nav/KvittApp.kt` |
|
||||
| D-197 | 2026-10-24 | Bottom-bar labels shrink to fit (K2; revises design-spec §3.11): with four destinations (D-172) a label that does not fit its slot at a large font scale ("Inställningar" at 200 %) shrinks step by step to fit, down to 70 % of its capped size (`KvittFontCaps.NAVIGATION_LABEL_MIN`), on one line, instead of being cut. The 1.6× cap stays. | Four slots on a phone are narrower than three; a cut label is worse than a smaller one. | design-spec §3.11; `android/core/design/.../component/Scaffold.kt` (`KvittNavigationBar`), `theme/KvittTypography.kt` |
|
||||
| D-198 | 2026-10-24 | Wrapping chips (K2; revises §13.3 S-25 "segmented choice"): S-23's four filter chips wrap to a second row rather than scroll, so the selected one is always in view; S-25's and S-26's history choice ("7 dagar" … "180 dagar") is a row of wrapping filter chips with a check on the selected one, not a segmented button row. | The Swedish labels with their counts do not fit one row, and a segmented row cuts "180 dagar" at large font scales. | SPEC §13.3 S-23, S-25, S-26; design-spec S-23, S-25, S-26; `feature/cards/.../CardsScreen.kt` (`Filters`), `CardParts.kt` (`HistoryChips`) |
|
||||
| D-199 | 2026-10-24 | Two small screen rules (K2): S-13's "Anslut kort" subtitle reads "Har upphört" (`settings_cards_expired`) for an expired connection, besides "Ansluten · giltig till <date>" and "Läs in dina kortköp"; S-5's overflow item "Från kortköp" is hidden on an archived account (it has no add-expense FAB either, AC-6.4). | An expired consent is not "Ansluten"; an archived account takes no new expenses. | SPEC §13.3 S-5, S-13; design-spec S-5, S-13; `feature/settings/.../SettingsHubScreen.kt`, `feature/account/.../AccountDetailScreen.kt` |
|
||||
| D-200 | 2026-10-24 | K2's string keys and artwork measures: four keys beyond §13.3's card table, in `docs/ux/strings.csv` (77 rows added in all): `cards_foreign_amount` ("%1$s %2$s", the foreign amount, decimal comma in Swedish), `cards_connection_settings` ("Kortanslutning", TalkBack click label of S-23's header card), `cards_connect_offline_body` (S-24's offline banner body), `settings_cards_expired` (D-199). Card artwork measures in `KvittDimens` (`:core:design`): `CARD_ART_ASPECT` 1.586 (the box; the bundled picture keeps its own 1313 × 829 inside it), `cardArtCorner` 16 dp and `cardArtNameInset` 16 dp (the drawn fallback card), `cardArtHeader` 72 dp wide (S-23's header card), `cardArtLarge` at most 280 dp wide (S-23's empty state, S-25, S-26); S-24's tiles are full width. | §13.3 left TalkBack labels and unlisted keys to K2; measures live in one design file (`CLAUDE.md`). | SPEC §13.3 "Card purchase strings"; design-spec §3.12; `docs/ux/strings.csv`; `android/core/design/.../theme/KvittDimens.kt` |
|
||||
| D-211 | 2026-10-24 | Image builds on the Mac (revises D-156 point 5 and D-104's "fallback" wording). `infra/build-images.sh v1.0.N` on the owner's Mac with Podman is the normal way to build and push `kvitt-server` and `kvitt-backup`; the Gitea Actions workflow on the Pi 3 runner stays as the fallback (about three hours per run, disk pressure; the owner cancels the runs it queues). Tag: after `git fetch --tags`, the next `v1.0.N` after the highest `v1.0.*` tag on `origin`, also pushed as a git tag on the built commit (`v1.0.3` = `8fa9198`). Registry from the Mac: `gitea.lan` on port 80, plain HTTP; `infra/secrets/registry.env` (git-ignored) holds `KVITT_REGISTRY=gitea.lan/jacobh2`, the user and a `write:package` token; the Pi's `.env` keeps `gitea.lan:3001/jacobh2`, the port it reaches locally; both name the same registry. Podman needs the registry marked insecure in `~/.config/containers/registries.conf.d/kvitt.conf` on the Mac and `/etc/containers/registries.conf.d/kvitt.conf` inside the Podman machine (`[[registry]] location = "gitea.lan" insecure = true`), owner's allowance 2026-10-24. Script fixes: the server build gets `KVITT_VERSION_CODE` as in the workflow (`f15fa23`); Podman detection without a pipe (`8fa9198`: `grep -q` under `pipefail` closed the pipe, `docker version` died of SIGPIPE and the script chose docker). Deployed and verified by the owner 2026-10-24: `v1.0.3` on the Kvitt Pi (`server` and `backup` healthy); the web client at `https://kvitt.suorra.duckdns.org/app/`, a laptop browser paired with a device invite; "All seems to work" for Home and an account, add, edit and delete an expense, CSV export, record and delete a payment, analytics with category drill-down and Insights, the settings pages, revoking the browser from the phone, and the Android Insights section on S-11 against the new server. This answers the open question "image-build location". | A cold arm64 server build took 48 s on the M4 (2026-10-18, before the web stage) against 2 h 42 min on the 1 GB Pi 3 (run 69), whose disk fills during builds; the workflow is kept because it needs no Mac. One tag sequence for both paths keeps `KVITT_TAG` monotonic whichever path built it. | SPEC §10.3, §10.10, §11.2 W1 to W7, §11.3, §11.4, §15; `infra/README.md` (Layout, How it fits together, the image-build sections, "Pi: what the owner does" steps 2 and 7, Owner decisions); `docs/HANDOFF.md` |
|
||||
|
||||
+159
-7
@@ -19,7 +19,7 @@ Status: binding design spec for v1, SPEC §13.4 phase 2, written 2026-10-18 for
|
||||
| Category suggestion rule | `category-suggestion.md` (CS-1 … CS-7), keyword table `category-keywords.csv` | — |
|
||||
| Navigation, screen inventory, data ownership | `ia.md`; flows `flows.md` | — |
|
||||
|
||||
Swedish words in quotes are as they appear on screen; `strings.csv` holds the final wording and the English. Keys cited in parentheses (`editor_amount`) are the keys of the current `strings.csv` (D-017 module prefixes: `onboarding_`, `home_`, `account_`, `expense_`, `settle_`, `analytics_`, `settings_`; `:core:design` keys such as `common_`, `editor_`, `ledger_`, `balance_`, `update_`, `snackbar_` carry no feature prefix). Every label quoted here has a key in `strings.csv`. `directions/3-kalas/index.html` is the U1 record the token values were copied from; it is not maintained.
|
||||
Swedish words in quotes are as they appear on screen; `strings.csv` holds the final wording and the English. Keys cited in parentheses (`editor_amount`) are the keys of the current `strings.csv` (D-017 module prefixes: `onboarding_`, `home_`, `account_`, `expense_`, `settle_`, `analytics_`, `settings_`, and `cards_` for `:feature:cards` since K2; `:core:design` keys such as `common_`, `editor_`, `ledger_`, `balance_`, `update_`, `snackbar_` carry no feature prefix). Every label quoted here has a key in `strings.csv`. `directions/3-kalas/index.html` is the U1 record the token values were copied from; it is not maintained.
|
||||
|
||||
**Decisions this file builds on.** UX-1 … UX-8 (`proposed-decisions.md`) are recorded in SPEC v0.8 as D-047 … D-054 (UX-1 D-047, UX-2 D-048, UX-3 D-049, UX-4 D-050, UX-5 D-051, UX-6 D-052, UX-7 D-053, UX-8 D-054). Still open: the §9.7 hex values of D-051 are `TODO(U2)` in SPEC v0.8 and are the values of §2.3 here; UX-9 is not yet in the spec. Labels used in this file:
|
||||
|
||||
@@ -313,7 +313,9 @@ Icons are dp-sized (24 default; 18 in chips; 15–16 in row meta; 20 in buttons)
|
||||
|
||||
| Icon | Where |
|
||||
|---|---|
|
||||
| `home`, `bar_chart`, `settings` | bottom navigation (filled when selected) |
|
||||
| `home`, `credit_card`, `bar_chart`, `settings` | bottom navigation (filled when selected; `credit_card` is Kortköp, D-172) |
|
||||
| `credit_card` | S-13 "Anslut kort" row, S-5 menu "Från kortköp", S-8 card line, S-23 "Anslut kort" button (K2, `KvittIcon.CreditCard`) |
|
||||
| `link`, `block`, `unarchive`, `open_in_new`, `close`, `key` | S-23: match chips and "Välj en annan utgift", "Ignorera", "Sluta ignorera", "Öppna utgift", "Ta bort koppling"; "Anslut med BankID" / "Förnya med BankID" on S-23, S-25, S-26 |
|
||||
| `add` | FAB, "Nytt konto", "Lägg till konto", "Ny kategori", "Ny återkommande" |
|
||||
| `close` | editor top bars (S-6, S-7, S-9, S-15 editor, S-19 editor) |
|
||||
| `arrow_back` | pushed screens |
|
||||
@@ -628,7 +630,7 @@ TalkBack: "Konto: Vardag. Dubbeltryck för att byta konto"; "Datum: idag"; "Beta
|
||||
|
||||
**`KvittTopAppBar(title, navigation: Back | Close | None, actions)`**: 64 dp, `surface` (scrolled: `surface-container`), navigation icon button 48 (`arrow_back` TalkBack "Tillbaka" / `close` TalkBack "Avbryt"), title title-large 1 line ellipsis (pad 16 when there is no navigation icon), optional subtitle body-small muted. Top destinations (S-4) show the wordmark instead of a title: a 22 dp Sunny `primary` dot with a filled `receipt_long` (15) in `on-primary`, then "Kvitt" 24 sp weight 700 `primary` (capped 1.5×); semantics heading "Kvitt".
|
||||
|
||||
**`KvittNavigationBar(selected)`**: Expressive `ShortNavigationBar` (fallback `NavigationBar`), `surface-container`, items 64 dp high: indicator pill 56 × 32 (`primary-container` when selected) with the icon (filled when selected), label label-medium (selected: `on-surface` weight 700) capped at 1.6×. Items: "Översikt" `home`, "Statistik" `bar_chart`, "Inställningar" `settings` (`nav_*`). Labels always visible.
|
||||
**`KvittNavigationBar(selected)`**: Expressive `ShortNavigationBar` (fallback `NavigationBar`), `surface-container`, items 64 dp high: indicator pill 56 × 32 (`primary-container` when selected) with the icon (filled when selected), label label-medium (selected: `on-surface` weight 700) capped at 1.6×. Items (D-172, four since K2): "Översikt" `home`, "Kortköp" `credit_card`, "Statistik" `bar_chart`, "Inställningar" `settings` (`nav_*`). Labels always visible, on one line; a label that does not fit its slot at a large font scale shrinks step by step to fit, down to 70 % of its capped size (`KvittFontCaps.NAVIGATION_LABEL_MIN`, D-197; at 200 % "Inställningar" is the one that shrinks, `navigation_bar_sv_light_200`).
|
||||
|
||||
**`AddExpenseFab(expanded: Boolean)`**: medium extended FAB, 56 dp, radius 20, `primary` / `on-primary`, `add` 24 + "Lägg till utgift" (16 / 22 sp weight 600), 16 dp from the right edge and above the navigation bar; collapses to the 56 dp icon-only FAB while the list scrolls down and at font scale ≥ 1.5; TalkBack "Lägg till utgift" in both forms. Same component with "Ny återkommande" on S-19.
|
||||
|
||||
@@ -658,6 +660,12 @@ TalkBack: "Konto: Vardag. Dubbeltryck för att byta konto"; "Datum: idag"; "Beta
|
||||
|
||||
**`Skeleton`**: `surface-container-highest` blocks, radius 12 (pills full), no shimmer under reduced motion; only for the first-sync and online-fetch loading states.
|
||||
|
||||
### 3.12 Card artwork (`CardArt` in `:feature:cards`, measures in `KvittDimens`; D-173, D-180, D-200)
|
||||
|
||||
**`CardArt(imageKey, name)`**: a box of aspect ratio 1.586:1 (`KvittDimens.CARD_ART_ASPECT`). For the image key `sj_prio_mastercard` it draws the committed SJ Prio Mastercard picture (1313 × 829) at its own aspect ratio inside the box (`ContentScale.Fit`), with no clip and no background, so the picture's own rounded corners and the semicircular notch on its right edge show. Any other key draws the fallback card: radius 16 (`cardArtCorner`), fill the `green` category token's light value in both themes, the card's name in white, title-medium weight 600, at most 2 lines, inset 16 from the lower left (`cardArtNameInset`); nothing else on it (`s24_fallback_artwork`). TalkBack reads the card's name only.
|
||||
|
||||
Widths: `cardArtHeader` 72 dp (S-23's header card), `cardArtLarge` 280 dp (S-23's empty state, S-25, S-26, centred), full width in S-24's tiles; S-24 draws a disabled tile's artwork faded.
|
||||
|
||||
---
|
||||
|
||||
## 4. Screens
|
||||
@@ -774,7 +782,7 @@ Each screen: layout top to bottom, the **binding states** (exactly the frames of
|
||||
2. Ledger: per month a `LedgerMonthHeader` and a `SegmentedGroup` of `ExpenseRow` / `SettlementRow` in AC-8.10 order; settlement amounts are the allocation to this account (AC-11.7).
|
||||
3. `AddExpenseFab` (account preset to this one); hidden for archived accounts (UX-6).
|
||||
|
||||
Overflow menu (`KvittMenu`): "Ändra konto" (`edit`) → S-6; "Arkivera" (`inventory_2`) or "Återställ från arkiv" (`unarchive`); "Ta bort konto" (`delete`) enabled only when `deletable`, otherwise disabled with "Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter" (`account_delete_blocked`).
|
||||
Overflow menu (`KvittMenu`): first, only while the viewer has a card connection (`CardRepository.hasConnection`) and the account is not archived, "Från kortköp" (`credit_card`, `account_from_card_purchases`) → S-27 (AC-22.9, D-199; local, so it works offline); "Ändra konto" (`edit`) → S-6; "Arkivera" (`inventory_2`) or "Återställ från arkiv" (`unarchive`); "Ta bort konto" (`delete`) enabled only when `deletable`, otherwise disabled with "Kontot kan inte tas bort så länge det har utgifter, betalningar eller återkommande utgifter" (`account_delete_blocked`).
|
||||
|
||||
**States**
|
||||
|
||||
@@ -782,6 +790,7 @@ Overflow menu (`KvittMenu`): "Ändra konto" (`edit`) → S-6; "Arkivera" (`inven
|
||||
|---|---|---|
|
||||
| `S-5-account` | loaded | Vardag: "Anna är skyldig dig 1 184,50 kr", October and September groups, the SL-kort row pending |
|
||||
| `S-5-account-menu` | variant: overflow menu open | menu over the content; "Ta bort konto" disabled with its reason |
|
||||
| `S-5-account-menu-card` | variant: menu with a card connection | "Från kortköp" first, above "Ändra konto" (K2) |
|
||||
| `S-5-account-archived` | variant archived | Gotland 2026: "Arkiverat" badge, "Kvitt", "Återställ från arkiv" (tonal) in the header, no FAB |
|
||||
| `S-5-account-empty` | empty | new account "Sommarstugan": header "Kvitt" without a settle button; `EmptyState` "Inga utgifter än" / "Lägg till den första utgiften i Sommarstugan." (no illustration, no button); FAB shown |
|
||||
| `S-5-account-offline` | offline | menu open: all three items disabled, each with "Kräver anslutning" as its second line; rows, "Gör upp det här kontot" and FAB work |
|
||||
@@ -843,9 +852,11 @@ Full screen (§13.2), works offline (AC-8.2). The flow and the category rule are
|
||||
| `S-7-editor-full` | variant: full split + notes (edit mode) | "Ändra utgift", SL-kort 30 dagar, Anna, Transport, 14 okt., "Allt på en person" with Anna as bearer, notes expanded, "Ta bort utgift" |
|
||||
| `S-7-editor-edit` | variant edit mode | "Ändra utgift" for ICA Maxi Lindhagen (1 247,60 kr, Mat & hushåll, Vardag, 16 okt., Betalt av Anna); nothing focused, keypad hidden; "Ta bort utgift" visible at the end |
|
||||
| `S-7-editor-200` | long, 200 %, light only | amount and "Spara" visible, quick row in 3 columns, the middle scrolls, keypad keys grow |
|
||||
| `s7_from_card` | variant: new from a card purchase (CP-9, K2) | opened with `cardPurchaseId`: amount, description (the merchant text), date and "Betalt av Du" from the purchase, the account it was opened from (S-27) or the AC-8.6 default, "Kontots standard", the category from the §9.11 suggestion; nothing else differs |
|
||||
| `s7_from_card_foreign` | variant: from a foreign-currency purchase | as `s7_from_card` with the amount empty ("0,00") and the keypad up; "Spara" disabled until an amount is typed (the currency note is in S-23's sheet) |
|
||||
|
||||
**Interactions**
|
||||
* Defaults (new): account per AC-8.6; category per §5 (UX-3: suggestion, falling back to AC-8.6); date today; payer the viewer (AC-8.3); mode `default`. From S-5 the account is that account.
|
||||
* Defaults (new): account per AC-8.6; category per §5 (UX-3: suggestion, falling back to AC-8.6); date today; payer the viewer (AC-8.3); mode `default`. From S-5 the account is that account. From a card purchase (S-23's sheet, S-27) the form starts from `CardRepository.prefill` (CP-9) and "Spara" also links the purchase to the new expense (`link`, local at once).
|
||||
* Changing the account: mode `default` re-copies the account's ratio (AC-8.8); the suggestion re-runs (CS-3) unless the category was picked by hand.
|
||||
* "Spara" enabled when the amount is > 0, the description is non-blank and `preview` is not null; tap → `save`. `SaveResult.Invalid` maps each `FieldError` inline: amount `required` "Skriv ett belopp", out of range "Beloppet måste vara mellan 0,01 kr och 10 000 000,00 kr"; description `required` "Skriv en beskrivning", too long; date out of range "Datumet ligger för långt fram"; `exact_sum_mismatch` / exact share > amount "Andelen är större än beloppet". Focus moves to the first field in error.
|
||||
* `Saved` (new) → back to the opener with snackbar "Utgiften sparades" (online) / "Sparad. Synkas när du är online." (offline), no "Ångra" (UX-6). `Saved` (edit) → back to S-8, no snackbar (the timeline shows the pending line).
|
||||
@@ -866,6 +877,8 @@ Full screen (§13.2), works offline (AC-8.2). The flow and the category rule are
|
||||
2. Facts (`FactRow`s): Datum (`event`, "fredag 16 oktober 2026"); Betalt av (`person`, avatar 24 + name, "Du" for the viewer); Delning (`call_split`, mode label "Kontots standard 50/50" / "Egen fördelning 70/30" / "Exakta belopp" / "Allt på Anna", a thin `SplitBar`, "Du 623,80 kr · Anna 623,80 kr"); Anteckning (`notes`, full text) only when notes exist.
|
||||
3. `Timeline`.
|
||||
|
||||
For the card holder only, when a purchase of theirs is linked to the expense (`CardRepository.purchaseForExpense`, AC-22.10), a card between the hero and the facts: `credit_card` (outline, `on-surface-variant`), "Från kortköp: <merchant>, <date>" (body-medium, the §9.8 list date, wraps; `expense_detail_from_card`) and the text button "Ta bort koppling" (`expense_detail_unlink_card`), which unlinks at once (CP-9). Not shown on a deleted expense.
|
||||
|
||||
**States**
|
||||
|
||||
| Frame | State | What differs |
|
||||
@@ -873,6 +886,7 @@ Full screen (§13.2), works offline (AC-8.2). The flow and the category rule are
|
||||
| `S-8-expense` | loaded + pending | ICA Maxi Lindhagen, notes, timeline with create, two changes and "Din ändring väntar på synk" |
|
||||
| `S-8-expense-recurring` | variant recurring | Hyra september, Boende · Huset, badge "Återkommande: Hyra", timeline "Skapad automatiskt (återkommande) · 25 sep. 06:00" |
|
||||
| `S-8-expense-deleted` | variant deleted | Neutral banner on top: filled `delete` in `error` + "Anna tog bort den här utgiften · 17 okt. 09:12" + "Den räknas inte längre i saldon eller statistik."; hero and facts at 62 % opacity, tile desaturated; no "Ändra" / "Ta bort" |
|
||||
| `s8_from_card` | variant: linked card purchase (K2) | Willys Hornstull with the card line "Från kortköp: WILLYS HORNSTULL, 28 sep." and "Ta bort koppling" |
|
||||
| `S-8-expense-missing` | empty | `ExpenseRepository.detail` emits null: `EmptyState` (Cookie9 tile, `sync_problem`) "Utgiften finns inte på telefonen än" / "Den kan finnas på servern. Synka för att hämta den." with "Synka nu" (tonal, `sync`); no actions in the app bar |
|
||||
|
||||
**Interactions**: "Ändra" → S-7 edit. "Ta bort" → the delete dialog of S-7, then back with snackbar "Utgiften togs bort". "Synka nu" → `SyncRepository.syncNow()`; the screen fills in when the row arrives. Opened from a deletion notification → deleted state (AC-13.6 opens Home for deletions; the deleted state also shows when the row is deleted while open).
|
||||
@@ -1030,7 +1044,7 @@ Goldens (no catalogue frame): `s12_category`, `s12_category_groceries`, `s12_cat
|
||||
|
||||
**Layout** (app bar "Inställningar"; column gap 12; bottom navigation with Inställningar selected):
|
||||
1. Household card (padding 16 / 18, gap 12; role lines only, no device name: `SessionState` carries none): viewer row: avatar 52 (Sunny) + name (title-medium) over "Du · Administratör" (`settings_role_admin`) or "Du" (`settings_role_member`) in body-small muted; 1 dp divider; partner row: avatar 52 + name over "Partner" (`common_partner`) or "Admin" (`settings_role_admin_other`).
|
||||
2. Group 1: `SettingsRow`s "Profil" (`person`, orange, "Namn och bild"), "Kategorier" (`category`, green, "14 aktiva"), "Återkommande utgifter" (`event_repeat`, brown, "3 aktiva, 1 pausad" / "Inga").
|
||||
2. Group 1: `SettingsRow`s "Profil" (`person`, orange, "Namn och bild"), "Kategorier" (`category`, green, "14 aktiva"), "Återkommande utgifter" (`event_repeat`, brown, "3 aktiva, 1 pausad" / "Inga"), "Anslut kort" (`credit_card`, indigo; "Ansluten · giltig till 22 apr. 2027" in the §9.8 list form, "Har upphört" for an expired connection, else "Läs in dina kortköp"; D-172, D-199) → S-24.
|
||||
3. Group 2: "Enheter" (`devices`, blue, no subtitle: the list is online only); admin only: "Bjud in partner" (`person_add`, teal) while the partner has not joined, else "Para ihop en enhet" (`add_link`, teal); "Synkstatus" (`sync`, purple, the pill's text, e.g. "Synkad för 3 min sedan" / "2 väntar på synk"); "Om Kvitt" (`info`, grey, "Version 1.1" / "Uppdatering finns").
|
||||
|
||||
**States**
|
||||
@@ -1038,9 +1052,10 @@ Goldens (no catalogue frame): `s12_category`, `s12_category_groceries`, `s12_cat
|
||||
| Frame | State | What differs |
|
||||
|---|---|---|
|
||||
| `S-13-settings` | loaded (admin) | Jacob "Du · Administratör", Anna "Partner"; "Para ihop en enhet" shown |
|
||||
| `s13_hub_card_connected` | variant: card connected (K2) | the "Anslut kort" row reads "Ansluten · giltig till 22 apr. 2027" |
|
||||
| `S-13-settings-member` | variant member | Anna's phone: Anna "Du" first, Jacob "Admin"; no invite or pairing row (AC-2.2) |
|
||||
|
||||
**Interactions**: each row pushes its screen (S-14, S-15, S-19, S-20, S-21, S-16, S-22). Rows to online-only screens stay enabled; those screens show their offline state.
|
||||
**Interactions**: each row pushes its screen (S-14, S-15, S-19, S-24, S-20, S-21, S-16, S-22). Rows to online-only screens stay enabled; those screens show their offline state.
|
||||
|
||||
**TalkBack**: household card two nodes ("Jacob, du, administratör"; "Anna, partner"); rows "Kategorier, 14 aktiva, knapp".
|
||||
|
||||
@@ -1213,6 +1228,143 @@ Not a screen: the `UpdateBanner` on S-4 (and S-22) and one dialog.
|
||||
|
||||
**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".
|
||||
|
||||
### S-23 Card purchases, Kortköp (`:feature:cards`, top level; D-172)
|
||||
|
||||
Written from K2's goldens (`a8f91ee`); the rules are SPEC §9.12 and §13.3 S-23. Only the card holder sees these screens' content (CP-11); nothing on them reaches a log.
|
||||
|
||||
**Layout** (app bar "Kortköp" with no navigation icon; bottom navigation with Kortköp selected; a `LazyColumn` with gutter 16 and `sectionGap` between items, inside `KvittPullToRefresh`):
|
||||
1. **Banner**, at most one, in this order of precedence: expired (Error `Banner`, filled `error`, "Kortanslutningen har upphört", action "Förnya med BankID" as an error-filled button with `key`) before expiring (Warning `Banner`, filled `warning`, "Kortanslutningen upphör 27 okt." with the §9.8 list date, text button "Förnya med BankID" with `key`) before fetch failed (Error `Banner`, "Kunde inte hämta köp. Senast uppdaterad för 16 h sedan", text button "Försök igen" with `refresh`). A `consent_expired` fetch error shows the expired banner (CP-14). The renew buttons are disabled offline.
|
||||
2. **Header card** (`KvittCard`): a row of the small artwork (`CardArt`, 72 dp, §3.12), then the card's name (title-medium) over "Bara du ser dina kortköp" and "Uppdaterad för 15 min sedan" (body-small muted; the §9.8 relative time, refreshed every minute; no line before the first fetch has finished); the row opens S-26. Under it "Uppdatera" (tonal, `refresh`); while this phone's E-42 call runs it reads "Uppdaterar…" with the button's progress; disabled when offline (with `NeedsConnectionHint` under it) or expired.
|
||||
3. **Filters**: four filter chips "Att hantera", "Kopplade", "Ignorerade", "Alla", each followed by its count in bold, wrapping to a second row (D-198); the selected chip is filled (Material 3 `FilterChip`).
|
||||
4. **Days**, newest first: a day header (the §9.8 list date, "17 okt.", title-small, heading, the ledger's month-header padding) over a `SegmentedGroup` of purchase rows.
|
||||
5. **Purchase row** (`CardPurchaseItem`, `ledgerRowMin` high): merchant (title-medium, one line, ellipsis, the bank's text as it comes, often upper case) with the badges under it ("Preliminär" pending, "Insättning" neutral for a credit, "Utländsk valuta" neutral for a foreign amount); under "Kopplade" the line "Kopplad till Willys Hornstull · Vardag" (body-small muted, at most 2 lines); the amount on the right (`MoneyText` small, muted for a credit; a foreign amount as "24,99 EUR" in title-medium), under the text at a font scale of 1.5 or more. Under "Att hantera" the match chip below the text: one match "Koppla till Systembolaget" (tonal small button, `link`), two or three "2 möjliga utgifter" (outlined, `link`).
|
||||
|
||||
**Purchase sheet** (`ModalBottomSheet`, fully expanded, gutter 16, gap 12; scrolls): merchant (title-large) over the purchase date in the §9.8 detail form ("söndag 11 oktober 2026", body-medium muted); a row with the amount (`MoneyText` large, or "24,99 EUR" in headline-small), the status badge ("Preliminär" pending kind, "Bokförd" neutral) and "Insättning" for a credit. Then by case:
|
||||
* to handle: for a foreign amount the note "Beloppet är i EUR: skriv beloppet i kronor." (body-medium muted); "Skapa utgift" (filled medium, full width, `add`); "Koppla till befintlig utgift" (title-small) over the matches as a `SegmentedGroup` of match rows (description title-medium one line, "Vardag · Anna · 12 okt." body-small muted, the amount on the right; one tap links) or the note "Ingen utgift med samma belopp runt det här datumet"; "Välj en annan utgift" (outlined, full width, `link`); "Ignorera" (text, `block`).
|
||||
* linked: the note "Kopplad till Willys Hornstull · Vardag"; "Öppna utgift" (filled medium, full width, `open_in_new`) → S-8; "Ta bort koppling" (outlined, full width, `close`).
|
||||
* ignored: "Sluta ignorera" (tonal, full width, `unarchive`).
|
||||
* credit: the note "Insättningar och återbetalningar är inga utgifter. Ändra den ursprungliga utgiften för hand vid behov." and no action.
|
||||
|
||||
**"Välj en annan utgift"** replaces the sheet's content: title "Välj en annan utgift" (title-large) over the CP-10 list (match rows as above, newest first, any amount within 14 days), or the no-match note when the list is empty. Back returns to the purchase sheet.
|
||||
|
||||
**States** (goldens `<name>_sv_light`, `_sv_dark`, `_en_light`; `_sv_light_200` where marked):
|
||||
|
||||
| Golden | State | What differs |
|
||||
|---|---|---|
|
||||
| `s23_no_connection` | empty: no connection | one `EmptyState`-style section (`surface-container-low`, centred): the large artwork, "Anslut ditt kort" (headline-small), "Se dina köp här och gör om dem till utgifter med några tryck. Bara du ser dem." (body-large muted), "Anslut kort" (filled medium, `credit_card`) → S-24; no header, no filters |
|
||||
| `s23_first_fetch` | loading: first fetch | header without "Uppdaterad"; below it a progress indicator and "Hämtar dina köp…" (title-medium); no filters, no list |
|
||||
| `s23_to_handle` | loaded, "Att hantera" | counts 6 / 2 / 1 / 10; PRESSBYRAN ODENPLAN pending without a match; SYSTEMBOLAGET 0152 STHLM pending and ICA MAXI LINDHAGEN each with "Koppla till …"; WOLT without a match |
|
||||
| `s23_linked` | loaded, "Kopplade" | two rows with their "Kopplad till … · Vardag" lines |
|
||||
| `s23_ignored` | loaded, "Ignorerade" | the one ignored purchase |
|
||||
| `s23_all` | loaded, "Alla" | every purchase without match chips; the credit with "Insättning", the foreign one with "Utländsk valuta" |
|
||||
| `s23_nothing_to_handle` | empty filter, "Att hantera" | a flat section "Inget att hantera" (headline-small) / "Nya kortköp visas här." (body-large muted) |
|
||||
| `s23_filter_empty` | empty filter, other | "Inga köp här" (title-medium) in the flat section |
|
||||
| `s23_offline` | offline | "Uppdatera" disabled with "Kräver anslutning" under it; the list, chips and sheet work (CP-9) |
|
||||
| `s23_refreshing` | variant: pull to refresh | the refresh indicator over the list (§3.6) |
|
||||
| `s23_expiring` | expiring (CP-6) | the warning banner with "Förnya med BankID" (text button) above the header |
|
||||
| `s23_expired` | expired | the error banner with "Förnya med BankID" (error filled); "Uppdatera" disabled; the list kept |
|
||||
| `s23_fetch_failed` | fetch failed (CP-14) | the error banner "Kunde inte hämta köp. Senast uppdaterad för 16 h sedan" with "Försök igen" |
|
||||
| `s23_long` | long, also 200 % | long merchants ellipsized at one line, 7-digit amounts whole; at 200 % amounts under the text |
|
||||
| `s23_sheet_one_match` | sheet, one match, also 200 % | ICA MAXI LINDHAGEN: "Skapa utgift", one match row, "Välj en annan utgift", "Ignorera" |
|
||||
| `s23_sheet_two_matches` | sheet, two matches | KLARNA*HEMKOP 384,40 kr "Bokförd": Apotek Hjärtat and Hemköp Odenplan as matches |
|
||||
| `s23_sheet_no_match` | sheet, no match | WOLT: "Ingen utgift med samma belopp runt det här datumet" |
|
||||
| `s23_sheet_foreign` | sheet, foreign currency | AMAZON.DE "24,99 EUR": the EUR note above "Skapa utgift", no match |
|
||||
| `s23_sheet_linked` | sheet, linked | WILLYS HORNSTULL: "Kopplad till …", "Öppna utgift", "Ta bort koppling" |
|
||||
| `s23_sheet_ignored` | sheet, ignored | "Sluta ignorera" only |
|
||||
| `s23_sheet_credit` | sheet, credit | BETALNING TACK 5 000,00 kr, "Bokförd" and "Insättning", the credit note, no action |
|
||||
| `s23_sheet_other` | "Välj en annan utgift" | eight expenses within 14 days, any amount, newest first |
|
||||
|
||||
**Interactions**
|
||||
* A row opens the purchase sheet; a one-match chip links at once; a two-or-three chip opens the sheet. Linking (from a chip or a match row) closes the sheet and shows the snackbar "Kopplad" with "Ångra"; "Ignorera" closes it with "Ignorerad" and "Ångra"; "Ångra" reverts that purchase's change. All of it is local first and works offline (CP-9).
|
||||
* "Skapa utgift" closes the sheet and opens S-7 prefilled (CP-9); its "Utgiften sparades" shows here (S-23 hosts `:app`'s snackbar host, D-071). "Öppna utgift" → S-8.
|
||||
* "Uppdatera", pull to refresh and the banner's "Försök igen" call E-42 (CP-5); offline the pull ends at once; a network failure of "Uppdatera" shows the snackbar "Kunde inte nå servern".
|
||||
* The header row → S-26; "Förnya med BankID" → S-25 in its renewal form; "Anslut kort" → S-24.
|
||||
* A purchase linked to an expense that is not on the phone yet is listed under "Kopplade" without its "Kopplad till" line and its sheet shows the to-handle actions until the expense arrives (SPEC §12 OQ-9).
|
||||
|
||||
**TalkBack**: a row is one button node, "PRESSBYRAN ODENPLAN, 54,00 kronor, 17 okt., Preliminär" (merchant, amount, date, badges, the linked line), and its match chip a separate button; the header row is one node with the click label "Kortanslutning" (`cards_connection_settings`); day headers are headings; match rows read description, account, payer, date and amount.
|
||||
|
||||
**200 %**: amounts move under the merchant; chips wrap; the sheet scrolls.
|
||||
|
||||
### S-24 Connect card, Anslut kort (`:feature:cards`, pushed from S-13)
|
||||
|
||||
**Layout** (app bar: back, "Anslut kort"; scrolling column, gutter 16, `sectionGap`): the intro "Välj ditt kort. Kvitt läser dina köp men kan aldrig betala eller flytta pengar." (body-large muted); then one tile per catalogue entry (E-36): a `KvittCard` with the artwork full width (§3.12), the name (title-medium), the issuer (body-medium muted) and, when needed, one line in body-small muted: "Ansluten · giltig till 22 apr. 2027" (§9.8 list date) for a connected card, "Inte konfigurerat på servern" for a provider not available (tile disabled). A connected card's tile opens S-26, another S-25.
|
||||
|
||||
**States**
|
||||
|
||||
| Golden | State | What differs |
|
||||
|---|---|---|
|
||||
| `s24_catalogue` | loaded, also 200 % | the SJ Prio Mastercard tile, SEB Kort |
|
||||
| `s24_connected` | loaded, connected | the tile adds "Ansluten · giltig till 22 apr. 2027" |
|
||||
| `s24_unavailable` | provider not available | "Inte konfigurerat på servern", the tile disabled and its artwork faded |
|
||||
| `s24_fallback_artwork` | unknown image key | a second entry "Nordea Visa" drawn as the fallback card (§3.12) |
|
||||
| `s24_loading` | loading | one skeleton tile (an artwork-sized block and a pill) |
|
||||
| `s24_offline` | offline | `NeedsConnectionBanner` "Kräver anslutning" / "Listan över kort hämtas från servern. Anslut telefonen till internet." (`cards_connect_offline_body`); the cached card's tile below it disabled with its artwork faded (D-194) |
|
||||
| `s24_error` | error | Error `Banner` "Kunde inte nå servern" with "Försök igen" (`refresh`) |
|
||||
|
||||
**Interactions**: E-36 runs when S-24 opens, on "Försök igen" and when the phone comes online after an offline open.
|
||||
|
||||
**TalkBack**: each tile is one button node (name, issuer, line).
|
||||
|
||||
### S-25 Connect a card (`:feature:cards`, pushed from S-24 or from "Förnya med BankID")
|
||||
|
||||
**Layout** (app bar: back, the card's name; scrolling column, gutter 16, items centred, gap 20): the large artwork (280 dp, §3.12); then by phase:
|
||||
* choose: "Hämta köp från de senaste" (title-small) over four wrapping filter chips "7 dagar", "30 dagar", "90 dagar", "180 dagar" with a `check` on the selected one (default 30; a renewal presets the connection's value; D-198); the note "Du kan korta perioden senare. En längre period kräver BankID igen." (body-medium muted); "Anslut med BankID" (filled medium, full width, `key`; "Förnya med BankID" for a renewal), with progress inside while E-37 runs; offline disabled with `NeedsConnectionHint`; after a network failure an Error `Banner` "Kunde inte nå servern" above the button.
|
||||
* waiting: a progress indicator, "Väntar på BankID…" (title-medium), "Slutför i webbläsaren och i BankID och kom sedan tillbaka hit." (body-medium muted), then "Avbryt" (text).
|
||||
* connected: a filled `check_circle` (in `primary`, the empty-state tile size) and "Kortet är anslutet" (headline-small); after 1.5 s S-23 opens.
|
||||
* failed: filled `error` and "Det gick inte att ansluta kortet", with "Försök igen" (filled medium, `refresh`).
|
||||
* timed out: filled `schedule` and "Anslutningsförsöket tog för lång tid", with "Försök igen".
|
||||
|
||||
**States**
|
||||
|
||||
| Golden | State |
|
||||
|---|---|
|
||||
| `s25_choose` | choose, 30 days selected (also 200 %) |
|
||||
| `s25_renew` | choose in the renewal form ("Förnya med BankID") |
|
||||
| `s25_waiting` | waiting for BankID |
|
||||
| `s25_connected` | connected |
|
||||
| `s25_failed` | failed |
|
||||
| `s25_timed_out` | timed out |
|
||||
| `s25_offline` | offline: the button disabled with "Kräver anslutning" |
|
||||
|
||||
**Interactions**: the button calls E-37 and opens the bank's page in a Custom Tab; S-25 polls E-38 every 2 s while it is open and resumed (the Custom Tab pauses it) and once at once when the return link arrives (D-196); "Avbryt" stops polling and returns to the choice (the attempt expires on the server); "Försök igen" starts a new attempt. No browser to open the page shows the failed state. Timed out shows when E-38 answers `expired`; the phone does not check the clock itself.
|
||||
|
||||
**TalkBack**: the chip row is labelled "Hämta köp från de senaste"; the waiting and outcome blocks are polite live regions.
|
||||
|
||||
### S-26 Card connection (`:feature:cards`, pushed from S-24 or from S-23's header)
|
||||
|
||||
**Layout** (app bar: back, the card's name; scrolling column, gutter 16, `detailGap`):
|
||||
1. The large artwork (280 dp, centred).
|
||||
2. Facts card (`KvittCard`, gap 4): the status row, a filled `check_circle` in `owed` with "Ansluten" or a filled `error` in `error` with "Har upphört" (title-medium); "Giltig till torsdag 22 april 2027" (body-medium, §9.8 detail form); "Senast uppdaterad för 15 min sedan" (body-medium muted).
|
||||
3. "Historik vid anslutning" (title-small) over the wrapping history chips showing the current value and the shorter ones only (D-198; a shorter one calls E-41); the note "En längre period kräver BankID igen" (body-medium muted).
|
||||
4. "Förnya med BankID" (full width, `key`; tonal, filled when expiring or expired) → S-25 renewal; "Koppla från" (danger text, `delete`); offline both disabled with `NeedsConnectionHint`.
|
||||
|
||||
"Koppla från" opens a destructive `ConfirmDialog` (`delete` tile): "Koppla från SJ Prio Mastercard?" / "Köp som du inte har kopplat eller ignorerat raderas. Utgifter du har skapat finns kvar." with the checkbox row "Radera även kopplade och ignorerade köp" (off by default) and "Avbryt" / "Koppla från" (error filled). Success closes S-26; a network failure shows the snackbar "Kunde inte nå servern".
|
||||
|
||||
**States**
|
||||
|
||||
| Golden | State |
|
||||
|---|---|
|
||||
| `s26_active` | active, 30 days (also 200 %) |
|
||||
| `s26_expiring` | expiring: "Förnya med BankID" filled |
|
||||
| `s26_expired` | expired: "Har upphört" with the error icon, "Förnya med BankID" filled |
|
||||
| `s26_offline` | offline: the chips and both buttons disabled, "Kräver anslutning" |
|
||||
| `s26_disconnect` | the disconnect dialog |
|
||||
|
||||
**TalkBack**: the checkbox row is one checkbox node; the chip row is labelled "Historik vid anslutning".
|
||||
|
||||
### S-27 From card purchases, Från kortköp (`:feature:cards`, pushed from S-5)
|
||||
|
||||
**Layout** (app bar: back, "Från kortköp" with the account's name as subtitle): S-23's day headers and purchase rows (§S-23 5) for the viewer's purchases to handle (debit, SEK or foreign, neither linked nor ignored), newest first, with no match chips. Empty: a flat section with "Inga kortköp att hantera" (title-medium). Local only; works offline.
|
||||
|
||||
**States**
|
||||
|
||||
| Golden | State |
|
||||
|---|---|
|
||||
| `s27_to_handle` | the Vardag account's list (also 200 %) |
|
||||
| `s27_empty` | nothing to handle |
|
||||
|
||||
**Interactions**: a row opens S-7 prefilled with this account (CP-9); "Utgiften sparades" shows here on return (S-27 hosts `:app`'s snackbar host).
|
||||
|
||||
### Notifications (`:app` builds them, copy in `:core:design`)
|
||||
|
||||
Built locally after the sync per AC-13.5; channel "Aktivitet" (`notif_channel_activity`). Small icon: the monochrome `receipt_long` app mark; accent colour `primary`; large icon: the actor's avatar when cached (else none). Title and body per §13.3 in the phone's language (`notif_*`), e.g. "Anna lade till en utgift" / "ICA Maxi Lindhagen · 1 247,60 kr · Vardag"; "Anna registrerade en betalning" / "Anna betalade dig 800,00 kr". Generic fallback without amounts when the sync fails. Tap per AC-13.6.
|
||||
|
||||
Reference in New Issue
Block a user