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:
+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.
|
Reference in New Issue
Block a user