SPEC v0.32: web plans W1 to W5 (D-144 to D-154), APK install failures (D-155)
Build Kvitt images / build (push) Failing after 14m44s
Build Kvitt images / build (push) Failing after 14m44s
D-144 to D-154 record the web feature plans W1 to W5 (934eee7,a65ab71,f9a9bfb,1aecf51,3ebe57d): preset account and payment in the query string, Home empty state, the CSV format, AC-8.6 in the browser and search in the URL, the expense editor's banner and controls, live settle-up errors, analytics drill-down routes and chart accessibility, W-18's route, the partner intro and About's fallback; D-150 unused. D-155 recordsd686ee0: new AC-16.7, UpdateFailure in §14.2.4, design-spec §3.6, S-17 and S-22. SPEC §11.2, §11.3, §15.3, §15.6 and §15.7 follow the code; HANDOFF gets the state, the owner's browser checks and the web follow-ups for a W0b plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Kvitt: specification
|
# Kvitt: specification
|
||||||
|
|
||||||
Version **0.31** (2026-10-21). Schema version **3**. Sync protocol version **1**. Push payload version **1**.
|
Version **0.32** (2026-10-21). Schema version **3**. Sync protocol version **1**. Push payload version **1**.
|
||||||
|
|
||||||
This file is the single source of truth for Kvitt. It is written for implementation agents that have no other context. If code, a plan or `CLAUDE.md` disagrees with this file, this file is right. Words in `code` are literal identifiers, paths, enum values or error codes and must be used exactly as written. Anything not decided here is listed in §12 as `TODO(spec)`; do not invent a value for it.
|
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.
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ The server starts with an empty database. The household is created from the comm
|
|||||||
* AC-8.3 The payer is either user. The editor defaults the payer to the current user.
|
* AC-8.3 The payer is either user. The editor defaults the payer to the current user.
|
||||||
* AC-8.4 The date is any date within §9.6 limits, past or future. It defaults to today in the phone's time zone. This date is the only date used in analytics and lists.
|
* AC-8.4 The date is any date within §9.6 limits, past or future. It defaults to today in the phone's time zone. This date is the only date used in analytics and lists.
|
||||||
* AC-8.5 Split modes (enum §9.1): `default` (the account's default ratio, copied into the expense when saved), `ratio` (a custom ratio), `exact` (exact öre per user), `full` (the whole amount borne by one chosen user, who may be the payer or not). Share computation per §9.1.
|
* AC-8.5 Split modes (enum §9.1): `default` (the account's default ratio, copied into the expense when saved), `ratio` (a custom ratio), `exact` (exact öre per user), `full` (the whole amount borne by one chosen user, who may be the payer or not). Share computation per §9.1.
|
||||||
* AC-8.6 The editor defaults: account = the account last used on this phone if still active, else the oldest active account; category = the category suggestion of §9.11 for a new expense, whose last step is the category last used in that account on this phone if still active, else `other` (D-049); when `other` is archived too and no active last-used category exists, the editor shows no category until the user picks one (v0.9); editing an existing expense keeps its category; split mode `default`.
|
* AC-8.6 The editor defaults: account = the account last used on this phone if still active, else the oldest active account; category = the category suggestion of §9.11 for a new expense, whose last step is the category last used in that account on this phone if still active, else `other` (D-049); when `other` is archived too and no active last-used category exists, the editor shows no category until the user picks one (v0.9); editing an existing expense keeps its category; split mode `default`. On the web "this phone" means this browser (`localStorage`, D-147).
|
||||||
* AC-8.7 Exact mode: the user types one user's share; the other user's share is the amount minus it and is shown read-only. A typed share larger than the amount blocks saving with an inline error.
|
* AC-8.7 Exact mode: the user types one user's share; the other user's share is the amount minus it and is shown read-only. A typed share larger than the amount blocks saving with an inline error.
|
||||||
* AC-8.8 Changing the account of an expense in mode `default` re-copies the new account's ratio. In other modes the split is kept.
|
* AC-8.8 Changing the account of an expense in mode `default` re-copies the new account's ratio. In other modes the split is kept.
|
||||||
* AC-8.9 Deleting asks for confirmation and is a soft delete. Deleted expenses disappear from lists, balances and analytics on both phones after sync.
|
* AC-8.9 Deleting asks for confirmation and is a soft delete. Deleted expenses disappear from lists, balances and analytics on both phones after sync.
|
||||||
@@ -286,6 +286,7 @@ The server starts with an empty database. The household is created from the comm
|
|||||||
* AC-16.4 A verification failure deletes the file and shows "Download failed, try again".
|
* AC-16.4 A verification failure deletes the file and shows "Download failed, try again".
|
||||||
* AC-16.5 Debug builds never check for updates.
|
* AC-16.5 Debug builds never check for updates.
|
||||||
* AC-16.6 "Check for update" on S-22 calls `UpdateRepository.checkNow()` (D-140; `check()` before v0.30), which returns an `UpdateCheck` (D-092, revising D-073): `UpToDate` (nothing newer published; a 404 from E-5 counts as up to date) reports "You have the latest version" (`settings_about_up_to_date`, "Du har den senaste versionen"); `Failed` (no answer or another error) reports "Could not reach the server" (`common_could_not_reach_server`); `Available` shows the update banner. Debug builds report `UpToDate` without a request (AC-16.5). S-22 "Check for update" always calls `GET /apk/latest`, whatever the 6-hour throttle of AC-16.2 says, and restarts that window; the automatic checks on cold start and resume keep the throttle (D-140).
|
* AC-16.6 "Check for update" on S-22 calls `UpdateRepository.checkNow()` (D-140; `check()` before v0.30), which returns an `UpdateCheck` (D-092, revising D-073): `UpToDate` (nothing newer published; a 404 from E-5 counts as up to date) reports "You have the latest version" (`settings_about_up_to_date`, "Du har den senaste versionen"); `Failed` (no answer or another error) reports "Could not reach the server" (`common_could_not_reach_server`); `Available` shows the update banner. Debug builds report `UpToDate` without a request (AC-16.5). S-22 "Check for update" always calls `GET /apk/latest`, whatever the 6-hour throttle of AC-16.2 says, and restarts that window; the automatic checks on cold start and resume keep the throttle (D-140).
|
||||||
|
* AC-16.7 If the system installer refuses or fails the install, Home and S-22 show the Error banner "Kunde inte installera uppdateringen" (`update_install_failed`) with the reason (blocked by the phone's security settings, `update_install_blocked`, or the installer's own message) and "Försök igen", which downloads again; declining the system dialog shows "Uppdatera" again without an error (D-155). The cached APK is deleted once the installer answers.
|
||||||
|
|
||||||
### FR-17 Splitwise import
|
### FR-17 Splitwise import
|
||||||
|
|
||||||
@@ -1342,11 +1343,11 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
|
|||||||
| M10 | First release | Release build signed, published, both phones paired, push working both ways, nightly backup on Drive, restore drill report, memory check | `docs/validation/first-release.md` and `docs/validation/restore-drill.md` complete; server RSS under 100 MB | Create the release keystore with the `keytool` command of §10.6 at the repository root, or allow an agent to (D-101), and keep a copy and the password in the password manager; run the Pi steps of the M2c-pi compose project (D-100, D-125); install the APK on both phones from the landing page; allow "Install unknown apps"; partner pairs with the invite; confirm notifications arrive | M2c-pi (the owner's run of the bundle), M3, M4, M8 |
|
| M10 | First release | Release build signed, published, both phones paired, push working both ways, nightly backup on Drive, restore drill report, memory check | `docs/validation/first-release.md` and `docs/validation/restore-drill.md` complete; server RSS under 100 MB | Create the release keystore with the `keytool` command of §10.6 at the repository root, or allow an agent to (D-101), and keep a copy and the password in the password manager; run the Pi steps of the M2c-pi compose project (D-100, D-125); install the APK on both phones from the landing page; allow "Install unknown apps"; partner pairs with the invite; confirm notifications arrive | M2c-pi (the owner's run of the bundle), M3, M4, M8 |
|
||||||
| M11 | Import history | Rehearsed on 2026-10-18: the real run reconciled on both files in a throwaway database (Vardag 1746 rows, Hemma 37 rows; 21 rows unmapped before D-044). Dry run of 2026-10-18 with the M12b mapping (D-053): Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped; both reconciled. Real import run on the Pi per §10.8 "Production run" (D-025), `docs/validation/splitwise-import.md` with per-account totals compared to Splitwise's balances (aggregates only) | Imported balances equal Splitwise's "Total balance" rows per group | Provide the CSVs; confirm the balances on the phone match Splitwise | M9, M10 |
|
| M11 | Import history | Rehearsed on 2026-10-18: the real run reconciled on both files in a throwaway database (Vardag 1746 rows, Hemma 37 rows; 21 rows unmapped before D-044). Dry run of 2026-10-18 with the M12b mapping (D-053): Vardag 1746 rows, 0 unmapped; Hemma 37 rows, 0 unmapped; both reconciled. Real import run on the Pi per §10.8 "Production run" (D-025), `docs/validation/splitwise-import.md` with per-account totals compared to Splitwise's balances (aggregates only) | Imported balances equal Splitwise's "Total balance" rows per group | Provide the CSVs; confirm the balances on the phone match Splitwise | M9, M10 |
|
||||||
| W0 | Web foundation (D-131, D-134) | Landed `2ae65a5` (2026-10-21). `web/`: Vite, React 19, TypeScript strict, React Router 7, TanStack Query 5, `openapi-typescript` types in `web/src/api/schema.d.ts` and `openapi-fetch`, Vitest and Testing Library, ESLint and Prettier; design tokens from `docs/ux/design-spec.md` §2 as CSS custom properties; layout shell (sidebar, bottom bar under 720 px), router with every route of §15.3, keyboard shortcuts and W-20; login (W-1), token store and 401 handling; API client and household snapshot (§15.5); i18n (sv, en); formatting (§9.8); shared components; the money functions of §14.5.2 passing their vectors; `npm run check` | Web gate | none | SPEC v0.28 |
|
| W0 | Web foundation (D-131, D-134) | Landed `2ae65a5` (2026-10-21). `web/`: Vite, React 19, TypeScript strict, React Router 7, TanStack Query 5, `openapi-typescript` types in `web/src/api/schema.d.ts` and `openapi-fetch`, Vitest and Testing Library, ESLint and Prettier; design tokens from `docs/ux/design-spec.md` §2 as CSS custom properties; layout shell (sidebar, bottom bar under 720 px), router with every route of §15.3, keyboard shortcuts and W-20; login (W-1), token store and 401 handling; API client and household snapshot (§15.5); i18n (sv, en); formatting (§9.8); shared components; the money functions of §14.5.2 passing their vectors; `npm run check` | Web gate | none | SPEC v0.28 |
|
||||||
| W1 | Web: home and accounts | Planned. W-2, W-3, W-4 in `web/src/features/{home,accounts}/` | Web gate | Look at Home and an account in a browser against the development stack (yes/no) | W0 |
|
| W1 | Web: home and accounts | Landed `934eee7` (2026-10-21, D-144, D-145, SPEC v0.32). W-2, W-3, W-4 in `web/src/features/{home,accounts}/` | Web gate | Look at Home and an account in a browser against the development stack (yes/no) | W0 |
|
||||||
| W2 | Web: expenses, search and export | Planned. W-5 with the §9.11 suggestion (`suggest_category.json`), W-6 with the timeline, W-12 with the CSV export, in `web/src/features/{expenses,search,export}/` | Web gate | Add, edit and delete an expense in a browser; open an exported CSV in Numbers (yes/no) | W0 |
|
| W2 | Web: expenses, search and export | Landed `a65ab71` (2026-10-21, D-146 to D-148, SPEC v0.32; the shared editor fields in `web/src/components/editor/`). W-5 with the §9.11 suggestion (`suggest_category.json`, in the vector test since `83f5293`), W-6 with the timeline, W-12 with the CSV export, in `web/src/features/{expenses,search,export}/` | Web gate | Add, edit and delete an expense in a browser; open an exported CSV in Numbers (yes/no) | W0 |
|
||||||
| W3 | Web: settle up | Planned. W-7, W-8 in `web/src/features/settle/` | Web gate | Record and delete a settlement in a browser (yes/no) | W0 |
|
| W3 | Web: settle up | Landed `f9a9bfb` (2026-10-21, D-144, D-149, SPEC v0.32). W-7, W-8 in `web/src/features/settle/` | Web gate | Record and delete a settlement in a browser (yes/no) | W0 |
|
||||||
| W4 | Web: analytics and insights | Planned. W-9, W-10 with the D-128 line chart, W-11 on the D-133 endpoints (stubbed until W7 lands); `resolveRange` and `spendSeries` passing their vectors; in `web/src/features/analytics/` | Web gate | Look at the analytics pages in a browser (yes/no) | W0; W-11 needs W7 |
|
| W4 | Web: analytics and insights | Landed `1aecf51` (2026-10-21, D-151, D-152, SPEC v0.32). W-9, W-10 with the D-128 line chart, W-11 on the D-133 endpoints (E-34, E-35); `resolveRange` and `spendSeries` passing their vectors (`features/analytics/domain/analytics.vectors.test.ts`); in `web/src/features/analytics/` | Web gate | Look at the analytics pages in a browser (yes/no) | W0; W-11 needs W7 |
|
||||||
| W5 | Web: settings | Planned. W-13 to W-19 in `web/src/features/settings/` | Web gate | Pair a browser, see it on the phone's Devices, revoke it from the phone (yes/no) | W0 |
|
| W5 | Web: settings | Landed `3ebe57d` (2026-10-21, D-153, D-154, SPEC v0.32). W-13 to W-19 in `web/src/features/settings/` | Web gate | Pair a browser, see it on the phone's Devices, revoke it from the phone (yes/no) | W0 |
|
||||||
| 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) | 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) | W7 landed, its image deployed |
|
||||||
| 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) | 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) | SPEC v0.28 |
|
||||||
|
|
||||||
@@ -1378,7 +1379,7 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
|
|||||||
* **Infra overhaul** (landed `2fac1ec` to `e70fe0d`, no decision): one Gradle lock helper `tools/gradle-lock.sh` for `tools/gradle-serial.sh`, `tools/clean-builds.sh` and `infra/build-images.sh`; the pre-commit hook runs the Android tests through `tools/gradle-serial.sh` with JDK 25 on `PATH`, `JAVA_HOME` untouched; `install.sh` keeps the deployed `KVITT_TAG` (moot since D-125: no `install.sh`); `--provenance=false` on buildx pushes and the workflow token through the environment (§10.3; reversed by D-120 point 7); `RCLONE_REMOTE` dropped from the development env (§10 intro). **Infra security** (landed `7fb86f5`, D-117): Caddy log redaction and response headers (§8.7, §10.5.2), the non-root Caddy container (§8.9, §10.2), the development stack's single secret (§10 intro), the digest record (§10.3, §10.10; dropped by D-125 for `docker image inspect`), `kvitt_cicd` with `write:package` only (§10.3), the restricted cert-sync key (§10.5.1). D-120 carries the log redaction and the headers over to NPM's Custom Nginx Configuration (§10.5.2) and removes the Caddy container, the digest of its image and the cert-sync key.
|
* **Infra overhaul** (landed `2fac1ec` to `e70fe0d`, no decision): one Gradle lock helper `tools/gradle-lock.sh` for `tools/gradle-serial.sh`, `tools/clean-builds.sh` and `infra/build-images.sh`; the pre-commit hook runs the Android tests through `tools/gradle-serial.sh` with JDK 25 on `PATH`, `JAVA_HOME` untouched; `install.sh` keeps the deployed `KVITT_TAG` (moot since D-125: no `install.sh`); `--provenance=false` on buildx pushes and the workflow token through the environment (§10.3; reversed by D-120 point 7); `RCLONE_REMOTE` dropped from the development env (§10 intro). **Infra security** (landed `7fb86f5`, D-117): Caddy log redaction and response headers (§8.7, §10.5.2), the non-root Caddy container (§8.9, §10.2), the development stack's single secret (§10 intro), the digest record (§10.3, §10.10; dropped by D-125 for `docker image inspect`), `kvitt_cicd` with `write:package` only (§10.3), the restricted cert-sync key (§10.5.1). D-120 carries the log redaction and the headers over to NPM's Custom Nginx Configuration (§10.5.2) and removes the Caddy container, the digest of its image and the cert-sync key.
|
||||||
* **Overhaul outcomes** (2026-10-18). Server: landed through `b985ce9` (see the server overhaul above). Infra: landed through `e70fe0d` (see the infra overhaul above). **Android overhaul** (landed `e4e6e5d` to `c582363`, D-118, no prompt file, orchestrator brief): one screenshot and test harness in `:core:design`'s test fixtures and the convention plugin `kvitt.android.screenshots` (§6.2); the shared failure texts, limits and `savedSplitLabel` in `:core:design` (§6.2); dead code removed (`KvittMotion` down to the seesaw spring, `KvittIcon.Description`, unused dimens and shapes, catalogue and fake samples); the catalog entries `roborazzi-junit-rule`, `roborazzi-compose`, `androidx-hilt-navigation-compose` and `androidx-lifecycle-viewmodel-compose` removed; one `CopyCategoryKeywords` task in `build-logic` shared by `:data` and `:core:fakes` (D-055 holds); one `configureAndroidModule` for the application and library plugins; S-7's state types in `ExpenseEditorState.kt`. Gate at landing: 941 tests, no golden changed; release APK 10.46 MB. Pushed to `origin` at `c582363`.
|
* **Overhaul outcomes** (2026-10-18). Server: landed through `b985ce9` (see the server overhaul above). Infra: landed through `e70fe0d` (see the infra overhaul above). **Android overhaul** (landed `e4e6e5d` to `c582363`, D-118, no prompt file, orchestrator brief): one screenshot and test harness in `:core:design`'s test fixtures and the convention plugin `kvitt.android.screenshots` (§6.2); the shared failure texts, limits and `savedSplitLabel` in `:core:design` (§6.2); dead code removed (`KvittMotion` down to the seesaw spring, `KvittIcon.Description`, unused dimens and shapes, catalogue and fake samples); the catalog entries `roborazzi-junit-rule`, `roborazzi-compose`, `androidx-hilt-navigation-compose` and `androidx-lifecycle-viewmodel-compose` removed; one `CopyCategoryKeywords` task in `build-logic` shared by `:data` and `:core:fakes` (D-055 holds); one `configureAndroidModule` for the application and library plugins; S-7's state types in `ExpenseEditorState.kt`. Gate at landing: 941 tests, no golden changed; release APK 10.46 MB. Pushed to `origin` at `c582363`.
|
||||||
* **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).
|
* **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".
|
||||||
|
|
||||||
### 11.4 Validation reports
|
### 11.4 Validation reports
|
||||||
|
|
||||||
@@ -1453,7 +1454,7 @@ Each screen must be designed in these states where they apply: **loaded**, **emp
|
|||||||
| 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 |
|
||||||
| S-17 | Update available | Banner on Home with version name; dialog explaining "Install unknown apps" the first time; download progress; failure | Update; Later |
|
| S-17 | Update available | Banner on Home with version name; dialog explaining "Install unknown apps" the first time; download progress; failure (download, or the install refused or failed with its reason, AC-16.7, D-155) | Update; Later |
|
||||||
| S-18 | Device removed | Explanation that this phone's access was removed and local data deleted | Pair again (goes to S-1) |
|
| S-18 | Device removed | Explanation that this phone's access was removed and local data deleted | Pair again (goes to S-1) |
|
||||||
| S-19 | Recurring expenses | List: description, amount, account, "Every month on day 25", next date, paused badge; editor with the expense fields of S-7 (no date); account, payer and split as three fixed full-width rows (Konto, Betalt av, Delning; D-102, D-107) plus day of month (a new template defaults to today's day, AC-9.8, D-072), first month, optional last month, pause switch; a new template is sent as `NewRecurringTemplate` (`RecurringForm.newTemplate()`), an edit as `RecurringTemplate` (`RecurringForm.template(original, …)`), D-091; the discard dialog has no body text (D-080) | Add; Edit; Pause/Resume; Delete |
|
| S-19 | Recurring expenses | List: description, amount, account, "Every month on day 25", next date, paused badge; editor with the expense fields of S-7 (no date); account, payer and split as three fixed full-width rows (Konto, Betalt av, Delning; D-102, D-107) plus day of month (a new template defaults to today's day, AC-9.8, D-072), first month, optional last month, pause switch; a new template is sent as `NewRecurringTemplate` (`RecurringForm.newTemplate()`), an edit as `RecurringTemplate` (`RecurringForm.template(original, …)`), D-091; the discard dialog has no body text (D-080) | Add; Edit; Pause/Resume; Delete |
|
||||||
| 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) |
|
||||||
@@ -2111,9 +2112,14 @@ sealed interface UpdateState {
|
|||||||
data object None : UpdateState
|
data object None : UpdateState
|
||||||
data class Available(val info: UpdateInfo) : UpdateState
|
data class Available(val info: UpdateInfo) : UpdateState
|
||||||
data class Downloading(val info: UpdateInfo, val progress: Float) : UpdateState
|
data class Downloading(val info: UpdateInfo, val progress: Float) : UpdateState
|
||||||
data class Failed(val info: UpdateInfo) : UpdateState
|
data class Failed(val info: UpdateInfo, val reason: UpdateFailure = UpdateFailure.Download) : UpdateState // reason: v0.32, D-155
|
||||||
data class NeedsInstallPermission(val info: UpdateInfo) : UpdateState
|
data class NeedsInstallPermission(val info: UpdateInfo) : UpdateState
|
||||||
}
|
}
|
||||||
|
sealed interface UpdateFailure { // v0.32, D-155: why Failed; the downloaded file is deleted in every case
|
||||||
|
data object Download : UpdateFailure // no answer, an error, or size or SHA-256 mismatch (AC-16.4)
|
||||||
|
data object Blocked : UpdateFailure // STATUS_FAILURE_BLOCKED: the phone's security settings
|
||||||
|
data class Install(val detail: String?) : UpdateFailure // any other installer failure; detail = the installer's status message
|
||||||
|
}
|
||||||
sealed interface UpdateCheck { // v0.13, D-092
|
sealed interface UpdateCheck { // v0.13, D-092
|
||||||
data object UpToDate : UpdateCheck
|
data object UpToDate : UpdateCheck
|
||||||
data class Available(val info: UpdateInfo) : UpdateCheck
|
data class Available(val info: UpdateInfo) : UpdateCheck
|
||||||
@@ -3008,7 +3014,7 @@ Owner interview 2026-10-21 (D-129 to D-134). Kvitt has a web client, built in `w
|
|||||||
|
|
||||||
* `web/` at the repository root: Vite, React 19, TypeScript in strict mode, React Router 7, TanStack Query 5, `openapi-typescript` (types generated from `contracts/openapi.yaml` into `web/src/api/schema.d.ts`, generated and committed) and `openapi-fetch` as the API client, Recharts for charts, Vitest with Testing Library for tests, ESLint and Prettier, plain CSS modules. npm with a committed `package-lock.json`; Node 25 on the Mac. Exact versions are W0's, each the latest stable at the time (recorded in its report).
|
* `web/` at the repository root: Vite, React 19, TypeScript in strict mode, React Router 7, TanStack Query 5, `openapi-typescript` (types generated from `contracts/openapi.yaml` into `web/src/api/schema.d.ts`, generated and committed) and `openapi-fetch` as the API client, Recharts for charts, Vitest with Testing Library for tests, ESLint and Prettier, plain CSS modules. npm with a committed `package-lock.json`; Node 25 on the Mac. Exact versions are W0's, each the latest stable at the time (recorded in its report).
|
||||||
* **Design.** Tokens from `docs/ux/design-spec.md` §2 as CSS custom properties: the same colour roles (light and dark), category colours (§9.7), type scale, spacing, the 4/2 corner-radius language of the charts (D-097), the same icons (Material Symbols, Apache-2.0, bundled as SVG, never from a CDN). The typeface is the platform's system font stack (D-020's "no bundled font"), amounts with tabular figures. Components follow the design spec's anatomy where one exists (ledger rows, balance header, category tile, avatar, segmented controls); desktop layouts are W0's and each plan's, within the tokens.
|
* **Design.** Tokens from `docs/ux/design-spec.md` §2 as CSS custom properties: the same colour roles (light and dark), category colours (§9.7), type scale, spacing, the 4/2 corner-radius language of the charts (D-097), the same icons (Material Symbols, Apache-2.0, bundled as SVG, never from a CDN). The typeface is the platform's system font stack (D-020's "no bundled font"), amounts with tabular figures. Components follow the design spec's anatomy where one exists (ledger rows, balance header, category tile, avatar, segmented controls); desktop layouts are W0's and each plan's, within the tokens.
|
||||||
* **Strings.** A small JSON dictionary per language (`sv.json`, `en.json`). A string that exists in `docs/ux/strings.csv` uses its key and its text; web-only strings get keys prefixed `web_` and are listed in the plan's report (they are not added to `strings.csv`, which is the app's). Placeholders are named (`{name}`), not Android format arguments. Both languages always have every key; the gate checks it.
|
* **Strings.** A small JSON dictionary per language (`sv.json`, `en.json`). A string that exists in `docs/ux/strings.csv` uses its key and its text; web-only strings get keys prefixed `web_` and are listed in the plan's report (they are not added to `strings.csv`, which is the app's). Placeholders are named (`{name}`), not Android format arguments. As built (W0, `web/README.md` "Strings"): `web/src/i18n/{sv,en}.json` are generated from `docs/ux/strings.csv` plus `web/src/i18n/web-strings.csv` (the shared web-only strings, W0's), with numbered placeholders `{1}`, `{2}` in place of `%1$s`; each feature keeps its own strings in `web/src/features/<name>/i18n/{sv,en}.json` (keys `web_<name>_…`), merged at build time; a string two features render moves to `web-strings.csv` (`83f5293`). Both languages always have every key; the gate checks it.
|
||||||
* **Language.** Swedish when the browser's preferred language is Swedish, English otherwise (G-8); W-13 offers a choice that overrides it in this browser (`localStorage`).
|
* **Language.** Swedish when the browser's preferred language is Swedish, English otherwise (G-8); W-13 offers a choice that overrides it in this browser (`localStorage`).
|
||||||
* **Money and dates.** Integer öre everywhere, formatting per §9.8 (the web formats with the same rules and the same outputs as the app for the same input). The money functions of §14.5.2 (`computeSplit`, `computeBalances`, `proposeAllocation`, `validateSettlement`, `overSettledAccounts`) exist in TypeScript and pass `contracts/test-vectors/*.json` for those functions in a Vitest test; `resolveRange` and `spendSeries` (§14.5.4) pass their vectors (W4), the category suggestion of §9.11 passes `suggest_category.json` (W2). The rules live in `kvitt-domain`, `:domain` and the web's ports of them; the vectors keep the three equal (G-1).
|
* **Money and dates.** Integer öre everywhere, formatting per §9.8 (the web formats with the same rules and the same outputs as the app for the same input). The money functions of §14.5.2 (`computeSplit`, `computeBalances`, `proposeAllocation`, `validateSettlement`, `overSettledAccounts`) exist in TypeScript and pass `contracts/test-vectors/*.json` for those functions in a Vitest test; `resolveRange` and `spendSeries` (§14.5.4) pass their vectors (W4), the category suggestion of §9.11 passes `suggest_category.json` (W2). The rules live in `kvitt-domain`, `:domain` and the web's ports of them; the vectors keep the three equal (G-1).
|
||||||
* **Source layout** (agreed for W0 to W5): `web/src/app/` (entry, providers, router with every route, layout shell, keyboard shortcuts), `web/src/api/` (generated schema, client, token store, household snapshot and writes, §15.5), `web/src/i18n/`, `web/src/design/` (tokens, global styles), `web/src/components/` (shared components), `web/src/lib/` (formatting, the money functions under `lib/domain/`, utilities), `web/src/features/<name>/` (one folder per feature plan, each with its own pages, components, tests and strings). Routes: `/app/` Home, `/app/login`, `/app/accounts/:id`, `/app/expenses/new`, `/app/expenses/:id`, `/app/settle`, `/app/settlements/:id`, `/app/analytics`, `/app/analytics/:groupKind/:groupId`, `/app/insights`, `/app/search`, `/app/settings/*`; sub-routes and query parameters are the owning plan's.
|
* **Source layout** (agreed for W0 to W5): `web/src/app/` (entry, providers, router with every route, layout shell, keyboard shortcuts), `web/src/api/` (generated schema, client, token store, household snapshot and writes, §15.5), `web/src/i18n/`, `web/src/design/` (tokens, global styles), `web/src/components/` (shared components), `web/src/lib/` (formatting, the money functions under `lib/domain/`, utilities), `web/src/features/<name>/` (one folder per feature plan, each with its own pages, components, tests and strings). Routes: `/app/` Home, `/app/login`, `/app/accounts/:id`, `/app/expenses/new`, `/app/expenses/:id`, `/app/settle`, `/app/settlements/:id`, `/app/analytics`, `/app/analytics/:groupKind/:groupId`, `/app/insights`, `/app/search`, `/app/settings/*`; sub-routes and query parameters are the owning plan's.
|
||||||
@@ -3033,34 +3039,34 @@ The web's pages. "Mirrors" names the app screen whose content, rules and states
|
|||||||
| Id | Page | Route | Mirrors | Must show (beyond the mirrored screen) | Actions | Plan |
|
| Id | Page | Route | Mirrors | Must show (beyond the mirrored screen) | Actions | Plan |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| W-1 | Login | `/app/login` | S-1 (code and name only), S-18's message | App name and one-line purpose; invite code field (12 characters in groups of 4, §8.3); name field for a first login; where the code comes from (S-21 on the owner's phone); errors `invite_invalid`, `rate_limited`, network | Log in | W0 |
|
| W-1 | Login | `/app/login` | S-1 (code and name only), S-18's message | App name and one-line purpose; invite code field (12 characters in groups of 4, §8.3); name field for a first login; where the code comes from (S-21 on the owner's phone); errors `invite_invalid`, `rate_limited`, network | Log in | W0 |
|
||||||
| W-2 | Home | `/app/` | S-4; S-3 while there is no partner | Balance header, Settle up, active accounts with balance and this month's count, collapsed archived section, empty state; no sync pill, no update banner, no pull to refresh | New expense (`N`), Settle up, open account, New account | W1 |
|
| W-2 | Home | `/app/` | S-4; S-3 while there is no partner | Balance header, Settle up, active accounts with balance and this month's count, collapsed archived section (its open state kept for the page session, D-145), empty state with the "Du och <partner> är med" card in place of the balance header (D-145); no sync pill, no update banner, no pull to refresh | New expense (`N`), Settle up, open account, New account | W1 (`934eee7`) |
|
||||||
| W-3 | Account detail | `/app/accounts/:id` | S-5 | Month groups with month totals, expense and settlement rows, archived badge | New expense, Settle this account, Edit, Archive / Unarchive, Delete | W1 |
|
| W-3 | Account detail | `/app/accounts/:id` | S-5 | Month groups with month totals, expense and settlement rows, archived badge | New expense (`/app/expenses/new?account=<id>`), Settle this account (`/app/settle?account=<id>`, D-144), Edit, Archive / Unarchive, Delete | W1 (`934eee7`) |
|
||||||
| W-4 | Account editor | the owning plan's (a dialog or `/app/accounts/…`) | S-6 | Name, default split with the 100,00 kr preview | Save, Cancel | W1 |
|
| W-4 | Account editor | `/app/accounts/new`, `/app/accounts/:id/edit` | S-6 | Name, default split with the 100,00 kr preview | Save, Cancel | W1 (`934eee7`) |
|
||||||
| W-5 | Expense editor | `/app/expenses/new` (optional account), the owning plan's edit route | S-7 | Amount, description, category grid with the §9.11 suggestion for a new expense, account, date, payer, split modes `default`, `ratio`, `exact`, `full` with live shares, notes; validation per §9.6 inline | Save, Delete (edit), Cancel | W2 |
|
| W-5 | Expense editor | `/app/expenses/new` (`?account=<id>` presets the account, D-144), `/app/expenses/:id/edit` | S-7 | Amount, description, category grid with the §9.11 suggestion for a new expense, account, date, payer, split modes `default`, `ratio`, `exact`, `full` with live shares, notes; validation per §9.6 inline; a rejected or failed save in a banner with "Visa nuvarande version", the typed values kept; browser controls in place of S-7's keypad, slider and summary row (D-148) | Save, Delete (edit), Cancel (`Esc`, with the discard dialog) | W2 (`a65ab71`) |
|
||||||
| W-6 | Expense detail | `/app/expenses/:id` | S-8 | Every field, shares and mode, "Recurring: <template>", the audit timeline (AC-12.4, AC-12.5), deleted state | Edit, Delete | W2 |
|
| W-6 | Expense detail | `/app/expenses/:id` | S-8 | Every field, shares and mode, "Recurring: <template>", the audit timeline (AC-12.4, AC-12.5), deleted state | Edit, Delete | W2 (`a65ab71`) |
|
||||||
| W-7 | Settle up | `/app/settle` (optional account; editing a settlement) | S-9 | Direction with swap, amount, allocation rows with debts and over-settle warnings, remaining indicator, add account, date, note, "Föreslå igen" | Confirm, Cancel | W3 |
|
| W-7 | Settle up | `/app/settle` (`?account=<id>` one account, `?settlement=<id>` editing a payment, D-144) | S-9 | Direction with swap, amount, allocation rows with debts and over-settle warnings, allocation errors as you type (D-149), remaining indicator, add account, date, note, "Föreslå igen" | Confirm, Cancel | W3 (`f9a9bfb`) |
|
||||||
| W-8 | Settlement detail | `/app/settlements/:id` | S-10 | Payer, recipient, amount, date, note, allocations, timeline | Edit, Delete | W3 |
|
| W-8 | Settlement detail | `/app/settlements/:id` | S-10 | Payer, recipient, amount, date, note, allocations, timeline | Edit (`/app/settle?settlement=<id>`), Delete | W3 (`f9a9bfb`) |
|
||||||
| W-9 | Analytics | `/app/analytics` | S-11 | Range, granularity, measure, group-by; total; stacked bars with legend; category and account breakdowns | Open a drill-down | W4 |
|
| W-9 | Analytics | `/app/analytics` | S-11 | Range, granularity, measure, group-by; total; stacked bars with legend; category and account breakdowns; "Visa som tabell" under each chart, bar labels as keyboard stops (D-152) | Open a drill-down: a bar opens `/app/analytics/bucket/<key>`, a breakdown row `/app/analytics/:groupKind/:groupId`, the filter in the query string (D-151) | W4 (`1aecf51`) |
|
||||||
| W-10 | Drill-down | `/app/analytics/:groupKind/:groupId` | S-12 with D-128's line chart and average overlays | Title naming the filter, total, line chart for a category or account over the range, expense rows | Open expense | W4 |
|
| W-10 | Drill-down | `/app/analytics/:groupKind/:groupId` (`groupKind` `category`, `account` or `bucket`; query `range`, `from`, `to`, `interval`, `measure`, `group`, `bucket`, D-151) | S-12 with D-128's line chart and average overlays | Title naming the filter, total, line chart for a category or account over the range, expense rows; its own period changes replace the query string (D-151) | Open expense | W4 (`1aecf51`) |
|
||||||
| W-11 | Insights | `/app/insights` | none (D-133) | Per-category averages and trends per month (overall and per category), the 2-month forecast per account with its parts (recurring, average) | Choose category or account | W4 |
|
| W-11 | Insights | `/app/insights` | none (D-133) | Per-category averages and trends per month (overall and per category) over 3, 6 or 12 whole months (default 6, D-152), the 2-month forecast per account with its parts (recurring, average) | Choose category or account; choose months | W4 (`1aecf51`) |
|
||||||
| W-12 | Search | `/app/search` | none (D-129) | Free-text search in descriptions and notes (case-insensitive), filters for amount range, date range, account, category, payer; result count and sum; expense rows | Open expense; Export CSV of the filtered list | W2 |
|
| W-12 | Search | `/app/search` | none (D-129) | Free-text search in descriptions and notes (case-insensitive), filters for amount range, date range, account, category, payer, kept in the URL query string (D-147); result count and sum; expense rows | Open expense; Export CSV of the filtered list (format D-146) | W2 (`a65ab71`) |
|
||||||
| W-13 | Settings | `/app/settings` | S-13 | Own and partner's avatar and name; rows Profile, Categories, Recurring expenses, Devices, Invites (admin), About, Language; Log out | Navigate; Log out | W5 |
|
| W-13 | Settings | `/app/settings` | S-13 | Own and partner's avatar and name; rows Profile, Categories, Recurring expenses, Devices, Invites (admin), About, Language; Log out | Navigate; Log out | W5 (`3ebe57d`) |
|
||||||
| W-14 | Profile | `/app/settings/profile` | S-14 (and S-2 once after a `partner` login) | Avatar with change and remove (file picker), display name | Save | W5 |
|
| W-14 | Profile | `/app/settings/profile` | S-14 (and S-2 once after a `partner` login, as an intro remembered per browser device in `kvitt.profileStepDone`, D-154) | Avatar with change and remove (file picker), display name | Save | W5 (`3ebe57d`) |
|
||||||
| W-15 | Categories | `/app/settings/categories…` | S-15 | List with archived section; editor with name, reset name, 33 icons, 14 colours, delete or archive with the reason | Add, Edit, Reorder (drag, and by keyboard), Archive, Delete | W5 |
|
| W-15 | Categories | `/app/settings/categories`, editor `/app/settings/categories/new` and `/app/settings/categories/:id` (D-153) | S-15 | List with archived section; editor with name, reset name, 33 icons, 14 colours, delete or archive with the reason | Add, Edit, Reorder (drag, and by keyboard), Archive, Delete | W5 (`3ebe57d`) |
|
||||||
| W-16 | Recurring expenses | `/app/settings/recurring…` | S-19 | List and editor as S-19 | Add, Edit, Pause / Resume, Delete | W5 |
|
| W-16 | Recurring expenses | `/app/settings/recurring`, editor `/app/settings/recurring/new` and `/app/settings/recurring/:id` (D-153) | S-19 | List and editor as S-19 | Add, Edit, Pause / Resume, Delete | W5 (`3ebe57d`) |
|
||||||
| W-17 | Devices | `/app/settings/devices` | S-20 | Rows per AC-3.1; this browser marked as "This device" | Revoke (with confirmation; not this browser) | W5 |
|
| W-17 | Devices | `/app/settings/devices` | S-20 | Rows per AC-3.1; this browser marked as "This device" | Revoke (with confirmation; not this browser) | W5 (`3ebe57d`) |
|
||||||
| W-18 | Invites | `/app/settings/invites` | S-21 | Invite partner (before the partner joins) and pair a device; a created invite's code in groups of 4, link and expiry; pending invites | Create, Copy (code and link, to the clipboard), Cancel | W5 |
|
| W-18 | Invites | `/app/settings/invite` (D-153; `/app/settings/invites` opens it too) | S-21 | Invite partner (before the partner joins) and pair a device; a created invite's code in groups of 4, link and expiry; pending invites | Create, Copy (code and link, to the clipboard), Cancel | W5 (`3ebe57d`) |
|
||||||
| W-19 | About | `/app/settings/about` | S-22 | The web build's version name and code; the server's version and schema from E-1 `/health` when it answers (on the LAN), otherwise the `X-Kvitt-Server-Version` of the last API response without the schema (the S-22 fallback, D-110; `/health` is closed from outside, §15.2) | none | W5 |
|
| W-19 | About | `/app/settings/about` | S-22 | The web build's version name and code; the server's version and schema from E-1 `/health` when it answers (on the LAN), otherwise the `X-Kvitt-Server-Version` header of a fresh `GET /me` without the schema (the S-22 fallback, D-110, D-154; `/health` is closed from outside, §15.2) | none | W5 (`3ebe57d`) |
|
||||||
| W-20 | Keyboard help | dialog on `?` | none (D-129) | The shortcuts and what they do | Close (`Esc`) | W0 |
|
| W-20 | Keyboard help | dialog on `?` | none (D-129) | The shortcuts and what they do | Close (`Esc`) | W0 |
|
||||||
|
|
||||||
Navigation (W0): a sidebar from 720 px wide and a bottom bar under it, with Home, Analytics, Insights, Search and Settings; New expense is a button in the sidebar and on Home and account detail (the app's FAB). Dialogs close with `Esc`. Destination changes are instant (D-108).
|
Navigation (W0): a sidebar from 720 px wide and a bottom bar under it, with Home, Analytics, Insights, Search and Settings; New expense is a button in the sidebar and on Home and account detail (the app's FAB). Dialogs close with `Esc`. Destination changes are instant (D-108).
|
||||||
|
|
||||||
### 15.7 Search, export and shortcuts (D-129)
|
### 15.7 Search, export and shortcuts (D-129)
|
||||||
|
|
||||||
* **Search** (W-12) runs in the browser over the snapshot (§15.5): non-deleted expenses whose description or notes contain the query, ignoring case; filters combine with AND. Settlements are not searched. Results sort by date, newest first.
|
* **Search** (W-12) runs in the browser over the snapshot (§15.5): non-deleted expenses whose description or notes contain the query, ignoring case; filters combine with AND. Settlements are not searched. Results sort by date, newest first. The query and filters live in the URL query string (`q`, `min` and `max` in öre, `from`, `to`, `account`, `category`, `payer`), so a search can be bookmarked (D-147).
|
||||||
* **CSV export** (W-12) downloads the filtered list as a file, one row per expense: date, description, category, account, payer, amount, each user's share, notes, with a header row in the page's language. The file format (separator, decimal mark, byte-order mark, file name) is chosen by W2 so that the file opens correctly in Numbers and Excel with a Swedish locale, and proposed as a decision in its report. The export is built in the browser; the server has no export endpoint.
|
* **CSV export** (W-12) downloads the filtered list as a file, one row per expense: date, description, category, account, payer, amount, each user's share, notes, with a header row in the page's language. The file format (D-146, W2 `a65ab71`): UTF-8 with a byte-order mark, `;` separator, CRLF, RFC 4180 quoting, amounts in kronor with a decimal comma and no grouping ("1234,50") in every page language, ISO dates, one share column per household user ("Andel <namn>"); a text cell starting with `=`, `+`, `-`, `@`, TAB or CR gets a leading `'`; file name `kvitt-utgifter-<YYYY-MM-DD>.csv` (Swedish) or `kvitt-expenses-<YYYY-MM-DD>.csv` (English). It opens correctly in Numbers and Excel with a Swedish locale. The export is built in the browser; the server has no export endpoint.
|
||||||
* **Keyboard shortcuts** (W0 binds them in the layout shell; the targets are the owning plans'): `N` opens W-5, `/` opens W-12 with the search field focused, `Esc` closes the open dialog or editor (with the discard rule of the mirrored screen), `?` opens W-20. `N`, `/` and `?` do nothing while focus is in a text field.
|
* **Keyboard shortcuts** (W0 binds them in the layout shell; the targets are the owning plans'): `N` opens W-5, `/` opens W-12 with the search field focused, `Esc` closes the open dialog or editor (with the discard rule of the mirrored screen), `?` opens W-20. `N`, `/` and `?` do nothing while focus is in a text field.
|
||||||
* **Category suggestion** (W-5, §9.11): the web ports the rule and passes `suggest_category.json`; the keyword table is read at build time from `contracts/category-keywords.csv` (the single source, never copied by hand), as the phone does (D-055).
|
* **Category suggestion** (W-5, §9.11): the web ports the rule and passes `suggest_category.json`; the keyword table is read at build time from `contracts/category-keywords.csv` (the single source, never copied by hand), as the phone does (D-055); as built it is a raw import, allowed by `server.fs.allow` in `web/vite.config.ts` (D-148).
|
||||||
|
|
||||||
### 15.8 Advanced analytics on the server (D-133)
|
### 15.8 Advanced analytics on the server (D-133)
|
||||||
|
|
||||||
@@ -3121,3 +3127,4 @@ Plans with disjoint paths (§14.7), one Opus agent each in its own worktree. W0
|
|||||||
| 0.29 | 2026-10-21 | The server side of the web client programme (W7, `50a0d67`) and the unattended APK deploy (`70cb25c`). D-135 web serving: `KVITT_WEB_DIR` (default `/app`); `GET /app` → 308 `/app/`; files from the directory, every other path `index.html`, a missing `assets/*` 404; `assets/*` `public, max-age=31536000, immutable`, `index.html` `no-store`, others `no-cache`; the `$kvitt_csp` web policy plus `nosniff`, `DENY`, `no-referrer`; safe paths at most 512 bytes and 8 segments of `[A-Za-z0-9._-]` not starting with `.`; no directory → `/app` 404 (§10.4, §15.4, AC-21.1). D-136: `/app`, `/app/`, `/app/*` count against the unauthenticated 60/min per IP, bearer token or not (§8.6). D-137 image build: repository-root context with an allowlist `.dockerignore` (`server`, `web`, `contracts`), `server/.dockerignore` removed; a Node `node:24.22.1-bookworm-slim` stage pinned by digest on the build platform, its `dist` passed through the Rust stage so npm and rustc never run together on the Pi 3; workflow context `.`, sparse checkout and path filter add `web` and `contracts`; `infra/build-images.sh` and `infra/dev/build.sh` follow; `Cargo.lock` bumps `rustls-webpki` 0.103.16 and `webpki-root-certs` 1.0.10 (RUSTSEC-2026-0382, -0383) (§10.3). D-138 analytics: E-34 `GET /analytics/trends` and E-35 `GET /analytics/forecast`, authenticated, read-only, with examples; rules in new §14.5.7 (`kvitt-domain::analytics`) (§7, §14.3.4, §14.3.5, AC-14.1, §15.8). D-139 (`70cb25c`): `infra/publish-apk.sh --deploy` copies the APK and `latest.json` with a forced-command key and checks `/apk/latest` (§10.6). Server tests 299; contracts-check 136. |
|
| 0.29 | 2026-10-21 | The server side of the web client programme (W7, `50a0d67`) and the unattended APK deploy (`70cb25c`). D-135 web serving: `KVITT_WEB_DIR` (default `/app`); `GET /app` → 308 `/app/`; files from the directory, every other path `index.html`, a missing `assets/*` 404; `assets/*` `public, max-age=31536000, immutable`, `index.html` `no-store`, others `no-cache`; the `$kvitt_csp` web policy plus `nosniff`, `DENY`, `no-referrer`; safe paths at most 512 bytes and 8 segments of `[A-Za-z0-9._-]` not starting with `.`; no directory → `/app` 404 (§10.4, §15.4, AC-21.1). D-136: `/app`, `/app/`, `/app/*` count against the unauthenticated 60/min per IP, bearer token or not (§8.6). D-137 image build: repository-root context with an allowlist `.dockerignore` (`server`, `web`, `contracts`), `server/.dockerignore` removed; a Node `node:24.22.1-bookworm-slim` stage pinned by digest on the build platform, its `dist` passed through the Rust stage so npm and rustc never run together on the Pi 3; workflow context `.`, sparse checkout and path filter add `web` and `contracts`; `infra/build-images.sh` and `infra/dev/build.sh` follow; `Cargo.lock` bumps `rustls-webpki` 0.103.16 and `webpki-root-certs` 1.0.10 (RUSTSEC-2026-0382, -0383) (§10.3). D-138 analytics: E-34 `GET /analytics/trends` and E-35 `GET /analytics/forecast`, authenticated, read-only, with examples; rules in new §14.5.7 (`kvitt-domain::analytics`) (§7, §14.3.4, §14.3.5, AC-14.1, §15.8). D-139 (`70cb25c`): `infra/publish-apk.sh --deploy` copies the APK and `latest.json` with a forced-command key and checks `/apk/latest` (§10.6). Server tests 299; contracts-check 136. |
|
||||||
| 0.30 | 2026-10-21 | The manual update check (`c0f4345`). D-140 (owner, 2026-10-21): S-22 "Check for update" shared the throttled automatic check, so a tap within 6 hours of the last automatic check reported the stored answer (the owner deployed a new APK and was told "You have the latest version"); `UpdateRepository.checkNow()` (default implementation calls `check()`, so fakes are unchanged) always asks the server and restarts the 6-hour window, debug builds still return `UpToDate` without a request; `AboutViewModel.check()` calls `checkNow()`; the automatic checks on cold start and resume keep the throttle (AC-16.2, AC-16.6, §13.3 S-22, §14.2.4). |
|
| 0.30 | 2026-10-21 | The manual update check (`c0f4345`). D-140 (owner, 2026-10-21): S-22 "Check for update" shared the throttled automatic check, so a tap within 6 hours of the last automatic check reported the stored answer (the owner deployed a new APK and was told "You have the latest version"); `UpdateRepository.checkNow()` (default implementation calls `check()`, so fakes are unchanged) always asks the server and restarts the 6-hour window, debug builds still return `UpToDate` without a request; `AboutViewModel.check()` calls `checkNow()`; the automatic checks on cold start and resume keep the throttle (AC-16.2, AC-16.6, §13.3 S-22, §14.2.4). |
|
||||||
| 0.31 | 2026-10-21 | Android Insights on S-11 (W6, `1c9856b`; Android gate green, which also fixes the E-34/E-35 example tests red since `50a0d67`). D-141 UI contract: `:core:model` `InsightsRepository` with `trends(months: 1..24, measure)` and `forecast(measure)`, both `OnlineResult` (`Offline` without a request, `Failed("network")` when unreachable); types `SpendTrends`, `SpendTrend`, `TrendPoint`, `CategoryTrend` (`category: Category?`), `SpendForecast`, `AccountForecast` (`account: Account?`), `ForecastMonth`, named after the E-34/E-35 fields; names resolved from Room, archived and deleted included, unknown id → null; nothing stored; `:data` `AnalyticsDtos.kt`, `InsightsRepositoryImpl`, two `KvittApi` calls; fakes `FakeInsightsRepository`, `InsightsScenarios`, `insightsFailure` (§14.2.4). D-142 placement and loading: an "Insights" ("Insikter") section below S-11's local analytics, following S-11's measure toggle, always 6 whole months (E-34's default); loads when S-11 opens, when the measure changes to one not yet loaded, when the phone comes online and on "Försök igen"; data kept in memory per measure while S-11 is open; the last data stays visible under the offline or error banner; S-11's offline analytics unchanged (AC-14.1) (§13.3 S-11, design-spec S-11). D-143 display rules: the household chart and a per-category chart on D-128's `SpendLineChart` with the server's average and moving average; categories in the server's order, the open one defaults to the largest total; unknown category ids skipped; one forecast card per account (archived badge) with the current month so far and two months as "Recurring + average = forecast"; an all-zero card shows one line "No recurring expenses and no recent spending" (design-spec S-11, §3.10). `strings.csv` 588 rows (16 `analytics_insights_*` keys, 19 rows) (§13.4). Goldens `s11_insights_<state>` for 13 states in three variants plus `s11_insights_long_sv_light_200`. |
|
| 0.31 | 2026-10-21 | Android Insights on S-11 (W6, `1c9856b`; Android gate green, which also fixes the E-34/E-35 example tests red since `50a0d67`). D-141 UI contract: `:core:model` `InsightsRepository` with `trends(months: 1..24, measure)` and `forecast(measure)`, both `OnlineResult` (`Offline` without a request, `Failed("network")` when unreachable); types `SpendTrends`, `SpendTrend`, `TrendPoint`, `CategoryTrend` (`category: Category?`), `SpendForecast`, `AccountForecast` (`account: Account?`), `ForecastMonth`, named after the E-34/E-35 fields; names resolved from Room, archived and deleted included, unknown id → null; nothing stored; `:data` `AnalyticsDtos.kt`, `InsightsRepositoryImpl`, two `KvittApi` calls; fakes `FakeInsightsRepository`, `InsightsScenarios`, `insightsFailure` (§14.2.4). D-142 placement and loading: an "Insights" ("Insikter") section below S-11's local analytics, following S-11's measure toggle, always 6 whole months (E-34's default); loads when S-11 opens, when the measure changes to one not yet loaded, when the phone comes online and on "Försök igen"; data kept in memory per measure while S-11 is open; the last data stays visible under the offline or error banner; S-11's offline analytics unchanged (AC-14.1) (§13.3 S-11, design-spec S-11). D-143 display rules: the household chart and a per-category chart on D-128's `SpendLineChart` with the server's average and moving average; categories in the server's order, the open one defaults to the largest total; unknown category ids skipped; one forecast card per account (archived badge) with the current month so far and two months as "Recurring + average = forecast"; an all-zero card shows one line "No recurring expenses and no recent spending" (design-spec S-11, §3.10). `strings.csv` 588 rows (16 `analytics_insights_*` keys, 19 rows) (§13.4). Goldens `s11_insights_<state>` for 13 states in three variants plus `s11_insights_long_sv_light_200`. |
|
||||||
|
| 0.32 | 2026-10-21 | The web feature plans W1 to W5 (`934eee7`, `a65ab71`, `f9a9bfb`, `1aecf51`, `3ebe57d`; web gate on `main` 59 files, 532 tests at `1aecf51`, 598 at `83f5293` with the `suggest_category` vectors) and the update install-failure fix (`d686ee0`). D-144: the preset account travels as `?account=<id>` for `/app/expenses/new` and `/app/settle`, a payment being edited as `/app/settle?settlement=<id>`; builders in `features/accounts/links.ts` and `features/settle/links.ts` until W0 adds them to `routes.ts`. D-145: web Home's empty state shows the "Du och <partner> är med" card in place of the balance header; the archived section's open state lasts for the page session in memory. D-146: CSV export format (UTF-8 with BOM, `;`, CRLF, RFC 4180 quoting, "1234,50" in every language, ISO dates, header in the page language, one share column per user, formula cells prefixed `'`, `kvitt-utgifter-<date>.csv` / `kvitt-expenses-<date>.csv`). D-147: AC-8.6's "on this phone" is this browser (`localStorage`); search filters in the URL (amounts in öre). D-148: web expense editor: rejections and failures in a banner with S-16's text and "Visa nuvarande version", the server's row into the snapshot, typed values kept; "Utgiften sparades" after edits too; the discard dialog on `Esc` and in-app navigation; browser controls in place of S-7's keypad, ratio slider and summary row; shared fields in `web/src/components/editor/`; the keyword table imported raw (`server.fs.allow`). D-149: W-7 shows the §14.5.3 allocation errors as you type. D-150 unused. D-151: a bar opens `/app/analytics/bucket/<key>`; the filter in the query string (`range`, `from`, `to`, `interval`, `measure`, `group`, `bucket`), replaced by W-10's own period changes. D-152: chart SVGs hidden from screen readers, bar labels as keyboard stops, "Visa som tabell"; W-11 offers 3, 6 or 12 months. D-153: W-18's route is `/app/settings/invite` (`/invites` still opens it); W-15 and W-16 editor routes. D-154: the partner's first-login step is an intro on W-14 remembered in `kvitt.profileStepDone` (the device id); W-19's fallback reads the server-version header of a fresh `GET /me` (§15.3, §15.6, §15.7, AC-8.6). D-155 (`d686ee0`): a refused or failed install shows "Kunde inte installera uppdateringen" with the reason (blocked by the security settings, or the installer's message) and "Försök igen", which downloads again; a declined dialog returns to `Available`; `UpdateState.Failed` gains `reason: UpdateFailure` (`Download`, `Blocked`, `Install(detail)`) (new AC-16.7, §13.3 S-17, §14.2.4; design-spec §3.6, S-17, S-22). `strings.csv` 590 rows (`update_install_failed`, `update_install_blocked`). Goldens `S-17-install-blocked`, `S-17-install-failed`, `s22_about_install_blocked` new, `update_banner` re-recorded. contracts-check 136. |
|
||||||
|
|||||||
+37
-14
@@ -3,16 +3,24 @@
|
|||||||
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-21 late. `main` at `1c9856b` (W6 Android Insights on S-11, D-141 to D-143), pushed to `origin/main` (Gitea), after `415f9f5` (CLAUDE.md: web client, three gates), `2ae65a5` (W0 web foundation), `3fbd393` (SPEC v0.29) and `c0f4345` (update-check fix, D-140). The Android gate is green again on `main` (W6's E-34/E-35 DTOs fixed the three `:data` example tests red since `50a0d67`). This docs pass (`agent-docs-v31`) records D-140 as SPEC v0.30 and D-141 to D-143 as v0.31. Running: web W1 to W5 (D-144 to D-154 reserved for them). Landed after `1c9856b`, not yet recorded in the spec: `d686ee0`, the APK install-failure fix (D-155; two `strings.csv` keys `update_install_failed`, `update_install_blocked`, so the file has 590 rows against SPEC §13.4's 588; the web dictionaries are regenerated with them in this pass). The push of `1c9856b` started the Pi image build with the web client and E-34/E-35 (in progress on the Pi); then the owner deploys it (§3 step 4), and after the Pi runs the new image the release APK with Insights is published (`infra/publish-apk.sh --deploy`).
|
- State as of 2026-10-21, night. `main` at `83f5293` (web fix: deterministic feature strings, `suggest_category` vectors, raw bodies in the fake server), after the five web feature plans W1 `934eee7`, W3 `f9a9bfb`, W5 `3ebe57d`, W2 `a65ab71`, W4 `1aecf51` (D-144 to D-154; D-150 unused) and `d268dd0` (`.gitignore` anchors `export/`); before them `aaa09d9` (SPEC v0.30, v0.31), `d686ee0` (APK install failures visible, D-155) and `1c9856b` (W6). All five web plans are landed and pushed (orchestrator). The web gate is green on `main` (59 test files, 532 tests at `1aecf51`; 59 files, 598 tests at `83f5293` with the `suggest_category` vectors, run by this docs pass). This docs pass (`agent-docs-v32`) records D-144 to D-155 as SPEC v0.32. The Pi image build with the complete web app is in progress on the Pi; the owner deploys it (§3 step 4), then checks the web client in a browser (§3 step 5). Release APK `kvitt-140` deployed 2026-10-21 (`infra/publish-apk.sh --deploy`): install-failure messages (D-155) and S-11 Insights, which need the new server image (E-34, E-35).
|
||||||
- **Kvitt host name** (owner decision 2026-10-19): `kvitt.suorra.duckdns.org`, a subdomain of the owner's DuckDNS family domain `suorra.duckdns.org`; NPM issues its Let's Encrypt certificate; `KVITT_PUBLIC_HOST=kvitt.suorra.duckdns.org`, so `KVITT_PUBLIC_URL=https://kvitt.suorra.duckdns.org` (port 443, D-124). It is in `infra/pi/.env.example` and `infra/pi/.env` (`KVITT_PUBLIC_HOST`) and `android/local.properties` (`kvitt.publicHost`, git-ignored). SPEC §10.1 and §10.5.1 point here.
|
- **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).
|
||||||
- **Images**: `gitea.lan/jacobh2/kvitt-server` and `gitea.lan/jacobh2/kvitt-backup`, tags `v1.0.0` (run 69) and `v1.0.1` (the sparse-checkout push; the tag the Pi uses), plus `latest`; the packages are public, so the Pi pulls without `docker login`. Gitea Actions on the owner's Pi runner (act_runner 0.6.1, Alpine job container): the first failures were an empty secret (the user-level secret was misnamed on the owner's side, fixed 2026-10-19; a user-level secret of the repository owner reaches the repository); run 69 succeeded: login ok, `v1.0.0`, `kvitt-backup` 5 min 20 s, `kvitt-server` 2 h 42 min on the Pi 3 (first build, `CARGO_BUILD_JOBS=1`; later runs reuse the builder's cargo cache); the tag push works with the `write:package` token. SPEC §10.3.
|
- **Images**: `gitea.lan/jacobh2/kvitt-server` and `gitea.lan/jacobh2/kvitt-backup`, tags `v1.0.0` (run 69) and `v1.0.1` (the sparse-checkout push; the tag the Pi uses), plus `latest`; the packages are public, so the Pi pulls without `docker login`. Gitea Actions on the owner's Pi runner (act_runner 0.6.1, Alpine job container): the first failures were an empty secret (the user-level secret was misnamed on the owner's side, fixed 2026-10-19; a user-level secret of the repository owner reaches the repository); run 69 succeeded: login ok, `v1.0.0`, `kvitt-backup` 5 min 20 s, `kvitt-server` 2 h 42 min on the Pi 3 (first build, `CARGO_BUILD_JOBS=1`; later runs reuse the builder's cargo cache); the tag push works with the `write:package` token. SPEC §10.3.
|
||||||
- **Kvitt Pi** (D-125): Kvitt runs from `/home/jacobhagstedt/kvitt` as the owner's user `jacobhagstedt`, uid and gid 1000:1000 (`KVITT_UID`/`KVITT_GID`); Docker and the `gitea.lan` registry trust were already on the Pi; nothing outside that folder is installed or changed. **The stack is running** (owner, 2026-10-21): image `v1.0.1`, `server` and `backup` both healthy after a `chown` of `data/` (it had been created as root); compose's `mem_limit` warning (the Pi kernel has no memory cgroup, so the 160 MiB and 96 MiB limits are not enforced) is accepted. `/health` through NPM from outside answered `"status":"ok"` (public health OK). **Live** (2026-10-21): both phones are paired against it and the Splitwise import (M11) is done.
|
- **Kvitt Pi** (D-125): Kvitt runs from `/home/jacobhagstedt/kvitt` as the owner's user `jacobhagstedt`, uid and gid 1000:1000 (`KVITT_UID`/`KVITT_GID`); Docker and the `gitea.lan` registry trust were already on the Pi; nothing outside that folder is installed or changed. **The stack is running** (owner, 2026-10-21): image `v1.0.1`, `server` and `backup` both healthy after a `chown` of `data/` (it had been created as root); compose's `mem_limit` warning (the Pi kernel has no memory cgroup, so the 160 MiB and 96 MiB limits are not enforced) is accepted. `/health` through NPM from outside answered `"status":"ok"` (public health OK). **Live** (2026-10-21): both phones are paired against it and the Splitwise import (M11) is done.
|
||||||
- **App gate** (D-126, owner decision 2026-10-21): outside the home LAN `192.168.1.0/24` NPM closes (`return 444`) every request without `X-Kvitt-App: <key>`, every route included; the key is the build property `kvitt.appKey` (`android/local.properties`, git-ignored), generated by the owner with `openssl rand -hex 32` and pasted into NPM's block (`infra/npm/kvitt.conf`, `7521c01`); the server and `contracts/openapi.yaml` are unchanged. Android side `34e5cad`, docs `2acfc34` (SPEC v0.25, `infra/README.md` step 4a). `android/local.properties` has a `kvitt.appKey` and `infra/pi/out/apk/` holds `kvitt-125.apk` (checked 2026-10-21); whether both phones run a keyed APK and the gate is in NPM is not recorded here (orchestrator confirms with the owner, §3).
|
- **App gate** (D-126, owner decision 2026-10-21): outside the home LAN `192.168.1.0/24` NPM closes (`return 444`) every request without `X-Kvitt-App: <key>`, every route included; the key is the build property `kvitt.appKey` (`android/local.properties`, git-ignored), generated by the owner with `openssl rand -hex 32` and pasted into NPM's block (`infra/npm/kvitt.conf`, `7521c01`); the server and `contracts/openapi.yaml` are unchanged. Android side `34e5cad`, docs `2acfc34` (SPEC v0.25, `infra/README.md` step 4a). `android/local.properties` has a `kvitt.appKey` and `infra/pi/out/apk/` holds `kvitt-125.apk` (checked 2026-10-21); whether both phones run a keyed APK and the gate is in NPM is not recorded here (orchestrator confirms with the owner, §3).
|
||||||
- **Web client programme** (owner interview 2026-10-21, SPEC v0.28, D-129 to D-134, SPEC §15, FR-21): a React web client in `web/`, served by `kvitt-server` under `/app/` from the server image, with the app's features except the phone-only ones, plus search, CSV export, keyboard shortcuts and a language choice; login with an invite code (device `Web · <browser>`); the NPM gate also passes `/app/`, `POST /invites/accept` and bearer-token requests except `/health`, `/setup/`, `/i/`, `/apk/` (new block in `infra/npm/kvitt.conf`, README step 4, SPEC §10.5.2; checked with nginx 1.31.7, 17 requests); advanced analytics on the server (`/analytics/*`, D-133, forecast = recurring templates due + 3-month average of non-recurring spend). Plans `PLAN-W0.md` to `PLAN-W7.md`, prompts `prompts/W0.md` to `prompts/W7.md`. W7 landed `50a0d67` (D-135 to D-138, SPEC v0.29: `/app/` from `KVITT_WEB_DIR`, E-34 `GET /analytics/trends`, E-35 `GET /analytics/forecast`, rules SPEC §14.5.7, the Node stage and the repository-root build context); W0 landed `2ae65a5` (`web/**`: Vite, React 19, TypeScript strict, sync-pull ledger, invite-code login, ported money rules passing the vectors, tokens, i18n generated from `docs/ux/strings.csv` and `web-strings.csv`); W1 to W5 are running in parallel, each in its own `web/src/features/<name>/`; W6 (Android Insights) landed `1c9856b` and shows on the phones once the image with W7 and W0 is deployed and a release APK with W6 is published. The web gate is `npm run check` in `web/` (third gate, in `CLAUDE.md` since `415f9f5`).
|
- **Web client programme** (owner interview 2026-10-21, SPEC v0.28, D-129 to D-134, SPEC §15, FR-21): a React web client in `web/`, served by `kvitt-server` under `/app/` from the server image, with the app's features except the phone-only ones, plus search, CSV export, keyboard shortcuts and a language choice; login with an invite code (device `Web · <browser>`); the NPM gate also passes `/app/`, `POST /invites/accept` and bearer-token requests except `/health`, `/setup/`, `/i/`, `/apk/` (new block in `infra/npm/kvitt.conf`, README step 4, SPEC §10.5.2; checked with nginx 1.31.7, 17 requests); advanced analytics on the server (`/analytics/*`, D-133, forecast = recurring templates due + 3-month average of non-recurring spend). Plans `PLAN-W0.md` to `PLAN-W7.md`, prompts `prompts/W0.md` to `prompts/W7.md`. W7 landed `50a0d67` (D-135 to D-138, SPEC v0.29: `/app/` from `KVITT_WEB_DIR`, E-34 `GET /analytics/trends`, E-35 `GET /analytics/forecast`, rules SPEC §14.5.7, the Node stage and the repository-root build context); W0 landed `2ae65a5` (`web/**`: Vite, React 19, TypeScript strict, sync-pull ledger, invite-code login, ported money rules passing the vectors, tokens, i18n generated from `docs/ux/strings.csv` and `web-strings.csv`); W1 to W5 landed 2026-10-21 (`934eee7`, `a65ab71`, `f9a9bfb`, `1aecf51`, `3ebe57d`; D-144 to D-154, SPEC v0.32), each in its own `web/src/features/<name>/`, W2 with the shared editor fields in `web/src/components/editor/`; loose ends for a W0b plan in §2 "Web follow-ups"; W6 (Android Insights) landed `1c9856b` and shows on the phones once the image with W7 and W0 is deployed and a release APK with W6 is published. The web gate is `npm run check` in `web/` (third gate, in `CLAUDE.md` since `415f9f5`).
|
||||||
- **First release APK**: `apk/kvitt-123.apk` (10.0 MB, git-ignored by `*.apk`) handed to the owner from the main checkout for sideloading on both phones (both paired, 2026-10-21); it was built before any `kvitt.appKey` existed, so it sends no `X-Kvitt-App`: it works only until the gate is pasted (on the home Wi-Fi it keeps working afterwards). The phones need a keyed APK before the gate goes in (§3).
|
- **First release APK**: `apk/kvitt-123.apk` (10.0 MB, git-ignored by `*.apk`) handed to the owner from the main checkout for sideloading on both phones (both paired, 2026-10-21); it was built before any `kvitt.appKey` existed, so it sends no `X-Kvitt-App`: it works only until the gate is pasted (on the home Wi-Fi it keeps working afterwards). The phones need a keyed APK before the gate goes in (§3).
|
||||||
- History rewrite and first push, 2026-10-18 (owner authorised): superseded PNG versions were dropped from the whole history (no LFS), the current goldens and UX mock-ups restored in `18c527f`; pack 45 MiB; `origin/main` pushed at `18c527f`. A bundle of the pre-rewrite history (`kvitt-pre-rewrite.bundle`) is kept outside the repository, in the orchestrator's scratchpad. Every commit from M2b on got a new hash; the oxipng pass (`7c453d8` before the rewrite) no longer exists as a commit (it only touched PNGs). SPEC, `docs/decisions.md`, this file and `prompts/README.md` cite the rewritten hashes since v0.18 (`7c453d8` stays as the oxipng pass's name; `docs/validation/ac-coverage.md` keeps its pre-rewrite hashes verbatim); since v0.19 the plans cite rewritten hashes too (eight pre-rewrite hashes in `PLAN-*.md` replaced by the commit with the same subject); `docs/spikes/pi-image-arm64.md` and the READMEs had none stale. For any other old hash, find the commit by its subject with `git log --oneline --grep`.
|
- History rewrite and first push, 2026-10-18 (owner authorised): superseded PNG versions were dropped from the whole history (no LFS), the current goldens and UX mock-ups restored in `18c527f`; pack 45 MiB; `origin/main` pushed at `18c527f`. A bundle of the pre-rewrite history (`kvitt-pre-rewrite.bundle`) is kept outside the repository, in the orchestrator's scratchpad. Every commit from M2b on got a new hash; the oxipng pass (`7c453d8` before the rewrite) no longer exists as a commit (it only touched PNGs). SPEC, `docs/decisions.md`, this file and `prompts/README.md` cite the rewritten hashes since v0.18 (`7c453d8` stays as the oxipng pass's name; `docs/validation/ac-coverage.md` keeps its pre-rewrite hashes verbatim); since v0.19 the plans cite rewritten hashes too (eight pre-rewrite hashes in `PLAN-*.md` replaced by the commit with the same subject); `docs/spikes/pi-image-arm64.md` and the READMEs had none stale. For any other old hash, find the commit by its subject with `git log --oneline --grep`.
|
||||||
- History (rewritten hashes): `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `c277ee3` M2b, `06f48ef` M2a, `6dce4c7` SPEC v0.4, `7869ec4` M3, `8f0e156` M5, `853c038` SPEC v0.5, `15056dc` M4, `d9dd584` SPEC v0.6, `50339ae` M9, `86e08a1` SPEC v0.7, `aa765bb` M12a, `d39eb2c` SPEC v0.8, `b09f322` M12c, `6eaccec` M12b, `0006bd7` UX Kalas categories and tokens, `6da6b1b` UX sprint U2, `e676812` SPEC v0.9, `557af7e` M12d, `7730a31` M6, `9ee67d4` SPEC v0.10, `a41f240` and `6453932` app wiring before M8, `1ea752c` M7a, `ea7d494` M7e, `7380e50` M7d, `0e1d99d` M7b, `0b140aa` M7c, `c4ab35f` M7f, `b24c8b5` M6b, `f4dc10c` SPEC v0.11, `3dda414` M7g, `cb832a7` and `3d9eea8` M8, `6159e18` SPEC v0.12, `8c60715` M12e, `b7b7483` M12e launcher icon, `51d6ff8` SPEC v0.13, `a312758` Vico removal, `f56dcbe` FCM `fid`, `3e6c337` Pi image arm64 check, `e2dba84` SPEC v0.14, `328c466` M8b, `6eb3f85` `FcmMessageBody.fid`, `bfde12c` SPEC v0.15, `75b549f` M8c goldens, `a0c25b0` M8c licences row, `c30a49e` R8 release build, `bacbd18` M8d, `bfc3acb` SPEC v0.16, `2476667` M2c-pi bundle, `a59abeb` M8e, `b4d9467` Dockerfile `CARGO_BUILD_JOBS`, `956908a` M8f, `1328afe` chips cleanup, `400eac9` SPEC v0.17, `18c527f` goldens restored after the rewrite; then `2fac1ec` to `e70fe0d` infra overhaul, `8660b7f` server gap pass, `7fb86f5` infra security, `dc7a296` to `a0259ab` M12f, `8103dae` to `b985ce9` server overhaul, `48fe119` SPEC v0.18, `e4e6e5d` to `c582363` Android overhaul, `3ef2c15` SPEC v0.19, `96e4cbd` APK download rate limit (D-119), `481889a` SPEC v0.20, `f1abb0d` `cargo audit` in the server gate, `4f69ae0` Gitea login in the run line, `d05f13a` Pi stack behind NPM, `62f5279` and `9231a1d` infra README for NPM (D-120), `d090c6c` Android host property (D-121), `4bcff2c` SPEC v0.21, `dd07ca4` temporary workflow diagnostic step, `4a4e45f` S-22's release server and `4b1ae0c` `publish-apk.sh` host (D-122), `c9abd24` and `e381e37` server host defaults and OpenAPI `{host}` (D-123), `ac17b41` `install.sh` step number, `cce3e4e` SPEC v0.22, `a86ad24` server and contracts on port 443, `4852803` bundle on port 443 (D-124), `038c603` Android on port 443 and the pinned-root template, `ad35231` portless Android fixtures, `b1f4c10` SPEC v0.23, `3820f9c` sparse-checkout workflow, `200c99f` `.env` without informational keys, `6e5981d` self-contained Pi compose project (D-125), `28fa71b` compose deploy hint in the workflow and `infra/build-images.sh`.
|
- History (rewritten hashes): `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `c277ee3` M2b, `06f48ef` M2a, `6dce4c7` SPEC v0.4, `7869ec4` M3, `8f0e156` M5, `853c038` SPEC v0.5, `15056dc` M4, `d9dd584` SPEC v0.6, `50339ae` M9, `86e08a1` SPEC v0.7, `aa765bb` M12a, `d39eb2c` SPEC v0.8, `b09f322` M12c, `6eaccec` M12b, `0006bd7` UX Kalas categories and tokens, `6da6b1b` UX sprint U2, `e676812` SPEC v0.9, `557af7e` M12d, `7730a31` M6, `9ee67d4` SPEC v0.10, `a41f240` and `6453932` app wiring before M8, `1ea752c` M7a, `ea7d494` M7e, `7380e50` M7d, `0e1d99d` M7b, `0b140aa` M7c, `c4ab35f` M7f, `b24c8b5` M6b, `f4dc10c` SPEC v0.11, `3dda414` M7g, `cb832a7` and `3d9eea8` M8, `6159e18` SPEC v0.12, `8c60715` M12e, `b7b7483` M12e launcher icon, `51d6ff8` SPEC v0.13, `a312758` Vico removal, `f56dcbe` FCM `fid`, `3e6c337` Pi image arm64 check, `e2dba84` SPEC v0.14, `328c466` M8b, `6eb3f85` `FcmMessageBody.fid`, `bfde12c` SPEC v0.15, `75b549f` M8c goldens, `a0c25b0` M8c licences row, `c30a49e` R8 release build, `bacbd18` M8d, `bfc3acb` SPEC v0.16, `2476667` M2c-pi bundle, `a59abeb` M8e, `b4d9467` Dockerfile `CARGO_BUILD_JOBS`, `956908a` M8f, `1328afe` chips cleanup, `400eac9` SPEC v0.17, `18c527f` goldens restored after the rewrite; then `2fac1ec` to `e70fe0d` infra overhaul, `8660b7f` server gap pass, `7fb86f5` infra security, `dc7a296` to `a0259ab` M12f, `8103dae` to `b985ce9` server overhaul, `48fe119` SPEC v0.18, `e4e6e5d` to `c582363` Android overhaul, `3ef2c15` SPEC v0.19, `96e4cbd` APK download rate limit (D-119), `481889a` SPEC v0.20, `f1abb0d` `cargo audit` in the server gate, `4f69ae0` Gitea login in the run line, `d05f13a` Pi stack behind NPM, `62f5279` and `9231a1d` infra README for NPM (D-120), `d090c6c` Android host property (D-121), `4bcff2c` SPEC v0.21, `dd07ca4` temporary workflow diagnostic step, `4a4e45f` S-22's release server and `4b1ae0c` `publish-apk.sh` host (D-122), `c9abd24` and `e381e37` server host defaults and OpenAPI `{host}` (D-123), `ac17b41` `install.sh` step number, `cce3e4e` SPEC v0.22, `a86ad24` server and contracts on port 443, `4852803` bundle on port 443 (D-124), `038c603` Android on port 443 and the pinned-root template, `ad35231` portless Android fixtures, `b1f4c10` SPEC v0.23, `3820f9c` sparse-checkout workflow, `200c99f` `.env` without informational keys, `6e5981d` self-contained Pi compose project (D-125), `28fa71b` compose deploy hint in the workflow and `infra/build-images.sh`.
|
||||||
|
- Landed since SPEC v0.31 (recorded in v0.32):
|
||||||
|
- W1 web Home and accounts (`934eee7`, D-144, D-145): W-2 with the balance hero and seesaw, account cards, the archived section (open state per page session) and the "Du och <partner> är med" empty card; W-3 with month groups and the overflow menu; W-4 at `/app/accounts/new` and `/app/accounts/:id/edit`; `?account=<id>` links into W-5 and W-7.
|
||||||
|
- W2 web expenses, search and CSV export (`a65ab71`, D-146 to D-148): W-5 with the §9.11 suggestion, the rejection banner and the discard dialog; W-6 with the audit timeline; W-12 with filters in the URL; the CSV export (`;`, BOM, decimal comma); the shared editor fields `web/src/components/editor/`; `vite.config.ts` `server.fs.allow` for `contracts/category-keywords.csv`.
|
||||||
|
- W3 web settle up (`f9a9bfb`, D-144, D-149): W-7 with live allocation errors, `?account=` and `?settlement=`; W-8 with allocations and timeline.
|
||||||
|
- W4 web analytics (`1aecf51`, D-151, D-152): W-9 stacked bars, W-10 drill-down with the D-128 line chart (bucket route `/app/analytics/bucket/<key>`, filter in the query string), W-11 Insights on E-34/E-35 with 3, 6 or 12 months; "Visa som tabell"; `resolveRange` and `spendSeries` ported with their vectors.
|
||||||
|
- W5 web settings (`3ebe57d`, D-153, D-154): W-13 hub, W-14 profile with the partner's first-login intro, W-15 categories, W-16 recurring, W-17 devices, W-18 invites at `/app/settings/invite`, W-19 about with the `GET /me` header fallback.
|
||||||
|
- Web fix (`83f5293`, no decision): Vitest's module cache kept a stale `catalog.ts` glob result in the main checkout (raw keys such as `web_accounts_load_failed` on screen); the glob lost its type argument and `catalog.test.ts` compares the loaded feature catalogs with the files; `web_accounts_load_failed` became the shared `web_ledger_load_failed` in `web-strings.csv`; `suggest_category` moved to PORTED in `lib/vectors.test.ts`; the fake server keeps non-JSON bodies raw.
|
||||||
|
- APK install failures (`d686ee0`, D-155): installer status reaches `UpdateRepository` by session id; blocked → "Kunde inte installera uppdateringen" with the security-settings text, aborted → `Available`, other failures → the installer's message; "Försök igen" downloads again; new AC-16.7; `strings.csv` 590 rows. Release `kvitt-140` deployed 2026-10-21.
|
||||||
- Landed since SPEC v0.29 (recorded in v0.30 and v0.31):
|
- Landed since SPEC v0.29 (recorded in v0.30 and v0.31):
|
||||||
- Update-check fix (`c0f4345`, D-140, v0.30): S-22 "Check for update" calls `UpdateRepository.checkNow()`, which always asks `GET /apk/latest` and restarts the 6-hour window; the automatic checks keep the throttle (AC-16.2, AC-16.6).
|
- Update-check fix (`c0f4345`, D-140, v0.30): S-22 "Check for update" calls `UpdateRepository.checkNow()`, which always asks `GET /apk/latest` and restarts the 6-hour window; the automatic checks keep the throttle (AC-16.2, AC-16.6).
|
||||||
- W6 Android Insights (`1c9856b`, D-141 to D-143, v0.31): `InsightsRepository` (E-34 trends, E-35 forecast, online only, names from Room); an "Insikter" section below S-11's local analytics following the measure toggle, 6 whole months, household and per-category line charts with the server's average and moving average, a forecast card per account ("Återkommande + snitt = prognos"); offline and error banners over the last data; 40 `s11_insights_*` goldens; Android gate green (791 tests in the feature run). `strings.csv` 588 rows (16 `analytics_insights_*` keys); the web dictionaries regenerated.
|
- W6 Android Insights (`1c9856b`, D-141 to D-143, v0.31): `InsightsRepository` (E-34 trends, E-35 forecast, online only, names from Room); an "Insikter" section below S-11's local analytics following the measure toggle, 6 whole months, household and per-category line charts with the server's average and moving average, a forecast card per account ("Återkommande + snitt = prognos"); offline and error banners over the last data; 40 `s11_insights_*` goldens; Android gate green (791 tests in the feature run). `strings.csv` 588 rows (16 `analytics_insights_*` keys); the web dictionaries regenerated.
|
||||||
@@ -57,23 +65,23 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
|
|||||||
- Validation reports (SPEC §11.4): `docs/validation/ac-coverage.md` (taken at `956908a`, `ed1b130` before the rewrite): 270 rows, 260 Done, 5 Partial, 1 Missing, 4 device-only; feature complete in code, not yet against §4's acceptance rule; its S-16 and S-22 mismatches are fixed by M12f. `docs/validation/security-review.md` (taken at `18c527f`): Critical 0, High 1, Medium 3, Low 10, Info 8; L-6 kept (D-116), L-7 fixed (D-116), L-1, L-2, L-5, L-9, M-2 fixed (D-117), L-8 fixed (`e70fe0d`); H-1 (`725901c`, D-115), M-3 (`baa5806`) and L-4 (`b985ce9`, D-114) fixed by the server overhaul; L-10 left as the spec says, decision pending (§4). Owner answers of 2026-10-19: M-1 no (backups stay unencrypted, rclone keeps the `drive` scope; accepted risk in SPEC §8.1 and §8.7), L-3 accepted and fixed (`96e4cbd`, D-119), the 16 MP and 512-character limits (D-114) confirmed; M-2's remaining question (the Home Assistant SSH key form) is moot with the NPM rework (D-120) and closed; D-120 also moves L-1 and the headers to NPM, makes L-2 moot, reverses the L-8 fix and replaces I-5's check with a conntrack check (dated notes in the review, SPEC §11.4). H-1 is fixed (`725901c`).
|
- Validation reports (SPEC §11.4): `docs/validation/ac-coverage.md` (taken at `956908a`, `ed1b130` before the rewrite): 270 rows, 260 Done, 5 Partial, 1 Missing, 4 device-only; feature complete in code, not yet against §4's acceptance rule; its S-16 and S-22 mismatches are fixed by M12f. `docs/validation/security-review.md` (taken at `18c527f`): Critical 0, High 1, Medium 3, Low 10, Info 8; L-6 kept (D-116), L-7 fixed (D-116), L-1, L-2, L-5, L-9, M-2 fixed (D-117), L-8 fixed (`e70fe0d`); H-1 (`725901c`, D-115), M-3 (`baa5806`) and L-4 (`b985ce9`, D-114) fixed by the server overhaul; L-10 left as the spec says, decision pending (§4). Owner answers of 2026-10-19: M-1 no (backups stay unencrypted, rclone keeps the `drive` scope; accepted risk in SPEC §8.1 and §8.7), L-3 accepted and fixed (`96e4cbd`, D-119), the 16 MP and 512-character limits (D-114) confirmed; M-2's remaining question (the Home Assistant SSH key form) is moot with the NPM rework (D-120) and closed; D-120 also moves L-1 and the headers to NPM, makes L-2 moot, reverses the L-8 fix and replaces I-5's check with a conntrack check (dated notes in the review, SPEC §11.4). H-1 is fixed (`725901c`).
|
||||||
- Server overhaul (landed on `main` `8103dae` to `b985ce9`): shared helpers and one test harness, clippy pedantic wins; H-1 rate limiter (`725901c`, D-115), pictures at most 16 MP and 64 MB decoded (`baa5806`, D-114), `fcm_token` at most 512 characters (`b985ce9`, D-114); shared row inserts, invite creation and CLI helpers, `tests/common/` harness, synchronous `tasks::start`; 268 server tests; all crates at their latest versions. Recorded in SPEC v0.18.
|
- 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-v31`: SPEC v0.30 and v0.31, D-140 to D-143, `strings.csv`, the web dictionaries, design-spec S-11 and §3.10, this file).
|
- This docs pass (`agent-docs-v32`: SPEC v0.32, D-144 to D-155, design-spec §3.6, S-17, S-22, this file).
|
||||||
- Web W1 to W5 (`web/src/features/**`, D-144 to D-154 reserved); the Pi image build with the web client and E-34/E-35.
|
- The Pi image build with the complete web app (in progress on the Pi), then the owner's deploy and browser checks (§3 steps 4 and 5).
|
||||||
- Record D-155 (`d686ee0`, install failures visible on S-17 and S-22) in the next spec version, v0.32, with `strings.csv` at 590 rows (docs agent).
|
|
||||||
- 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 `7521c01`. The Gitea secret `kvitt_cicd` (a user-level secret of `jacobh2`, renamed by the owner 2026-10-19) works; the Drive token is `infra/secrets/rclone.conf` (`[gdrive]`; folder id in `.env` as `RCLONE_FOLDER_ID`). Unused on the Mac since D-120: `infra/secrets/ha_ssh_key` and `ha_ssh_key.pub` (git-ignored; may be deleted).
|
- Remote: `origin` on Gitea, SSH key works; pushed at `7521c01`. The Gitea secret `kvitt_cicd` (a user-level secret of `jacobh2`, renamed by the owner 2026-10-19) works; the Drive token is `infra/secrets/rclone.conf` (`[gdrive]`; folder id in `.env` as `RCLONE_FOLDER_ID`). Unused on the Mac since D-120: `infra/secrets/ha_ssh_key` and `ha_ssh_key.pub` (git-ignored; may be deleted).
|
||||||
- Both gates exist: Android `tools/gradle-serial.sh check` (from `android/`), server `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test` (from `server/`, `cargo audit` since `f1abb0d`). Both green on `main` as far as reported (Android green at `1c9856b`, which fixed the three `:data` example tests red since `50a0d67`; server 299 tests at `50a0d67`). The third gate (W0, D-131): `npm run check` in `web/` (`tsc --noEmit`, `eslint .`, `vitest run`, `vite build`, generated-types freshness); a change to `contracts/openapi.yaml` regenerates `web/src/api/schema.d.ts` in the same landing.
|
- Both gates exist: Android `tools/gradle-serial.sh check` (from `android/`), server `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test` (from `server/`, `cargo audit` since `f1abb0d`). Both green on `main` as far as reported (Android green at `1c9856b`, which fixed the three `:data` example tests red since `50a0d67`; server 299 tests at `50a0d67`). The third gate (W0, D-131): `npm run check` in `web/` (generated files, `tsc --noEmit`, `eslint .`, Prettier, `vitest run`, `vite build`), green on `main` with W1 to W5 (59 files, 598 tests at `83f5293`); a change to `contracts/openapi.yaml` regenerates `web/src/api/schema.d.ts` in the same landing.
|
||||||
- Importer: the dry run of 2026-10-18 reconciled on both real files (Vardag 1746 rows, Hemma 37 rows, 0 unmapped). The production import on the Kvitt Pi is done (owner, 2026-10-21). The CSV files stay in `export/` (git-ignored).
|
- 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)
|
||||||
- Land `agent-docs-v31` (orchestrator): SPEC v0.30 and v0.31, D-140 to D-143, `strings.csv`, `web/src/i18n/*.json`, design-spec, HANDOFF; gate `python3 tools/contracts-check.py` (the web gate for the regenerated dictionaries).
|
- Land `agent-docs-v32` (orchestrator): SPEC v0.32, D-144 to D-155, design-spec, HANDOFF; gates `python3 tools/contracts-check.py` and `npm run check` in `web/`.
|
||||||
- Pi image with the web client and E-34/E-35 (built from the `1c9856b` push): watch the workflow run, then the owner deploys it (§3 step 4), then HUMAN step 3; then publish a release APK with W6 (`infra/publish-apk.sh --deploy`) so the phones show Insights.
|
- Pi image with the complete web app (built from the push of the web plans): watch the workflow run, then the owner deploys it (§3 step 4) and checks the web client (§3 step 5); HUMAN step 3 if the NPM block is not yet current. Release APK `kvitt-140` (W6 Insights, D-155) is already deployed; its Insights need this image.
|
||||||
|
- W0b web cleanup plan (agent, no plan yet): the "Web follow-ups" below.
|
||||||
- Closed by `28fa71b`: the `/opt/kvitt` leftovers in `.gitea/workflows/build-images.yaml` and `infra/build-images.sh` (compose deploy hint, `docker image inspect`). Still stale in the workflow header (agent, next infra pass): "NOT YET RUN" and "First-build duration on the Pi: unknown" (now 2 h 42 min).
|
- Closed by `28fa71b`: the `/opt/kvitt` leftovers in `.gitea/workflows/build-images.yaml` and `infra/build-images.sh` (compose deploy hint, `docker image inspect`). Still stale in the workflow header (agent, next infra pass): "NOT YET RUN" and "First-build duration on the Pi: unknown" (now 2 h 42 min).
|
||||||
- Pushed: through `1c9856b` (W6 Android Insights).
|
- Pushed: through the five web plans (orchestrator, 2026-10-21).
|
||||||
- Closed by `2acfc34` (`agent-gate-docs`): `infra/README.md`'s stale `CARGO_BUILD_JOBS` text and its "Owner decisions (open)" (now answered).
|
- Closed by `2acfc34` (`agent-gate-docs`): `infra/README.md`'s stale `CARGO_BUILD_JOBS` text and its "Owner decisions (open)" (now answered).
|
||||||
- `infra/backup/Dockerfile`: stale comment "on the Pi the container runs as uid 10001" (since D-125 the owner's uid); left as is, because editing it triggers a 5-minute image build (agent, with the next change there).
|
- `infra/backup/Dockerfile`: stale comment "on the Pi the container runs as uid 10001" (since D-125 the owner's uid); left as is, because editing it triggers a 5-minute image build (agent, with the next change there).
|
||||||
- Historical `:8443` and `icehack.asuscomm.com` mentions in `docs/ux/` and `docs/validation/` are left as the record of their time (no action unless the owner wants them rewritten).
|
- Historical `:8443` and `icehack.asuscomm.com` mentions in `docs/ux/` and `docs/validation/` are left as the record of their time (no action unless the owner wants them rewritten).
|
||||||
@@ -88,15 +96,30 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
|
|||||||
- `PLAN-M2c-pi.md` is superseded where it names Caddy, cert-sync, scripts or `/opt/kvitt` (note at its top, D-120 and D-125); SPEC §10 and `infra/README.md` are current. `PLAN-M11.md` has a note that `/opt/kvitt/import` is now `~/kvitt/import`.
|
- `PLAN-M2c-pi.md` is superseded where it names Caddy, cert-sync, scripts or `/opt/kvitt` (note at its top, D-120 and D-125); SPEC §10 and `infra/README.md` are current. `PLAN-M11.md` has a note that `/opt/kvitt/import` is now `~/kvitt/import`.
|
||||||
- Plans (SPEC §11.2 v0.24), in dependency order; prompt `prompts/<id>.md`:
|
- Plans (SPEC §11.2 v0.24), in dependency order; prompt `prompts/<id>.md`:
|
||||||
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7g, M8 (phone test passed), M8b to M8f, M9, M12a to M12f, U1, U2, R8 release build, M2c-pi bundle with its NPM rework and the self-contained compose project (D-125), server gap pass, infra overhaul, infra security, the server overhaul, the Android overhaul, the APK download rate limit, `cargo audit`, the Android host property and its follow-ups (D-122), the server host defaults (D-123), port 443 (D-124), pull-to-refresh (D-127), the editor keyboard fix, the S-12 line chart (D-128): landed.
|
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7g, M8 (phone test passed), M8b to M8f, M9, M12a to M12f, U1, U2, R8 release build, M2c-pi bundle with its NPM rework and the self-contained compose project (D-125), server gap pass, infra overhaul, infra security, the server overhaul, the Android overhaul, the APK download rate limit, `cargo audit`, the Android host property and its follow-ups (D-122), the server host defaults (D-123), port 443 (D-124), pull-to-refresh (D-127), the editor keyboard fix, the S-12 line chart (D-128): landed.
|
||||||
- Web client programme (D-134, SPEC §11.2 rows W0 to W7): W0 (`2ae65a5`), W6 (`1c9856b`) and W7 (`50a0d67`) landed; W1 to W5 running in parallel (briefs from `prompts/W1.md` to `prompts/W5.md`); their decisions D-144 to D-154 are recorded by a docs agent when they land.
|
- Web client programme (D-134, SPEC §11.2 rows W0 to W7): all landed: W0 (`2ae65a5`), W1 (`934eee7`), W2 (`a65ab71`), W3 (`f9a9bfb`), W4 (`1aecf51`), W5 (`3ebe57d`), W6 (`1c9856b`), W7 (`50a0d67`); D-144 to D-154 recorded in v0.32. Next for the web: W0b (follow-ups below).
|
||||||
- Next: the app key and gate if not yet done (D-126, §3); M10 First release (keystore per §10.6, D-101; the reports `first-release.md` and `restore-drill.md`). Done 2026-10-21: release pairing of both phones against the Kvitt Pi and M11 Import history (SPEC §10.8 "Production run").
|
- Next: the app key and gate if not yet done (D-126, §3); M10 First release (keystore per §10.6, D-101; the reports `first-release.md` and `restore-drill.md`). Done 2026-10-21: release pairing of both phones against the Kvitt Pi and M11 Import history (SPEC §10.8 "Production run").
|
||||||
|
|
||||||
|
### Web follow-ups (for a W0b plan; agent)
|
||||||
|
- Shared pieces built two or three times in feature folders, to move into `web/src/components/` or `web/src/lib/`: an account tile (`features/accounts/AccountTile`, `features/analytics/components/AccountTile`); the loading and error cards (`PageStates` in `accounts`, `expenses`, `analytics`); `useToday` (`features/accounts/useToday.ts`, `features/analytics/shared.ts`); `categoryName`; the audit `Timeline` and its line builders (`features/expenses/Timeline.tsx`, `timelineLines.ts`, `features/settle/timeline.ts`); `apiErrorKey` and the rejection texts (`features/expenses/messages.ts`, `features/settle/messages.ts`); W1's `BalanceHero`, `Seesaw`, `AccountBalanceHeader`, `OverflowMenu` (D-145).
|
||||||
|
- `components/AmountField`: reads `,` as the decimal point in English too (should follow the page language, as `lib/money.ts`); needs `hideLabel` and `placeholder` props.
|
||||||
|
- `lib/ids.ts`: type the `random` parameter as `(bytes: Uint8Array<ArrayBuffer>) => void`.
|
||||||
|
- Vitest cache `web/node_modules/.vitest-cache`: its stale `catalog.ts` result is fixed at the root by `83f5293` (glob without a type argument, `catalog.test.ts` compares with the files); still decide whether to disable the cache or key it on the feature i18n files.
|
||||||
|
- `vite.config.ts`: the dev proxy lacks `/analytics` (W-11 under `npm run dev`); Recharts in its own chunk (the vendor chunk is 515 kB).
|
||||||
|
- `app/routes.ts`: `bucket` in `DrillDownKind` (D-151); builders for `?account=<id>` and `?settlement=<id>` (D-144), replacing `features/accounts/links.ts` and `features/settle/links.ts`.
|
||||||
|
- `lib/vectors.test.ts`: `spend_series` and `resolve_range` are still listed as not ported (their vectors run in `features/analytics/domain/analytics.vectors.test.ts`); move them to `PORTED` against the analytics port (`suggest_category` done in `83f5293`).
|
||||||
|
- W5's recurring editor (`features/settings/recurring/fields.tsx`) uses its own fields; swap them for `components/editor` (D-148).
|
||||||
|
- `editor_paid_by_toggle` says "Dubbeltryck" (TalkBack wording) on the web; a `web_` variant for a click.
|
||||||
|
- `web/package.json` `kvitt.versionCode` is fixed at 1 (`X-Kvitt-App-Version`, W-19); PLAN-W0 wanted the commit count at build time.
|
||||||
|
- `/app/settings/invites` is kept as an alias of `/app/settings/invite` (D-153); drop it if nothing links there.
|
||||||
|
- Closed by `83f5293`: the fake test server keeps non-JSON bodies raw.
|
||||||
|
|
||||||
## 3. HUMAN steps pending (relay in chat)
|
## 3. HUMAN steps pending (relay in chat)
|
||||||
Done 2026-10-21: the Pi install (stack running, `v1.0.1`, both containers healthy), the public health check through NPM, bootstrap, release pairing of both phones, the Splitwise import.
|
Done 2026-10-21: the Pi install (stack running, `v1.0.1`, both containers healthy), the public health check through NPM, bootstrap, release pairing of both phones, the Splitwise import.
|
||||||
1. App key (D-126; `infra/README.md` step 4a items 1 to 3; the key exists on the Mac and the Pi serves the keyed `kvitt-133.apk` since the first `--deploy` run, D-139; whether both phones run it is unconfirmed): generate `kvitt.appKey` in `android/local.properties` on the Mac (an agent may run the one-line `openssl` command; the value is never printed); build and publish a keyed APK (`infra/publish-apk.sh`, copy to `data/apk/`); both phones on it (update banner, or sideload).
|
1. App key (D-126; `infra/README.md` step 4a items 1 to 3; the key exists on the Mac and the Pi serves the keyed `kvitt-133.apk` since the first `--deploy` run, D-139; whether both phones run it is unconfirmed): generate `kvitt.appKey` in `android/local.properties` on the Mac (an agent may run the one-line `openssl` command; the value is never printed); build and publish a keyed APK (`infra/publish-apk.sh`, copy to `data/apk/`); both phones on it (update banner, or sideload).
|
||||||
2. Gate (step 4a items 4 and 5), only after both phones run the keyed APK: NPM, Hosts, Proxy Hosts, Kvitt row, Edit, Advanced: paste the step 4 block with the key in place of `KVITT_APP_KEY`, Save (row Online, yes/no). Acceptance test: on home Wi-Fi Chrome opens `https://kvitt.suorra.duckdns.org/i/TEST` and the invite page shows (yes/no); with Wi-Fi off the same URL gives a connection error (yes/no); with Wi-Fi off Kvitt's About screen shows the server version (yes/no).
|
2. Gate (step 4a items 4 and 5), only after both phones run the keyed APK: NPM, Hosts, Proxy Hosts, Kvitt row, Edit, Advanced: paste the step 4 block with the key in place of `KVITT_APP_KEY`, Save (row Online, yes/no). Acceptance test: on home Wi-Fi Chrome opens `https://kvitt.suorra.duckdns.org/i/TEST` and the invite page shows (yes/no); with Wi-Fi off the same URL gives a connection error (yes/no); with Wi-Fi off Kvitt's About screen shows the server version (yes/no).
|
||||||
3. Web client gate lines (D-130, SPEC v0.28): when the NPM block is pasted (item 2, or again if the D-126 block is already in NPM), paste the current `infra/npm/kvitt.conf` content (step 4), which also lets the web client through and sets its Content-Security-Policy; Save (row Online, yes/no). After the first image with the web client is deployed (W7, §10.10), acceptance test with Wi-Fi off on the phone: `https://kvitt.suorra.duckdns.org/app/` shows the Kvitt login page (yes/no) and `https://kvitt.suorra.duckdns.org/health` still gives a connection error (yes/no) (step 4a item 5 d and e).
|
3. Web client gate lines (D-130, SPEC v0.28): when the NPM block is pasted (item 2, or again if the D-126 block is already in NPM), paste the current `infra/npm/kvitt.conf` content (step 4), which also lets the web client through and sets its Content-Security-Policy; Save (row Online, yes/no). After the first image with the web client is deployed (W7, §10.10), acceptance test with Wi-Fi off on the phone: `https://kvitt.suorra.duckdns.org/app/` shows the Kvitt login page (yes/no) and `https://kvitt.suorra.duckdns.org/health` still gives a connection error (yes/no) (step 4a item 5 d and e).
|
||||||
4. Deploy the new image (web client and E-34/E-35, SPEC §10.10), once the Gitea workflow run for `1c9856b` has succeeded: on the Kvitt Pi in `~/kvitt`, set `KVITT_TAG` in `.env` to the tag the run printed, then `docker compose pull && docker compose up -d`; `server` and `backup` healthy (yes/no). Then the orchestrator publishes the release APK with Insights.
|
4. Deploy the new image (the complete web app and E-34/E-35, SPEC §10.10), once the Gitea workflow run (in progress on the Pi) has succeeded: on the Kvitt Pi in `~/kvitt`, set `KVITT_TAG` in `.env` to the tag the run printed, then `docker compose pull && docker compose up -d`; `server` and `backup` healthy (yes/no). The release APK `kvitt-140` is already deployed; S-11's Insights work once this image runs.
|
||||||
|
5. Web client in a browser (after step 4): on the phone, Inställningar › Para ihop en enhet › För mig, create a code; on the Mac open `https://kvitt.suorra.duckdns.org/app/`, "Logga in i Kvitt", type the code, log in: Home shows (yes/no). Then, each yes/no: Home and one account open; add, edit and delete an expense; Sök, export a CSV and open it in Numbers (columns and amounts right); Gör upp: record a payment and delete it; Statistik, open a category row (line chart), Insikter; each Settings page opens; on the phone Inställningar › Enheter shows `Web · <browser>`, "Ta bort åtkomst" on it, and the browser shows the login page with the removed-device message.
|
||||||
|
|
||||||
## 4. Owner questions (asked, unanswered)
|
## 4. Owner questions (asked, unanswered)
|
||||||
- OQ-6 (SPEC §12): a confirm step on S-1 for an unusual server; default "not in v1".
|
- OQ-6 (SPEC §12): a confirm step on S-1 for an unusual server; default "not in v1".
|
||||||
@@ -108,9 +131,9 @@ Done 2026-10-21: the Pi install (stack running, `v1.0.1`, both containers health
|
|||||||
- Nothing.
|
- Nothing.
|
||||||
|
|
||||||
## 6. Numbering
|
## 6. Numbering
|
||||||
- Decisions: D-001 to D-143 used, except D-008 and D-009 (never used). Reserved: **D-144 to D-154** for the web feature agents W1 to W5, **D-155** for the APK install-failure fix. Next free after those: **D-156**.
|
- Decisions: D-001 to D-155 used, except D-008, D-009 and D-150 (never used). Next free: **D-156**.
|
||||||
- Spec version: v0.31 written (v0.30 D-140, v0.31 D-141 to D-143). Next **v0.32**.
|
- Spec version: v0.32 written (D-144 to D-155). Next **v0.33**.
|
||||||
- Server schema version **3** (migration 0003); next **4**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); next **2**. Unchanged by v0.27 to v0.31 (W7 and W6 needed no migration).
|
- Server schema version **3** (migration 0003); next **4**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); next **2**. Unchanged by v0.27 to v0.32 (no plan since needed a migration).
|
||||||
- Sync protocol **1**, push payload **1**.
|
- Sync protocol **1**, push payload **1**.
|
||||||
|
|
||||||
## 7. How the work is run
|
## 7. How the work is run
|
||||||
|
|||||||
@@ -148,3 +148,15 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
|
|||||||
| D-141 | 2026-10-21 | Insights UI contract. `:core:model` (`model/Insights.kt`) `InsightsRepository` with `trends(months: 1..24, measure)` and `forecast(measure)`, both returning `OnlineResult`: `Offline` without a request when offline (§9.9), `Failed("network")` when the server cannot be reached, else the §14.3.2 code. Types `SpendTrends`, `SpendTrend`, `TrendPoint`, `CategoryTrend` (`category: Category?`), `SpendForecast`, `AccountForecast` (`account: Account?`), `ForecastMonth`, named after the E-34/E-35 fields (§14.5.7). Category and account names resolved from Room, archived and deleted rows included; an unknown id gives null. Nothing is stored. `:data`: `api/AnalyticsDtos.kt`, two `KvittApi` calls, `repo/InsightsRepositoryImpl.kt`, bound in `DataModule`; `ExampleRoundTripTest` covers the E-34 and E-35 examples (fixing the gate red since `50a0d67`). Fakes: `FakeInsightsRepository`, `analytics/InsightsScenarios.kt`, scenario flag `insightsFailure`. Landed `1c9856b`. | The insights are server results (D-133), so the contract mirrors the wire objects and adds only what the phone knows better: names, colours and archived state from its own copy. A null for an unknown id lets the UI skip rows the phone has not synced yet instead of failing. | SPEC §14.2.4, §14.8; `android/core/model/src/main/kotlin/se/hagstedt/kvitt/model/Insights.kt`, `android/data/src/main/kotlin/se/hagstedt/kvitt/data/api/AnalyticsDtos.kt`, `android/data/src/main/kotlin/se/hagstedt/kvitt/data/repo/InsightsRepositoryImpl.kt`, `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/FakeInsightsRepository.kt` |
|
| D-141 | 2026-10-21 | Insights UI contract. `:core:model` (`model/Insights.kt`) `InsightsRepository` with `trends(months: 1..24, measure)` and `forecast(measure)`, both returning `OnlineResult`: `Offline` without a request when offline (§9.9), `Failed("network")` when the server cannot be reached, else the §14.3.2 code. Types `SpendTrends`, `SpendTrend`, `TrendPoint`, `CategoryTrend` (`category: Category?`), `SpendForecast`, `AccountForecast` (`account: Account?`), `ForecastMonth`, named after the E-34/E-35 fields (§14.5.7). Category and account names resolved from Room, archived and deleted rows included; an unknown id gives null. Nothing is stored. `:data`: `api/AnalyticsDtos.kt`, two `KvittApi` calls, `repo/InsightsRepositoryImpl.kt`, bound in `DataModule`; `ExampleRoundTripTest` covers the E-34 and E-35 examples (fixing the gate red since `50a0d67`). Fakes: `FakeInsightsRepository`, `analytics/InsightsScenarios.kt`, scenario flag `insightsFailure`. Landed `1c9856b`. | The insights are server results (D-133), so the contract mirrors the wire objects and adds only what the phone knows better: names, colours and archived state from its own copy. A null for an unknown id lets the UI skip rows the phone has not synced yet instead of failing. | SPEC §14.2.4, §14.8; `android/core/model/src/main/kotlin/se/hagstedt/kvitt/model/Insights.kt`, `android/data/src/main/kotlin/se/hagstedt/kvitt/data/api/AnalyticsDtos.kt`, `android/data/src/main/kotlin/se/hagstedt/kvitt/data/repo/InsightsRepositoryImpl.kt`, `android/core/fakes/src/main/kotlin/se/hagstedt/kvitt/fakes/FakeInsightsRepository.kt` |
|
||||||
| D-142 | 2026-10-21 | Insights placement and loading. An "Insights" ("Insikter") section below S-11's local analytics (after "Per konto", or after the empty state), with the caption "Senaste 6 hela månaderna · Hushållet" ("· Min del" for my share). It follows S-11's measure toggle and always shows 6 whole months (E-34's default), whatever S-11's range. Both reads run in parallel and load when S-11 opens, when the measure changes to one not yet loaded, when the phone comes online, and on "Försök igen". The data is kept in memory per measure while S-11 is on the back stack, never stored. Offline shows the "Kräver anslutning" banner with "Insikterna räknas fram på servern. Anslut till internet för att se dem."; a failure shows an error banner ("Kunde inte nå servern" for `network`, else the code's text) with "Försök igen"; the last data stays visible under either banner; before the first load skeleton cards read "Hämtar insikter". S-11's offline analytics are unchanged (AC-14.1). Landed `1c9856b`. | The owner wanted the server's analytics in the app too (D-133); placing them under the local charts keeps S-11 fully usable offline, and a fixed 6-month window matches the server's default and S-11's default range without a second range control. | SPEC §13.3 S-11, AC-14.1, §15.8; `docs/ux/design-spec.md` S-11 "Insights"; `android/feature/analytics/src/main/kotlin/se/hagstedt/kvitt/feature/analytics/InsightsViewModel.kt`, `InsightsModel.kt` |
|
| D-142 | 2026-10-21 | Insights placement and loading. An "Insights" ("Insikter") section below S-11's local analytics (after "Per konto", or after the empty state), with the caption "Senaste 6 hela månaderna · Hushållet" ("· Min del" for my share). It follows S-11's measure toggle and always shows 6 whole months (E-34's default), whatever S-11's range. Both reads run in parallel and load when S-11 opens, when the measure changes to one not yet loaded, when the phone comes online, and on "Försök igen". The data is kept in memory per measure while S-11 is on the back stack, never stored. Offline shows the "Kräver anslutning" banner with "Insikterna räknas fram på servern. Anslut till internet för att se dem."; a failure shows an error banner ("Kunde inte nå servern" for `network`, else the code's text) with "Försök igen"; the last data stays visible under either banner; before the first load skeleton cards read "Hämtar insikter". S-11's offline analytics are unchanged (AC-14.1). Landed `1c9856b`. | The owner wanted the server's analytics in the app too (D-133); placing them under the local charts keeps S-11 fully usable offline, and a fixed 6-month window matches the server's default and S-11's default range without a second range control. | SPEC §13.3 S-11, AC-14.1, §15.8; `docs/ux/design-spec.md` S-11 "Insights"; `android/feature/analytics/src/main/kotlin/se/hagstedt/kvitt/feature/analytics/InsightsViewModel.kt`, `InsightsModel.kt` |
|
||||||
| D-143 | 2026-10-21 | Insights display rules. "Utgifter per månad": the household (or my-share) trend on D-128's `SpendLineChart` in `primary`, with the server's average (dashed) and moving average, omitted when the average is 0 or there is one point; under it "Snitt X i månaden", the last whole month against the average in D-128's words, and "Hittills i <månad>: X". "Kategorier per månad": the open category's chart in its category colour over a radio list of the categories in the server's order (tile, name, "Snitt X i månaden", change "+N %" / "−N %" / "0 %"); the open one defaults to the first, the largest total; category ids the phone does not hold are skipped. With no spending at all, one card "Inga utgifter de senaste 6 hela månaderna" replaces both charts. "Prognos" with its caption, then one card per account in the server's order (tile, muted when archived, name, "Hittills i <månad>: X", "Arkiverat" badge), its two months as rows "<Månad>" / "Återkommande X + snitt Y" with the forecast amount on the right (recurring + average = forecast); an account with nothing this month and zero forecasts shows one line "No recurring expenses and no recent spending" ("Inga återkommande utgifter och inga utgifter på sistone"). Goldens `s11_insights_<state>` for 13 states (loaded, loaded_scrolled, category, my_share, loading, offline, offline_kept, unreachable, unreachable_empty, empty, long, long_scrolled, screen) in three variants plus `s11_insights_long_sv_light_200`. Landed `1c9856b`. | Reusing D-128's chart and caption words keeps S-11's Insights and S-12 reading alike; showing the forecast's two parts makes the number explainable without a help text. | SPEC §13.3 S-11, §13.4; `docs/ux/design-spec.md` S-11 "Insights", §3.10; `docs/ux/strings.csv` `analytics_insights_*`; `android/feature/analytics/src/main/kotlin/se/hagstedt/kvitt/feature/analytics/InsightsSection.kt` |
|
| D-143 | 2026-10-21 | Insights display rules. "Utgifter per månad": the household (or my-share) trend on D-128's `SpendLineChart` in `primary`, with the server's average (dashed) and moving average, omitted when the average is 0 or there is one point; under it "Snitt X i månaden", the last whole month against the average in D-128's words, and "Hittills i <månad>: X". "Kategorier per månad": the open category's chart in its category colour over a radio list of the categories in the server's order (tile, name, "Snitt X i månaden", change "+N %" / "−N %" / "0 %"); the open one defaults to the first, the largest total; category ids the phone does not hold are skipped. With no spending at all, one card "Inga utgifter de senaste 6 hela månaderna" replaces both charts. "Prognos" with its caption, then one card per account in the server's order (tile, muted when archived, name, "Hittills i <månad>: X", "Arkiverat" badge), its two months as rows "<Månad>" / "Återkommande X + snitt Y" with the forecast amount on the right (recurring + average = forecast); an account with nothing this month and zero forecasts shows one line "No recurring expenses and no recent spending" ("Inga återkommande utgifter och inga utgifter på sistone"). Goldens `s11_insights_<state>` for 13 states (loaded, loaded_scrolled, category, my_share, loading, offline, offline_kept, unreachable, unreachable_empty, empty, long, long_scrolled, screen) in three variants plus `s11_insights_long_sv_light_200`. Landed `1c9856b`. | Reusing D-128's chart and caption words keeps S-11's Insights and S-12 reading alike; showing the forecast's two parts makes the number explainable without a help text. | SPEC §13.3 S-11, §13.4; `docs/ux/design-spec.md` S-11 "Insights", §3.10; `docs/ux/strings.csv` `analytics_insights_*`; `android/feature/analytics/src/main/kotlin/se/hagstedt/kvitt/feature/analytics/InsightsSection.kt` |
|
||||||
|
| D-144 | 2026-10-21 | Preset account and payment on the web travel in the query string. W-5 New expense takes `/app/expenses/new?account=<id>` and W-7 Settle up `/app/settle?account=<id>` (S-5's "New expense" and "Settle this account"); W-7 also takes `/app/settle?settlement=<id>` to edit a saved payment (W-8 "Edit", AC-11.6). Link builders: `newExpenseFor`, `settleFor` in `web/src/features/accounts/links.ts` and `settleAccountPath`, `settlementEditPath` in `web/src/features/settle/links.ts`, until W0 adds builders for them to `web/src/app/routes.ts` (follow-up). On W-5 an id that is not an active account falls back to the AC-8.6 default. Landed `934eee7` (W1), `f9a9bfb` (W3). | §15.3 leaves query parameters to the owning plan; a query parameter keeps one route per page, survives a reload and needs no change in `src/app/`. | SPEC §15.6 W-3, W-5, W-7, W-8; `web/src/features/accounts/links.ts`, `web/src/features/settle/links.ts` |
|
||||||
|
| D-145 | 2026-10-21 | Web Home empty state and archived section. When the partner has joined and no account exists, W-2 shows the S-4-home-empty card ("Du och <partner> är med", `home_empty_welcome`, with both avatars and `home_empty_subtitle`) in place of the balance header, then the empty state with "Nytt konto"; while there is no partner it shows S-3's content. The "Arkiverade (n)" section is collapsed by default; its open state is kept in memory for the page session (navigation keeps it, a reload collapses it), never stored. W1 built `home/BalanceHero`, `home/Seesaw`, `accounts/AccountBalanceHeader`, `accounts/OverflowMenu` and `accounts/AccountTile` inside its own folders, because W0 has no shared versions yet (follow-up W0b). Landed `934eee7`. | A balance header over no accounts would show a meaningless 0 kr; S-4 has this card for the same moment. Keeping the archived state in memory matches S-4 (survives navigation, not a cold start) and adds no stored key. | SPEC §15.6 W-2; `web/src/features/home/HomePage.tsx` |
|
||||||
|
| D-146 | 2026-10-21 | Web CSV export format (W-12, the choice §15.7 left to W2). UTF-8 with a byte-order mark; `;` between fields; CRLF after every row; RFC 4180 quoting (a field holding `;`, `"`, CR or LF, or starting or ending with a space, is quoted, `"` doubled); amounts in kronor with a decimal comma, two decimals, no grouping and no sign ("1234,50", in every page language); dates ISO `YYYY-MM-DD`. Header row in the page language; columns date, description, category, account, payer, amount, one share column per household user ("Andel <namn>", users in avatar-slot order), notes; one row per expense in the list's order. A text cell starting with `=`, `+`, `-`, `@`, TAB or CR gets a leading `'` (CSV injection). File name `kvitt-utgifter-<YYYY-MM-DD>.csv` (Swedish) or `kvitt-expenses-<YYYY-MM-DD>.csv` (English), built in the browser as a `Blob` download. Landed `a65ab71`. | Excel and Numbers with a Swedish locale read the BOM as UTF-8, split on `;` and take "1234,50" as a number; a fixed decimal comma keeps the file the same whatever the page language. Formula neutralising follows OWASP. | SPEC §15.7, AC-21.10; `web/src/features/export/csv.ts`, `download.ts` |
|
||||||
|
| D-147 | 2026-10-21 | AC-8.6 and search on the web. AC-8.6's "on this phone" means this browser: the last-used account and the last category per account are kept in `localStorage` (`kvitt.editor.lastAccount`, `kvitt.editor.lastCategory`, per-viewer conveniences of §15.5). W-12's query and filters live in the URL query string (`q`, `min` and `max` in öre, `from`, `to`, `account`, `category`, `payer`), so a search can be bookmarked and survives a reload. Landed `a65ab71`. | The web has no device database; `localStorage` is the browser's equivalent of the phone's preferences. A search in the URL is the web's natural "remembered filter". | SPEC AC-8.6, §15.5, §15.7; `web/src/features/expenses/model.ts`, `web/src/features/search/filter.ts` |
|
||||||
|
| D-148 | 2026-10-21 | Web expense editor behaviour (W-5). A rejected save shows an error banner above the form with S-16's text for the §14.4.6 reason (`home_sync_reject_*`; `clock_skew` names the computer's clock, `web_expenses_reject_clock_skew`) and, when the server returned its row, "Visa nuvarande version" (`home_sync_lost_show_current`, opens W-6); that row goes into the snapshot and the form keeps what was typed. A failed request shows "Kunde inte nå servern" or the code's `error_*` text the same way. "Utgiften sparades" (`snackbar_expense_saved`) shows after an edit too (S-7 shows it after an add only). The discard dialog covers `Esc` and in-app navigation (a router blocker). Differences from design-spec S-7: no amount keypad (the browser keyboard), a native select for the account, a native date input with an "Idag"/"Igår" hint, two percent fields plus presets instead of the ratio slider, no compact summary row. The shared editor fields of design-spec §3.7 live in `web/src/components/editor/` (exported from `index.ts`: `AccountDot`, `CategoryPicker`, `ConfirmDialog`, `DateControl`, `DateField`, `NotesField`, `OptionRow`, `OptionRows`, `PayerRow`, `SelectControl`, `SelectField`, `SplitBar`, `SplitPreview`, `SplitSection`, and the helpers of `categories.ts` and `split.ts`), for W2 and W5's recurring editor. The §9.11 keyword table is imported raw from `contracts/category-keywords.csv` at build time; `web/vite.config.ts` got `server.fs.allow: ['.', '../contracts']` for it. Landed `a65ab71`. | The web keeps no pending rows (§15.5), so a rejection is reported where it happened, with the phone's words; showing the server's version without losing the typed values lets the user redo the change. The desktop controls replace phone-only gestures without changing the rules. | SPEC §15.5, §15.6 W-5; `web/src/features/expenses/ExpenseEditorPage.tsx`, `messages.ts`, `web/src/components/editor/index.ts`, `web/vite.config.ts` |
|
||||||
|
| D-149 | 2026-10-21 | W-7 allocation errors as you type. Once the amount is a valid number, the §14.5.3 tokens a user can fix in the rows (`allocation_not_positive`, `no_allocations`, `too_many_allocations`) show under the allocation rows while typing, not only after "Bekräfta"; `allocation_sum_mismatch` stays the remaining indicator's job and an empty amount the field's. The hint "Skriv ett belopp så föreslår Kvitt en fördelning" (`settle_empty_hint`) shows only while the amount is empty and there are no rows. Landed `f9a9bfb`. | design-spec S-9 puts invalid allocations inline under the rows; on a desktop page the rows and the Confirm button are visible together, so waiting for Confirm only delays the message. | SPEC §15.6 W-7; `web/src/features/settle/SettlePage.tsx` |
|
||||||
|
| D-150 | 2026-10-21 | Unused (reserved for the web feature plans W1 to W5; none needed it). | | |
|
||||||
|
| D-151 | 2026-10-21 | Web analytics drill-down routes and filter. Clicking a bar of W-9 (AC-14.8, a whole bucket) opens `/app/analytics/bucket/<key>` (`bucketHref`), beside `/app/analytics/:groupKind/:groupId` for a breakdown row; the filter travels in the query string: `range` (the preset), `from` and `to` (custom range only), `interval` (month or week), `measure`, `group` and `bucket` (a group within one bucket). W-10's own period and interval changes replace that query string (no new history entry). Landed `1aecf51`. | The drill-down must survive a reload and be linkable like every web page; reusing the `:groupKind` route with the kind `bucket` keeps W0's route table unchanged (`DrillDownKind` in `routes.ts` gains `bucket` in W0b). Replacing the query keeps Back going to W-9 rather than through every period tried. | SPEC §15.6 W-9, W-10; `web/src/features/analytics/shared.ts`, `web/src/features/analytics/domain/rules.ts` (`filterToSearch`, `filterFromSearch`) |
|
||||||
|
| D-152 | 2026-10-21 | Web chart accessibility and Insights months. The chart SVGs (`StackedBarChart`, `SpendLineChart`) and their legends are hidden from screen readers; the month or week labels under the bars are keyboard stops that open the bucket drill-down; "Visa som tabell" (`web_analytics_show_table`) under each chart shows its numbers as a table. W-11 Insights offers 3, 6 or 12 months (default 6, E-34's default) through `web_insights_months`, and follows the measure. Landed `1aecf51`. | A Recharts SVG is not readable by a screen reader; a table carries the same numbers, and real buttons make every bar reachable by keyboard (AC-21.11). 3, 6 and 12 cover the useful windows of E-34 (1 to 24) without a free field. | SPEC §15.6 W-9, W-10, W-11, AC-21.11; `web/src/features/analytics/components/ChartTable.tsx`, `web/src/features/analytics/domain/insights.ts` (`INSIGHT_MONTHS`) |
|
||||||
|
| D-153 | 2026-10-21 | W-18's route is `/app/settings/invite` (W0's `paths.settingsInvite`), not `/app/settings/invites` as §15.6 said; W5 also routes `/app/settings/invites` to the same page so the old address works. Sub-routes of W-15 and W-16: `/app/settings/categories/new`, `/app/settings/categories/:id`, `/app/settings/recurring/new`, `/app/settings/recurring/:id`. Landed `3ebe57d`. | W0's route table was fixed before W5 started and the singular matches the S-21 screen; the spec follows the code. | SPEC §15.6 W-15, W-16, W-18; `web/src/features/settings/index.ts` |
|
||||||
|
| D-154 | 2026-10-21 | Partner first login and About on the web. The S-2 step after a `partner` login (AC-2.10) is an intro on W-14 Profile with "Fortsätt"; this browser remembers it in `localStorage` under `kvitt.profileStepDone`, holding the device id, so a later login on the same browser as a new device shows it again. W-19 About reads E-1 `/health`; when it does not answer (from outside the LAN, §15.2), it reads the `X-Kvitt-Server-Version` header of a fresh `GET /me` and shows the version without the schema (D-110's fallback). Landed `3ebe57d`. | AC-2.10 asks for the step once; the device id ties "once" to this login rather than to the browser forever. A fresh request gives a current header instead of a cached one. | SPEC AC-2.10, §15.2, §15.6 W-14, W-19; `web/src/features/settings/ProfilePage.tsx`, `AboutPage.tsx` |
|
||||||
|
| D-155 | 2026-10-21 | A failed or declined update install is no longer silent. The `PackageInstaller` session's final status reaches `UpdateRepository` by session id (`InstallResultReceiver` hands it to the waiting `AndroidApkInstaller` call; `ApkInstaller.install` returns a `PendingInstall`). `STATUS_FAILURE_BLOCKED` gives `UpdateState.Failed(info, UpdateFailure.Blocked)`: "Kunde inte installera uppdateringen" (`update_install_failed`) with the security-settings text (`update_install_blocked`); `STATUS_FAILURE_ABORTED` (the user declined the system dialog) goes back to `Available` without an error; every other failure gives `Failed(info, UpdateFailure.Install(detail))` with the installer's own status message as the body. `UpdateState.Failed` gains `reason: UpdateFailure = UpdateFailure.Download` (`sealed interface UpdateFailure { Download; Blocked; Install(detail: String?) }`, `:core:model` `Sync.kt`), so existing fakes are unchanged. The cached APK is deleted once the installer answers, so "Försök igen" downloads again. Goldens `S-17-install-blocked`, `S-17-install-failed`, `s22_about_install_blocked` new, `update_banner` re-recorded. Landed `d686ee0`. | A refused or failed install left the banner as it was, so a tap on "Uppdatera" seemed to do nothing. Showing the reason tells the user which setting to change or what the installer said; a declined dialog is the user's choice, not an error. | SPEC AC-16.7, §14.2.4; `docs/ux/design-spec.md` §3.6 `UpdateBanner`, S-17; `docs/ux/strings.csv`; `android/core/model/src/main/kotlin/se/hagstedt/kvitt/model/Sync.kt`, `android/data/src/main/kotlin/se/hagstedt/kvitt/data/update/Updater.kt`, `android/core/design/src/main/kotlin/se/hagstedt/kvitt/design/component/Sync.kt` |
|
||||||
|
|||||||
@@ -508,7 +508,7 @@ Every default category has its own colour; `teal` is shared with the archived ex
|
|||||||
|
|
||||||
**`Banner(kind: Lantern | Error | Warning | Neutral, icon, title, body, actions)`**: radius 24, padding 14 / 16 / 10, gap 6; icon 24 (filled) + title (title-small) + body (body-medium); actions right-aligned, text buttons in the banner's content colour. Lantern = `tertiary-container`, Error = `error-container`, Warning = `warning-container`, Neutral = `surface-container-high`. `role` status (alert for Error).
|
**`Banner(kind: Lantern | Error | Warning | Neutral, icon, title, body, actions)`**: radius 24, padding 14 / 16 / 10, gap 6; icon 24 (filled) + title (title-small) + body (body-medium); actions right-aligned, text buttons in the banner's content colour. Lantern = `tertiary-container`, Error = `error-container`, Warning = `warning-container`, Neutral = `surface-container-high`. `role` status (alert for Error).
|
||||||
|
|
||||||
**`UpdateBanner(state: UpdateState, onUpdate, onLater, onRetry)`** (S-4, S-22): `Available`: Lantern, `system_update`, "Version 1.2 finns", "Nyheter och rättningar · 8,4 MB" (size from `UpdateInfo.sizeBytes`), actions "Senare" (text) and "Uppdatera" (tertiary filled, `download`); offline: "Uppdatera" disabled and the body reads "Kräver anslutning". `Downloading`: Lantern, `downloading`, "Laddar ner… 64 %", `LinearWavyProgressIndicator` in `tertiary` (determinate, track `tertiary` at 28 %), no actions; TalkBack progress "Laddar ner version 1.2, 64 procent". `Failed`: Error, `error`, "Nedladdningen misslyckades. Försök igen." with "Senare" and "Försök igen" (error filled, `refresh`). `NeedsInstallPermission`: Available look; "Uppdatera" opens the S-17 dialog.
|
**`UpdateBanner(state: UpdateState, onUpdate, onLater, onRetry)`** (S-4, S-22): `Available`: Lantern, `system_update`, "Version 1.2 finns", "Nyheter och rättningar · 8,4 MB" (size from `UpdateInfo.sizeBytes`), actions "Senare" (text) and "Uppdatera" (tertiary filled, `download`); offline: "Uppdatera" disabled and the body reads "Kräver anslutning". `Downloading`: Lantern, `downloading`, "Laddar ner… 64 %", `LinearWavyProgressIndicator` in `tertiary` (determinate, track `tertiary` at 28 %), no actions; TalkBack progress "Laddar ner version 1.2, 64 procent". `Failed`: Error, `error`, "Senare" and "Försök igen" (error filled, `refresh`; downloads again); title and body follow `UpdateFailure` (D-155): `Download` "Nedladdningen misslyckades. Försök igen." (`update_failed`), no body; `Blocked` "Kunde inte installera uppdateringen" (`update_install_failed`) with the body "Installationen blockerades av telefonens säkerhetsinställningar (Auto Blocker eller Installera okända appar). Ändra inställningen och försök igen." (`update_install_blocked`); `Install(detail)` the same title with the installer's own message as the body (none when it gave none). A declined system install dialog is not a failure: the banner returns to `Available`. `NeedsInstallPermission`: Available look; "Uppdatera" opens the S-17 dialog.
|
||||||
|
|
||||||
**`NeedsConnectionBanner(body: String)`**: Warning banner, `cloud_off`, title "Kräver anslutning", screen-specific body. **`NeedsConnectionHint`**: supporting line under a control: `cloud_off` 16 + "Kräver anslutning" in body-small weight 600 `on-surface-variant`, padding 4 / 16.
|
**`NeedsConnectionBanner(body: String)`**: Warning banner, `cloud_off`, title "Kräver anslutning", screen-specific body. **`NeedsConnectionHint`**: supporting line under a control: `cloud_off` 16 + "Kräver anslutning" in body-small weight 600 `on-surface-variant`, padding 4 / 16.
|
||||||
|
|
||||||
@@ -1126,7 +1126,9 @@ Not a screen: the `UpdateBanner` on S-4 (and S-22) and one dialog.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `S-17-update` | variant first-time dialog | the dialog over S-4 with the `Available` banner |
|
| `S-17-update` | variant first-time dialog | the dialog over S-4 with the `Available` banner |
|
||||||
| `S-17-update-progress` | loading (downloading) | banner `Downloading` 64 % |
|
| `S-17-update-progress` | loading (downloading) | banner `Downloading` 64 % |
|
||||||
| `S-17-update-failed` | error | banner `Failed` |
|
| `S-17-update-failed` | error | banner `Failed` (download) |
|
||||||
|
| `S-17-install-blocked` | error | banner `Failed(Blocked)` with the security-settings body (D-155) |
|
||||||
|
| `S-17-install-failed` | error | banner `Failed(Install(detail))` with the installer's message (D-155) |
|
||||||
|
|
||||||
**Interactions**: "Uppdatera" → `install()`; after the system setting, returning to the app continues; verified download → system installer (out of the app's hands). "Senare" hides the banner until the next cold start. Offline: "Uppdatera" disabled with "Kräver anslutning". Debug builds never show it (AC-16.5).
|
**Interactions**: "Uppdatera" → `install()`; after the system setting, returning to the app continues; verified download → system installer (out of the app's hands). "Senare" hides the banner until the next cold start. Offline: "Uppdatera" disabled with "Kräver anslutning". Debug builds never show it (AC-16.5).
|
||||||
|
|
||||||
@@ -1207,8 +1209,9 @@ Not a screen: the `UpdateBanner` on S-4 (and S-22) and one dialog.
|
|||||||
| Frame | State |
|
| Frame | State |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `S-22-about` | loaded |
|
| `S-22-about` | loaded |
|
||||||
|
| `s22_about_install_blocked` | the `UpdateBanner` `Failed(Blocked)` variant above the facts (D-155) |
|
||||||
|
|
||||||
**Interactions**: "Sök efter uppdatering" → `check()` with progress in the button; result: the banner appears, or snackbar "Du har den senaste versionen"; network → "Kunde inte nå servern".
|
**Interactions**: "Sök efter uppdatering" → `checkNow()` (D-140; skips the 6-hour throttle) with progress in the button; result: the banner appears, or snackbar "Du har den senaste versionen"; network → "Kunde inte nå servern".
|
||||||
|
|
||||||
### Notifications (`:app` builds them, copy in `:core:design`)
|
### Notifications (`:app` builds them, copy in `:core:design`)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user