Records the decisions proposed by M3 (503cee5) and M5 (c28c52b). M5's proposals D-030 to D-033 are renumbered D-040 to D-043; D-035 to D-039 are reserved for M4. Adds the D-027 clock-skew re-stamp rule, M5's contract requests for M8 (PLAN-M8, SPEC 11.3), the connect-info note for M4 (PLAN-M4), the picture size note (PLAN-M6), and updates prompts/README status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
13 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
- SPEC §6.2 Android app (module table, the paragraph on the architecture check) and §13.2 Navigation model.
- SPEC §14.6 Push payload v1, "Phone handling" (items 1 to 4).
- 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.
- SPEC §8.2 (token never logged), §8.5 Transport, §8.8 Phone hardening, §10.5.3 (the
kvitt.tls.pinnedRootproperty), §10.6 (version code and name properties, release signing keys), §10.7 (google-services.jsonininfra/secrets/, copied byinfra/dev/copy-google-services.sh). - SPEC §14.4.8 Epoch recovery and re-pairing (the
Unrecognisedpath), §14.2.4 (SessionState,UpdateState). - SPEC §10 intro "Development first (D-001)" and
infra/README.md(dev stack section) and the scripts ininfra/dev/(how to run and bootstrap the development server). - SPEC §11.2 (row M8), §11.3 (note M8), §14.7, §6.2 "Feature convention (D-017)";
docs/decisions.mdD-017, D-022. - Files:
android/app/**,android/build-logic/**,android/gradle/libs.versions.toml, the public APIs ofandroid/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.mdD-001,CLAUDE.md(THE PHONE, THE OWNER, BUILD AND VERIFY).
3. Already established
- Only
:appsees every module and binds the:datarepositories to the:core:modelinterfaces through Hilt; features never see:dataor:domain(§6.2, CLAUDE.md Layering). M8 is the only plan that edits:appafter 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;:appowns theNavHostand connects every lambda to a route; fakes are bound only in test and debug-preview source sets. - Strings (D-017):
:appholds only the app name; the notification copy and the channel name come from:core:design's shared strings. google-services.jsonis ininfra/secrets/of the main checkout (M2c-dev, D-022); M8 copies it intoandroid/app/(git-ignored) withinfra/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).
- Deep links:
kvitt://invite?server=…&code=…andhttps://icehack.asuscomm.com:8443/i/<CODE>(unverified link) open S-1 with both fields filled (AC-2.6). OnlyMainActivityand the FCM service are exported (§8.8). - After pairing: full sync, then the
POST_NOTIFICATIONSrequest (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 ofentity_id, fixed id forsync_batch; tap opens S-8 or S-10, Home for deletions andsync_batch; unknowntypeorv≠1: sync only (§14.6, AC-13.5, AC-13.6). :adapter:fcmmaps messages toPushEvent(type, entityType, entityId, actorUserId, count)and registers the token after pairing and on rotation viaPUT /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",dataExtractionRulesexcluding everything (§8.8). Release network security config: cleartext off, system anchors only unlesskvitt.tls.pinnedRoot=true; debug also allows cleartext tolocalhost,10.0.2.2and private LAN addresses (§8.5, AC-2.8, §10.5.3). - Version properties
kvitt.versionCode,kvitt.versionName(debug1anddev); release signing fromandroid/local.propertieskeyskvitt.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):
:appprovidesAppBuildInfo(versionCode, versionName, debug)to:dataand includesDataBindingsModule(@DisableInstallInCheck) in the release graph, while debug binds the fakes; WorkManager is configured withKvittWorkerFactoryand the defaultWorkManagerInitializerdisabled in the manifest;:appcallsSyncTriggers.onAppStart,SyncTriggers.onForegroundandSyncTriggers.syncForPush(push handling, 20 s wait), andUpdateRepository.checkon cold start and every resume; theImageLoaderprovided by:datais set as Coil's singleton. - Open follow-up (D-027): on the first sync attempt after a
clock_skewrejection the phone re-stampsupdated_atof each such pending row to the current clock before pushing. Not in:dataatc28c52b; the orchestrator assigns it to M8 (as a contract request on:data) or to a small:dataagent.
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 (viagoogle-services.json), version properties, release signing config read fromlocal.properties.android/app/src/main/AndroidManifest.xml:MainActivitywith 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, WorkManagerConfiguration.ProviderwithKvittWorkerFactory, Coil singletonImageLoader),MainActivity, navigation graph, deep-link parsing, theAppBuildInfoprovider and the release graph includingDataBindingsModule, theSyncTriggersandUpdateRepository.checkcalls, push event handling and notification building, revoke andUnrecognisedrouting, 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:designstrings.android/adapter/fcm/**:FirebaseMessagingService, token registration, mapping toPushEvent, 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
- 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. Copyandroid/local.propertiesfrom the main checkout, then runinfra/dev/copy-google-services.sh /Users/jhag/projects/kvittto placeandroid/app/google-services.jsonfrom the main checkout'sinfra/secrets/(never commit either; addsdk.dirtolocal.propertiesif missing). Runtools/install-git-hooks.sh. - Read §2 in order. Confirm M3, M4, M5, M7a to M7f and M2c-dev have landed; if not, stop and report.
- 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 andPackageInstallerusage from official docs; record each. - Hilt modules and the navigation graph; wire every feature screen; deep links.
:adapter:fcm, push handling, notification channel and building, tap routing.- Revoke wipe,
Unrecognisedrouting, notification permission after pairing, update banner wiring. - Manifest hardening, network security configs, version properties, release signing config (no keystore needed for debug).
- Run the gate's build part:
PATH=/opt/homebrew/opt/openjdk@25/bin:$PATH ../tools/gradle-serial.sh checkfromandroid/. - Development server: start it per
infra/README.md(dev stack section) and the scripts ininfra/dev/with an image built from currentmain(no Gradle build running while Docker builds), reset its dev data, runkvitt-server bootstrap, note the code (never log it to a committed file). - 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.kvittto 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. - 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
curlas 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. - 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 onmain. - 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.mdordocs/decisions.mdby this agent) orTODO(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.