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:
@@ -1,6 +1,6 @@
|
|||||||
# Kvitt: specification
|
# Kvitt: specification
|
||||||
|
|
||||||
Version **0.38** (2026-10-24). Schema version **4**. Sync protocol version **1**. Push payload version **1**.
|
Version **0.39** (2026-10-24). Schema version **4**. Sync protocol version **1**. Push payload version **1**.
|
||||||
|
|
||||||
This file is the single source of truth for Kvitt. It is written for implementation agents that have no other context. If code, a plan or `CLAUDE.md` disagrees with this file, this file is right. Words in `code` are literal identifiers, paths, enum values or error codes and must be used exactly as written. Anything not decided here is listed in §12 as `TODO(spec)`; do not invent a value for it.
|
This file is the single source of truth for Kvitt. It is written for implementation agents that have no other context. If code, a plan or `CLAUDE.md` disagrees with this file, this file is right. Words in `code` are literal identifiers, paths, enum values or error codes and must be used exactly as written. Anything not decided here is listed in §12 as `TODO(spec)`; do not invent a value for it.
|
||||||
|
|
||||||
@@ -1477,7 +1477,7 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
|
|||||||
| W6 | Android: Insights | Landed `1c9856b` (2026-10-21, D-141 to D-143, SPEC v0.31; Android gate green, 40 `s11_insights_*` goldens). An Insights section on S-11 from the D-133 endpoints, online only (§9.9): a UI-contract change (§14.8) in `:core:model`, `:data`, `:core:fakes` and `:feature:analytics`, with goldens | Android gate | Look at S-11's Insights on the phone (yes/no); done 2026-10-24 against `v1.0.3` (D-211) | W7 landed, its image deployed |
|
| W6 | Android: Insights | Landed `1c9856b` (2026-10-21, D-141 to D-143, SPEC v0.31; Android gate green, 40 `s11_insights_*` goldens). An Insights section on S-11 from the D-133 endpoints, online only (§9.9): a UI-contract change (§14.8) in `:core:model`, `:data`, `:core:fakes` and `:feature:analytics`, with goldens | Android gate | Look at S-11's Insights on the phone (yes/no); done 2026-10-24 against `v1.0.3` (D-211) | W7 landed, its image deployed |
|
||||||
| K0 | Card purchases spike: local test app (D-176) | The test app landed `2ce317f`: `docs/spikes/k0/app.py` and its `README.md`, one command (`python3 docs/spikes/k0/app.py`, the standard library only, the RS256 JWT signed with `openssl`), a page on `http://localhost:8765/` that runs the real Enable Banking flow (bank list, BankID, paste-back of the redirect address, accounts, "Uppdatera" with PSU headers, "Bakgrundshämtning" without, balances, the purchases, an aggregate "Analys", "Koppla från"), key and state in the git-ignored `docs/spikes/k0/.local/`. Done 2026-10-24: run with the owner on his card; report `docs/spikes/K0-enable-banking.md` (aggregates only), its answers recorded in SPEC v0.36 (D-177 to D-180); the partner's card and the next-day checks were not run (owner's decision, D-177) and move to K1's owner steps | The flow worked end to end with the owner's card (owner's yes); the report answers every K0 item; `git status` shows nothing from `.local/`; no gate (not part of any gate) | Done 2026-10-24: Enable Banking account and Terms of Service, the restricted production application, the key in `.local/`, the owner's card linked with BankID, the test app run (PLAN-K0 §8) | SPEC v0.35 |
|
| K0 | Card purchases spike: local test app (D-176) | The test app landed `2ce317f`: `docs/spikes/k0/app.py` and its `README.md`, one command (`python3 docs/spikes/k0/app.py`, the standard library only, the RS256 JWT signed with `openssl`), a page on `http://localhost:8765/` that runs the real Enable Banking flow (bank list, BankID, paste-back of the redirect address, accounts, "Uppdatera" with PSU headers, "Bakgrundshämtning" without, balances, the purchases, an aggregate "Analys", "Koppla från"), key and state in the git-ignored `docs/spikes/k0/.local/`. Done 2026-10-24: run with the owner on his card; report `docs/spikes/K0-enable-banking.md` (aggregates only), its answers recorded in SPEC v0.36 (D-177 to D-180); the partner's card and the next-day checks were not run (owner's decision, D-177) and move to K1's owner steps | The flow worked end to end with the owner's card (owner's yes); the report answers every K0 item; `git status` shows nothing from `.local/`; no gate (not part of any gate) | Done 2026-10-24: Enable Banking account and Terms of Service, the restricted production application, the key in `.local/`, the owner's card linked with BankID, the test app run (PLAN-K0 §8) | SPEC v0.35 |
|
||||||
| K1 | Card purchases: server (D-158 to D-175) | Landed `671c00d` (2026-10-24; D-181 to D-188, SPEC v0.38). Migration 0004 with its contract text and migration test, server schema 4; `src/cards/` (registry, Enable Banking adapter, the windowed fetch and de-duplication of §9.12 CP-7 and CP-8, poller, callback page); E-36 to E-44 in the server and `contracts/openapi.yaml` with examples; new error codes; the link clearing in §14.4.4 step 7; `contracts/test-vectors/match_card_purchase.json` and its Python reference in `tools/contracts-check.py`; config (§10.4), `infra/pi/compose.yaml`, `.env.example`, `infra/dev/**`, `infra/npm/kvitt.conf`, `infra/README.md` (step 4 block, card runbook); `web/src/api/schema.d.ts` regenerated | Server gate; `python3 tools/contracts-check.py`; the web gate (the regenerated types); an integration test suite against a local Enable Banking stub. At landing: server 356 tests (34 in `tests/cards_api/`), contracts-check 157 (17 `match_card_purchase` cases), web 609 (`match_card_purchase` listed as not ported until K3), the nginx check and both compose configs ok; the Android gate red by design until K2 lands (`VectorFilesTest`, `ExampleRoundTripTest` pin the contract's vector and example sets), so the commit bypassed the pre-commit hook (contract-first landing) | Copy the key (already on the Mac from K0) to `~/kvitt/secrets/cards/`, set the application id in `.env`, paste the new NPM block, deploy; once a client with the card screens runs: the partner's card, the next-day fetch without BankID and pending→booked (D-177) (PLAN-K1 §8) | SPEC v0.36 (K0's answers) |
|
| K1 | Card purchases: server (D-158 to D-175) | Landed `671c00d` (2026-10-24; D-181 to D-188, SPEC v0.38). Migration 0004 with its contract text and migration test, server schema 4; `src/cards/` (registry, Enable Banking adapter, the windowed fetch and de-duplication of §9.12 CP-7 and CP-8, poller, callback page); E-36 to E-44 in the server and `contracts/openapi.yaml` with examples; new error codes; the link clearing in §14.4.4 step 7; `contracts/test-vectors/match_card_purchase.json` and its Python reference in `tools/contracts-check.py`; config (§10.4), `infra/pi/compose.yaml`, `.env.example`, `infra/dev/**`, `infra/npm/kvitt.conf`, `infra/README.md` (step 4 block, card runbook); `web/src/api/schema.d.ts` regenerated | Server gate; `python3 tools/contracts-check.py`; the web gate (the regenerated types); an integration test suite against a local Enable Banking stub. At landing: server 356 tests (34 in `tests/cards_api/`), contracts-check 157 (17 `match_card_purchase` cases), web 609 (`match_card_purchase` listed as not ported until K3), the nginx check and both compose configs ok; the Android gate red by design until K2 lands (`VectorFilesTest`, `ExampleRoundTripTest` pin the contract's vector and example sets), so the commit bypassed the pre-commit hook (contract-first landing) | Copy the key (already on the Mac from K0) to `~/kvitt/secrets/cards/`, set the application id in `.env`, paste the new NPM block, deploy; once a client with the card screens runs: the partner's card, the next-day fetch without BankID and pending→booked (D-177) (PLAN-K1 §8) | SPEC v0.36 (K0's answers) |
|
||||||
| K2 | Card purchases: Android (D-169, D-170, D-172, D-173) | `:feature:cards` (S-23 to S-27 and the sheet), Room version 2 with its migration test, the `CardRepository` UI contract in `:core:model`, `matchCardPurchase` in `:domain` passing its vectors, `:data` (DTOs, card sync, queued link changes, Custom Tab, deep link), fakes and scenarios, the fourth bottom destination, S-5's "Från kortköp", S-7's prefill and link, S-8's card line, the committed card artwork mapped (D-180), goldens; strings in `docs/ux/strings.csv` | Android gate | Connect the card on the phone with BankID; create an expense from a purchase; link one; disconnect and reconnect (PLAN-K2 §8) | SPEC v0.36; may start then on the spec and fakes; lands after K1 |
|
| K2 | Card purchases: Android (D-169, D-170, D-172, D-173) | Landed `a8f91ee` (2026-10-24; D-191 to D-200, SPEC v0.39). `:feature:cards` (S-23 to S-27 and the sheet), Room version 2 with its migration test, the `CardRepository` UI contract in `:core:model`, `matchCardPurchase` in `:domain` passing its vectors, `:data` (DTOs, card sync, queued link changes, Custom Tab, deep link), fakes and scenarios, the fourth bottom destination, S-5's "Från kortköp", S-7's prefill and link, S-8's card line, the committed card artwork mapped (D-180), goldens; strings in `docs/ux/strings.csv` (77 rows); out-of-path edits ratified by the orchestrator: `ArchitectureRules.kt` knows `cards`, `:core:design` gains `ic_credit_card` / `KvittIcon.CreditCard`, the card measures in `KvittDimens` (D-200), the nav-label shrink-to-fit (D-197), `StringsCatalogueTest` takes `cards_` as a feature prefix | Android gate. At landing: `BUILD SUCCESSFUL`, 791 actionable tasks, 1154 tests, 0 failures; the 11 card examples (E-36 to E-44) round-trip, the 17 `match_card_purchase` cases pass, `VectorFilesTest` and `ExampleRoundTripTest` green again on `main`; 150 goldens added (`:feature:cards` 135 for every state of S-23 to S-27 with `_sv_light_200` for the long ones; `S-5-account-menu-card`, `s7_from_card`, `s7_from_card_foreign`, `s8_from_card`, `s13_hub_card_connected`) and 147 re-recorded for the fourth bar destination (S-3, S-4, S-11, S-13, S-17 and the navigation-bar component; only the bar changed) | Connect the card on the phone with BankID; create an expense from a purchase; link one; disconnect and reconnect (PLAN-K2 §8 steps 1 to 7, pending: the phone was not attached; the release APK with the card screens is installed by the orchestrator once K1's image runs on the Pi) | SPEC v0.36; may start then on the spec and fakes; lands after K1 |
|
||||||
| K3 | Card purchases: web (D-174) | W-21 to W-23 in `web/src/features/cards/`, the sidebar entry, the S-7-style prefill in W-5 and the card line in W-6, the committed card artwork mapped (D-180), `matchCardPurchase` in TypeScript passing its vectors | Web gate | Connect the card from the laptop browser with BankID QR; create and link from W-21 (PLAN-K3 §8) | SPEC v0.36; lands after K1 and K2 (it takes the card strings K2 adds to `docs/ux/strings.csv`) |
|
| K3 | Card purchases: web (D-174) | W-21 to W-23 in `web/src/features/cards/`, the sidebar entry, the S-7-style prefill in W-5 and the card line in W-6, the committed card artwork mapped (D-180), `matchCardPurchase` in TypeScript passing its vectors | Web gate | Connect the card from the laptop browser with BankID QR; create and link from W-21 (PLAN-K3 §8) | SPEC v0.36; lands after K1 and K2 (it takes the card strings K2 adds to `docs/ux/strings.csv`) |
|
||||||
| W7 | Server: web serving and analytics endpoints (D-132, D-133) | Landed `50a0d67` (2026-10-21, D-135 to D-138, SPEC v0.29; 299 server tests, contracts-check 136). `/app/` from `KVITT_WEB_DIR` with the SPA fallback, cache headers and the `/app/` CSP; the `/analytics/*` endpoints (D-135 to D-138, SPEC v0.29) in `contracts/openapi.yaml` and the server; the Node stage in `server/Dockerfile` with the repository root as build context (workflow, `infra/build-images.sh`, `infra/dev/**`) | Server gate; `python3 tools/contracts-check.py`; the web gate when `web/` exists and the contract changed (`web/src/api/schema.d.ts` regenerated) | Paste the v0.28 NPM block (`infra/README.md` step 4); deploy the new image (§10.10); deployed 2026-10-24 as `v1.0.3` (D-211) | SPEC v0.28 |
|
| W7 | Server: web serving and analytics endpoints (D-132, D-133) | Landed `50a0d67` (2026-10-21, D-135 to D-138, SPEC v0.29; 299 server tests, contracts-check 136). `/app/` from `KVITT_WEB_DIR` with the SPA fallback, cache headers and the `/app/` CSP; the `/analytics/*` endpoints (D-135 to D-138, SPEC v0.29) in `contracts/openapi.yaml` and the server; the Node stage in `server/Dockerfile` with the repository root as build context (workflow, `infra/build-images.sh`, `infra/dev/**`) | Server gate; `python3 tools/contracts-check.py`; the web gate when `web/` exists and the contract changed (`web/src/api/schema.d.ts` regenerated) | Paste the v0.28 NPM block (`infra/README.md` step 4); deploy the new image (§10.10); deployed 2026-10-24 as `v1.0.3` (D-211) | SPEC v0.28 |
|
||||||
|
|
||||||
@@ -1511,7 +1511,7 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
|
|||||||
* **M11** runs the import per §10.8 "Production run" (D-025). The rehearsal on 2026-10-18 ran both real files against a throwaway database and reconciled on both (Vardag 1746 rows, Hemma 37 rows); 21 rows were unmapped before D-044. The dry run of 2026-10-18 with M12b's mapping confirmed it: Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped; both reconciled.
|
* **M11** runs the import per §10.8 "Production run" (D-025). The rehearsal on 2026-10-18 ran both real files against a throwaway database and reconciled on both (Vardag 1746 rows, Hemma 37 rows); 21 rows were unmapped before D-044. The dry run of 2026-10-18 with M12b's mapping confirmed it: Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped; both reconciled.
|
||||||
* **W0 to W7** (D-134, §15.9): the web client programme. W0 and W7 run first in parallel (disjoint paths); W1 to W5 run in parallel once W0 has landed; W4's W-11 and W6 wait for W7. W0 owns `web/**` except the feature folders, registers every route with a placeholder page per feature folder (as M2a registered every route, D-004), and lists its module paths in its report; W1 to W5 build only inside their feature folders and take everything shared from W0's modules (a missing shared piece is a request to the orchestrator, as M7x's requests went to M6b). A second agent to land that changed `web/package.json` resolves `web/package-lock.json` with `npm install` on rebase (as `Cargo.lock`, §14.7). A plan that changes `contracts/openapi.yaml` regenerates `web/src/api/schema.d.ts` (§15.3). Web agents do not use the phone or `tools/gradle-serial.sh`; a manual check runs against a throwaway server, never the owner's development or Pi data (§15.4). The orchestrator adds the web gate to `CLAUDE.md` "Build and verify" (D-131). W1 to W5 landed 2026-10-21 (`934eee7`, `a65ab71`, `f9a9bfb`, `1aecf51`, `3ebe57d`; D-144 to D-154, D-150 unused) with the web gate green on `main` (59 test files, 532 tests at `1aecf51`); a web fix (`83f5293`) followed (598 tests). Pieces built twice or three times in feature folders and other loose ends are listed for a W0b plan in `docs/HANDOFF.md` "Web follow-ups". W0b landed `5305f37` (v0.34). All of W0 to W7 are landed and verified: `v1.0.3` (built on the Mac from `8fa9198`, D-211) runs on the Kvitt Pi and the owner checked the web client and the phone's Insights against it on 2026-10-24 (§11.4).
|
* **W0 to W7** (D-134, §15.9): the web client programme. W0 and W7 run first in parallel (disjoint paths); W1 to W5 run in parallel once W0 has landed; W4's W-11 and W6 wait for W7. W0 owns `web/**` except the feature folders, registers every route with a placeholder page per feature folder (as M2a registered every route, D-004), and lists its module paths in its report; W1 to W5 build only inside their feature folders and take everything shared from W0's modules (a missing shared piece is a request to the orchestrator, as M7x's requests went to M6b). A second agent to land that changed `web/package.json` resolves `web/package-lock.json` with `npm install` on rebase (as `Cargo.lock`, §14.7). A plan that changes `contracts/openapi.yaml` regenerates `web/src/api/schema.d.ts` (§15.3). Web agents do not use the phone or `tools/gradle-serial.sh`; a manual check runs against a throwaway server, never the owner's development or Pi data (§15.4). The orchestrator adds the web gate to `CLAUDE.md` "Build and verify" (D-131). W1 to W5 landed 2026-10-21 (`934eee7`, `a65ab71`, `f9a9bfb`, `1aecf51`, `3ebe57d`; D-144 to D-154, D-150 unused) with the web gate green on `main` (59 test files, 532 tests at `1aecf51`); a web fix (`83f5293`) followed (598 tests). Pieces built twice or three times in feature folders and other loose ends are listed for a W0b plan in `docs/HANDOFF.md` "Web follow-ups". W0b landed `5305f37` (v0.34). All of W0 to W7 are landed and verified: `v1.0.3` (built on the Mac from `8fa9198`, D-211) runs on the Kvitt Pi and the owner checked the web client and the phone's Insights against it on 2026-10-24 (§11.4).
|
||||||
|
|
||||||
* **K0 to K3** (D-176, §11.1): the card-purchase programme. K0 runs first, on the Mac only, with the owner (a local test app for fast feedback before any Android work, owner's refinement 2026-10-24; the app landed `2ce317f`); K0 is done (run 2026-10-24, report `docs/spikes/K0-enable-banking.md`) and its answers are in SPEC v0.36 (D-177 to D-180), so K1 to K3 can start. The reports of K1 to K3 propose spec text and decisions that a docs agent records: K1's in SPEC v0.38 (landed `671c00d`, D-181 to D-188), K2's in v0.39, K3's in v0.40 (decisions from D-191 to D-210, disjoint ranges per plan). Then K1, K2 and K3 run in parallel in their own worktrees (disjoint paths, §14.7): K1 owns the contract (`contracts/**`, the migration text, the vectors) and lands first; K2 and K3 build against the spec and fakes, and rebase on K1 before their gates (K2's DTO tests and K3's generated types need K1's contract); K3 lands after K2, whose `docs/ux/strings.csv` rows it imports (`npm run i18n:import`). K2 uses the phone only when its brief names it; K1 and K3 never use the phone. No plan connects to the owner's real card except through the owner's own BankID step; no plan stores, prints or commits a real purchase.
|
* **K0 to K3** (D-176, §11.1): the card-purchase programme. K0 runs first, on the Mac only, with the owner (a local test app for fast feedback before any Android work, owner's refinement 2026-10-24; the app landed `2ce317f`); K0 is done (run 2026-10-24, report `docs/spikes/K0-enable-banking.md`) and its answers are in SPEC v0.36 (D-177 to D-180), so K1 to K3 can start. The reports of K1 to K3 propose spec text and decisions that a docs agent records: K1's in SPEC v0.38 (landed `671c00d`, D-181 to D-188), K2's in v0.39 (landed `a8f91ee`, D-191 to D-200), K3's in v0.40 (landed `faeb752`) (decisions from D-191 to D-210, disjoint ranges per plan). Then K1, K2 and K3 run in parallel in their own worktrees (disjoint paths, §14.7): K1 owns the contract (`contracts/**`, the migration text, the vectors) and lands first; K2 and K3 build against the spec and fakes, and rebase on K1 before their gates (K2's DTO tests and K3's generated types need K1's contract); K3 lands after K2, whose `docs/ux/strings.csv` rows it imports (`npm run i18n:import`). K2 uses the phone only when its brief names it; K1 and K3 never use the phone. No plan connects to the owner's real card except through the owner's own BankID step; no plan stores, prints or commits a real purchase.
|
||||||
|
|
||||||
### 11.4 Validation reports
|
### 11.4 Validation reports
|
||||||
|
|
||||||
@@ -1536,6 +1536,7 @@ Only things that cannot be decided without the owner. Everything else in this sp
|
|||||||
| OQ-6 | Answered 2026-10-24 (D-156): no confirmation step on S-1 for an invite link that prefills a server other than the release default (`RELEASE_SERVER_URL`, AC-2.6) (security review L-6); the default stands: S-1 opens its server row expanded for a non-default server so the address is visible (AC-2.6, D-116). | M10 | answered |
|
| OQ-6 | Answered 2026-10-24 (D-156): no confirmation step on S-1 for an invite link that prefills a server other than the release default (`RELEASE_SERVER_URL`, AC-2.6) (security review L-6); the default stands: S-1 opens its server row expanded for a non-default server so the address is visible (AC-2.6, D-116). | M10 | answered |
|
||||||
| OQ-7 | Should a new card purchase raise a notification on the holder's phone (a local one, built from the holder's own data; never an FCM payload with content)? | K2 | Not in this version: purchases appear on the next sync without a notification (D-171) |
|
| OQ-7 | Should a new card purchase raise a notification on the holder's phone (a local one, built from the holder's own data; never an FCM payload with content)? | K2 | Not in this version: purchases appear on the next sync without a notification (D-171) |
|
||||||
| OQ-8 | Should several purchases be turned into expenses at once ("Lägg till i konto…", one expense each with the account's default split)? | after K2 | Not in this version (D-171); one purchase at a time through S-7 |
|
| OQ-8 | Should several purchases be turned into expenses at once ("Lägg till i konto…", one expense each with the account's default split)? | after K2 | Not in this version (D-171); one purchase at a time through S-7 |
|
||||||
|
| OQ-9 | `CardPurchase.linked` (§14.2.4) is null both when a purchase is unlinked and while its linked expense is not yet on the phone (for example a link made in the web client to an expense the phone has not pulled yet), so the purchase is listed under "Kopplade" without its "Kopplad till" line and its sheet briefly offers "Skapa utgift" and the matches. Add `linkedExpenseId` to `CardPurchase` so the sheet can say "Kopplad" without the expense's details (a UI-contract change, §14.8)? | next Android card pass | Not now: the window lasts until the next household sync brings the expense |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1562,7 +1563,7 @@ A brief for one UX agent (Opus) who has read this spec and nothing else. It runs
|
|||||||
* A floating action button "Add expense" (Lägg till utgift) on Home and on Account detail.
|
* A floating action button "Add expense" (Lägg till utgift) on Home and on Account detail.
|
||||||
* Account detail, expense editor, expense detail, settle up, settlement detail and all settings sub-screens are pushed screens with a top app bar and back.
|
* Account detail, expense editor, expense detail, settle up, settlement detail and all settings sub-screens are pushed screens with a top app bar and back.
|
||||||
* The expense editor is a full-screen screen, not a bottom sheet (the numeric keyboard needs the room).
|
* The expense editor is a full-screen screen, not a bottom sheet (the numeric keyboard needs the room).
|
||||||
* Wiring (D-071, as built by M7a to M7f and connected by M8): the top-level screens S-3, S-4, S-11 and S-13 render their own `KvittNavigationBar` and take `onNavigate: (KvittDestination) -> Unit`; the `:app` scaffold draws no second bar, and the `NavHost` switches top-level destinations from those lambdas. Since D-172 S-23 is the fourth top-level screen that draws the bar, and `KvittDestination` (`:core:design`) gains `Cards` between `Home` and `Analytics`. Navigation-argument keys are constants in each feature (`ARG_EXPENSE_ID` and `ARG_ACCOUNT_ID`; `SettleArgs.SETTLEMENT_ID` and `SettleArgs.ACCOUNT_ID`; `DrillDownArgs.KEYS`; settings `"categoryId"` and `"templateId"`), read through `SavedStateHandle`.
|
* Wiring (D-071, as built by M7a to M7f and connected by M8): the top-level screens S-3, S-4, S-11 and S-13 render their own `KvittNavigationBar` and take `onNavigate: (KvittDestination) -> Unit`; the `:app` scaffold draws no second bar, and the `NavHost` switches top-level destinations from those lambdas. Since D-172 S-23 is the fourth top-level screen that draws the bar, and `KvittDestination` (`:core:design`) gains `Cards` between `Home` and `Analytics` (as built by K2, `a8f91ee`: `enum class KvittDestination { Home, Cards, Analytics, Settings }`, icon `credit_card`, label `nav_cards`; a label that does not fit its slot at a large font scale shrinks to fit, down to 70 %, D-197). Navigation-argument keys are constants in each feature (`ARG_EXPENSE_ID` and `ARG_ACCOUNT_ID`; `SettleArgs.SETTLEMENT_ID` and `SettleArgs.ACCOUNT_ID`; `DrillDownArgs.KEYS`; settings `"categoryId"` and `"templateId"`), read through `SavedStateHandle`.
|
||||||
* Result snackbars (D-071): the result of S-7 ("Utgiften sparades"), of S-9 and of archiving an account is shown by the screen returned to; the account-archived snackbar has an undo action (`common_undo`, "Ångra"). As built by M12e (D-095): six screens host `:app`'s shared `snackbarHostState` in their own scaffold (Home, AccountDetail, ExpenseDetail, SettlementDetail, DrillDown, SyncStatus), so a snackbar never overlaps a FAB or the bottom bar; `:app` has no overlay host. Features hand `:app` string resource ids, never resolved strings: `ExpenseEditorScreen.onSaved(ExpenseId, @StringRes Int?)`, and `onDeleted()` on S-7 and S-8 takes no argument, `:app` then shows `snackbar_expense_deleted`.
|
* Result snackbars (D-071): the result of S-7 ("Utgiften sparades"), of S-9 and of archiving an account is shown by the screen returned to; the account-archived snackbar has an undo action (`common_undo`, "Ångra"). As built by M12e (D-095): six screens host `:app`'s shared `snackbarHostState` in their own scaffold (Home, AccountDetail, ExpenseDetail, SettlementDetail, DrillDown, SyncStatus), so a snackbar never overlaps a FAB or the bottom bar; `:app` has no overlay host. Features hand `:app` string resource ids, never resolved strings: `ExpenseEditorScreen.onSaved(ExpenseId, @StringRes Int?)`, and `onDeleted()` on S-7 and S-8 takes no argument, `:app` then shows `snackbar_expense_deleted`.
|
||||||
* Session gate (as built by M8): S-1 for an unpaired or unrecognised session, S-2 for the profile step after a `partner` pairing only (AC-2.10, D-086; a member pairing a second phone with a `device` invite goes to Home), S-18 for a revoked device (back on S-18 closes the app). Home shows S-3 while the household has no partner (`partner == null`).
|
* Session gate (as built by M8): S-1 for an unpaired or unrecognised session, S-2 for the profile step after a `partner` pairing only (AC-2.10, D-086; a member pairing a second phone with a `device` invite goes to Home), S-18 for a revoked device (back on S-18 closes the app). Home shows S-3 while the household has no partner (`partner == null`).
|
||||||
|
|
||||||
@@ -1576,7 +1577,7 @@ Each screen must be designed in these states where they apply: **loaded**, **emp
|
|||||||
| S-2 | Partner profile step | Display name field (prefilled; a blank name shows `onboarding_error_name_required` "Skriv ditt namn"), avatar with "Add picture"; the owner's name and avatar ("You are joining Jacob"). Shown only after a `partner` pairing (`Pairing.inviteKind`, D-086) | Continue |
|
| S-2 | Partner profile step | Display name field (prefilled; a blank name shows `onboarding_error_name_required` "Skriv ditt namn"), avatar with "Add picture"; the owner's name and avatar ("You are joining Jacob"). Shown only after a `partner` pairing (`Pairing.inviteKind`, D-086) | Continue |
|
||||||
| S-3 | Waiting for partner | Explanation that the partner must join first; the pending invite's expiry if any; error state when fetching the invite failed (snackbar with "Försök igen") | Invite partner (goes to S-21); Settings |
|
| S-3 | Waiting for partner | Explanation that the partner must join first; the pending invite's expiry if any; error state when fetching the invite failed (snackbar with "Försök igen") | Invite partner (goes to S-21); Settings |
|
||||||
| S-4 | Home | Global balance header: both avatars, wording per §9.3, amount; "Settle up" button when non-zero; sync line: "Last synced …" and pending count when > 0, tap opens S-16 ("Sync failed" when `SyncSummary.lastAttemptFailed`; `SyncStatusPill` takes no `failed` parameter, D-088); update banner (S-17) when available ("Later" hides it for the ViewModel's lifetime only, so it returns on the next start); list of active accounts, each with name, balance wording and amount, number of expenses this month; collapsed "Archived (n)" section; empty state when no accounts ("Create your first account") | Add expense (FAB); Settle up; open account; New account; Pull to refresh (sync now, D-127) |
|
| S-4 | Home | Global balance header: both avatars, wording per §9.3, amount; "Settle up" button when non-zero; sync line: "Last synced …" and pending count when > 0, tap opens S-16 ("Sync failed" when `SyncSummary.lastAttemptFailed`; `SyncStatusPill` takes no `failed` parameter, D-088); update banner (S-17) when available ("Later" hides it for the ViewModel's lifetime only, so it returns on the next start); list of active accounts, each with name, balance wording and amount, number of expenses this month; collapsed "Archived (n)" section; empty state when no accounts ("Create your first account") | Add expense (FAB); Settle up; open account; New account; Pull to refresh (sync now, D-127) |
|
||||||
| S-5 | Account detail | Account name, default split with names (ratios written "60/40", `common_ratio`), balance header; list of expenses and settlements interleaved by date, grouped by month with month totals (household spend; month headers are not sticky), each expense row per AC-8.10, each settlement row per AC-11.7 (its title may wrap to 2 lines, 3 at 200 % font scale, and its second line is the type label `ledger_settlement_label` "Betalning" from `:core:design`, D-068); archived badge if archived (an archived account hides the add-expense FAB, AC-6.4, and keeps "Settle this account", D-052); pending-sync marker per row | Add expense (not on an archived account); Settle this account; Edit account; Archive / Unarchive; Delete (only if allowed); Pull to refresh (sync now, D-127); "Från kortköp" (overflow menu, only while the viewer has a card connection, opens S-27, AC-22.9) |
|
| S-5 | Account detail | Account name, default split with names (ratios written "60/40", `common_ratio`), balance header; list of expenses and settlements interleaved by date, grouped by month with month totals (household spend; month headers are not sticky), each expense row per AC-8.10, each settlement row per AC-11.7 (its title may wrap to 2 lines, 3 at 200 % font scale, and its second line is the type label `ledger_settlement_label` "Betalning" from `:core:design`, D-068); archived badge if archived (an archived account hides the add-expense FAB, AC-6.4, and keeps "Settle this account", D-052); pending-sync marker per row | Add expense (not on an archived account); Settle this account; Edit account; Archive / Unarchive; Delete (only if allowed); Pull to refresh (sync now, D-127); "Från kortköp" (overflow menu, first item, only while the viewer has a card connection and not on an archived account, D-199; opens S-27, AC-22.9) |
|
||||||
| S-6 | Account editor | Name field; default split control (one slider or stepper plus two percent fields that always sum to 100, two decimals allowed); preview "Of 100,00 kr: Jacob 60,00 kr, Anna 40,00 kr"; "Needs connection" state; the discard dialog has a title and no body text (D-080) | Save; Cancel |
|
| S-6 | Account editor | Name field; default split control (one slider or stepper plus two percent fields that always sum to 100, two decimals allowed); preview "Of 100,00 kr: Jacob 60,00 kr, Anna 40,00 kr"; "Needs connection" state; the discard dialog has a title and no body text (D-080) | Save; Cancel |
|
||||||
| S-7 | Expense editor | Amount entry (large, numeric keyboard, `kr`); description; account picker (active accounts); category picker (grid of icon + colour + name, active categories; for a new expense the category is selected from the description per §9.11, with no marker); date (today default, date picker); payer toggle (two avatars: "Paid by"); account, date, payer and split shown as four fixed full-width rows in that order, which do not move when a value changes (D-102); split section: mode chooser (`default` "Account default 60/40", `ratio` "Custom ratio", `exact` "Exact amounts", `full` "All on one person") and its controls; live preview of each person's share; notes (collapsed by default); validation errors inline; delete in edit mode; opened from a card purchase (S-23's sheet or S-27, CP-9, D-169) it starts as a new expense prefilled from the purchase, and "Spara" also links the purchase (a foreign-currency purchase leaves the amount empty); after saving, the snackbar "Utgiften sparades" without "Ångra" (an undo would be an audited create and delete that also notifies the partner, D-052), shown by the screen returned to (D-071), `onSaved` handing `:app` the string resource id (D-095); the quick row is filled by `quickRowCategories` (D-075) and category labels take up to two centred lines, a longer custom name ending the second line in "…" (D-076); the discard dialog has no body text (D-080) | Save; Cancel; Delete (edit mode) |
|
| S-7 | Expense editor | Amount entry (large, numeric keyboard, `kr`); description; account picker (active accounts); category picker (grid of icon + colour + name, active categories; for a new expense the category is selected from the description per §9.11, with no marker); date (today default, date picker); payer toggle (two avatars: "Paid by"); account, date, payer and split shown as four fixed full-width rows in that order, which do not move when a value changes (D-102); split section: mode chooser (`default` "Account default 60/40", `ratio` "Custom ratio", `exact` "Exact amounts", `full` "All on one person") and its controls; live preview of each person's share; notes (collapsed by default); validation errors inline; delete in edit mode; opened from a card purchase (S-23's sheet or S-27, CP-9, D-169) it starts as a new expense prefilled from the purchase, and "Spara" also links the purchase (a foreign-currency purchase leaves the amount empty); after saving, the snackbar "Utgiften sparades" without "Ångra" (an undo would be an audited create and delete that also notifies the partner, D-052), shown by the screen returned to (D-071), `onSaved` handing `:app` the string resource id (D-095); the quick row is filled by `quickRowCategories` (D-075) and category labels take up to two centred lines, a longer custom name ending the second line in "…" (D-076); the discard dialog has no body text (D-080) | Save; Cancel; Delete (edit mode) |
|
||||||
| S-8 | Expense detail | Amount, description, category, account, date, payer, each user's share and the split mode, notes, "Recurring: <template>" when generated; for the card holder only, when a purchase of theirs is linked to it, the line "Från kortköp: <merchant>, <date>" (`expense_detail_from_card`) with "Ta bort koppling" (AC-22.10, D-169); timeline (AC-12.4, AC-12.5); "deleted" state when opened from a notification of a deletion; "missing" state (not yet on this phone) with "Synka nu" (`expense_detail_sync_now`), online only (§9.9, D-080); after a delete `:app` shows `snackbar_expense_deleted` on the screen returned to (D-095) | Edit; Delete |
|
| S-8 | Expense detail | Amount, description, category, account, date, payer, each user's share and the split mode, notes, "Recurring: <template>" when generated; for the card holder only, when a purchase of theirs is linked to it, the line "Från kortköp: <merchant>, <date>" (`expense_detail_from_card`) with "Ta bort koppling" (AC-22.10, D-169); timeline (AC-12.4, AC-12.5); "deleted" state when opened from a notification of a deletion; "missing" state (not yet on this phone) with "Synka nu" (`expense_detail_sync_now`), online only (§9.9, D-080); after a delete `:app` shows `snackbar_expense_deleted` on the screen returned to (D-095) | Edit; Delete |
|
||||||
@@ -1584,7 +1585,7 @@ Each screen must be designed in these states where they apply: **loaded**, **emp
|
|||||||
| S-10 | Settlement detail | Payer, recipient, amount, date, note, allocations per account; timeline; "missing" state with "Synka nu" (`settle_detail_sync_now`), online only (§9.9, D-080) | Edit; Delete |
|
| S-10 | Settlement detail | Payer, recipient, amount, date, note, allocations per account; timeline; "missing" state with "Synka nu" (`settle_detail_sync_now`), online only (§9.9, D-080) | Edit; Delete |
|
||||||
| S-11 | Analytics | Range selector (presets of AC-14.2 plus custom); granularity toggle month/week; measure toggle household / my share; group-by toggle category/account; total for range; chart 1 stacked bars over time with legend; chart 2 category breakdown; chart 3 account breakdown; empty state ("No expenses in this period"); a chart whose range has no expenses shows only the 0 baseline, with no gridline values (D-068). Total card (D-069): caption "<range> · Hushållet" for the household measure and "<range> · N veckor" in week granularity; for my share the label is "Totalt · Min del" so the caption does not repeat it; month presets show whole-month ranges ("maj–okt 2026"), custom ranges and week granularity show days (§9.8, D-077). Legend and stacking order: categories by `sortOrder`, accounts in `AccountRepository.all()` order (`AnalyticsState.groups` order is unspecified, §14.2.4). Loading means `SyncSummary.lastSuccessAt == null` and shows skeleton cards; offline looks like loaded; no error state (analytics are local, AC-14.1). Insights section (D-142, D-143), online only. | Change filters; tap a bar or a row to open S-12; pick a category in Insights; "Försök igen" |
|
| S-11 | Analytics | Range selector (presets of AC-14.2 plus custom); granularity toggle month/week; measure toggle household / my share; group-by toggle category/account; total for range; chart 1 stacked bars over time with legend; chart 2 category breakdown; chart 3 account breakdown; empty state ("No expenses in this period"); a chart whose range has no expenses shows only the 0 baseline, with no gridline values (D-068). Total card (D-069): caption "<range> · Hushållet" for the household measure and "<range> · N veckor" in week granularity; for my share the label is "Totalt · Min del" so the caption does not repeat it; month presets show whole-month ranges ("maj–okt 2026"), custom ranges and week granularity show days (§9.8, D-077). Legend and stacking order: categories by `sortOrder`, accounts in `AccountRepository.all()` order (`AnalyticsState.groups` order is unspecified, §14.2.4). Loading means `SyncSummary.lastSuccessAt == null` and shows skeleton cards; offline looks like loaded; no error state (analytics are local, AC-14.1). Insights section (D-142, D-143), online only. | Change filters; tap a bar or a row to open S-12; pick a category in Insights; "Försök igen" |
|
||||||
| S-12 | Analytics drill-down | Title naming the filter ("Mat & hushåll, oktober 2026"; a week as in `docs/ux/strings.csv`, "Vardag, vecka 41"; a category or account over the whole range "<name>, <range caption>") (D-070); total; expense rows as in S-5. A category or account over the range adds, above the total, its own range selector (AC-14.2 presets plus custom) and month/week toggle, both starting from S-11's values and changing S-12 only; below the total, the line chart of AC-14.11; then the rows. A range with no expenses reads "No expenses in this period" (D-128). No loading and no error state (D-070) | Open expense; change range or granularity |
|
| S-12 | Analytics drill-down | Title naming the filter ("Mat & hushåll, oktober 2026"; a week as in `docs/ux/strings.csv`, "Vardag, vecka 41"; a category or account over the whole range "<name>, <range caption>") (D-070); total; expense rows as in S-5. A category or account over the range adds, above the total, its own range selector (AC-14.2 presets plus custom) and month/week toggle, both starting from S-11's values and changing S-12 only; below the total, the line chart of AC-14.11; then the rows. A range with no expenses reads "No expenses in this period" (D-128). No loading and no error state (D-070) | Open expense; change range or granularity |
|
||||||
| S-13 | Settings hub | Own avatar and name; partner's avatar and name; rows: Profile, Categories, Recurring expenses, Anslut kort ("Connect card", subtitle "Ansluten · giltig till <date>" or "Läs in dina kortköp", D-172), Devices, Invite partner or Pair a device (admin), Sync status, About | Navigate |
|
| S-13 | Settings hub | Own avatar and name; partner's avatar and name; rows: Profile, Categories, Recurring expenses, Anslut kort ("Connect card", fourth row of the first group, subtitle "Ansluten · giltig till <date>", "Har upphört" for an expired connection (D-199) or "Läs in dina kortköp", D-172), Devices, Invite partner or Pair a device (admin), Sync status, About | Navigate |
|
||||||
| S-14 | Profile | Avatar with change/remove; display name; "Needs connection" state | Save |
|
| S-14 | Profile | Avatar with change/remove; display name; "Needs connection" state | Save |
|
||||||
| S-15 | Categories | List: icon, colour, name, archived section; editor: name (with "Reset name" for defaults), icon grid (33 icons of §9.7), colour swatches (14 tokens); delete or archive with the reason when delete is not allowed | Add; Edit; Reorder (drag); Archive; Delete |
|
| S-15 | Categories | List: icon, colour, name, archived section; editor: name (with "Reset name" for defaults), icon grid (33 icons of §9.7), colour swatches (14 tokens); delete or archive with the reason when delete is not allowed | Add; Edit; Reorder (drag); Archive; Delete |
|
||||||
| S-16 | Sync status | Last successful sync time; pending changes count with a list (the shared ledger rows of `:core:design`, D-019, not a separate 36 dp row); "Changes not synced" list from `sync_rejections` with what was lost and why (copy per §14.4.6 reasons); clock-skew warning; "Sync failed" when online and `SyncSummary.lastAttemptFailed` (D-088); the stale-change copy names `Rejection.actor`, the partner when it is null (D-089); "Sync now", online only (§9.9, D-080); "Öppna" on a rejection shows the phone's lost version read-only (D-109): a dialog "Din version" (`home_sync_lost_title`, body `home_sync_lost_body`) with the ledger row it would have been (read-only: the row's `onClick` is null), the account, payer, split and notes (`Rejection.lost`, §14.2.4), the badge "Du tog bort den här" (`home_sync_lost_deleted`) when the refused change was a deletion, and "Visa nuvarande version" (`home_sync_lost_show_current`), which opens S-8 or S-10; when `lost` is null (unreadable payload, or its category or a user is not on the phone) "Öppna" opens the current version directly | Sync now; open a rejected change (read-only); dismiss it |
|
| S-16 | Sync status | Last successful sync time; pending changes count with a list (the shared ledger rows of `:core:design`, D-019, not a separate 36 dp row); "Changes not synced" list from `sync_rejections` with what was lost and why (copy per §14.4.6 reasons); clock-skew warning; "Sync failed" when online and `SyncSummary.lastAttemptFailed` (D-088); the stale-change copy names `Rejection.actor`, the partner when it is null (D-089); "Sync now", online only (§9.9, D-080); "Öppna" on a rejection shows the phone's lost version read-only (D-109): a dialog "Din version" (`home_sync_lost_title`, body `home_sync_lost_body`) with the ledger row it would have been (read-only: the row's `onClick` is null), the account, payer, split and notes (`Rejection.lost`, §14.2.4), the badge "Du tog bort den här" (`home_sync_lost_deleted`) when the refused change was a deletion, and "Visa nuvarande version" (`home_sync_lost_show_current`), which opens S-8 or S-10; when `lost` is null (unreadable payload, or its category or a user is not on the phone) "Öppna" opens the current version directly | Sync now; open a rejected change (read-only); dismiss it |
|
||||||
@@ -1594,11 +1595,11 @@ Each screen must be designed in these states where they apply: **loaded**, **emp
|
|||||||
| S-20 | Devices | Rows per AC-3.1; the app version shows `Device.appVersionName` ("Kvitt 1.1"), else the version code (D-090) | Revoke (with confirmation) |
|
| S-20 | Devices | Rows per AC-3.1; the app version shows `Device.appVersionName` ("Kvitt 1.1"), else the version code (D-090) | Revoke (with confirmation) |
|
||||||
| S-21 | Invites | For the admin: "Invite partner" (only before the partner joins) and "Pair another device" for either user; a created invite shows the code in groups of 4, the link, expiry, and a share action (Android share sheet); list of pending invites with cancel | Create; Share; Cancel |
|
| S-21 | Invites | For the admin: "Invite partner" (only before the partner joins) and "Pair another device" for either user; a created invite shows the code in groups of 4, the link, expiry, and a share action (Android share sheet); list of pending invites with cancel | Create; Share; Cancel |
|
||||||
| S-22 | About | App version name and code, server URL (the paired server; before pairing the build's release server, `AppBuildInfo.releaseServerUrl`, D-122), server version from E-1 `/health`, fetched when S-22 opens while online, shown as "<version> (schema <n>)" (`settings_about_server_version_schema`, "1.1.0 (schema 3)"); offline or when the call fails, the `X-Kvitt-Server-Version` header of the last response without the schema (`AboutState`, §14.2.4, D-110); no open-source licences row (OQ-5, D-099); after "Check for update": `UpdateCheck.UpToDate` shows "Du har den senaste versionen" (`settings_about_up_to_date`), `UpdateCheck.Failed` shows "Kunde inte nå servern" (`common_could_not_reach_server`) (AC-16.6, D-092); the button calls `UpdateRepository.checkNow()`, which always asks the server (D-140) | Check for update |
|
| S-22 | About | App version name and code, server URL (the paired server; before pairing the build's release server, `AppBuildInfo.releaseServerUrl`, D-122), server version from E-1 `/health`, fetched when S-22 opens while online, shown as "<version> (schema <n>)" (`settings_about_server_version_schema`, "1.1.0 (schema 3)"); offline or when the call fails, the `X-Kvitt-Server-Version` header of the last response without the schema (`AboutState`, §14.2.4, D-110); no open-source licences row (OQ-5, D-099); after "Check for update": `UpdateCheck.UpToDate` shows "Du har den senaste versionen" (`settings_about_up_to_date`), `UpdateCheck.Failed` shows "Kunde inte nå servern" (`common_could_not_reach_server`) (AC-16.6, D-092); the button calls `UpdateRepository.checkNow()`, which always asks the server (D-140) | Check for update |
|
||||||
| S-23 | Card purchases (Kortköp; `:feature:cards`, top-level, D-172) | Top app bar "Kortköp"; a header card with the card's artwork (small, `CardArt`), its name, "Bara du ser dina kortköp" and "Uppdaterad <relative time>" (§9.8 relative form) with an "Uppdatera" button (a spinner and "Uppdaterar…" while E-42 runs); filter chips "Att hantera" (default), "Kopplade", "Ignorerade", "Alla", each with its count; the purchases of the filter, newest first, grouped by day (day headers in the §9.8 list form), each row: merchant (one line, ellipsis), amount (`MoneyText`, or "<amount> <currency>" with "Utländsk valuta"), "Preliminär" chip when pending, "Insättning" for a credit (only under "Alla"), and under "Att hantera" the match chip of CP-10 ("Koppla till <description>" or "<n> möjliga utgifter"); under "Kopplade" the line "Kopplad till <description> · <account>". States: no connection (empty: large artwork, "Anslut ditt kort", "Se dina köp här och gör om dem till utgifter med några tryck. Bara du ser dem.", "Anslut kort" → S-24); first fetch ("Hämtar dina köp…" with a progress indicator, while connected and no fetch has finished); loaded; empty filter ("Inget att hantera" / "Nya kortköp visas här." for "Att hantera", "Inga köp här" for the others); offline (the stored list, "Uppdatera" disabled with "Kräver anslutning"; linking and ignoring still work, CP-9); expiring (warning banner "Kortanslutningen upphör <date>" with "Förnya med BankID", CP-6); expired (banner "Kortanslutningen har upphört" with "Förnya med BankID"; list kept); fetch failed (banner "Kunde inte hämta köp. Senast uppdaterad <time>" with "Försök igen", CP-14); long content (long merchant texts, 7-digit amounts). Purchase sheet (bottom sheet on tapping a row): merchant, the purchase date in the detail form, amount, status ("Preliminär" / "Bokförd"); for a debit purchase to handle: "Skapa utgift" (primary) → S-7 prefilled (CP-9); "Koppla till befintlig utgift" with up to 3 matches (each: description, account, payer, date, amount; one tap links and closes the sheet with the snackbar "Kopplad" and "Ångra"), or "Ingen utgift med samma belopp runt det här datumet"; "Välj en annan utgift" (the CP-10 list in a second sheet); "Ignorera" (snackbar "Ignorerad" with "Ångra"). Linked: "Öppna utgift" (S-8), "Ta bort koppling". Ignored: "Sluta ignorera". Credit: the note "Insättningar och återbetalningar är inga utgifter. Ändra den ursprungliga utgiften för hand vid behov." and no action. Foreign currency: the note "Beloppet är i <currency>: skriv beloppet i kronor." above "Skapa utgift" | Uppdatera; Pull to refresh; filter; open a purchase; Skapa utgift; link a match (also from the row's chip); Välj en annan utgift; Ignorera / Sluta ignorera; Ta bort koppling; Öppna utgift; Förnya med BankID; Anslut kort |
|
| S-23 | Card purchases (Kortköp; `:feature:cards`, top-level, D-172) | Top app bar "Kortköp"; a header card with the card's artwork (small, `CardArt`), its name, "Bara du ser dina kortköp" and "Uppdaterad <relative time>" (§9.8 relative form) with an "Uppdatera" button (a spinner and "Uppdaterar…" while E-42 runs); filter chips "Att hantera" (default), "Kopplade", "Ignorerade", "Alla", each with its count; the purchases of the filter, newest first, grouped by day (day headers in the §9.8 list form), each row: merchant (one line, ellipsis), amount (`MoneyText`, or "<amount> <currency>" with "Utländsk valuta"), "Preliminär" chip when pending, "Insättning" for a credit (only under "Alla"), and under "Att hantera" the match chip of CP-10 ("Koppla till <description>" or "<n> möjliga utgifter"); under "Kopplade" the line "Kopplad till <description> · <account>". States: no connection (empty: large artwork, "Anslut ditt kort", "Se dina köp här och gör om dem till utgifter med några tryck. Bara du ser dem.", "Anslut kort" → S-24); first fetch ("Hämtar dina köp…" with a progress indicator, while connected and no fetch has finished); loaded; empty filter ("Inget att hantera" / "Nya kortköp visas här." for "Att hantera", "Inga köp här" for the others); offline (the stored list, "Uppdatera" disabled with "Kräver anslutning"; linking and ignoring still work, CP-9); expiring (warning banner "Kortanslutningen upphör <date>" with "Förnya med BankID", CP-6); expired (banner "Kortanslutningen har upphört" with "Förnya med BankID"; list kept); fetch failed (banner "Kunde inte hämta köp. Senast uppdaterad <time>" with "Försök igen", CP-14); long content (long merchant texts, 7-digit amounts). Purchase sheet (bottom sheet on tapping a row): merchant, the purchase date in the detail form, amount, status ("Preliminär" / "Bokförd"); for a debit purchase to handle: "Skapa utgift" (primary) → S-7 prefilled (CP-9); "Koppla till befintlig utgift" with up to 3 matches (each: description, account, payer, date, amount; one tap links and closes the sheet with the snackbar "Kopplad" and "Ångra"), or "Ingen utgift med samma belopp runt det här datumet"; "Välj en annan utgift" (the CP-10 list in a second sheet); "Ignorera" (snackbar "Ignorerad" with "Ångra"). Linked: "Öppna utgift" (S-8), "Ta bort koppling". Ignored: "Sluta ignorera". Credit: the note "Insättningar och återbetalningar är inga utgifter. Ändra den ursprungliga utgiften för hand vid behov." and no action. Foreign currency: the note "Beloppet är i <currency>: skriv beloppet i kronor." above "Skapa utgift". As built by K2 (`a8f91ee`, design-spec S-23): at most one banner, above the header card, expired before expiring before fetch failed; expired also disables "Uppdatera"; the header card opens S-26 (TalkBack click label "Kortanslutning", `cards_connection_settings`); during the first fetch the header has no "Uppdaterad" line and no filters show; the four filter chips wrap to a second row (D-198); a row shows "Utländsk valuta" as a badge next to "Preliminär" or "Insättning"; the one-match chip links in one tap (snackbar "Kopplad" with "Ångra"), the "<n> möjliga utgifter" chip opens the sheet; back from "Välj en annan utgift" returns to the purchase sheet; a failed "Uppdatera" on the network shows the snackbar "Kunde inte nå servern"; a purchase linked to an expense not yet on the phone is listed under "Kopplade" without the "Kopplad till" line and its sheet shows the to-handle actions until the expense arrives (`CardPurchase.linked` is null then, OQ-9) | Uppdatera; Pull to refresh; filter; open a purchase; Skapa utgift; link a match (also from the row's chip); Välj en annan utgift; Ignorera / Sluta ignorera; Ta bort koppling; Öppna utgift; Förnya med BankID; Anslut kort |
|
||||||
| S-24 | Connect card (Anslut kort; `:feature:cards`, pushed from S-13, D-172) | App bar "Anslut kort"; intro "Välj ditt kort. Kvitt läser dina köp men kan aldrig betala eller flytta pengar."; one large tile per catalogue entry (E-36): the card's artwork (`CardArt`, full width, card aspect ratio 1.586:1, corner radius 16), name, issuer; a connected card's tile adds "Ansluten · giltig till <date>" and opens S-26; a provider not available shows "Inte konfigurerat på servern" and is disabled (AC-22.1); loading (skeleton tile), offline ("Kräver anslutning", tiles disabled; the catalogue comes from the server), error ("Kunde inte nå servern" with "Försök igen") | Open a card (S-25 or S-26) |
|
| S-24 | Connect card (Anslut kort; `:feature:cards`, pushed from S-13, D-172) | App bar "Anslut kort"; intro "Välj ditt kort. Kvitt läser dina köp men kan aldrig betala eller flytta pengar."; one large tile per catalogue entry (E-36): the card's artwork (`CardArt`, full width, card aspect ratio 1.586:1, corner radius 16), name, issuer; a connected card's tile adds "Ansluten · giltig till <date>" and opens S-26; a provider not available shows "Inte konfigurerat på servern" and is disabled (AC-22.1); loading (skeleton tile), offline ("Kräver anslutning", tiles disabled; the catalogue comes from the server), error ("Kunde inte nå servern" with "Försök igen"). As built by K2: offline shows the `NeedsConnectionBanner` "Listan över kort hämtas från servern. Anslut telefonen till internet." (`cards_connect_offline_body`, D-200) and the cached catalogue entry's tile disabled (D-194); E-36 is asked again when the phone comes online; the connected line's date is in the §9.8 list form | Open a card (S-25 or S-26) |
|
||||||
| S-25 | Connect a card (`:feature:cards`, pushed from S-24, or from "Förnya med BankID") | App bar with the card's name; the artwork (large); "Hämta köp från de senaste" with a segmented choice "7 dagar", "30 dagar" (default), "90 dagar", "180 dagar" (CP-3); the note "Du kan korta perioden senare. En längre period kräver BankID igen."; the button "Anslut med BankID" (renewal: "Förnya med BankID", the history preset to the connection's). After the tap: waiting state "Väntar på BankID…" with "Slutför i webbläsaren och i BankID och kom sedan tillbaka hit." and "Avbryt" (stops polling; the attempt expires by itself); connected: "Kortet är anslutet" then S-23; failed: "Det gick inte att ansluta kortet" with "Försök igen"; timed out: "Anslutningsförsöket tog för lång tid" with "Försök igen"; offline: the button disabled with "Kräver anslutning" | Anslut med BankID; Avbryt; Försök igen |
|
| S-25 | Connect a card (`:feature:cards`, pushed from S-24, or from "Förnya med BankID") | App bar with the card's name; the artwork (large); "Hämta köp från de senaste" with ~~a segmented choice~~ wrapping filter chips (D-198) "7 dagar", "30 dagar" (default), "90 dagar", "180 dagar" (CP-3); the note "Du kan korta perioden senare. En längre period kräver BankID igen."; the button "Anslut med BankID" (renewal: "Förnya med BankID", the history preset to the connection's). After the tap: waiting state "Väntar på BankID…" with "Slutför i webbläsaren och i BankID och kom sedan tillbaka hit." and "Avbryt" (stops polling; the attempt expires by itself); connected: "Kortet är anslutet" then S-23; failed: "Det gick inte att ansluta kortet" with "Försök igen"; timed out: "Anslutningsförsöket tog för lång tid" with "Försök igen"; offline: the button disabled with "Kräver anslutning". As built by K2: E-38 is polled every 2 s only while S-25 is open and resumed (the Custom Tab on top pauses it) and once at once when the return link arrives (D-196); "Kortet är anslutet" shows for 1.5 s, then S-23; "Avbryt" returns to the choice; E-37 failing on the network shows "Kunde inte nå servern" above the button, any other failure, or no browser to open the bank page, the failed state; timed out is shown when E-38 answers `expired` (the server's 15 minutes), with no clock check on the phone | Anslut med BankID; Avbryt; Försök igen |
|
||||||
| S-26 | Card connection (`:feature:cards`, pushed from S-24, or from S-23's header) | App bar with the card's name; artwork; status line ("Ansluten", "Har upphört"); "Giltig till <date>" (detail form); "Senast uppdaterad <time>"; "Historik vid anslutning" with the current value, shorter values selectable (E-41), a longer one only through "Förnya med BankID" with the note "En längre period kräver BankID igen" (CP-3); "Förnya med BankID" (always offered; highlighted from 14 days before `valid_until`); "Koppla från" (error colour) → dialog "Koppla från <card>?" / "Köp som du inte har kopplat eller ignorerat raderas. Utgifter du har skapat finns kvar." with the checkbox "Radera även kopplade och ignorerade köp" and "Koppla från" / "Avbryt" (CP-12); offline: every action disabled with "Kräver anslutning" | Change history; Förnya med BankID; Koppla från |
|
| S-26 | Card connection (`:feature:cards`, pushed from S-24, or from S-23's header) | App bar with the card's name; artwork; status line ("Ansluten", "Har upphört"); "Giltig till <date>" (detail form); "Senast uppdaterad <time>"; "Historik vid anslutning" with the current value, shorter values selectable (E-41), a longer one only through "Förnya med BankID" with the note "En längre period kräver BankID igen" (CP-3); "Förnya med BankID" (always offered; highlighted from 14 days before `valid_until`, and when expired: filled instead of tonal); "Koppla från" (error colour) → dialog "Koppla från <card>?" / "Köp som du inte har kopplat eller ignorerat raderas. Utgifter du har skapat finns kvar." with the checkbox "Radera även kopplade och ignorerade köp" and "Koppla från" / "Avbryt" (CP-12); offline: every action disabled with "Kräver anslutning". As built by K2: artwork, then a facts card (status line with a filled `check_circle` in `owed` or a filled `error` in `error`, "Giltig till", "Senast uppdaterad"), then the history as wrapping chips listing only the current value and shorter ones (D-198), then the two buttons; a network failure shows the snackbar "Kunde inte nå servern"; after "Koppla från" the screen closes | Change history; Förnya med BankID; Koppla från |
|
||||||
| S-27 | From card purchases (Från kortköp; `:feature:cards`, pushed from S-5) | App bar "Från kortköp" with the account's name as subtitle; the viewer's purchases to handle (debit, SEK or foreign, neither linked nor ignored), newest first, as S-23's rows without chips; empty: "Inga kortköp att hantera"; works offline | Tap a purchase: S-7 prefilled with this account (CP-9) |
|
| S-27 | From card purchases (Från kortköp; `:feature:cards`, pushed from S-5) | App bar "Från kortköp" with the account's name as subtitle; the viewer's purchases to handle (debit, SEK or foreign, neither linked nor ignored), newest first, as S-23's rows without chips, grouped by day under S-23's day headers (as built by K2); empty: "Inga kortköp att hantera"; works offline; S-7's "Utgiften sparades" shows here on return | Tap a purchase: S-7 prefilled with this account (CP-9) |
|
||||||
|
|
||||||
System notifications (copy needed in both languages, built per AC-13.5):
|
System notifications (copy needed in both languages, built per AC-13.5):
|
||||||
|
|
||||||
@@ -1718,6 +1719,8 @@ Card purchase strings (K2 adds them to `docs/ux/strings.csv` with these keys; th
|
|||||||
| `cards_pick_title` | From card purchases | Från kortköp |
|
| `cards_pick_title` | From card purchases | Från kortköp |
|
||||||
| `cards_pick_empty` | No card purchases to handle | Inga kortköp att hantera |
|
| `cards_pick_empty` | No card purchases to handle | Inga kortköp att hantera |
|
||||||
|
|
||||||
|
K2 added these keys to `docs/ux/strings.csv` (`a8f91ee`, 77 rows with `nav_cards` and the plural forms) and four more of its own (D-200): `cards_foreign_amount` ("%1$s %2$s", the bank's decimal amount and the currency, a decimal comma in Swedish), `cards_connection_settings` (TalkBack click label of S-23's header card: "Card connection" / "Kortanslutning"), `cards_connect_offline_body` (S-24's offline banner: "The list of cards comes from the server. Connect the phone to the internet." / "Listan över kort hämtas från servern. Anslut telefonen till internet.") and `settings_cards_expired` (S-13's subtitle of an expired connection: "Ended" / "Har upphört", D-199). Every card key is in both `strings.xml` files of its module.
|
||||||
|
|
||||||
### 13.4 Deliverables
|
### 13.4 Deliverables
|
||||||
|
|
||||||
**Phase 1 (U1): directions.** In `docs/ux/directions/`:
|
**Phase 1 (U1): directions.** In `docs/ux/directions/`:
|
||||||
@@ -2225,7 +2228,7 @@ Local-only semantics on `expenses` and `settlements`:
|
|||||||
* `pending = true` means the row has a local change not yet accepted by the server. Every local create, edit or delete sets `pending = true`, sets a new UUIDv7 `pending_mutation_id`, sets `updated_at` to the phone's current time in the fixed RFC 3339 form (and `created_at` on create), and sets `local_updated_at`.
|
* `pending = true` means the row has a local change not yet accepted by the server. Every local create, edit or delete sets `pending = true`, sets a new UUIDv7 `pending_mutation_id`, sets `updated_at` to the phone's current time in the fixed RFC 3339 form (and `created_at` on create), and sets `local_updated_at`.
|
||||||
* A local delete sets `deleted_at` = `updated_at` = now. The row stays as a tombstone.
|
* A local delete sets `deleted_at` = `updated_at` = now. The row stays as a tombstone.
|
||||||
* The pending-changes count (AC-15.4) is `count(expenses where pending) + count(settlements where pending)`; pending card rows are not counted (they are the holder's own and need no attention, CP-9).
|
* The pending-changes count (AC-15.4) is `count(expenses where pending) + count(settlements where pending)`; pending card rows are not counted (they are the holder's own and need no attention, CP-9).
|
||||||
* Card purchases follow the same pattern for the holder's link and ignore changes (CP-9): a local `link`, `unlink` or `setIgnored` writes `expense_id` and `ignored`, sets `pending = true` and `local_updated_at`; the card cycle sends `PUT /cards/purchases/{id}` (E-44) for each pending row and, on 200, overwrites the row with the answer and clears `pending`; on `not_found` with `field` `expense_id` (the expense is not on the server yet) it stays pending for the next cycle; on `not_found` for the purchase it deletes the local row; on `expense_already_linked` or `validation_failed` it takes the server's state at the next pull and clears `pending`. A card pull skips a pending local row, as §14.4.2 step 3 does. Deleted purchases (tombstones) are deleted locally.
|
* Card purchases follow the same pattern for the holder's link and ignore changes (CP-9): a local `link`, `unlink` or `setIgnored` writes `expense_id` and `ignored`, sets `pending = true` and `local_updated_at`; the card cycle sends `PUT /cards/purchases/{id}` (E-44) for each pending row and, on 200, overwrites the row with the answer and clears `pending`; on `not_found` with `field` `expense_id` (the expense is not on the server yet) it stays pending for the next cycle; on `not_found` for the purchase it deletes the local row; on `expense_already_linked` or `validation_failed` it takes the server's state at the next pull and clears `pending`: the row's `card_version` is reset to 0 and `card_sync_state.cursor` moved back to just before the row's version when it is ahead, so the unchanged server row is pulled again (D-192). A link whose expense is not on the phone or is deleted there is cleared before E-44, and every link to an expense is cleared when the phone deletes it locally or removes it after a rejection (D-193). A card pull skips a pending local row, as §14.4.2 step 3 does. Deleted purchases (tombstones) are deleted locally.
|
||||||
|
|
||||||
#### 14.2.2 Local-only tables
|
#### 14.2.2 Local-only tables
|
||||||
|
|
||||||
@@ -2235,11 +2238,11 @@ Local-only semantics on `expenses` and `settlements`:
|
|||||||
| `CardSyncStateEntity` (v2, D-173) | `card_sync_state` | `id` (always 1), `cursor` Long (last applied `next_since` of E-40, 0 initially), `last_success_at` String? | `id` |
|
| `CardSyncStateEntity` (v2, D-173) | `card_sync_state` | `id` (always 1), `cursor` Long (last applied `next_since` of E-40, 0 initially), `last_success_at` String? | `id` |
|
||||||
| `SyncRejectionEntity` | `sync_rejections` | `mutation_id`, `entity_type`, `entity_id`, `reason` (§14.4.6), `message`, `rejected_payload` (JSON of the entity as pushed), `server_current` (JSON or null), `rejected_at` String, `dismissed` Boolean | `mutation_id` |
|
| `SyncRejectionEntity` | `sync_rejections` | `mutation_id`, `entity_type`, `entity_id`, `reason` (§14.4.6), `message`, `rejected_payload` (JSON of the entity as pushed), `server_current` (JSON or null), `rejected_at` String, `dismissed` Boolean | `mutation_id` |
|
||||||
|
|
||||||
Per-phone preferences (last used account, last used category per account, "install unknown apps" explanation shown, and the installed version code that received a 426 `protocol_unsupported`, D-043) live in DataStore `prefs.preferences_pb`, not Room. The pairing status lives in `auth.preferences_pb` with the token (§8.2, D-043).
|
Per-phone preferences (last used account, last used category per account, "install unknown apps" explanation shown, the installed version code that received a 426 `protocol_unsupported`, D-043, and the last card catalogue of E-36 as JSON under `card_providers`, D-194) live in DataStore `prefs.preferences_pb`, not Room. The pairing status lives in `auth.preferences_pb` with the token (§8.2, D-043).
|
||||||
|
|
||||||
#### 14.2.3 Migrations
|
#### 14.2.3 Migrations
|
||||||
|
|
||||||
Version 1 has no migrations. Every later Room version adds a `Migration` and a migration test in `:data` (`CLAUDE.md`), and is a contract change (§14.8). Version 2 (D-173, K2): `MIGRATION_1_2` creates `card_connections`, `card_purchases` and `card_sync_state` (with the row `id` 1, `cursor` 0) and changes no existing table; its test opens a version 1 database built from the exported `1.json` holding users, accounts, categories, expenses, settlements, audit entries, a pending expense and a rejection, migrates it, and asserts every row is kept and the new tables exist as in the exported `2.json`. The revoke wipe (AC-3.3) deletes the card tables with the rest. Room 3 migrations operate on an `SQLiteConnection` (the driver API), not on a `SupportSQLiteDatabase` (D-012).
|
Version 1 has no migrations. Every later Room version adds a `Migration` and a migration test in `:data` (`CLAUDE.md`), and is a contract change (§14.8). Version 2 (D-173, K2): `MIGRATION_1_2` creates `card_connections`, `card_purchases` and `card_sync_state` (with the row `id` 1, `cursor` 0) and changes no existing table; its test opens a version 1 database built from the exported `1.json` holding users, accounts, categories, expenses, settlements, audit entries, a pending expense and a rejection, migrates it, and asserts every row is kept and the new tables exist as in the exported `2.json`. The revoke wipe (AC-3.3) deletes the card tables with the rest. Landed by K2 (`a8f91ee`): `KvittDatabase` version 2 with `MIGRATION_1_2`, the exported `android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/2.json` committed (a contract file, §14.8) and the migration test in `:data`'s `MigrationTest`. Room 3 migrations operate on an `SQLiteConnection` (the driver API), not on a `SupportSQLiteDatabase` (D-012).
|
||||||
|
|
||||||
#### 14.2.4 UI contract: types and repository interfaces in `:core:model`
|
#### 14.2.4 UI contract: types and repository interfaces in `:core:model`
|
||||||
|
|
||||||
@@ -2527,11 +2530,20 @@ interface CardRepository {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
* **Where it is used.** `:feature:cards` (S-23 to S-27); `:feature:expense` (S-7 takes the optional navigation argument `cardPurchaseId`, starts from `prefill` and calls `link` after a successful save; S-8 reads `purchaseForExpense` and calls `unlink`); `:feature:account` (S-5 reads `hasConnection` for "Från kortköp"). `:app` binds `CardRepository` to `:data`'s `CardRepositoryImpl` in `DataBindingsModule`, routes `kvitt://cards/connected?attempt=<id>` to S-25 (or S-23 when S-25 is not open), and adds the destination `KvittDestination.Cards` (§13.2).
|
* **Where it is used.** `:feature:cards` (S-23 to S-27); `:feature:expense` (S-7 takes the optional navigation argument `cardPurchaseId`, starts from `prefill` and calls `link` after a successful save; S-8 reads `purchaseForExpense` and calls `unlink`); `:feature:account` (S-5 reads `hasConnection` for "Från kortköp"); `:feature:settings` (S-13 reads `cards(CardFilter.ALL).connection` for the "Anslut kort" subtitle, as built). `:app` binds `CardRepository` to `:data`'s `CardRepositoryImpl` in `DataBindingsModule`, routes `kvitt://cards/connected?attempt=<id>` to S-25 (or S-23 when S-25 is not open), and adds the destination `KvittDestination.Cards` (§13.2).
|
||||||
* **`:data`.** DTOs for E-36 to E-44 with round-trip tests over K1's examples; `CardRepositoryImpl` on the Room tables of §14.2.1 and `card_sync_state`; the card cycle (push pending card rows with E-44, then pull E-40 until `has_more` is false) runs after every household sync cycle (§14.4.1) and after `refresh`; a 404 `not_found` from E-40 (a server before schema 4) is treated as "no card support" and leaves the tables empty; `cursor_ahead` or a household epoch change drops the card cache (pending rows kept) and pulls from 0. `matches` and `others` come from `:domain`'s `matchCardPurchase` (§14.5.8) over Room's expenses. `prefill` applies CP-9 and the AC-8.6 default account; the editor runs the §9.11 suggestion itself as for any new expense. A local expense delete clears a local link to it (CP-9). The bank URL is opened by `:feature:cards` through a Custom Tab (`androidx.browser`, a catalog entry K2 adds).
|
* **`:data`.** DTOs for E-36 to E-44 with round-trip tests over K1's examples; `CardRepositoryImpl` on the Room tables of §14.2.1 and `card_sync_state`; the card cycle (push pending card rows with E-44, then pull E-40 until `has_more` is false) runs after every household sync cycle (§14.4.1) and after `refresh`; a 404 `not_found` from E-40 (a server before schema 4) is treated as "no card support" and leaves the tables empty; `cursor_ahead` or a household epoch change drops the card cache (pending rows kept) and pulls from 0. `matches` and `others` come from `:domain`'s `matchCardPurchase` (§14.5.8) over Room's expenses. `prefill` applies CP-9 and the AC-8.6 default account; the editor runs the §9.11 suggestion itself as for any new expense. A local expense delete clears a local link to it (CP-9). The bank URL is opened by `:feature:cards` through a Custom Tab (`androidx.browser`, a catalog entry K2 adds).
|
||||||
* **`:core:fakes`.** `FakeCardRepository` with scenarios in `fakes/cards/`: no connection, first fetch, loaded (to handle, linked, ignored, credit, pending, foreign), empty filter, offline, expiring, expired, fetch failed, long content, attempt pending / connected / failed / expired; `matches` computed with `:domain`'s function, as every fake computes derived values (§6.2).
|
* **`:core:fakes`.** `FakeCardRepository` with scenarios in `fakes/cards/`: no connection, first fetch, loaded (to handle, linked, ignored, credit, pending, foreign), empty filter, offline, expiring, expired, fetch failed, long content, attempt pending / connected / failed / expired; `matches` computed with `:domain`'s function, as every fake computes derived values (§6.2).
|
||||||
* **`ExpenseDraft`** is unchanged: a prefilled draft is a draft with `id` null.
|
* **`ExpenseDraft`** is unchanged: a prefilled draft is a draft with `id` null.
|
||||||
|
|
||||||
|
Readings fixed in v0.39 (D-191 to D-196, as landed by K2 `a8f91ee`; no type changes: `model/Cards.kt` declares exactly the block above):
|
||||||
|
|
||||||
|
* **The card cycle (D-191).** `:data`'s `CardSync` runs after each successful household cycle (`SyncEngine`) and after `refresh`, one at a time; its outcome never changes the household outcome or `SyncSummary`. A 404 from E-40 is "no card support": nothing is stored and the next cycle asks again.
|
||||||
|
* **E-44 refusals and dead links (D-192, D-193).** See §14.2.1: a refused change resets the row's `card_version` and moves the card cursor back before it; links to expenses no longer on the phone are cleared before E-44 and at every local delete or rejection that removes an expense.
|
||||||
|
* **Catalogue cache (D-194).** `CardsState.provider` comes from the last E-36 answer, cached in DataStore (`card_providers`, §14.2.2), so S-23, S-25 and S-26 name the card and draw its artwork offline; `providers()` stays online only and refreshes the cache; after a connect or an "Uppdatera" the phone asks E-36 when the cache is empty or lacks the connected provider.
|
||||||
|
* **Following a fetch (D-195).** After `attempt` returns `Connected` or E-42 answers `fetching` true, `:data` pulls E-40 every 3 s, at most 40 times, until the connection's `last_fetch_at` or `last_fetch_error` changes. `refresh` while this phone's E-42 call runs returns `Ok` without a request; `CardsState.fetching` is true for the duration of the call.
|
||||||
|
* **Return link (D-196).** `:app` parses `kvitt://cards/connected?attempt=<id>` (`AppLink.CardConnected`, the id 1 to 64 ASCII letters, digits or `-`); S-25, when open, polls that attempt at once; otherwise `AppViewModel` calls `attempt` once and opens S-23.
|
||||||
|
* **`CardPurchase.linked` while the expense is missing (OQ-9).** `linked` is null both when the purchase is unlinked and when it is linked to an expense not yet on the phone; the filters read the stored link, so such a purchase is listed under "Kopplade", without its "Kopplad till" line, and its sheet shows the to-handle actions until the expense arrives. A `linkedExpenseId` field would tell the two apart; it is a contract change (§14.8) and waits for the owner (§12 OQ-9).
|
||||||
|
|
||||||
Readings fixed in v0.4 (no type changes):
|
Readings fixed in v0.4 (no type changes):
|
||||||
|
|
||||||
* **`FieldError.error`.** Allowed values: a domain token of §14.5.3, `required` (a required value is missing or blank), or `validation_failed` (a §9.6 rule with no domain token: lengths, dates, `day_of_month`, periods).
|
* **`FieldError.error`.** Allowed values: a domain token of §14.5.3, `required` (a required value is missing or blank), or `validation_failed` (a §9.6 rule with no domain token: lengths, dates, `day_of_month`, periods).
|
||||||
@@ -3600,3 +3612,4 @@ Plans with disjoint paths (§14.7), one Opus agent each in its own worktree. W0
|
|||||||
| 0.36 | 2026-10-24 | K0's results (the spike run on the owner's SJ Prio Mastercard on 2026-10-24 with `docs/spikes/k0/app.py`, report `docs/spikes/K0-enable-banking.md`), D-177 to D-180; no contract file changes, server schema 4 and Room 2 as specified in v0.35. D-177: K0's outcome; every K0 `TODO(spec)` of §9.12 answered: ASPSP `SJ Prio Mastercard`, `SE` (CP-1); one `CARD` account per session (CP-2); at least 150 days of history right after BankID, so 7/30/90/180 stay (CP-3, §9.6); a background fetch without PSU headers accepted (CP-4); `required_psu_headers` [`psu-ip-address`], compared case-insensitively (CP-5); `maximum_consent_validity` 180 days, `valid_until` 2027-04-22 (CP-6); foreign purchases arrive in SEK (CP-7); credits were card-bill payments (CP-8); and the owner's assumptions of 2026-10-24 for what was not measured: the partner's card links to the owner's application (CP-13, the D-175 fallback stays), a pending transaction keeps its `entry_reference` when booked (CP-8, the amount-and-date rule becomes the secondary rule), the session is reused on later days without BankID (CP-6), all three verified by K1's owner steps. D-178: 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, newest first, a window at 100 rows split in half down to one day, de-duplicated across windows by `provider_ref`; `strategy=longest` dropped; `WRONG_TRANSACTIONS_PERIOD` ends the walk back; only a complete fetch deletes vanished pending purchases (CP-7, CP-8); `card_fetch` counts requests, splits, truncated windows, `booked_by_ref` and `booked_by_match` (§8.7). D-179: transaction mapping from K0's field survey: merchant = `remittance_information`, else `creditor.name`, else "Kortköp"; `purchase_date` = `booking_date`, else `value_date`, else `transaction_date`; `bank_transaction_code` unused, so a debit `FEE` is a purchase to handle; the holder's name, the masked card number (`CPAN`) and the merchant's address are never stored, logged or shown (CP-7). D-180: the owner's SJ Prio Mastercard artwork cropped to the card (1313 × 829) and committed for Android and the web, mapped from the first commit of K2 and K3, the goldens use it, the owner's artwork steps removed (revises D-173, D-174). The K plans' proposed spec text goes into v0.37 (§11.1, §11.3). |
|
| 0.36 | 2026-10-24 | K0's results (the spike run on the owner's SJ Prio Mastercard on 2026-10-24 with `docs/spikes/k0/app.py`, report `docs/spikes/K0-enable-banking.md`), D-177 to D-180; no contract file changes, server schema 4 and Room 2 as specified in v0.35. D-177: K0's outcome; every K0 `TODO(spec)` of §9.12 answered: ASPSP `SJ Prio Mastercard`, `SE` (CP-1); one `CARD` account per session (CP-2); at least 150 days of history right after BankID, so 7/30/90/180 stay (CP-3, §9.6); a background fetch without PSU headers accepted (CP-4); `required_psu_headers` [`psu-ip-address`], compared case-insensitively (CP-5); `maximum_consent_validity` 180 days, `valid_until` 2027-04-22 (CP-6); foreign purchases arrive in SEK (CP-7); credits were card-bill payments (CP-8); and the owner's assumptions of 2026-10-24 for what was not measured: the partner's card links to the owner's application (CP-13, the D-175 fallback stays), a pending transaction keeps its `entry_reference` when booked (CP-8, the amount-and-date rule becomes the secondary rule), the session is reused on later days without BankID (CP-6), all three verified by K1's owner steps. D-178: 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, newest first, a window at 100 rows split in half down to one day, de-duplicated across windows by `provider_ref`; `strategy=longest` dropped; `WRONG_TRANSACTIONS_PERIOD` ends the walk back; only a complete fetch deletes vanished pending purchases (CP-7, CP-8); `card_fetch` counts requests, splits, truncated windows, `booked_by_ref` and `booked_by_match` (§8.7). D-179: transaction mapping from K0's field survey: merchant = `remittance_information`, else `creditor.name`, else "Kortköp"; `purchase_date` = `booking_date`, else `value_date`, else `transaction_date`; `bank_transaction_code` unused, so a debit `FEE` is a purchase to handle; the holder's name, the masked card number (`CPAN`) and the merchant's address are never stored, logged or shown (CP-7). D-180: the owner's SJ Prio Mastercard artwork cropped to the card (1313 × 829) and committed for Android and the web, mapped from the first commit of K2 and K3, the goldens use it, the owner's artwork steps removed (revises D-173, D-174). The K plans' proposed spec text goes into v0.37 (§11.1, §11.3). |
|
||||||
| 0.37 | 2026-10-24 | D-211: image builds on the Mac with Podman (`infra/build-images.sh v1.0.N`) are the normal path, the Gitea workflow on the Pi 3 runner the fallback (revises D-156 point 5); tag rule (the next `v1.0.N` after the highest tag on `origin`, pushed as a git tag too, `v1.0.3` = `8fa9198`); the registry from the Mac is `gitea.lan` on port 80, the Pi's `.env` keeps `gitea.lan:3001`; Podman's insecure-registry files on the Mac and in the Podman machine; the script fixes `f15fa23` and `8fa9198` (§10.3, §10.10). `v1.0.3` deployed on the Kvitt Pi and the web client and S-11's Insights verified by the owner (§11.2 W1 to W7, §11.3, §11.4, §15). No contract file changes. |
|
| 0.37 | 2026-10-24 | D-211: image builds on the Mac with Podman (`infra/build-images.sh v1.0.N`) are the normal path, the Gitea workflow on the Pi 3 runner the fallback (revises D-156 point 5); tag rule (the next `v1.0.N` after the highest tag on `origin`, pushed as a git tag too, `v1.0.3` = `8fa9198`); the registry from the Mac is `gitea.lan` on port 80, the Pi's `.env` keeps `gitea.lan:3001`; Podman's insecure-registry files on the Mac and in the Podman machine; the script fixes `f15fa23` and `8fa9198` (§10.3, §10.10). `v1.0.3` deployed on the Kvitt Pi and the web client and S-11's Insights verified by the owner (§11.2 W1 to W7, §11.3, §11.4, §15). No contract file changes. |
|
||||||
| 0.38 | 2026-10-24 | K1's report (card purchases on the server, landed `671c00d`: migration 0004 byte-identical to §14.1.3, server schema 4 in code, E-36 to E-44 with four new error codes and a 502 `BadGateway` response in `contracts/openapi.yaml`, examples, `match_card_purchase.json` with its Python reference; gates at landing: server 356 tests, contracts-check 157, web 609, nginx check and compose configs ok, the Android gate red until K2 lands), D-181 to D-188 (D-189 and D-190 unused). D-181: the history counts today, `date_from` = today − (`history_days` − 1), 6 windows for 180 days (revises CP-3 and CP-7). D-182: the server's CS-4 first token for CP-8 uses Rust's standard Unicode classes without NFC. D-183: Enable Banking's errors mapped to CP-14 (`EXPIRED_SESSION`, `CLOSED_SESSION`, `REVOKED_SESSION`, `SESSION_DOES_NOT_EXIST` → `consent_expired` and `expired`; `ASPSP_RATE_LIMIT_EXCEEDED` or 429 → `rate_limited`; `ASPSP_ERROR`, `ASPSP_TIMEOUT`, 408, 5xx or no answer → `bank_unavailable`; anything else `failed`); E-37's `card_bank_unavailable` when `/aspsps` or `/auth` fails. D-184: the callback (an expired `state` stores `expired`, revising CP-2's `failed`; a used one changes nothing; neither `code` nor `error` → `failed`/`bank_error`; a session without accounts is revoked; the atomic claim). D-185: the poller's start-up slot, background starts counted whatever their outcome, deletion after `WRONG_TRANSACTIONS_PERIOD` only over the windows that answered, a split window's cut rows discarded. D-186: PSU headers only with a value, `/aspsps` cached 24 hours, the two fixed headers when it fails. D-187: E-41 with the current value and E-43 on a disconnected connection make no new version (`erase_handled=true` still erases), E-44's `field` for a credit purchase, `CardAttempt.failure` a plain nullable string. D-188: `src/wire/card.rs`, row helpers in `src/cards/mod.rs`, the client IP in a request extension, and the Pi's `compose.yaml` refusing to start until `secrets/cards/` exists. Changed: §9.12 CP-2, CP-3, CP-4, CP-5, CP-7, CP-8, CP-14; §10.2; §11.1, §11.2 K1 row, §11.3 (K2's text goes into v0.39, K3's into v0.40); §14.1.3; §14.3.3; §14.3.4 E-1 (`schema_version` 4), E-37, E-39, E-41, E-43, E-44; §14.3.5; §14.7. |
|
| 0.38 | 2026-10-24 | K1's report (card purchases on the server, landed `671c00d`: migration 0004 byte-identical to §14.1.3, server schema 4 in code, E-36 to E-44 with four new error codes and a 502 `BadGateway` response in `contracts/openapi.yaml`, examples, `match_card_purchase.json` with its Python reference; gates at landing: server 356 tests, contracts-check 157, web 609, nginx check and compose configs ok, the Android gate red until K2 lands), D-181 to D-188 (D-189 and D-190 unused). D-181: the history counts today, `date_from` = today − (`history_days` − 1), 6 windows for 180 days (revises CP-3 and CP-7). D-182: the server's CS-4 first token for CP-8 uses Rust's standard Unicode classes without NFC. D-183: Enable Banking's errors mapped to CP-14 (`EXPIRED_SESSION`, `CLOSED_SESSION`, `REVOKED_SESSION`, `SESSION_DOES_NOT_EXIST` → `consent_expired` and `expired`; `ASPSP_RATE_LIMIT_EXCEEDED` or 429 → `rate_limited`; `ASPSP_ERROR`, `ASPSP_TIMEOUT`, 408, 5xx or no answer → `bank_unavailable`; anything else `failed`); E-37's `card_bank_unavailable` when `/aspsps` or `/auth` fails. D-184: the callback (an expired `state` stores `expired`, revising CP-2's `failed`; a used one changes nothing; neither `code` nor `error` → `failed`/`bank_error`; a session without accounts is revoked; the atomic claim). D-185: the poller's start-up slot, background starts counted whatever their outcome, deletion after `WRONG_TRANSACTIONS_PERIOD` only over the windows that answered, a split window's cut rows discarded. D-186: PSU headers only with a value, `/aspsps` cached 24 hours, the two fixed headers when it fails. D-187: E-41 with the current value and E-43 on a disconnected connection make no new version (`erase_handled=true` still erases), E-44's `field` for a credit purchase, `CardAttempt.failure` a plain nullable string. D-188: `src/wire/card.rs`, row helpers in `src/cards/mod.rs`, the client IP in a request extension, and the Pi's `compose.yaml` refusing to start until `secrets/cards/` exists. Changed: §9.12 CP-2, CP-3, CP-4, CP-5, CP-7, CP-8, CP-14; §10.2; §11.1, §11.2 K1 row, §11.3 (K2's text goes into v0.39, K3's into v0.40); §14.1.3; §14.3.3; §14.3.4 E-1 (`schema_version` 4), E-37, E-39, E-41, E-43, E-44; §14.3.5; §14.7. |
|
||||||
|
| 0.39 | 2026-10-24 | K2's report (card purchases on Android, landed `a8f91ee`: Room version 2 with `MIGRATION_1_2`, its test and the exported `2.json`; the `CardRepository` UI contract of §14.2.4 unchanged in its names; the card cycle; `:feature:cards` S-23 to S-27; S-5, S-7, S-8, S-13 and the fourth bottom destination; Android gate at landing `BUILD SUCCESSFUL`, 791 actionable tasks, 1154 tests, 0 failures, `VectorFilesTest` and `ExampleRoundTripTest` green again; 150 goldens added, 147 re-recorded for the bar), D-191 to D-200. D-191: the card cycle after each successful household cycle and after `refresh`, never changing the household outcome; E-40's 404 is "no card support", retried every cycle. D-192: E-44's `expense_already_linked` or `validation_failed` resets the row's `card_version` to 0 and moves the card cursor back before it (§14.2.1). D-193: links to expenses no longer on the phone cleared before E-44 and at every local delete or rejection that removes an expense. D-194: the E-36 catalogue cached in DataStore (`card_providers`, §14.2.2). D-195: after a connect or E-42's `fetching`, E-40 every 3 s, at most 40 times, until `last_fetch_at` or `last_fetch_error` changes. D-196: the return link held by `AppViewModel`: S-25 polls it at once, else one E-38 call and S-23. D-197: bottom-bar labels shrink to fit down to 70 % (design-spec §3.11). D-198: S-23's filter chips wrap; S-25's and S-26's history as wrapping chips (revises S-25's segmented choice). D-199: S-13 "Har upphört" for an expired connection; S-5's "Från kortköp" hidden on archived accounts. D-200: four added string keys and the card artwork measures in `KvittDimens`. §13.2 (the destination enum as built), §13.3 S-5, S-13, S-23 to S-27 as built and the added keys, §14.2.1 to §14.2.4 (Room 2 landed; readings fixed in v0.39), §11.2 K2 row, §11.3, new OQ-9 (`CardPurchase.linked` while the expense is missing, §12). `docs/ux/design-spec.md`: S-23 to S-27 sections from the goldens, §3.11 four destinations and shrink-to-fit, §3.12 card artwork, the `credit_card` icon, S-5, S-7, S-8, S-13 card states. No contract file changes in this pass (Room 2 is K2's). contracts-check 157. |
|
||||||
|
|||||||
+15
-11
@@ -3,8 +3,8 @@
|
|||||||
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
|
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
|
||||||
|
|
||||||
## 1. Where things stand
|
## 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.
|
- 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). **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).
|
- **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).
|
- **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.
|
- **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).
|
- **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`).
|
- 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.
|
- 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:
|
- 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.
|
- 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.
|
||||||
- K2 (Android) and K3 (web), Opus agents in their own worktrees.
|
- 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).
|
- 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.
|
- 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).
|
- 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).
|
- 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`.
|
- 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.
|
- 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.
|
- 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)
|
## 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.
|
- 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 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 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).
|
- 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).
|
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).
|
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.
|
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)
|
## 4. Owner questions (asked, unanswered)
|
||||||
- OQ-7 (SPEC §12): a notification for new card purchases? Default: none in this version (D-171).
|
- 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-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.
|
- 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?
|
- 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`).
|
- 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.
|
- Nothing.
|
||||||
|
|
||||||
## 6. Numbering
|
## 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**.
|
- 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.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.
|
- 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 **1** in code; **2** specified (§14.2.3) for K2.
|
- 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 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**.
|
- Sync protocol **1**, push payload **1**.
|
||||||
|
|
||||||
## 7. How the work is run
|
## 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-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-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-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` |
|
| 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` | — |
|
| 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` | — |
|
| 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:
|
**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 |
|
| 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" |
|
| `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) |
|
| `close` | editor top bars (S-6, S-7, S-9, S-15 editor, S-19 editor) |
|
||||||
| `arrow_back` | pushed screens |
|
| `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".
|
**`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.
|
**`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.
|
**`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
|
## 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).
|
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).
|
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**
|
**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` | 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` | 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-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-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 |
|
| `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-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-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 |
|
| `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**
|
**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.
|
* 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.
|
* "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).
|
* `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.
|
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`.
|
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**
|
**States**
|
||||||
|
|
||||||
| Frame | State | What differs |
|
| 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` | 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-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" |
|
| `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 |
|
| `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).
|
**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):
|
**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`).
|
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").
|
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**
|
**States**
|
||||||
@@ -1038,9 +1052,10 @@ Goldens (no catalogue frame): `s12_category`, `s12_category_groceries`, `s12_cat
|
|||||||
| Frame | State | What differs |
|
| Frame | State | What differs |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `S-13-settings` | loaded (admin) | Jacob "Du · Administratör", Anna "Partner"; "Para ihop en enhet" shown |
|
| `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) |
|
| `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".
|
**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".
|
**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`)
|
### 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.
|
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