Files
kvitt/PLAN-M8.md
T
jacobh2 5e1be38364 Plans and start prompts for every milestone; SPEC v0.2
PLAN-<id>.md for U1, U2, M1, M2a, M2b, M2c-dev, M2c-pi, M3, M4, M5, M6,
M7a-M7f, M8, M9, M10, M11, each with the eleven fixed sections, and a
start prompt per plan in prompts/ plus prompts/README.md (dependency
order and parallel waves).

SPEC v0.2: owner answers D-001 (development-first, M2c split into
M2c-dev and M2c-pi, Gitea registry), D-002 (local backups deleted once
on Drive), D-003 (Splitwise CSV format, English and Swedish variants,
category mapping), OQ-1, OQ-2, OQ-4 answered; M1's readings of
ambiguous contract points stated explicitly in §14.3 to §14.5.
HANDOFF §2 lists the plans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 21:05:34 +02:00

11 KiB

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).
  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/dev/README.md (how to run and bootstrap the development server).
  7. SPEC §11.2 (row M8), §11.3 (note M8), §14.7.
  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), 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).
  • 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).
  • 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).

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.

Note: network security config (§8.5), manifest hardening (§8.8) and release signing and version properties (§10.6) are not named in §11.2's M8 row, but they live in :app, which only M8 edits (§11.3); they are listed as deliverables here. The orchestrator confirms or reassigns them in the start prompt.

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), MainActivity, navigation graph, deep-link parsing, Hilt modules binding interfaces to :data, 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: notification copy and channel name from docs/ux/strings.csv.
  • 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 and android/app/google-services.json from the main checkout (never commit them; 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/dev/README.md 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.
  • Whether the start prompt confirmed the network security config, hardening and signing deliverables.
  • Dev server state left behind (running or stopped; scripted partner device name).
  • Numbering used 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.