Files
kvitt/PLAN-M8.md
T

92 lines
14 KiB
Markdown

# PLAN-M8: App integration
## 1. Goal
Assemble the app: `:app` navigation and deep links, Hilt bindings from the `:core:model` interfaces to the `:data` implementations, the `:adapter:fcm` module, notification building, update-banner wiring and the revoke wipe. Prove it on the owner's phone against the development server on the Mac: pair, add an expense offline, reconnect, see it synced.
## 2. Read first
1. SPEC §6.2 Android app (module table, the paragraph on the architecture check) and §13.2 Navigation model.
2. SPEC §14.6 Push payload v1, "Phone handling" (items 1 to 4).
3. SPEC §4 FR-2 AC-2.6, AC-2.9, AC-2.10; FR-3 AC-3.3, AC-3.4; FR-5 AC-5.1; FR-13 Push notifications (AC-13.5 to AC-13.8); FR-15 AC-15.3; FR-16 In-app update; §13.3 system notification copy table.
4. SPEC §8.2 (token never logged), §8.5 Transport, §8.8 Phone hardening, §10.5.3 (the `kvitt.tls.pinnedRoot` property), §10.6 (version code and name properties, release signing keys), §10.7 (`google-services.json` in `infra/secrets/`, copied by `infra/dev/copy-google-services.sh`).
5. SPEC §14.4.8 Epoch recovery and re-pairing (the `Unrecognised` path), §14.2.4 (`SessionState`, `UpdateState`).
6. SPEC §10 intro "Development first (D-001)" and `infra/README.md` (dev stack section) and the scripts in `infra/dev/` (how to run and bootstrap the development server).
7. SPEC §11.2 (row M8), §11.3 (note M8), §14.7, §6.2 "Feature convention (D-017)"; `docs/decisions.md` D-017, D-022.
8. Files: `android/app/**`, `android/build-logic/**`, `android/gradle/libs.versions.toml`, the public APIs of `android/data/**` (its Hilt modules), `android/feature/*/**` (their screen entry points), `android/core/model/**`, `docs/ux/strings.csv` (notification copy, which lives in `:core:design`), `docs/decisions.md` D-001, `CLAUDE.md` (THE PHONE, THE OWNER, BUILD AND VERIFY).
## 3. Already established
- Only `:app` sees every module and binds the `:data` repositories to the `:core:model` interfaces through Hilt; features never see `:data` or `:domain` (§6.2, CLAUDE.md Layering). M8 is the only plan that edits `:app` after M2b (§11.3).
- Feature convention (D-017): each feature exposes one composable entry point per screen with a `hiltViewModel()`-provided ViewModel and navigation as plain lambdas; `:app` owns the `NavHost` and connects every lambda to a route; fakes are bound only in test and debug-preview source sets.
- Strings (D-017): `:app` holds only the app name; the notification copy and the channel name come from `:core:design`'s shared strings.
- `google-services.json` is in `infra/secrets/` of the main checkout (M2c-dev, D-022); M8 copies it into `android/app/` (git-ignored) with `infra/dev/copy-google-services.sh`.
- Navigation: bottom navigation Home (Översikt), Analytics (Statistik), Settings (Inställningar); FAB "Add expense" on Home and Account detail; account detail, editors, settle up, details and settings sub-screens are pushed screens with back; the expense editor is full screen (§13.2). Since SPEC v0.11 (D-071): the top-level screens S-3, S-4, S-11 and S-13 draw their own `KvittNavigationBar` and take `onNavigate: (KvittDestination) -> Unit`, so the `:app` scaffold draws no second bar; argument keys are constants in each feature (`ARG_EXPENSE_ID`/`ARG_ACCOUNT_ID`, `SettleArgs.SETTLEMENT_ID`/`ACCOUNT_ID`, `DrillDownArgs.KEYS`, settings `"categoryId"`/`"templateId"`), read through `SavedStateHandle`; result snackbars of S-7, S-9 and account archiving are shown by the screen returned to (Home's `snackbarHostState`).
- Deep links: `kvitt://invite?server=…&code=…` and `https://icehack.asuscomm.com:8443/i/<CODE>` (unverified link) open S-1 with both fields filled (AC-2.6). Only `MainActivity` and the FCM service are exported (§8.8).
- After pairing: full sync, then the `POST_NOTIFICATIONS` request (AC-2.9); partner pairing goes through S-2 before Home (AC-2.10).
- Push handling: enqueue an expedited sync and wait up to 20 s; build the notification from local data in the phone's language per the §13.3 copy, or the generic text when the sync failed or the entity is missing; channel id `activity`, name "Activity"/"Aktivitet", importance default; notification id = hash of `entity_id`, fixed id for `sync_batch`; tap opens S-8 or S-10, Home for deletions and `sync_batch`; unknown `type` or `v` ≠ `1`: sync only (§14.6, AC-13.5, AC-13.6).
- `:adapter:fcm` maps messages to `PushEvent(type, entityType, entityId, actorUserId, count)` and registers the token after pairing and on rotation via `PUT /devices/current/push-token` (§14.6, AC-13.7). No personal content in the payload (AC-13.8).
- Revoke: `device_revoked` → delete token, local database and cached pictures, show S-18 (AC-3.3). `unauthorized` → S-1 "Pair this phone again", data kept (§14.4.8).
- Update banner on Home when `UpdateState.Available`; debug builds never check (FR-16).
- Hardening: `android:allowBackup="false"`, `dataExtractionRules` excluding everything (§8.8). Release network security config: cleartext off, system anchors only unless `kvitt.tls.pinnedRoot=true`; debug also allows cleartext to `localhost`, `10.0.2.2` and private LAN addresses (§8.5, AC-2.8, §10.5.3).
- Version properties `kvitt.versionCode`, `kvitt.versionName` (debug `1` and `dev`); release signing from `android/local.properties` keys `kvitt.release.storeFile`, `kvitt.release.storePassword`, `kvitt.release.keyAlias`, `kvitt.release.keyPassword` (§10.6).
- During development the debug build pairs against the server on the Mac over the home Wi-Fi (D-001); development data on that server is disposable.
- Application ids `se.hagstedt.kvitt.debug` (debug), `se.hagstedt.kvitt` (release) (CLAUDE.md).
- M5's contract requests, landed with SPEC v0.5 (§11.3 note M8, D-041): `:app` provides `AppBuildInfo(versionCode, versionName, debug)` to `:data` and includes `DataBindingsModule` (`@DisableInstallInCheck`) in the release graph (done pre-M8 in `6453932`, with `AppBuildInfo` from `BuildConfig`); debug binds the fakes through the temporary `android/app/src/debug/.../DebugFakesModule.kt` (`a41f240`), which M8 deletes so both variants bind `:data`; WorkManager is configured with `KvittWorkerFactory` and the default `WorkManagerInitializer` disabled in the manifest; `:app` calls `SyncTriggers.onAppStart`, `SyncTriggers.onForeground` and `SyncTriggers.syncForPush` (push handling, 20 s wait), and `UpdateRepository.check` on cold start and every resume; the `ImageLoader` provided by `:data` is set as Coil's singleton.
- Open follow-up (D-027): on the first sync attempt after a `clock_skew` rejection the phone re-stamps `updated_at` of each such pending row to the current clock before pushing. Still not in `:data` at `b24c8b5`; since SPEC v0.11 it is on the M12e contract-pass list (§11.3), not M8's.
## 4. Owned paths
From SPEC §14.7, verbatim: `android/adapter/fcm/**`, `android/app/**` (after the M2b shell) (owner M8; after landing: "later integration plans"). Catalog entries and module includes may be added (§14.7, `android/gradle/**` row).
Edit nothing outside these paths; a needed change elsewhere is a contract request to the orchestrator.
Network security config (§8.5), manifest hardening (§8.8), release signing and version properties (§10.6) are M8's (§11.2 row M8, v0.3). Never edit `SPEC.md` or `docs/decisions.md`: put proposed decision entries in the report.
## 5. Deliverables
- `android/app/build.gradle.kts`: dependencies on every module, Hilt, Firebase (via `google-services.json`), version properties, release signing config read from `local.properties`.
- `android/app/src/main/AndroidManifest.xml`: `MainActivity` with launcher and deep-link filters, FCM service from `:adapter:fcm`, `POST_NOTIFICATIONS`, `REQUEST_INSTALL_PACKAGES`, backup disabled, data extraction rules.
- `android/app/src/main/res/xml/network_security_config.xml` (release) and the debug variant; `data_extraction_rules.xml`.
- `android/app/src/main/kotlin/se/hagstedt/kvitt/**`: `Application` (Hilt, WorkManager `Configuration.Provider` with `KvittWorkerFactory`, Coil singleton `ImageLoader`), `MainActivity`, navigation graph, deep-link parsing, the `AppBuildInfo` provider and the release graph including `DataBindingsModule`, the `SyncTriggers` and `UpdateRepository.check` calls, push event handling and notification building, revoke and `Unrecognised` routing, notification-permission request after pairing.
- `android/app/src/main/res/values/strings.xml`, `values-sv/strings.xml`: the app name only (from M2b's shell); notification text is built from `:core:design` strings.
- `android/adapter/fcm/**`: `FirebaseMessagingService`, token registration, mapping to `PushEvent`, unit tests for the mapping.
- Unit tests in `:app`: deep-link parsing, notification text building for each §14.6 type and the generic fallback, notification ids.
- An end-to-end run report in the plan report (no file): pairing and the offline test on the phone.
## 6. Steps
1. Worktree. From the main checkout `/Users/jhag/projects/kvitt`: `git worktree add .claude/worktrees/agent-m8 -b agent-m8 main`, then in `.claude/worktrees/agent-m8`: `git reset --hard main`. Copy `android/local.properties` from the main checkout, then run `infra/dev/copy-google-services.sh /Users/jhag/projects/kvitt` to place `android/app/google-services.json` from the main checkout's `infra/secrets/` (never commit either; add `sdk.dir` to `local.properties` if missing). Run `tools/install-git-hooks.sh`.
2. Read §2 in order. Confirm M3, M4, M5, M7a to M7f and M2c-dev have landed; if not, stop and report.
3. Run `python3 tools/dependency-report/latest.py`; record the lines. Versions: Firebase BoM and the Google services Gradle plugin at latest stable (firebase.google.com release notes); Hilt navigation and Navigation Compose at latest stable; confirm current FCM Android and `PackageInstaller` usage from official docs; record each.
4. Hilt modules and the navigation graph; wire every feature screen; deep links.
5. `:adapter:fcm`, push handling, notification channel and building, tap routing.
6. Revoke wipe, `Unrecognised` routing, notification permission after pairing, update banner wiring.
7. Manifest hardening, network security configs, version properties, release signing config (no keystore needed for debug).
8. Run the gate's build part: `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ../tools/gradle-serial.sh check` from `android/`.
9. Development server: start it per `infra/README.md` (dev stack section) and the scripts in `infra/dev/` with an image built from current `main` (no Gradle build running while Docker builds), reset its dev data, run `kvitt-server bootstrap`, note the code (never log it to a committed file).
10. Phone (only if the start prompt names this plan as the one on the phone): `/opt/homebrew/share/android-commandlinetools/platform-tools/adb shell pm list packages | grep se.hagstedt.kvitt` to confirm ids; `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH tools/gradle-serial.sh installDebug`. The debug build must not replace a build holding the owner's real data without asking.
11. End-to-end test with the owner (HUMAN, §8): pairing with the bootstrap code and the Mac's LAN URL; the agent then creates a partner invite and accepts it with `curl` as a scripted partner device on the dev server, so the household is complete (FR-5); the owner adds an expense offline, reconnects; the agent confirms the expense reached the server (pull with the scripted partner's token) and pulls the app's sync state over USB (`adb shell run-as se.hagstedt.kvitt.debug …`) to show pending count 0. If push is configured, the agent also posts an expense as the scripted partner and asks the owner whether a notification appeared.
12. Commit on `agent-m8`, `git rebase main`, rerun the gate. Never merge, push or remove the worktree.
## 7. Gate
From SPEC §11.2, verbatim: "Android gate; on the owner's phone against the development server on the Mac (D-001): pair, add an expense offline, reconnect, see it synced".
Commands: from `android/`, `PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ../tools/gradle-serial.sh check`; then step 11.
Green means: `BUILD SUCCESSFUL` in this session with test counts from fresh XML, 0 failures; on the phone the pairing succeeded, the offline expense showed as pending, and after reconnecting the server holds it (checked by the agent) and the app's pending count is 0 (pulled over USB).
## 8. HUMAN steps
From SPEC §11.2, verbatim: "Tap through the pairing and the offline test as relayed by the orchestrator".
Where in the flow: step 11, after the debug build is installed and the dev server is bootstrapped. The agent words each step for relay with exact labels from `docs/ux/strings.csv`, for example: open Kvitt; on the pairing screen enter the server URL and the code the agent gives; tap the pair button; allow notifications; turn on Airplane mode; tap "Lägg till utgift", type an amount and a description, tap save; turn off Airplane mode; answer yes/no: the expense shows without the pending marker. The owner taps and answers yes/no only; the agent reads every number itself.
## 9. Out of scope
- Feature screens and their states (M7a to M7f); data layer logic (M5); `:core:design` (M6).
- Signed release builds, publishing the APK, pairing the partner's real phone, production server on the Pi (M2c-pi, M10).
- Server changes; `infra/` (contract request if the dev stack needs a change).
- Editing `:core:model`, `:data`, features or fakes.
## 10. Report
- Branch `agent-m8`, head commit hash after rebasing on `main`.
- Gate output: BUILD line, actionable tasks line, test counts from fresh XML; the end-to-end result (pair, offline pending, synced, pending count 0), with what the agent checked itself.
- Versions chosen with sources; dependency-report lines.
- Dev server state left behind (running or stopped; scripted partner device name).
- Proposed decision entries (never written to `SPEC.md` or `docs/decisions.md` by this agent) or `TODO(spec)` occurrences.
- HUMAN steps done and outstanding, worded for relay.
- Contract requests, open questions, spec gaps (with section numbers).
## 11. Numbering
Decision ids, spec version and schema version are assigned by the orchestrator in the start prompt; use `TODO(spec)` if none was given.