SPEC v0.30 and v0.31: manual update check (D-140), Android Insights (D-141 to D-143)
Build Kvitt images / build (push) Failing after 3h14m40s
Build Kvitt images / build (push) Failing after 3h14m40s
v0.30: D-140 S-22 "Check for update" calls UpdateRepository.checkNow(), which always asks
GET /apk/latest and restarts the 6-hour window (AC-16.2, AC-16.6, S-22, §14.2.4).
v0.31: D-141 InsightsRepository UI contract (§14.2.4); D-142 Insights placement and loading
on S-11; D-143 display rules; design-spec S-11 "Insights" and §3.10; strings.csv 588 rows
(16 analytics_insights_* keys), web dictionaries regenerated; §11.2 W0 and W6 landed.
HANDOFF: state at 1c9856b, Pi image deploy step, numbering (D-144..D-155 reserved, next v0.32).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Kvitt: specification
|
||||
|
||||
Version **0.29** (2026-10-21). Schema version **3**. Sync protocol version **1**. Push payload version **1**.
|
||||
Version **0.31** (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.
|
||||
|
||||
@@ -257,7 +257,7 @@ The server starts with an empty database. The household is created from the comm
|
||||
|
||||
### FR-14 Analytics
|
||||
|
||||
* AC-14.1 Analytics (S-11, S-12) are computed on the phone from Room with the pure function of §14.5.4. They work offline and include pending local changes. Revised by D-133: the advanced analytics (per-category averages and trends per month, the two-month forecast per account) come from the server's `/analytics/*` endpoints (E-34 trends, E-35 forecast, rules §14.5.7, D-138), online only (§9.9), shown on the web (W-11) and in the app's Insights section (W6) (§15.8); the web client computes S-11 and S-12's equivalents in the browser with the same functions (§15.5).
|
||||
* AC-14.1 Analytics (S-11, S-12) are computed on the phone from Room with the pure function of §14.5.4. They work offline and include pending local changes. Revised by D-133: the advanced analytics (per-category averages and trends per month, the two-month forecast per account) come from the server's `/analytics/*` endpoints (E-34 trends, E-35 forecast, rules §14.5.7, D-138), online only (§9.9), shown on the web (W-11) and in the app's Insights section on S-11 (W6, as built D-141 to D-143) (§15.8); the web client computes S-11 and S-12's equivalents in the browser with the same functions (§15.5).
|
||||
* AC-14.2 Range presets: `this_month`, `last_3_months`, `last_6_months`, `last_12_months`, `this_year`, `all_time`, `custom` (start and end date pickers). "Last N months" means the current month and the N-1 months before it, whole months. Default `last_6_months`.
|
||||
* AC-14.3 Granularity: `month` or `week` (ISO, Monday start). Default `month`.
|
||||
* AC-14.4 Measure: `household` (sum of full expense amounts, whoever paid) or `my_share` (sum of the viewer's shares). Default `household`.
|
||||
@@ -281,11 +281,11 @@ The server starts with an empty database. The household is created from the comm
|
||||
### FR-16 In-app update
|
||||
|
||||
* AC-16.1 The server hosts the latest signed release APK and its metadata (§10.6).
|
||||
* AC-16.2 Release builds call `GET /apk/latest` on every cold start and at most once per 6 hours while resumed. When `version_code` is greater than the installed one, Home shows an "Update available" banner with the version name.
|
||||
* AC-16.2 Release builds call `GET /apk/latest` on every cold start and at most once per 6 hours while resumed (the manual check of AC-16.6 skips this throttle, D-140). When `version_code` is greater than the installed one, Home shows an "Update available" banner with the version name.
|
||||
* AC-16.3 "Update" downloads the APK to the app's cache, verifies the size and SHA-256 from the metadata, and installs through `PackageInstaller`. The first time, Android asks the user to allow "Install unknown apps" for Kvitt; the app explains this beforehand: `UpdateRepository` emits `UpdateState.NeedsInstallPermission` once (the explanation-shown flag in `prefs.preferences_pb`, §14.2.2), and the next `install()` proceeds to the system prompt.
|
||||
* 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.6 "Check for update" on S-22 calls `UpdateRepository.check()`, 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).
|
||||
* 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).
|
||||
|
||||
### FR-17 Splitwise import
|
||||
|
||||
@@ -1341,13 +1341,13 @@ After M1, M2a and M2b land, these run in parallel without touching each other's
|
||||
| M9 | Splitwise importer | Landed `50339ae` (D-044 to D-046). `kvitt-import` per §10.8 with tests over synthetic CSV fixtures (`crates/kvitt-import/tests/import.rs`), reconciliation against the totals rows, the committed category table | Server gate | Provide the CSV files (kept outside git) when OQ-2 is answered | M2a, M4 |
|
||||
| M10 | First release | Release build signed, published, both phones paired, push working both ways, nightly backup on Drive, restore drill report, memory check | `docs/validation/first-release.md` and `docs/validation/restore-drill.md` complete; server RSS under 100 MB | Create the release keystore with the `keytool` command of §10.6 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 |
|
||||
| W0 | Web foundation (D-131, D-134) | Running (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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| W6 | Android: Insights | Planned. 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 |
|
||||
|
||||
### 11.3 Notes per milestone
|
||||
@@ -1447,7 +1447,7 @@ Each screen must be designed in these states where they apply: **loaded**, **emp
|
||||
| S-8 | Expense detail | Amount, description, category, account, date, payer, each user's share and the split mode, notes, "Recurring: <template>" when generated; timeline (AC-12.4, AC-12.5); "deleted" state when opened from a notification of a deletion; "missing" state (not yet on this phone) with "Synka nu" (`expense_detail_sync_now`), online only (§9.9, D-080); after a delete `:app` shows `snackbar_expense_deleted` on the screen returned to (D-095) | Edit; Delete |
|
||||
| S-9 | Settle up | Direction ("Anna pays Jacob" with a swap control); amount; allocation list: account name, account debt, editable allocated amount, over-settle warning per row (AC-11.5); remaining-to-allocate indicator; "Add account"; date; note; per-account variant with a single fixed row, which keeps the direction swap (§9.4: per-account settle-ups go either way) and an editable amount (AC-11.4 "defaults to") (D-052); when editing, the saved rows count as edited and "Föreslå igen" re-proposes; the info line on untouched accounts reads `SettleState.untouched` (nets without the settlement being edited, empty for a per-account settle-up, D-093, revising D-074's reading of Home balances); the discard dialog has no body text (D-080) | Confirm; Cancel |
|
||||
| S-10 | Settlement detail | Payer, recipient, amount, date, note, allocations per account; timeline; "missing" state with "Synka nu" (`settle_detail_sync_now`), online only (§9.9, D-080) | Edit; Delete |
|
||||
| S-11 | Analytics | Range selector (presets of AC-14.2 plus custom); granularity toggle month/week; measure toggle household / my share; group-by toggle category/account; total for range; chart 1 stacked bars over time with legend; chart 2 category breakdown; chart 3 account breakdown; empty state ("No expenses in this period"); a chart whose range has no expenses shows only the 0 baseline, with no gridline values (D-068). Total card (D-069): caption "<range> · Hushållet" for the household measure and "<range> · N veckor" in week granularity; for my share the label is "Totalt · Min del" so the caption does not repeat it; month presets show whole-month ranges ("maj–okt 2026"), custom ranges and week granularity show days (§9.8, D-077). Legend and stacking order: categories by `sortOrder`, accounts in `AccountRepository.all()` order (`AnalyticsState.groups` order is unspecified, §14.2.4). Loading means `SyncSummary.lastSuccessAt == null` and shows skeleton cards; offline looks like loaded; no error state (analytics are local, AC-14.1) | Change filters; tap a bar or a row to open S-12 |
|
||||
| S-11 | Analytics | Range selector (presets of AC-14.2 plus custom); granularity toggle month/week; measure toggle household / my share; group-by toggle category/account; total for range; chart 1 stacked bars over time with legend; chart 2 category breakdown; chart 3 account breakdown; empty state ("No expenses in this period"); a chart whose range has no expenses shows only the 0 baseline, with no gridline values (D-068). Total card (D-069): caption "<range> · Hushållet" for the household measure and "<range> · N veckor" in week granularity; for my share the label is "Totalt · Min del" so the caption does not repeat it; month presets show whole-month ranges ("maj–okt 2026"), custom ranges and week granularity show days (§9.8, D-077). Legend and stacking order: categories by `sortOrder`, accounts in `AccountRepository.all()` order (`AnalyticsState.groups` order is unspecified, §14.2.4). Loading means `SyncSummary.lastSuccessAt == null` and shows skeleton cards; offline looks like loaded; no error state (analytics are local, AC-14.1). Insights section (D-142, D-143), online only. | Change filters; tap a bar or a row to open S-12; pick a category in Insights; "Försök igen" |
|
||||
| S-12 | Analytics drill-down | Title naming the filter ("Mat & hushåll, oktober 2026"; a week as in `docs/ux/strings.csv`, "Vardag, vecka 41"; a category or account over the whole range "<name>, <range caption>") (D-070); total; expense rows as in S-5. A category or account over the range adds, above the total, its own range selector (AC-14.2 presets plus custom) and month/week toggle, both starting from S-11's values and changing S-12 only; below the total, the line chart of AC-14.11; then the rows. A range with no expenses reads "No expenses in this period" (D-128). No loading and no error state (D-070) | Open expense; change range or granularity |
|
||||
| S-13 | Settings hub | Own avatar and name; partner's avatar and name; rows: Profile, Categories, Recurring expenses, Devices, Invite partner or Pair a device (admin), Sync status, About | Navigate |
|
||||
| S-14 | Profile | Avatar with change/remove; display name; "Needs connection" state | Save |
|
||||
@@ -1458,7 +1458,7 @@ Each screen must be designed in these states where they apply: **loaded**, **emp
|
||||
| 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-21 | Invites | For the admin: "Invite partner" (only before the partner joins) and "Pair another device" for either user; a created invite shows the code in groups of 4, the link, expiry, and a share action (Android share sheet); list of pending invites with cancel | Create; Share; Cancel |
|
||||
| S-22 | About | App version name and code, server URL (the paired server; before pairing the build's release server, `AppBuildInfo.releaseServerUrl`, D-122), server version from E-1 `/health`, fetched when S-22 opens while online, shown as "<version> (schema <n>)" (`settings_about_server_version_schema`, "1.1.0 (schema 3)"); offline or when the call fails, the `X-Kvitt-Server-Version` header of the last response without the schema (`AboutState`, §14.2.4, D-110); no open-source licences row (OQ-5, D-099); after "Check for update": `UpdateCheck.UpToDate` shows "Du har den senaste versionen" (`settings_about_up_to_date`), `UpdateCheck.Failed` shows "Kunde inte nå servern" (`common_could_not_reach_server`) (AC-16.6, D-092) | Check for update |
|
||||
| S-22 | About | App version name and code, server URL (the paired server; before pairing the build's release server, `AppBuildInfo.releaseServerUrl`, D-122), server version from E-1 `/health`, fetched when S-22 opens while online, shown as "<version> (schema <n>)" (`settings_about_server_version_schema`, "1.1.0 (schema 3)"); offline or when the call fails, the `X-Kvitt-Server-Version` header of the last response without the schema (`AboutState`, §14.2.4, D-110); no open-source licences row (OQ-5, D-099); after "Check for update": `UpdateCheck.UpToDate` shows "Du har den senaste versionen" (`settings_about_up_to_date`), `UpdateCheck.Failed` shows "Kunde inte nå servern" (`common_could_not_reach_server`) (AC-16.6, D-092); the button calls `UpdateRepository.checkNow()`, which always asks the server (D-140) | Check for update |
|
||||
|
||||
System notifications (copy needed in both languages, built per AC-13.5):
|
||||
|
||||
@@ -1502,7 +1502,7 @@ Suggested starting points the UX agent may use, replace or merge (it must delive
|
||||
2. **"Skärgård"**: Material 3 Expressive with a cool sea-and-granite palette, large rounded shapes, bold balance header, playful but quiet.
|
||||
3. **"Kontor"**: dense, utilitarian, data-forward. Neutral greys, strong typographic hierarchy, charts as the hero on Analytics, minimal decoration.
|
||||
|
||||
**Phase 2 (U2): design spec for the chosen direction.** After the owner picks (and comments), in `docs/ux/`. The owner picked direction 3 "Kalas" with the muted palette of `docs/ux/directions/3-kalas/`, typeface per D-020 (D-047). U2 landed `6da6b1b`, approved by the owner. `docs/ux/design-spec.md` is the binding design document for `:core:design` (§2 tokens, §3 components) and for every screen (§4, §5); this spec wins over it on rules. Amended after U2: chart bars have top corners 4 dp and bottom corners 2 dp (D-097, design-spec §2.9 and §3.10); S-7's account, date, payer and split options are four fixed rows, `EditorOptionRows`, and connected buttons have 8 dp side padding (D-102, design-spec §3.7 and §3.10); S-22 has no licences row (D-099, design-spec S-22); S-16's lost version (D-109) and S-22's "(schema <n>)" (D-110), design-spec S-16 and S-22; the mock-up PNGs are compressed losslessly (D-098). `docs/ux/strings.csv` (558 rows at U2, header included; 559 since v0.10 with `ledger_settlement_label`, D-068, and English "Dining" for `category_default_restaurants`, D-067; 578 since v0.11 with the shared keys `common_ok`, `common_undo` and `editor_split_full_on_you` and the module keys of M7b to M7f recorded as built, D-079; 577 since v0.16 without `settings_about_licences`, D-099; 562 since v0.18: M12f added `home_sync_lost_title`, `home_sync_lost_body`, `home_sync_lost_deleted`, `home_sync_lost_show_current` (D-109) and `settings_about_server_version_schema` (D-110) and removed the 19 keys (20 rows) that R8 found unused (`a0259ab`); 569 since v0.27 with the seven S-12 line-chart keys `analytics_average`, `analytics_moving_average`, `analytics_vs_average_above`, `analytics_vs_average_below`, `analytics_vs_average_on`, `analytics_summary_line`, `analytics_summary_line_single` (D-128); keys with the D-017 prefixes; plurals as `key:one` / `key:other`) is the source of every string resource:
|
||||
**Phase 2 (U2): design spec for the chosen direction.** After the owner picks (and comments), in `docs/ux/`. The owner picked direction 3 "Kalas" with the muted palette of `docs/ux/directions/3-kalas/`, typeface per D-020 (D-047). U2 landed `6da6b1b`, approved by the owner. `docs/ux/design-spec.md` is the binding design document for `:core:design` (§2 tokens, §3 components) and for every screen (§4, §5); this spec wins over it on rules. Amended after U2: chart bars have top corners 4 dp and bottom corners 2 dp (D-097, design-spec §2.9 and §3.10); S-7's account, date, payer and split options are four fixed rows, `EditorOptionRows`, and connected buttons have 8 dp side padding (D-102, design-spec §3.7 and §3.10); S-22 has no licences row (D-099, design-spec S-22); S-16's lost version (D-109) and S-22's "(schema <n>)" (D-110), design-spec S-16 and S-22; S-11's Insights section (D-142, D-143), design-spec S-11 and §3.10; the mock-up PNGs are compressed losslessly (D-098). `docs/ux/strings.csv` (558 rows at U2, header included; 559 since v0.10 with `ledger_settlement_label`, D-068, and English "Dining" for `category_default_restaurants`, D-067; 578 since v0.11 with the shared keys `common_ok`, `common_undo` and `editor_split_full_on_you` and the module keys of M7b to M7f recorded as built, D-079; 577 since v0.16 without `settings_about_licences`, D-099; 562 since v0.18: M12f added `home_sync_lost_title`, `home_sync_lost_body`, `home_sync_lost_deleted`, `home_sync_lost_show_current` (D-109) and `settings_about_server_version_schema` (D-110) and removed the 19 keys (20 rows) that R8 found unused (`a0259ab`); 569 since v0.27 with the seven S-12 line-chart keys `analytics_average`, `analytics_moving_average`, `analytics_vs_average_above`, `analytics_vs_average_below`, `analytics_vs_average_on`, `analytics_summary_line`, `analytics_summary_line_single` (D-128); 588 since v0.31 with the 16 S-11 Insights keys `analytics_insights_*`, three of them plurals (19 rows, D-142, D-143); keys with the D-017 prefixes; plurals as `key:one` / `key:other`) is the source of every string resource:
|
||||
|
||||
* `design-spec.md`: final tokens (colour roles light and dark, category colour hex values, typography scale, shapes, spacing scale in dp, elevation, motion durations and easing), component inventory (each with anatomy, states, sizes), and for every screen S-1 to S-22: layout description, all states of §13.3, interaction details, TalkBack labels.
|
||||
* `strings.csv`: every user-visible string of the app with columns `key,en,sv,notes`, keys in `snake_case` prefixed by the feature module that holds them (`home_balance_owes_you`, `expense_save`; shared strings for `:core:design` never start with a feature name; the app name is the only `:app` string; §6.2, D-017), placeholders as Android format arguments (`%1$s`), plurals marked in `notes` with their quantities. This becomes `values/strings.xml` and `values-sv/strings.xml` in M6 and M7; feature agents add no strings that are not in this file without a decisions entry. Keys recorded by D-079 (v0.11): shared `common_ok`, `common_undo`, `editor_split_full_on_you` ("All on you" / "Allt på dig"; `ledger_settlement_label` was D-068); module keys as built: `analytics_granularity`, `analytics_measure`, `analytics_group_by`, `analytics_range_apply`; `settle_row_amount`, `settle_detail_delete`, `settle_detail_sync_now`; `expense_detail_sync_now`; `home_archived_expanded`, `home_archived_collapsed`; `settings_category_archived_snackbar`, `settings_category_unarchived_snackbar`, `settings_recurring_summary_none`, `settings_expanded`, `settings_collapsed`, `settings_partner_missing`.
|
||||
@@ -2046,6 +2046,20 @@ interface AnalyticsRepository {
|
||||
fun drillDown(filter: AnalyticsFilter, bucketKey: String?, categoryId: CategoryId?, accountId: AccountId?): Flow<DrillDownState>
|
||||
}
|
||||
|
||||
// Insights (S-11, v0.31, D-141): model/Insights.kt; the server's E-34 trends and E-35 forecast, online only (§9.9), nothing stored.
|
||||
// Months "YYYY-MM" (Europe/Stockholm), amounts in öre for the requested measure, named after the E-34/E-35 fields (§14.5.7).
|
||||
data class TrendPoint(val month: String, val amountOre: Long, val movingAverageOre: Long)
|
||||
data class SpendTrend(val totalOre: Long, val averageOre: Long, val lastMonthOre: Long, val lastMonthVsAveragePct: Int?, val currentMonthOre: Long, val points: List<TrendPoint>)
|
||||
data class CategoryTrend(val categoryId: CategoryId, val category: Category?, val trend: SpendTrend) // category from Room, archived and deleted included; null when unknown
|
||||
data class SpendTrends(val measure: Measure, val today: String, val currentMonth: String, val months: Int, val household: SpendTrend, val categories: List<CategoryTrend>) // categories in the server's order
|
||||
data class ForecastMonth(val month: String, val recurringOre: Long, val baselineOre: Long, val forecastOre: Long)
|
||||
data class AccountForecast(val accountId: AccountId, val account: Account?, val currentMonthOre: Long, val months: List<ForecastMonth>) // account from Room, null when unknown
|
||||
data class SpendForecast(val measure: Measure, val today: String, val currentMonth: String, val accounts: List<AccountForecast>) // accounts in the server's order
|
||||
interface InsightsRepository {
|
||||
suspend fun trends(months: Int, measure: Measure): OnlineResult<SpendTrends> // months 1..24
|
||||
suspend fun forecast(measure: Measure): OnlineResult<SpendForecast>
|
||||
}
|
||||
|
||||
// Settings (S-13 to S-15, S-19 to S-22)
|
||||
interface ProfileRepository {
|
||||
fun household(): Flow<List<User>>
|
||||
@@ -2108,6 +2122,7 @@ sealed interface UpdateCheck { // v0.13, D-092
|
||||
interface UpdateRepository {
|
||||
val state: Flow<UpdateState>
|
||||
suspend fun check(): UpdateCheck // v0.13, D-092
|
||||
suspend fun checkNow(): UpdateCheck = check() // v0.30, D-140: S-22's button; skips the 6-hour throttle and restarts it
|
||||
suspend fun install()
|
||||
}
|
||||
data class AboutState(val versionName: String, val versionCode: Int, val serverUrl: String, val serverVersion: String?, val schemaVersion: Int?) // schemaVersion: v0.18, D-110
|
||||
@@ -2153,6 +2168,11 @@ Readings fixed in v0.10 (D-060 to D-062, as landed by M12d `557af7e`; no type ch
|
||||
* **`frequentCategoryIds` keys and ties (D-061).** The map has a key for every active account (an empty list when nothing was used there) and no key for an archived account. Ties: use count descending, then `sort_order` ascending, then category id ascending, so Room and the fakes give the identical order.
|
||||
* **`deletable` edge cases (D-062).** `CategoryRepository.deletable(id)` emits false for an unknown or already deleted id. Archived categories follow the same AC-7.6 rule as active ones. In `:core:fakes`, `delete` and `deletable` share one rule function, so a fake never offers a delete it then refuses.
|
||||
|
||||
Changes in v0.30 and v0.31 (D-140, `c0f4345`; D-141, `1c9856b`):
|
||||
|
||||
* **`UpdateRepository.checkNow()` (D-140).** S-22's "Check for update" calls it: it always asks E-5, whatever the 6-hour throttle of `check()` says, and restarts that window; the automatic checks on cold start and resume keep calling `check()`. The default implementation calls `check()`, so the fakes are unchanged; debug builds return `UpToDate` without a request (AC-16.5). Refines AC-16.2 and AC-16.6.
|
||||
* **`InsightsRepository` (D-141).** `trends(months: 1..24, measure)`, `forecast(measure)`; online only (§9.9): `Offline` is returned without a request, `Failed("network", null)` when the server cannot be reached, else the §14.3.2 code. Category and account resolved from Room, archived and deleted rows included, null when unknown; nothing is stored. `:data`: `api/AnalyticsDtos.kt`, two `KvittApi` calls, `repo/InsightsRepositoryImpl.kt` (through the online gate), bound in `DataModule`; `ExampleRoundTripTest` round-trips the E-34 and E-35 examples. `:core:fakes`: `FakeInsightsRepository`, `analytics/InsightsScenarios.kt`, scenario flag `insightsFailure`. S-11's section and its rules: §13.3 S-11, D-142, D-143.
|
||||
|
||||
Readings fixed in v0.11 (D-069, D-073 to D-075, as landed by M7a to M7f and M6b; no type changes):
|
||||
|
||||
* **`AnalyticsState.groups` order (D-069).** Unspecified; the UI does not rely on it. S-11 sorts legends and stacks itself: categories by `sortOrder`, accounts in `AccountRepository.all()` order.
|
||||
@@ -3046,7 +3066,7 @@ Navigation (W0): a sidebar from 720 px wide and a bottom bar under it, with Home
|
||||
|
||||
* Owner, 2026-10-21: "These calculations should be put in the backend because I will want to see this in the app as well." New read endpoints under `/analytics/*` (authenticated, any device), designed by W7: per-category averages and trends per month, overall and per category; and a forecast of the next 2 calendar months per account. As built (v0.29, D-138): E-34 `GET /analytics/trends` (averages and trends, household and per category; the two endpoints merged into one) and E-35 `GET /analytics/forecast` (§7, §14.3.4); averaging, rounding and month rules §14.5.7.
|
||||
* **Forecast rule** (settled): for each account and each of the next two calendar months, forecast = the sum of the account's recurring templates due in that month (the known future expenses: active, not paused, inside their start and end periods, §9.5) + the average per month of the account's non-recurring spend (expenses not generated from a template) over the last 3 whole months. The 3-month window is the constant of D-128's trailing moving average (`AnalyticsRules.MOVING_AVERAGE_WINDOW`, 3, counted in buckets there and in whole months here), shared, not a second number; the server holds its own copy of the value and a change to it changes both.
|
||||
* AC-14.1 is revised: the phone keeps computing S-11 and S-12 offline from Room; the advanced analytics come from the server, online only (§9.9), on the web (W-11) and in the app (W6, an "Insights" section on S-11).
|
||||
* AC-14.1 is revised: the phone keeps computing S-11 and S-12 offline from Room; the advanced analytics come from the server, online only (§9.9), on the web (W-11) and in the app (W6, an "Insights" section on S-11; as built `1c9856b`, D-141 to D-143, v0.31).
|
||||
|
||||
### 15.9 Plans and parallel work (D-134)
|
||||
|
||||
@@ -3099,3 +3119,5 @@ Plans with disjoint paths (§14.7), one Opus agent each in its own worktree. W0
|
||||
| 0.27 | 2026-10-21 | S-12 line chart for a category or account over the range (D-128, owner request 2026-10-21; Android `12e2508`). S-12 opened from a breakdown row shows its own period chip (AC-14.2 presets plus custom) and Month/Week toggle, both starting from S-11's values and changing chart, total and list on S-12 only (S-11 keeps its state; measure and group-by come from S-11 and are fixed on S-12), the header total, the "Utgifter över tid" card and the expense rows; a drill-down from a bar is unchanged. One point per bucket, empty buckets as zero (AC-14.7), from the group's bucket totals of `spendSeries` (§14.5.4); an empty range shows only the 0 baseline (D-068) and "Inga utgifter i den här perioden". Overlays: the range average (total ÷ buckets, half-up to öre) as a dashed line in the group colour with its amount at the right end; a trailing moving average over 3 buckets (`AnalyticsRules.MOVING_AVERAGE_WINDOW`; the first buckets use the 1 or 2 that exist) as a thinner neutral line; legend "Snitt", "Glidande snitt (3)"; caption "<Bucket>: N % över snittet" / "under snittet" / "på snittet" for the latest bucket the range does not cut short, N a whole percent half-up. Overlays and caption omitted when the average is 0 or the range has one bucket; caption omitted when no bucket is whole. Not interactive; one TalkBack summary. `:core:design` gains `SpendLineChart` and `LineLegend` (`chart/LineChart.kt`), axis code shared with `StackedBarChart` in `Charts.kt`; `AnalyticsFilterBar` measure and group-by toggles optional. Seven `strings.csv` keys (569 rows). New goldens `s12_category_groceries`, `s12_category_week`, `s12_category_empty_range`, `line_chart`, `line_chart_variants`; `s12_category`, `s12_account`, `s12_long` re-recorded; 977 Android tests. No change to `:core:model`, `:domain`, `:core:fakes`, Room, the server or the contracts (new AC-14.11; §13.3 S-12, §13.4; design-spec §3.10, S-12). |
|
||||
| 0.28 | 2026-10-21 | The web client programme (owner interview 2026-10-21; no implementation yet; v0.27 is D-128's). D-129: a web client with the Android app's features except the phone-only ones (pairing flow, push, offline, in-app update), plus free-text search, CSV export of the filtered list, keyboard shortcuts N, /, Esc, ? and a language choice; desktop first, one column under 720 px, sv and en, not a PWA (§1, §2 revised, new §15, FR-21). D-130: the browser logs in with an invite code through E-3 as device `Web · <browser>`, token in `localStorage`, revocable on Devices; the NPM gate also passes `/app` and `/app/…`, `POST /invites/accept` and bearer-token requests except `/health`, `/setup/`, `/i/` and `/apk/` (§8.5, §10.5, §10.5.2, `infra/README.md` steps 4 and 4a, `infra/npm/kvitt.conf`; checked with nginx 1.31.7, 17 requests). D-131: `web/` stack (Vite, React 19, TypeScript strict, React Router 7, TanStack Query 5, `openapi-typescript` and `openapi-fetch`, Recharts, Vitest, ESLint, Prettier, CSS modules with design-spec tokens, JSON dictionaries), the money functions passing the vectors, the web gate `npm run check` (§11, §15.3). D-132: the web client is built by a Node stage of `server/Dockerfile` (context the repository root) and served by `kvitt-server` under `/app/` from `KVITT_WEB_DIR` with an SPA fallback; immutable hashed assets, `no-store` `index.html`; the `/app/` CSP through `$kvitt_csp` in NPM (§10.3, §10.5.2, §10.6, §15.4). D-133: advanced analytics on the server under `/analytics/*` (per-category averages and trends, a two-month forecast per account = recurring templates due + the 3-month average of non-recurring spend), for the web and the app; AC-14.1 and G-5 revised (§15.8; endpoints D-135 to D-138, v0.29). D-134: plans W0 to W7 with disjoint paths (§11.1, §11.2, §11.3, §14.7, §15.9; `PLAN-W0.md` to `PLAN-W7.md`, `prompts/W0.md` to `prompts/W7.md`). The change log becomes §16. |
|
||||
| 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.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`. |
|
||||
|
||||
+18
-13
@@ -3,16 +3,20 @@
|
||||
Living document, rewritten in place (never a second copy). A new orchestrator session reads CLAUDE.md, then this.
|
||||
|
||||
## 1. Where things stand
|
||||
- State as of 2026-10-21 evening. `main` at `c0f4345` (update-check fix, D-140, its SPEC v0.30 recording pending), after `50a0d67` (W7: web serving under `/app/` and analytics E-34/E-35, D-135 to D-138), `7bddc50` (SPEC v0.28, D-129 to D-134, web client programme) and `70cb25c` (`publish-apk.sh --deploy`, D-139). `origin/main` (Gitea) at `50a0d67`: that push touches `server/**` and `contracts/**`, so it starts a Pi image build that fails at the Node stage until `web/package.json` lands with W0 (expected and harmless; the Pi keeps running `v1.0.1`). This docs pass (`agent-w7-docs`) records W7 and D-139 as SPEC v0.29. Running: W0 (web foundation), W6 (Android Insights), and the docs for the update-check fix (D-140, v0.30). W1 to W5 start after W0 lands.
|
||||
- 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`).
|
||||
- **Kvitt host name** (owner decision 2026-10-19): `kvitt.suorra.duckdns.org`, a subdomain of the owner's DuckDNS family domain `suorra.duckdns.org`; NPM issues its Let's Encrypt certificate; `KVITT_PUBLIC_HOST=kvitt.suorra.duckdns.org`, so `KVITT_PUBLIC_URL=https://kvitt.suorra.duckdns.org` (port 443, D-124). It is in `infra/pi/.env.example` and `infra/pi/.env` (`KVITT_PUBLIC_HOST`) and `android/local.properties` (`kvitt.publicHost`, git-ignored). SPEC §10.1 and §10.5.1 point here.
|
||||
- **NPM proxy host** for `kvitt.suorra.duckdns.org` is Online with a Let's Encrypt certificate (owner, 2026-10-19). The router needs no new rule: external TCP 443 already forwards to the Home Assistant Pi (owner confirmed, D-124).
|
||||
- **Images**: `gitea.lan/jacobh2/kvitt-server` and `gitea.lan/jacobh2/kvitt-backup`, tags `v1.0.0` (run 69) and `v1.0.1` (the sparse-checkout push; the tag the Pi uses), plus `latest`; the packages are public, so the Pi pulls without `docker login`. Gitea Actions on the owner's Pi runner (act_runner 0.6.1, Alpine job container): the first failures were an empty secret (the user-level secret was misnamed on the owner's side, fixed 2026-10-19; a user-level secret of the repository owner reaches the repository); run 69 succeeded: login ok, `v1.0.0`, `kvitt-backup` 5 min 20 s, `kvitt-server` 2 h 42 min on the Pi 3 (first build, `CARGO_BUILD_JOBS=1`; later runs reuse the builder's cargo cache); the tag push works with the `write:package` token. SPEC §10.3.
|
||||
- **Kvitt Pi** (D-125): Kvitt runs from `/home/jacobhagstedt/kvitt` as the owner's user `jacobhagstedt`, uid and gid 1000:1000 (`KVITT_UID`/`KVITT_GID`); Docker and the `gitea.lan` registry trust were already on the Pi; nothing outside that folder is installed or changed. **The stack is running** (owner, 2026-10-21): image `v1.0.1`, `server` and `backup` both healthy after a `chown` of `data/` (it had been created as root); compose's `mem_limit` warning (the Pi kernel has no memory cgroup, so the 160 MiB and 96 MiB limits are not enforced) is accepted. `/health` through NPM from outside answered `"status":"ok"` (public health OK). **Live** (2026-10-21): both phones are paired against it and the Splitwise import (M11) is done.
|
||||
- **App gate** (D-126, owner decision 2026-10-21): outside the home LAN `192.168.1.0/24` NPM closes (`return 444`) every request without `X-Kvitt-App: <key>`, every route included; the key is the build property `kvitt.appKey` (`android/local.properties`, git-ignored), generated by the owner with `openssl rand -hex 32` and pasted into NPM's block (`infra/npm/kvitt.conf`, `7521c01`); the server and `contracts/openapi.yaml` are unchanged. Android side `34e5cad`, docs `2acfc34` (SPEC v0.25, `infra/README.md` step 4a). `android/local.properties` has a `kvitt.appKey` and `infra/pi/out/apk/` holds `kvitt-125.apk` (checked 2026-10-21); whether both phones run a keyed APK and the gate is in NPM is not recorded here (orchestrator confirms with the owner, §3).
|
||||
- **Web client programme** (owner interview 2026-10-21, SPEC v0.28, D-129 to D-134, SPEC §15, FR-21): a React web client in `web/`, served by `kvitt-server` under `/app/` from the server image, with the app's features except the phone-only ones, plus search, CSV export, keyboard shortcuts and a language choice; login with an invite code (device `Web · <browser>`); the NPM gate also passes `/app/`, `POST /invites/accept` and bearer-token requests except `/health`, `/setup/`, `/i/`, `/apk/` (new block in `infra/npm/kvitt.conf`, README step 4, SPEC §10.5.2; checked with nginx 1.31.7, 17 requests); advanced analytics on the server (`/analytics/*`, D-133, forecast = recurring templates due + 3-month average of non-recurring spend). Plans `PLAN-W0.md` to `PLAN-W7.md`, prompts `prompts/W0.md` to `prompts/W7.md`. W7 landed `50a0d67` (D-135 to D-138, SPEC v0.29: `/app/` from `KVITT_WEB_DIR`, E-34 `GET /analytics/trends`, E-35 `GET /analytics/forecast`, rules SPEC §14.5.7, the Node stage and the repository-root build context); W0 (`web/**`) is running; W1 to W5 start in parallel when W0 has landed (each in its own `web/src/features/<name>/`); W6 (Android Insights) is running; it shows on the phones only once an image with W7 (which needs W0) is deployed. The web gate is `npm run check` in `web/` (third gate; `CLAUDE.md` "Build and verify" still to be updated by the orchestrator).
|
||||
- **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`).
|
||||
- **First release APK**: `apk/kvitt-123.apk` (10.0 MB, git-ignored by `*.apk`) handed to the owner from the main checkout for sideloading on both phones (both paired, 2026-10-21); it was built before any `kvitt.appKey` existed, so it sends no `X-Kvitt-App`: it works only until the gate is pasted (on the home Wi-Fi it keeps working afterwards). The phones need a keyed APK before the gate goes in (§3).
|
||||
- History rewrite and first push, 2026-10-18 (owner authorised): superseded PNG versions were dropped from the whole history (no LFS), the current goldens and UX mock-ups restored in `18c527f`; pack 45 MiB; `origin/main` pushed at `18c527f`. A bundle of the pre-rewrite history (`kvitt-pre-rewrite.bundle`) is kept outside the repository, in the orchestrator's scratchpad. Every commit from M2b on got a new hash; the oxipng pass (`7c453d8` before the rewrite) no longer exists as a commit (it only touched PNGs). SPEC, `docs/decisions.md`, this file and `prompts/README.md` cite the rewritten hashes since v0.18 (`7c453d8` stays as the oxipng pass's name; `docs/validation/ac-coverage.md` keeps its pre-rewrite hashes verbatim); since v0.19 the plans cite rewritten hashes too (eight pre-rewrite hashes in `PLAN-*.md` replaced by the commit with the same subject); `docs/spikes/pi-image-arm64.md` and the READMEs had none stale. For any other old hash, find the commit by its subject with `git log --oneline --grep`.
|
||||
- History (rewritten hashes): `5e1be38` plans and prompts, `c885aba` M2c-dev, `76ee957` SPEC v0.3, `c277ee3` M2b, `06f48ef` M2a, `6dce4c7` SPEC v0.4, `7869ec4` M3, `8f0e156` M5, `853c038` SPEC v0.5, `15056dc` M4, `d9dd584` SPEC v0.6, `50339ae` M9, `86e08a1` SPEC v0.7, `aa765bb` M12a, `d39eb2c` SPEC v0.8, `b09f322` M12c, `6eaccec` M12b, `0006bd7` UX Kalas categories and tokens, `6da6b1b` UX sprint U2, `e676812` SPEC v0.9, `557af7e` M12d, `7730a31` M6, `9ee67d4` SPEC v0.10, `a41f240` and `6453932` app wiring before M8, `1ea752c` M7a, `ea7d494` M7e, `7380e50` M7d, `0e1d99d` M7b, `0b140aa` M7c, `c4ab35f` M7f, `b24c8b5` M6b, `f4dc10c` SPEC v0.11, `3dda414` M7g, `cb832a7` and `3d9eea8` M8, `6159e18` SPEC v0.12, `8c60715` M12e, `b7b7483` M12e launcher icon, `51d6ff8` SPEC v0.13, `a312758` Vico removal, `f56dcbe` FCM `fid`, `3e6c337` Pi image arm64 check, `e2dba84` SPEC v0.14, `328c466` M8b, `6eb3f85` `FcmMessageBody.fid`, `bfde12c` SPEC v0.15, `75b549f` M8c goldens, `a0c25b0` M8c licences row, `c30a49e` R8 release build, `bacbd18` M8d, `bfc3acb` SPEC v0.16, `2476667` M2c-pi bundle, `a59abeb` M8e, `b4d9467` Dockerfile `CARGO_BUILD_JOBS`, `956908a` M8f, `1328afe` chips cleanup, `400eac9` SPEC v0.17, `18c527f` goldens restored after the rewrite; then `2fac1ec` to `e70fe0d` infra overhaul, `8660b7f` server gap pass, `7fb86f5` infra security, `dc7a296` to `a0259ab` M12f, `8103dae` to `b985ce9` server overhaul, `48fe119` SPEC v0.18, `e4e6e5d` to `c582363` Android overhaul, `3ef2c15` SPEC v0.19, `96e4cbd` APK download rate limit (D-119), `481889a` SPEC v0.20, `f1abb0d` `cargo audit` in the server gate, `4f69ae0` Gitea login in the run line, `d05f13a` Pi stack behind NPM, `62f5279` and `9231a1d` infra README for NPM (D-120), `d090c6c` Android host property (D-121), `4bcff2c` SPEC v0.21, `dd07ca4` temporary workflow diagnostic step, `4a4e45f` S-22's release server and `4b1ae0c` `publish-apk.sh` host (D-122), `c9abd24` and `e381e37` server host defaults and OpenAPI `{host}` (D-123), `ac17b41` `install.sh` step number, `cce3e4e` SPEC v0.22, `a86ad24` server and contracts on port 443, `4852803` bundle on port 443 (D-124), `038c603` Android on port 443 and the pinned-root template, `ad35231` portless Android fixtures, `b1f4c10` SPEC v0.23, `3820f9c` sparse-checkout workflow, `200c99f` `.env` without informational keys, `6e5981d` self-contained Pi compose project (D-125), `28fa71b` compose deploy hint in the workflow and `infra/build-images.sh`.
|
||||
- Landed since SPEC v0.29 (recorded in v0.30 and v0.31):
|
||||
- Update-check fix (`c0f4345`, D-140, v0.30): S-22 "Check for update" calls `UpdateRepository.checkNow()`, which always asks `GET /apk/latest` and restarts the 6-hour window; the automatic checks keep the throttle (AC-16.2, AC-16.6).
|
||||
- W6 Android Insights (`1c9856b`, D-141 to D-143, v0.31): `InsightsRepository` (E-34 trends, E-35 forecast, online only, names from Room); an "Insikter" section below S-11's local analytics following the measure toggle, 6 whole months, household and per-category line charts with the server's average and moving average, a forecast card per account ("Återkommande + snitt = prognos"); offline and error banners over the last data; 40 `s11_insights_*` goldens; Android gate green (791 tests in the feature run). `strings.csv` 588 rows (16 `analytics_insights_*` keys); the web dictionaries regenerated.
|
||||
- W0 web foundation (`2ae65a5`) and `CLAUDE.md` with the web client and three gates (`415f9f5`); W0's report has no decisions recorded here.
|
||||
- Landed since SPEC v0.28 (recorded in v0.29):
|
||||
- W7 server web serving and analytics (`50a0d67`, D-135 to D-138): `KVITT_WEB_DIR` (default `/app`) served under `/app/` (308 from `/app`, SPA fallback, missing `assets/*` 404, `assets/*` cached a year immutable, `index.html` `no-store`, the `$kvitt_csp` web policy plus `nosniff`, `DENY`, `no-referrer`, strict path allowlist); `/app` paths on the unauthenticated 60/min per IP (D-136); image build from the repository root with an allowlist `.dockerignore`, a Node 24 LTS stage pinned by digest passed through the Rust stage (D-137); E-34 trends and E-35 forecast with examples, rules in `kvitt-domain::analytics` (D-138); `Cargo.lock` `rustls-webpki` 0.103.16, `webpki-root-certs` 1.0.10 (RUSTSEC-2026-0382, -0383). 299 server tests; contracts-check 136.
|
||||
- Unattended APK deploy (`70cb25c`, D-139): `infra/publish-apk.sh --deploy` copies the APK, then `latest.json`, with the forced-command key `infra/secrets/pi_deploy_ed25519` and checks `/apk/latest` with `X-Kvitt-App`; first run 2026-10-21 served `version_code` 133.
|
||||
@@ -53,23 +57,23 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
|
||||
- Validation reports (SPEC §11.4): `docs/validation/ac-coverage.md` (taken at `956908a`, `ed1b130` before the rewrite): 270 rows, 260 Done, 5 Partial, 1 Missing, 4 device-only; feature complete in code, not yet against §4's acceptance rule; its S-16 and S-22 mismatches are fixed by M12f. `docs/validation/security-review.md` (taken at `18c527f`): Critical 0, High 1, Medium 3, Low 10, Info 8; L-6 kept (D-116), L-7 fixed (D-116), L-1, L-2, L-5, L-9, M-2 fixed (D-117), L-8 fixed (`e70fe0d`); H-1 (`725901c`, D-115), M-3 (`baa5806`) and L-4 (`b985ce9`, D-114) fixed by the server overhaul; L-10 left as the spec says, decision pending (§4). Owner answers of 2026-10-19: M-1 no (backups stay unencrypted, rclone keeps the `drive` scope; accepted risk in SPEC §8.1 and §8.7), L-3 accepted and fixed (`96e4cbd`, D-119), the 16 MP and 512-character limits (D-114) confirmed; M-2's remaining question (the Home Assistant SSH key form) is moot with the NPM rework (D-120) and closed; D-120 also moves L-1 and the headers to NPM, makes L-2 moot, reverses the L-8 fix and replaces I-5's check with a conntrack check (dated notes in the review, SPEC §11.4). H-1 is fixed (`725901c`).
|
||||
- Server overhaul (landed on `main` `8103dae` to `b985ce9`): shared helpers and one test harness, clippy pedantic wins; H-1 rate limiter (`725901c`, D-115), pictures at most 16 MP and 64 MB decoded (`baa5806`, D-114), `fcm_token` at most 512 characters (`b985ce9`, D-114); shared row inserts, invite creation and CLI helpers, `tests/common/` harness, synchronous `tasks::start`; 268 server tests; all crates at their latest versions. Recorded in SPEC v0.18.
|
||||
- In flight:
|
||||
- This docs pass (`agent-w7-docs`: SPEC v0.29, D-135 to D-139, `infra/README.md` build context, this file).
|
||||
- W0 web foundation (`web/**`); W6 Android Insights (D-141 on); the update-check fix's docs (D-140, SPEC v0.30; code landed `c0f4345`).
|
||||
- This docs pass (`agent-docs-v31`: SPEC v0.30 and v0.31, D-140 to D-143, `strings.csv`, the web dictionaries, design-spec S-11 and §3.10, this file).
|
||||
- Web W1 to W5 (`web/src/features/**`, D-144 to D-154 reserved); the Pi image build with the web client and E-34/E-35.
|
||||
- Record D-155 (`d686ee0`, install failures visible on S-17 and S-22) in the next spec version, v0.32, with `strings.csv` at 590 rows (docs agent).
|
||||
- Earlier outcomes (details in SPEC §11.2, §11.3 and `docs/decisions.md`): M8 end-to-end test passed on the owner's S26 against the dev server on 2026-10-18 (pairing, scripted partner, offline expense synced, push by `fid` delivered); M8b to M8f UI fixes; the M2c-pi bundle `infra/pi/` (D-103 to D-106, owner checklist in `infra/README.md` "Pi: what the owner does"); release builds R8-shrunk and signed (alias `kvitt`, RSA 4096, v2).
|
||||
- Compatibility: an APK from M12e on cannot pair or list devices against a pre-M12e server, so the server is rebuilt and redeployed before a new APK is installed (SPEC §14.8). The dev server already runs schema 3.
|
||||
- Remote: `origin` on Gitea, SSH key works; pushed at `7521c01`. The Gitea secret `kvitt_cicd` (a user-level secret of `jacobh2`, renamed by the owner 2026-10-19) works; the Drive token is `infra/secrets/rclone.conf` (`[gdrive]`; folder id in `.env` as `RCLONE_FOLDER_ID`). Unused on the Mac since D-120: `infra/secrets/ha_ssh_key` and `ha_ssh_key.pub` (git-ignored; may be deleted).
|
||||
- Both gates exist: Android `tools/gradle-serial.sh check` (from `android/`), server `cargo fmt --check && cargo clippy --all-targets -- -D warnings && cargo audit && cargo test` (from `server/`, `cargo audit` since `f1abb0d`). Both green on `main` as far as reported (Android 977 tests at `12e2508`; server 299 tests at `50a0d67`). Known red: since `50a0d67` the Android gate on `main` fails three `:data` contract tests (`ExampleCoverageTest.everyExampleFileHasADto`, `ExampleRoundTripTest` for `E-34-analytics-trends/response.json` and `E-35-analytics-forecast/response.json`) until W6's E-34/E-35 DTOs land. A third gate comes with 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/` (`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.
|
||||
- Importer: the dry run of 2026-10-18 reconciled on both real files (Vardag 1746 rows, Hemma 37 rows, 0 unmapped). The production import on the Kvitt Pi is done (owner, 2026-10-21). The CSV files stay in `export/` (git-ignored).
|
||||
- Development stack (M2c-dev): running on the Mac at `http://192.168.1.210:8080`, database at schema 3, data kept; backups `infra/dev/backups/kvitt-20261018T093143Z.sqlite.gz` and `kvitt-20261018T130443Z.sqlite.gz` (git-ignored). On this Podman setup run `infra/dev/up.sh --down`, then `infra/dev/up.sh`, to swap in a rebuilt image. Since `7fb86f5` `up.sh` copies only `fcm-service-account.json` into `infra/dev/secrets/` (git-ignored). The owner's phone is paired with it; a scripted partner exists (tokens held by the orchestrator outside the repo). Firebase project `kvitt-1337` with apps `se.hagstedt.kvitt` and `se.hagstedt.kvitt.debug`; service account `kvitt-fcm`.
|
||||
- Installed on the owner's phone (SM-S942B): the debug app `se.hagstedt.kvitt.debug`, paired against the dev server; the release app `se.hagstedt.kvitt`, rebuilt with the host `kvitt.suorra.duckdns.org` and no port (D-124) and installed 2026-10-19; paired against the Kvitt Pi 2026-10-21. The partner's phone is paired too.
|
||||
- Disk: the MacBook had about 72 GB free during the v0.20 docs pass (2026-10-19); run `tools/clean-builds.sh` when it drops under about 5 GB.
|
||||
|
||||
## 2. Open work (one line each, with who owns it: agent or owner)
|
||||
- Land `agent-w7-docs` (orchestrator): SPEC v0.29, D-135 to D-139, `infra/README.md`, HANDOFF; docs only, gate `python3 tools/contracts-check.py`; pushing it triggers no workflow run.
|
||||
- Android gate red on `main` since `50a0d67` (three `:data` example tests for E-34/E-35, §1); W6 fixes it with the DTOs (agent W6).
|
||||
- Pi image builds fail at the Node stage until W0 lands `web/package.json` (expected); the first image with the web client follows W0, then deploy (§10.10) and HUMAN step 3.
|
||||
- Land `agent-docs-v31` (orchestrator): SPEC v0.30 and v0.31, D-140 to D-143, `strings.csv`, `web/src/i18n/*.json`, design-spec, HANDOFF; gate `python3 tools/contracts-check.py` (the web gate for the regenerated dictionaries).
|
||||
- Pi image with the web client and E-34/E-35 (built from the `1c9856b` push): watch the workflow run, then the owner deploys it (§3 step 4), then HUMAN step 3; then publish a release APK with W6 (`infra/publish-apk.sh --deploy`) so the phones show Insights.
|
||||
- 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 `12e2508` (S-12 line chart, D-128).
|
||||
- Pushed: through `1c9856b` (W6 Android Insights).
|
||||
- Closed by `2acfc34` (`agent-gate-docs`): `infra/README.md`'s stale `CARGO_BUILD_JOBS` text and its "Owner decisions (open)" (now answered).
|
||||
- `infra/backup/Dockerfile`: stale comment "on the Pi the container runs as uid 10001" (since D-125 the owner's uid); left as is, because editing it triggers a 5-minute image build (agent, with the next change there).
|
||||
- Historical `:8443` and `icehack.asuscomm.com` mentions in `docs/ux/` and `docs/validation/` are left as the record of their time (no action unless the owner wants them rewritten).
|
||||
@@ -84,7 +88,7 @@ Living document, rewritten in place (never a second copy). A new orchestrator se
|
||||
- `PLAN-M2c-pi.md` is superseded where it names Caddy, cert-sync, scripts or `/opt/kvitt` (note at its top, D-120 and D-125); SPEC §10 and `infra/README.md` are current. `PLAN-M11.md` has a note that `/opt/kvitt/import` is now `~/kvitt/import`.
|
||||
- Plans (SPEC §11.2 v0.24), in dependency order; prompt `prompts/<id>.md`:
|
||||
- M1, M2a, M2b, M2c-dev, M3, M4, M5, M6, M6b, M7a to M7g, M8 (phone test passed), M8b to M8f, M9, M12a to M12f, U1, U2, R8 release build, M2c-pi bundle with its NPM rework and the self-contained compose project (D-125), server gap pass, infra overhaul, infra security, the server overhaul, the Android overhaul, the APK download rate limit, `cargo audit`, the Android host property and its follow-ups (D-122), the server host defaults (D-123), port 443 (D-124), pull-to-refresh (D-127), the editor keyboard fix, the S-12 line chart (D-128): landed.
|
||||
- Web client programme (D-134, SPEC §11.2 rows W0 to W7): W0 and W7 running; then W1 to W5 in parallel after W0 (briefs from `prompts/W1.md` to `prompts/W5.md` with W0's module map in the notes; if W0 lacks a shared piece the plans name, a W0b follow-up first); W4's W-11 and W6 after W7. W7's report is recorded (v0.29); a docs agent records W0's report when it lands.
|
||||
- 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.
|
||||
- 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").
|
||||
|
||||
## 3. HUMAN steps pending (relay in chat)
|
||||
@@ -92,6 +96,7 @@ Done 2026-10-21: the Pi install (stack running, `v1.0.1`, both containers health
|
||||
1. App key (D-126; `infra/README.md` step 4a items 1 to 3; the key exists on the Mac and the Pi serves the keyed `kvitt-133.apk` since the first `--deploy` run, D-139; whether both phones run it is unconfirmed): generate `kvitt.appKey` in `android/local.properties` on the Mac (an agent may run the one-line `openssl` command; the value is never printed); build and publish a keyed APK (`infra/publish-apk.sh`, copy to `data/apk/`); both phones on it (update banner, or sideload).
|
||||
2. Gate (step 4a items 4 and 5), only after both phones run the keyed APK: NPM, Hosts, Proxy Hosts, Kvitt row, Edit, Advanced: paste the step 4 block with the key in place of `KVITT_APP_KEY`, Save (row Online, yes/no). Acceptance test: on home Wi-Fi Chrome opens `https://kvitt.suorra.duckdns.org/i/TEST` and the invite page shows (yes/no); with Wi-Fi off the same URL gives a connection error (yes/no); with Wi-Fi off Kvitt's About screen shows the server version (yes/no).
|
||||
3. Web client gate lines (D-130, SPEC v0.28): when the NPM block is pasted (item 2, or again if the D-126 block is already in NPM), paste the current `infra/npm/kvitt.conf` content (step 4), which also lets the web client through and sets its Content-Security-Policy; Save (row Online, yes/no). After the first image with the web client is deployed (W7, §10.10), acceptance test with Wi-Fi off on the phone: `https://kvitt.suorra.duckdns.org/app/` shows the Kvitt login page (yes/no) and `https://kvitt.suorra.duckdns.org/health` still gives a connection error (yes/no) (step 4a item 5 d and e).
|
||||
4. Deploy the new image (web client and E-34/E-35, SPEC §10.10), once the Gitea workflow run for `1c9856b` has succeeded: on the Kvitt Pi in `~/kvitt`, set `KVITT_TAG` in `.env` to the tag the run printed, then `docker compose pull && docker compose up -d`; `server` and `backup` healthy (yes/no). Then the orchestrator publishes the release APK with Insights.
|
||||
|
||||
## 4. Owner questions (asked, unanswered)
|
||||
- OQ-6 (SPEC §12): a confirm step on S-1 for an unusual server; default "not in v1".
|
||||
@@ -103,9 +108,9 @@ Done 2026-10-21: the Pi install (stack running, `v1.0.1`, both containers health
|
||||
- Nothing.
|
||||
|
||||
## 6. Numbering
|
||||
- Decisions: D-001 to D-139 used, except D-008 and D-009 (never used); D-140 taken by the update-check fix (`c0f4345`, recording in progress). Next free: **D-141** (W6's range starts there).
|
||||
- Spec version: v0.29 written (W7 and D-139); v0.30 reserved for D-140. Next **v0.31**.
|
||||
- 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.29 (W7 needed no migration).
|
||||
- Decisions: D-001 to D-143 used, except D-008 and D-009 (never used). Reserved: **D-144 to D-154** for the web feature agents W1 to W5, **D-155** for the APK install-failure fix. Next free after those: **D-156**.
|
||||
- Spec version: v0.31 written (v0.30 D-140, v0.31 D-141 to D-143). Next **v0.32**.
|
||||
- Server schema version **3** (migration 0003); next **4**. Room schema version **1** (`android/data/schemas/se.hagstedt.kvitt.data.db.KvittDatabase/1.json`); next **2**. Unchanged by v0.27 to v0.31 (W7 and W6 needed no migration).
|
||||
- Sync protocol **1**, push payload **1**.
|
||||
|
||||
## 7. How the work is run
|
||||
|
||||
@@ -144,3 +144,7 @@ entry and the old one a strike-through plus a pointer. The spec is updated in th
|
||||
| D-137 | 2026-10-21 | Server image build with the web client (refines D-132). Build context: the repository root with an allowlist `.dockerignore` at the root (`*`, then `!server`, `!web`, `!contracts`; excluded inside them `server/target`, `server/data`, `web/node_modules`, `web/dist`, `**/*.sqlite*`, `**/*.db`, `**/*.apk`, `**/*.pem`, `**/.env`, `**/.env.*`, `**/.DS_Store`); `server/.dockerignore` removed. `server/Dockerfile` stage `web`: `node:24.22.1-bookworm-slim` pinned by the digest of its multi-arch index (Node 24, Active LTS on 2026-10-21), `--platform=$BUILDPLATFORM`, `npm ci` (npm cache mount), `npm run build` in `/repo/web` beside `/repo/contracts`, `test -f dist/index.html`; a `web/` without `package.json`/`package-lock.json` fails the build by design. Its `dist` is copied into the Rust stage (`/out/web`) and from there to `/app` in the runtime image, so npm and rustc never run together on the 1 GB Pi 3 runner; `CARGO_BUILD_JOBS=1` kept. Workflow: context `.` with `file: server/Dockerfile`, sparse checkout adds `web` and `contracts`, the path filter adds `web/**` and `contracts/**`; `infra/build-images.sh` builds `kvitt-server` with context `.`; `infra/dev/compose.yaml` has no build section, so `infra/dev/build.sh` changed (default context the repository root, `KVITT_SERVER_BUILD_CONTEXT` still overrides). Also: `cargo audit` needed `rustls-webpki` 0.103.16 and `webpki-root-certs` 1.0.10 in `Cargo.lock` (RUSTSEC-2026-0382, -0383, published 19–20 October 2026). The Node stage's duration on the Pi 3 is not yet measured; until `web/package.json` lands (W0) every server image build fails at the Node stage. Landed `50a0d67`. | An allowlist keeps the Android project, goldens, worktrees and secrets out of a root context; building the web on the build platform avoids emulating npm; chaining the stages keeps BuildKit from running npm and rustc in parallel on 1 GB. | SPEC §10.3, §15.4; `.dockerignore`, `server/Dockerfile`, `.gitea/workflows/build-images.yaml`, `infra/build-images.sh`, `infra/dev/build.sh`, `infra/README.md`, `server/Cargo.lock` |
|
||||
| D-138 | 2026-10-21 | Server analytics endpoints and rules (fills in D-133; new SPEC §14.5.7). E-34 `GET /analytics/trends?months=…&measure=…` (`months` 1 to 24, default 6; `measure` `household` or `my_share`, default `household`) → `{measure, today, current_month, months, household: Trend, categories: [{category_id, trend}]}`, `Trend = {total_ore, average_ore, last_month_ore, last_month_vs_average_pct?, current_month_ore, points: [{month, amount_ore, moving_average_ore}]}` (the brief's two endpoints, averages and trends, merged into one). E-35 `GET /analytics/forecast?measure=…` → `{measure, today, current_month, accounts: [{account_id, current_month_ore, months: [{month, recurring_ore, baseline_ore, forecast_ore}] ×2}]}`, `months` ignored. Both authenticated, read-only; invalid parameters 400 `invalid_request` with `field`; unknown keys ignored. Rules (pure functions in `kvitt-domain::analytics`: `div_half_up`, `average`, `moving_average`, `percent_vs_average`, `occurs_in_period`, `MOVING_AVERAGE_WINDOW` = 3): months are calendar months of `expense_date`; today is the Europe/Stockholm date; the whole months are the N before the current month; averages divide by N with empty months counting, rounding half away from zero; percent against the rounded average, half away from zero, null when that average is 0; the moving average trails over 3 months (D-128's window), the first points averaging what exists; "current month so far" runs from the 1st through today; categories listed only when non-zero, ordered by total descending, then id; the forecast counts non-paused, non-deleted templates once per month within their start and end periods and after `last_generated_period` (templates are monthly only, `day_of_month`), `my_share` with the shares a generation would write today; baseline = non-generated spend over the last 3 whole months ÷ 3 (a new account reads low); accounts are the non-deleted ones, archived included; settlements never count, deleted expenses never, archived accounts and archived or deleted categories do. Examples `contracts/examples/E-34-analytics-trends/response.json`, `E-35-analytics-forecast/response.json`; `contracts/openapi.yaml` operations `getAnalyticsTrends`, `getAnalyticsForecast`. Server tests 299; `tools/contracts-check.py` 136 checks (operations E-1 to E-35). A contract change (§14.8): the Android `:data` example tests need DTOs for the two examples (W6). Landed `50a0d67`. | One endpoint per screen section keeps W-11 and the app's Insights to two calls; computing in SQL per month and rounding in `kvitt-domain` keeps the rules beside the other money rules and equal to S-12's (D-128). | SPEC §7, §14.3.4, §14.3.5, §14.5.7, §15.8, AC-14.1; `server/crates/kvitt-domain/src/analytics.rs`, `server/crates/kvitt-server/src/api/analytics.rs`, `wire/analytics.rs`, `contracts/openapi.yaml`, `contracts/examples/E-34-*`, `E-35-*`, `tools/contracts-check.py` |
|
||||
| D-139 | 2026-10-21 | Unattended APK deploy. `infra/publish-apk.sh --deploy` copies `kvitt-<code>.apk` first and `latest.json` last into the server's `data/apk/` on the Kvitt Pi with a dedicated forced-command key `infra/secrets/pi_deploy_ed25519` (git-ignored; its public half in the owner's `~/.ssh/authorized_keys` on the Pi prefixed `command="scp -t /home/jacobhagstedt/kvitt/data/apk",restrict`, so it can only write files into `data/apk/`), with legacy scp (`-O`), the Pi's host key pinned on first use in `infra/secrets/pi_known_hosts`; then requests `https://<KVITT_PUBLIC_HOST>/apk/latest` with `X-Kvitt-App` (the key from `android/local.properties`, fed to curl on stdin, never printed) and fails unless it returns the new `version_code`. Without the key file or `kvitt.appKey`, `--deploy` refuses to start; without `--deploy` nothing changes, and the manual scp stays the fallback. First deploy 2026-10-21: `version_code` 133 served. Landed `70cb25c`. | Publishing an APK was a manual two-file copy the owner had to do; a forced-command key keeps the Mac's access to the Pi to one directory (no shell), and the `/apk/latest` check proves the phones will see the update. | SPEC §10.6; `infra/publish-apk.sh`, `infra/README.md` "Publishing an APK" |
|
||||
| D-140 | 2026-10-21 | Manual update check skips the throttle. The S-22 button 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'. A separate `UpdateRepository.checkNow()` (default implementation calls `check()`, so fakes are unchanged) always asks the server and restarts the window; debug builds still return `UpToDate` without a request. `AboutViewModel.check()` calls `checkNow()`. The automatic checks on cold start and resume keep the 6-hour throttle. Refines AC-16.2 and AC-16.6 (owner, 2026-10-21). Landed `c0f4345`. | A tap on "Check for update" is an explicit request and must reach the server; the throttle exists only to keep the automatic checks cheap. A defaulted interface member keeps every fake and test unchanged. | SPEC AC-16.2, AC-16.6, §13.3 S-22, §14.2.4; `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/feature/settings/src/main/kotlin/se/hagstedt/kvitt/feature/settings/about/AboutViewModel.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-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` |
|
||||
|
||||
+38
-1
@@ -611,6 +611,7 @@ TalkBack: "Konto: Vardag. Dubbeltryck för att byta konto"; "Datum: idag"; "Beta
|
||||
* Range average (`averageOre`): a 1.5 dp dashed line (6 dp on, 4 dp off) across the plot at 75 % opacity of `color`, with its amount in whole kronor at the right end in `chartAxis` typography and the same colour.
|
||||
* Moving average (`movingAverage`, one value per point): a 1.5 dp line in `chart-axis-label` at 85 %.
|
||||
* A single bucket shows one centred point and no average lines; a range without spending shows only the 0 baseline (D-068).
|
||||
* Reused by S-11's Insights (D-143) with server-provided overlays: one point per whole month of E-34, `averageOre` is the trend's `average` and `movingAverage` its per-point moving average, both taken from the server, not computed on the phone; omitted when the average is 0 or there is one point.
|
||||
* Not interactive. TalkBack: the chart is one node reading `summary` (`analytics_summary_line`, or `analytics_summary_line_single` for one bucket).
|
||||
|
||||
**`LineLegend(items, color)`**: wrapping row as `ChartLegend`; each item a 16 × 10 dp line swatch in its line's style (solid series in `color`, dashed average, thin neutral moving average) + label (12 / 16 sp weight 500 `on-surface-variant`): the series name, "Snitt" (`analytics_average`), "Glidande snitt (3)" (`analytics_moving_average`). Decorative for TalkBack (the summary names the lines).
|
||||
@@ -944,6 +945,7 @@ Full screen (§13.2), works offline (AC-8.2). The flow and the category rule are
|
||||
3. `ChartCard` "Utgifter över tid": `StackedBarChart` grouped by the group-by toggle, `ChartLegend`.
|
||||
4. `ChartCard` "Per kategori": `BreakdownList` of categories.
|
||||
5. `ChartCard` "Per konto": `BreakdownList` of accounts (archived included with badge, AC-14.9).
|
||||
6. The Insights section (below, D-142, D-143), after "Per konto" or after the empty state.
|
||||
|
||||
**States**
|
||||
|
||||
@@ -959,12 +961,47 @@ Full screen (§13.2), works offline (AC-8.2). The flow and the category rule are
|
||||
* Period chip → menu of the presets (AC-14.2: "Denna månad", "Senaste 3 mån", "Senaste 6 mån", "Senaste 12 mån", "I år", "All tid", "Anpassad…"); "Anpassad…" opens the Material date range picker; "Använd" is enabled only with both dates (AnalyticsState note in §14.2.4).
|
||||
* Toggles re-query immediately; the filter survives tab switches within the session.
|
||||
* Bar → S-12 for that bucket (all groups); breakdown row → S-12 for that category or account over the range (AC-14.8).
|
||||
* Settlements are never counted (AC-14.6). Works offline.
|
||||
* Settlements are never counted (AC-14.6). Works offline (the Insights section below is online only).
|
||||
|
||||
**TalkBack**: chart summary sentences as §3.10; each bar and row a node; toggles are radio groups.
|
||||
|
||||
**200 %**: `S-11-analytics-200`.
|
||||
|
||||
#### Insights (D-142, D-143)
|
||||
|
||||
The server's trends and forecast (E-34, E-35, D-133), online only (§9.9), below S-11's local analytics. It follows S-11's measure toggle (Hushållet / Min del) and always covers the last 6 whole months, whatever S-11's period and granularity; group-by does not apply.
|
||||
|
||||
**Layout** (same column and gap as S-11):
|
||||
1. `SectionHeader` "Insikter" and a caption in body-small `on-surface-variant`: "Senaste 6 hela månaderna · Hushållet" ("· Min del" for my share; `analytics_insights_caption`).
|
||||
2. Banner when needed: offline `NeedsConnectionBanner` "Insikterna räknas fram på servern. Anslut till internet för att se dem." (`analytics_insights_offline_body`); after a failure an error `Banner` (`cloud_off`) "Kunde inte nå servern" for `network`, else the code's text, with the text button "Försök igen" (`refresh`). The last data of this session stays visible under either banner.
|
||||
3. `ChartCard` "Utgifter per månad" (`analytics_insights_trend`): the household or my-share trend as `SpendLineChart` (§3.10) in `primary`, with the server's average and moving average and `LineLegend` (series named "Hushållet" / "Min del", "Snitt", "Glidande snitt (3)"); then body-small `on-surface-variant`: "Snitt <X> i månaden · September 2026: 12 % över snittet" (`analytics_insights_average_per_month` joined by " · " with D-128's `analytics_vs_average_*`), and "Hittills i oktober 2026: <X>" (`analytics_insights_so_far`).
|
||||
4. `ChartCard` "Kategorier per månad" (`analytics_insights_categories`): the open category's chart as in 3, in its category colour, then a radio list of the categories in the server's order (total descending): rows of at least the touch target, `CategoryTile` 32, name (body-large, one line, ellipsis) over "Snitt <X> i månaden", and on the right the last whole month against the average, "+12 %" / "−8 %" / "0 %" (`analytics_insights_change_up`, `_change_down`; label-large muted; none when the average is 0); the open row has a `secondary-container` fill. The open category defaults to the first (the largest total); category ids the phone does not hold are left out.
|
||||
5. With no spending in the 6 months, none this month and no category listed, one card "Inga utgifter de senaste 6 hela månaderna" (`analytics_insights_no_spending`, plural) replaces 3 and 4.
|
||||
6. "Prognos" (title-medium, `analytics_insights_forecast`) over the caption "Återkommande utgifter som förfaller, plus snittet av övriga utgifter de senaste 3 månaderna" (`analytics_insights_forecast_caption`, plural; 3 is D-128's window).
|
||||
7. One card per account in the server's order (archived included): `AccountTile` 32 (muted when archived), the name (title-medium, ellipsis) over "Hittills i oktober 2026: <X>", the "Arkiverat" badge when archived; then the two next months as rows: "November 2026" (body-large) over "Återkommande <X> + snitt <Y>" (`analytics_insights_forecast_parts`), the forecast as `MoneyText(Medium)` on the right. An account with nothing this month and both forecasts 0 shows one line "Inga återkommande utgifter och inga utgifter på sistone" (`analytics_insights_forecast_none`) instead of the rows. Accounts the phone does not hold are left out.
|
||||
|
||||
**Loading**: both reads run in parallel when S-11 opens, when the measure changes to one without data, when the phone comes online, and on "Försök igen"; each measure's data is kept in memory while S-11 is on the back stack, nothing is stored. Before the first data, skeleton cards (a chart card "Utgifter per månad" and an account card with two month rows), read as "Hämtar insikter" (`analytics_insights_loading`).
|
||||
|
||||
**States** (goldens `s11_insights_<state>` in `sv_light`, `sv_dark`, `en_light`, plus `s11_insights_long_sv_light_200`):
|
||||
|
||||
| Golden | State | What differs |
|
||||
|---|---|---|
|
||||
| `loaded` | loaded | household; caption, "Utgifter per månad" with average and moving average |
|
||||
| `loaded_scrolled` | loaded, scrolled | the category chart and list, "Prognos" and the account cards |
|
||||
| `category` | another category open | the chosen category's chart and the selected row |
|
||||
| `my_share` | my share | caption "· Min del", series "Min del" |
|
||||
| `loading` | loading | skeleton cards, no data yet |
|
||||
| `offline` | offline, no data | the "Kräver anslutning" banner only |
|
||||
| `offline_kept` | offline after a load | the banner over the last data |
|
||||
| `unreachable` | failed after a load | error banner with "Försök igen" over the last data |
|
||||
| `unreachable_empty` | failed, no data | the error banner only |
|
||||
| `empty` | no spending | one "Inga utgifter de senaste 6 hela månaderna" card; accounts with the one-line forecast |
|
||||
| `long` | long names and amounts | ellipsized names, whole amounts |
|
||||
| `long_scrolled` | long, scrolled | the forecast cards with long names |
|
||||
| `screen` | whole S-11 | the section below "Per konto" in the S-11 screen |
|
||||
|
||||
**TalkBack**: each chart is one node with D-128's summary sentence (`analytics_summary_line`); category rows are a radio group; each forecast month is one node, "November 2026: <forecast> kronor, återkommande <X> kronor, snitt <Y> kronor" (`analytics_insights_forecast_month_spoken`); the account header row is merged; the skeleton reads "Hämtar insikter".
|
||||
|
||||
### S-12 Analytics drill-down (`:feature:analytics`)
|
||||
|
||||
**Layout** (app bar: back, title formatted from the drill-down arguments: "Mat & hushåll, oktober 2026" / "Vardag, v. 41" / "Oktober 2026" / "Mat & hushåll, maj–okt 2026"; one line, ellipsis; column gap 10):
|
||||
|
||||
@@ -448,6 +448,25 @@ analytics_vs_average_on,%1$s: on average,%1$s: på snittet,module=analytics; S-1
|
||||
analytics_summary_line,"%1$s over time, %2$s: from %3$s in %4$s to %5$s in %6$s, %7$s in total, average %8$s.","%1$s över tid, %2$s: från %3$s i %4$s till %5$s i %6$s, totalt %7$s, snitt %8$s.",module=analytics; TalkBack summary of the S-12 line chart: group; range; first and last bucket; total; average (D-128)
|
||||
analytics_summary_line_single,"%1$s over time, %2$s: %3$s in total.","%1$s över tid, %2$s: totalt %3$s.",module=analytics; TalkBack summary of the S-12 line chart with one bucket (D-128)
|
||||
analytics_range_apply,Apply,Använd,module=analytics; confirm button of the custom range picker; M7e
|
||||
analytics_insights_title,Insights,Insikter,module=analytics; S-11 Insights section heading (D-142)
|
||||
analytics_insights_caption:one,Last %1$d whole month · %2$s,Senaste %1$d hela månaden · %2$s,module=analytics; plural one; S-11 Insights caption: whole months; %2$s the measure (Hushållet / Min del) (D-142)
|
||||
analytics_insights_caption:other,Last %1$d whole months · %2$s,Senaste %1$d hela månaderna · %2$s,module=analytics; plural other
|
||||
analytics_insights_offline_body,Insights are calculated on the server. Connect to the internet to see them.,Insikterna räknas fram på servern. Anslut till internet för att se dem.,module=analytics; S-11 Insights NeedsConnectionBanner text when offline (D-142)
|
||||
analytics_insights_loading,Loading insights,Hämtar insikter,module=analytics; TalkBack label of the Insights skeleton (D-142)
|
||||
analytics_insights_trend,Spending per month,Utgifter per månad,module=analytics; ChartCard title of the household or my-share trend (D-143)
|
||||
analytics_insights_categories,Categories per month,Kategorier per månad,module=analytics; ChartCard title of the per-category trends (D-143)
|
||||
analytics_insights_average_per_month,Average %1$s a month,Snitt %1$s i månaden,module=analytics; %1$s the server's monthly average (D-143)
|
||||
analytics_insights_so_far,So far in %1$s: %2$s,Hittills i %1$s: %2$s,module=analytics; %1$s the current month (lowercase month and year); %2$s its spend so far (D-143)
|
||||
analytics_insights_change_up,+%1$s,+%1$s,module=analytics; category row: last whole month above the average; %1$s a percent (D-143)
|
||||
analytics_insights_change_down,−%1$s,−%1$s,module=analytics; category row: last whole month below the average (U+2212 minus); %1$s a percent (D-143)
|
||||
analytics_insights_no_spending:one,No spending in the last %1$d whole month,Inga utgifter den senaste %1$d hela månaden,module=analytics; plural one; replaces the Insights charts when nothing was spent (D-143)
|
||||
analytics_insights_no_spending:other,No spending in the last %1$d whole months,Inga utgifter de senaste %1$d hela månaderna,module=analytics; plural other
|
||||
analytics_insights_forecast,Forecast,Prognos,module=analytics; Insights forecast heading (D-143)
|
||||
analytics_insights_forecast_caption:one,"Recurring expenses due, plus the average of other spending in the last %1$d month","Återkommande utgifter som förfaller, plus snittet av övriga utgifter den senaste %1$d månaden",module=analytics; plural one; how the forecast is made; %1$d the baseline window in months (D-128's 3) (D-143)
|
||||
analytics_insights_forecast_caption:other,"Recurring expenses due, plus the average of other spending in the last %1$d months","Återkommande utgifter som förfaller, plus snittet av övriga utgifter de senaste %1$d månaderna",module=analytics; plural other
|
||||
analytics_insights_forecast_parts,Recurring %1$s + average %2$s,Återkommande %1$s + snitt %2$s,module=analytics; forecast month row: %1$s recurring due; %2$s the baseline average (D-143)
|
||||
analytics_insights_forecast_none,No recurring expenses and no recent spending,Inga återkommande utgifter och inga utgifter på sistone,module=analytics; forecast card of an account with nothing to forecast (D-143)
|
||||
analytics_insights_forecast_month_spoken,"%1$s: %2$s, recurring %3$s, average %4$s","%1$s: %2$s, återkommande %3$s, snitt %4$s",module=analytics; TalkBack per forecast month: month; forecast; recurring; average (D-143)
|
||||
settings_profile_offline_banner,Needs connection. Your profile can only be changed online.,Kräver anslutning. Profilen kan bara ändras när telefonen är online.,module=settings
|
||||
settings_title,Settings,Inställningar,module=settings
|
||||
settings_role_admin,You · Admin,Du · Administratör,module=settings; S-13 own row when the viewer is the admin
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 156 and column 79.
|
@@ -38,6 +38,25 @@
|
||||
"analytics_group_by": "Group by",
|
||||
"analytics_group_by_account": "Account",
|
||||
"analytics_group_by_category": "Category",
|
||||
"analytics_insights_average_per_month": "Average {1} a month",
|
||||
"analytics_insights_caption:one": "Last {1} whole month · {2}",
|
||||
"analytics_insights_caption:other": "Last {1} whole months · {2}",
|
||||
"analytics_insights_categories": "Categories per month",
|
||||
"analytics_insights_change_down": "−{1}",
|
||||
"analytics_insights_change_up": "+{1}",
|
||||
"analytics_insights_forecast": "Forecast",
|
||||
"analytics_insights_forecast_caption:one": "Recurring expenses due, plus the average of other spending in the last {1} month",
|
||||
"analytics_insights_forecast_caption:other": "Recurring expenses due, plus the average of other spending in the last {1} months",
|
||||
"analytics_insights_forecast_month_spoken": "{1}: {2}, recurring {3}, average {4}",
|
||||
"analytics_insights_forecast_none": "No recurring expenses and no recent spending",
|
||||
"analytics_insights_forecast_parts": "Recurring {1} + average {2}",
|
||||
"analytics_insights_loading": "Loading insights",
|
||||
"analytics_insights_no_spending:one": "No spending in the last {1} whole month",
|
||||
"analytics_insights_no_spending:other": "No spending in the last {1} whole months",
|
||||
"analytics_insights_offline_body": "Insights are calculated on the server. Connect to the internet to see them.",
|
||||
"analytics_insights_so_far": "So far in {1}: {2}",
|
||||
"analytics_insights_title": "Insights",
|
||||
"analytics_insights_trend": "Spending per month",
|
||||
"analytics_measure": "Measure",
|
||||
"analytics_measure_household": "Household",
|
||||
"analytics_measure_my_share": "My share",
|
||||
@@ -564,6 +583,8 @@
|
||||
"update_downloading_spoken": "Downloading version {1}",
|
||||
"update_failed": "Download failed, try again",
|
||||
"update_install": "Update",
|
||||
"update_install_blocked": "The phone's security settings blocked the installation (Auto Blocker or Install unknown apps). Change the setting and try again.",
|
||||
"update_install_failed": "Could not install the update",
|
||||
"update_later": "Later",
|
||||
"update_permission_body": "Kvitt updates straight from your own server, not from the Play Store, so Android asks once whether Kvitt may Install unknown apps. Turn on Allow from this source and come back to Kvitt. The update continues by itself.",
|
||||
"update_permission_title": "Allow installation",
|
||||
|
||||
@@ -38,6 +38,25 @@
|
||||
"analytics_group_by": "Gruppera efter",
|
||||
"analytics_group_by_account": "Konto",
|
||||
"analytics_group_by_category": "Kategori",
|
||||
"analytics_insights_average_per_month": "Snitt {1} i månaden",
|
||||
"analytics_insights_caption:one": "Senaste {1} hela månaden · {2}",
|
||||
"analytics_insights_caption:other": "Senaste {1} hela månaderna · {2}",
|
||||
"analytics_insights_categories": "Kategorier per månad",
|
||||
"analytics_insights_change_down": "−{1}",
|
||||
"analytics_insights_change_up": "+{1}",
|
||||
"analytics_insights_forecast": "Prognos",
|
||||
"analytics_insights_forecast_caption:one": "Återkommande utgifter som förfaller, plus snittet av övriga utgifter den senaste {1} månaden",
|
||||
"analytics_insights_forecast_caption:other": "Återkommande utgifter som förfaller, plus snittet av övriga utgifter de senaste {1} månaderna",
|
||||
"analytics_insights_forecast_month_spoken": "{1}: {2}, återkommande {3}, snitt {4}",
|
||||
"analytics_insights_forecast_none": "Inga återkommande utgifter och inga utgifter på sistone",
|
||||
"analytics_insights_forecast_parts": "Återkommande {1} + snitt {2}",
|
||||
"analytics_insights_loading": "Hämtar insikter",
|
||||
"analytics_insights_no_spending:one": "Inga utgifter den senaste {1} hela månaden",
|
||||
"analytics_insights_no_spending:other": "Inga utgifter de senaste {1} hela månaderna",
|
||||
"analytics_insights_offline_body": "Insikterna räknas fram på servern. Anslut till internet för att se dem.",
|
||||
"analytics_insights_so_far": "Hittills i {1}: {2}",
|
||||
"analytics_insights_title": "Insikter",
|
||||
"analytics_insights_trend": "Utgifter per månad",
|
||||
"analytics_measure": "Mått",
|
||||
"analytics_measure_household": "Hushållet",
|
||||
"analytics_measure_my_share": "Min del",
|
||||
@@ -564,6 +583,8 @@
|
||||
"update_downloading_spoken": "Laddar ner version {1}",
|
||||
"update_failed": "Nedladdningen misslyckades. Försök igen.",
|
||||
"update_install": "Uppdatera",
|
||||
"update_install_blocked": "Installationen blockerades av telefonens säkerhetsinställningar (Auto Blocker eller Installera okända appar). Ändra inställningen och försök igen.",
|
||||
"update_install_failed": "Kunde inte installera uppdateringen",
|
||||
"update_later": "Senare",
|
||||
"update_permission_body": "Kvitt uppdateras direkt från er server, inte från Play Butik. Därför frågar Android en gång om Kvitt får Installera okända appar. Slå på Tillåt från den här källan och gå sedan tillbaka till Kvitt. Uppdateringen fortsätter av sig själv.",
|
||||
"update_permission_title": "Tillåt installation",
|
||||
|
||||
Reference in New Issue
Block a user