Closer/ClaudeReport.md

137 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Claude QA Report — Full-App QA (living report)
> **Verdict (2026-06-27): R11 confirmation round COMPLETE — FLAWLESS (0 open P0P2). Fixed the last open P2 (C-DARKART-001 — dark art now follows the in-app theme) + the open P3 (C-ART-EDGE-001 — art feathers into the surface), both in the shared `BrandIllustration`/`EmptyState` helpers, verified live on both decoupled theme directions (system-light+app-Dark → dark aubergine art; system-dark+app-Light → light pastel art), 0 FATAL. Re-confirmed all 5 R10 P2 fixes hold (C-HOME-001 single card · C-NAV-002 wheel-back popUpTo present · C-NAV-003 single app bar live · C-PW-001 dark paywall pills legible live · C-SEC-001 recovery row active for accepter live) → pruned. Entrypoint launch-integrity smoke green (splash-crash class clean on the fresh APK). Only remaining: 2 freshly-fixed art items pending 1 confirm + J-OBS (P3 touch targets). Art fixes in working tree — user commits.**
> **📖 Architecture reference:** see [`docs/Engineering_Reference_Manual.md`](docs/Engineering_Reference_Manual.md). Most fixed-and-pruned IDs above are documented in its [Known landmines and recent fixes](docs/Engineering_Reference_Manual.md#known-landmines-and-recent-fixes) section — read before re-touching the affected area.
>
> **Verdict (2026-06-26): R10 FULL ClaudeQAPlan run COMPLETE (AJ + fix phase). 0 open P0P2; 1 P3 (J-OBS). Found 5 P2 (Home dup card, wheel back-stack, duplicate app bar, dark paywall contrast, recovery-phrase wrong store) — ALL fixed + verified live + regression-clean. E-GAME-002 confirmed live + pruned. Security cornerstone clean (D1D7). [Pruned in R11.]**
>
> This report shows **current state only**. Fixed issues live here for **one** confirmation round, then they're pruned
> to the archived-ID line below (full detail stays in git history). See **Report hygiene** in `ClaudeQAPlan.md`.
## Run-state (current)
`R12 (2026-06-27) FRESH FULL ClaudeQAPlan run STARTED (user: "start from the start") | Baseline verified clean: QA free, Sam free (premium revoked), 0 active sessions; build HEAD 2cd0af6 + 3 uncommitted art files installed both emulators (5554=Dark, 5556=Light) | A ✅ (A-201 P1 Date-Match premium bypass) | B ✅ (4 async games full 2-device end-to-end + first-finisher nudge + C-NAV-002 + Ready=Start re-verified live; CC/MemoryLane/DateMatch render-checked; MemoryLane title/preview run-on→Future) | C ✅ (regression-clean vs R10 sweep; Messages/Today/Subscription + organic A/B + R11 decoupled art; NEW C-ART-EDGE-002 P3 direct-call hero hard edges on dark) | D ✅ (LIVE: D1 game answers enc:v1: · D3 non-member 403×4 + member-scoped · D5 self-grant→403; D2/D4/D6/D7 carried R7/R10, rules unchanged) — cornerstone clean | E ▶ in progress (Pass B already verified start/first-finisher/finish triggers→correct partners+copy; cold-start tap smoke running bg bjffibz4v) | FJ todo | Admin: scratchpad/qadmin.js + qa/* + scratchpad/d3neg.js (raw-API). Baseline restored (both free, 0 active). | NEXT: confirm smoke 6/6, then Pass F resilience. Carryover from R11 (still valid): C-DARKART-001 (P2) + C-ART-EDGE-001 (P3) fixed pending 1 confirm; J-OBS (P3) open touch targets.`
- **(prior) R11 (2026-06-27) confirmation round COMPLETE — FLAWLESS | Fixed last open P2 C-DARKART-001 (in-app-theme art) + P3 C-ART-EDGE-001 (feathered edges) in shared BrandIllustration/EmptyState; verified live both decoupled theme directions, 0 FATAL | 5 R10 P2 fixes re-confirmed + PRUNED (C-HOME-001/C-NAV-002/C-NAV-003/C-PW-001/C-SEC-001) | entrypoint smoke green on fresh APK | 0 open P0P2 | Baseline: both FREE, 0 active sessions; build (HEAD 2cd0af6 + 3 uncommitted art files) installed both emulators | NEXT (R12 = next session): confirm C-DARKART-001 + C-ART-EDGE-001 hold → prune; optional J-OBS (P3) touch targets; then declare program-complete for Android. Art fixes in working tree (user commits).`
- **Pass B progress (R12):** **1. This or That ✅** — full end-to-end 2-device, NEW style **Light×5 Quick** (R10 was Deep×10): QA started → answered 5 (alt A/B) → first-finisher state; **first-finisher nudge fired** (`partFinishNotifiedAt` set + Sam queue `partner_completed_part` "QA finished their part — your turn to play!"); Sam **joined via Play-hub active state** (at Q1/5, no dup session) → answered all-A → session→completed (0 active); **`partner_finished_game` to BOTH**; reveal **3/5 in sync** symmetric + correct Match/Differ + You/QA attribution on **both** devices (QA dark / Sam light). 0 FATAL. **2. Spin the Wheel ✅****Ready=Start session** (R11 change) verified; spun→Stress→10Q; **mixed answer types** (free-text + 15 scale) render+accept; Sam **joined active session** via Play hub (Q1, no dup/new spin); both finished→completed (0 active); results "Here's how you each answered" with You/QA free-text + scale; **C-NAV-002 RE-VERIFIED LIVE** — results → BACK → wheel hub (Spin/History), NOT the finished session (the R11-deferred confirm). 0 FATAL. **3. How Well ✅** — QA subject 5·Quick (answered 5 about self), Sam **joined as guesser** ("Predict how QA answered…", asymmetric), guessed 5 → score **5/5 "Perfect read"** + per-Q breakdown (✓ + answer, choice+scale) symmetric both devices; completed (0 active). **4. Desire Sync ✅** (premium, couple-shared via Sam-on) — free QA opened setup (no paywall); QA all-Yes, Sam joined + Y,Y,Y,N,N → reveal **"3 shared desires · 2 kept private"** (only mutual-Yes shown, 2 mismatches "Private") symmetric both devices; completed (0 active); Sam premium restored OFF. **All 4 async session games verified end-to-end.**
- **Uncommitted (user commits):** R11 art fixes only — `app/.../ui/theme/Theme.kt` (LocalAppInDarkTheme CompositionLocal), `app/.../ui/components/BrandIllustration.kt` (theme-correct `-night` variant via config-overridden context + edge feathering), `app/.../ui/components/EmptyState.kt` (routes its illustration through BrandIllustration). Everything else (splash fix, E-GAME-003, foreground banner, qa/ tooling) committed by user in `2cd0af6`.
- **Foreground "partner started a game" alert + bold Game Waiting card (R10, user-requested) — DONE+VERIFIED.** When the app is OPEN and a partner starts a game, a prominent **in-app top banner** ("<partner> started <Game>" + Join) slides in (mirrors chat's in-app surface) instead of the easy-to-miss system notification; **Join → joins the game**. Verified live for all 4 session games: banner name correct (Spin the Wheel / This or That / How Well Do You Know Me / Desire Sync); Join → joins wheel (ToT shows graceful "QA is playing a Wheel game" when types mismatch); **suppressed** when already on that game's screen (added `ActiveGameSessionMonitor.enter/leave` to `WheelSessionViewModel` — the others already had it). Home **"Game waiting"** card redesigned as a **bold purple-gradient hero** (glyph + game name + "Join the game"), promoted to top of "Waiting for you", verified **both themes** → tap **joins the specific game** (not the Play-hub fallback). FCM transport on the emulators is flaky (FcmRetry); the banner was exercised via a data-only high-priority send to the partner token (faithful to the deployed payload).
- **Pass C progress (R10):** **Settings family ✅** (dark + Security on light): Settings list, Subscription, Security, Delete account, Notifications all render clean both-relevant-themes; **4 illustrations confirmed in-context** (Security padlock, Delete-account doorway, Quiet-hours moon, + Subscription); back-stack OK (Security/Delete/Notif → BACK → Settings). **Found C-SEC-001 (P2)** — accepter's Recovery phrase disabled + wrong "invite your partner" copy (see Open issues). **Wheel back-stack RE-CHECKED = not a trap:** live wheel session → BACK → spin/setup → BACK → Play hub (2 backs, no dead-end); earlier "stuck" was automation cycling. Leaving mid-wheel leaves a resumable abandoned active session (normal; cleaned). Home ✅ both themes (stale game card gone).
- **6. Spin the Wheel ✅** — spun→Physical Intimacy→session; Sam joined at Q1; both answered 10 (A/B + 15 scale + free-text); per-Q You/QA breakdown renders; completed, 0 active. (Helper `wheel_drive.py` handles mixed types; free-text Qs hide "Next" behind IME.)
- **7. Date Match ✅** — swipe deck ("Swiping with Sam/QA"); QA+Sam mutual like → **"It is a match!" modal live**; new match persisted (date_matches 3→4); **swipe action `enc:v1:` at rest** (only swipedAt clear).
- **Pass B = COMPLETE (R10): all 7 games played end-to-end 2-device, 0 bugs.** 2 observations: CC day-counter desync (Future.md, by-design?) · **WATCH — wheel back-stack:** after finishing Spin-the-Wheel, system-BACK from the results re-enters the completed wheel-session screen (loop), needed an app relaunch to escape. Possibly automation artifact (missed taps) — **recheck deliberately in Pass C nav fuzzing; file if reproducible (P2 back-stack).**
- **5. Memory Lane ✅** — new capsule sealed (3-mo pick) with future open date; **title+content `enc:v1:` at rest** (admin-verified); lists cross-session. Minor cosmetic: "Opens in 2 mo" shown for a 3-month selection (relative-time display nit; not filed).
- **4. Connection Challenges ✅** — Gratitude Week (in-progress from R9): per-day step, "I did it today", "waiting for partner" both-gate, missed-day catch-up ("Pick it back up"), **streak 🔥→2 synced both devices**. UX note (Future.md): "Day N of 7" counter diverges between partners after asymmetric catch-up (QA D4/Sam D3) while streak stays synced — plausibly by-design, non-blocking.
- **Pass B progress (R10):** **1. This or That ✅** — Deep×10 (varied): QA started, Sam joined via Play-hub card (no duplicate, 1 session), both answered 10, results symmetric both devices ("8/10 in sync", per-Q Match labels correct), session→completed, 0 stale. **2. How Well ✅** — QA-subject 5·Quick: QA answered 5 about self, Sam joined as **guesser** (asymmetric join works), predicted 5, score+breakdown render correctly (1/5, ✓/✗ guess→actual incl. scale Q), completed, 0 stale.
- **R10 scratchpad drivers (reuse):** `r10_set_premium.js <QA|Sam> <on|off>` · `rv_gate.js`/`rv_markreveal.js` (raw-API) · `hw_drive.py <serial> <rounds>` (taps first option+Confirm per Q) · `rv_inspect.js`/`rv_sessions.js` (admin reads). Game-option taps: use uiautomator bounds, NOT fixed coords (layouts shift per question; last Q button = "Done →" not "Confirm →").
- **Admin writes:** user authorized this session (2026-06-26) → premium toggle + baseline reset now working. Baseline reset done (0 active sessions; stale 06-24/06-25 answers cleared). Premium toggle: `scratchpad/r10_set_premium.js <QA|Sam> <on|off>`.
- **Pass A ✅ (R10):** neither-premium → Desire Sync shows 🔒 + opens **paywall** ("Go deeper together"); toggled **Sam premium ON** → QA(free) Play hub badge cleared **live** + Desire Sync opens **setup (no paywall)** = couple-shared unlock holds. Code audit: all gates use `CouplePremiumChecker` except `SubscriptionScreen` (by-design own-status) + `DailyQuestionResolver` (per-user premium-question fallback — verify in Pass B/E it doesn't desync the couple's daily Q). Other 7 features share the verified path (R9 enumerated each).
- **Build:** HEAD `e6a8dee` — clean working tree (reveal feature committed: couple-key encryption, read-gated `secure` subdoc, `onAnswerWritten` both-answered copy, `onAnswerRevealed`). Rebuilt + installed on both emulators this session.
- **Daily-reveal QA (2026-06-26, live, both emulators 5554 dark / 5556 light):** **Gate (raw API):** only-1-answered → partner reads metadata 200 but content **403**, non-member **403/403**; both-answered → partners read each other **200/200**, non-member still **403/403**. **At-rest:** answer doc content-free metadata only; content in gated `secure/payload` (`enc:v1:`). **Reveal:** shows the partner's answer **both directions** (the fixed bug) — QA↔Sam. **Pushes:** `onAnswerWritten` fires (both-answered "unlocked ✨" copy is in deployed code); `onAnswerRevealed` fired live (`isRevealed` flip → "notified partner that X opened"). 0 FATAL either device. Today's test answers wiped after; baseline clean. One low-sev robustness note → `Future.md` (reveal `isRevealed` write isn't retried if it fails). Note: stale active wheel session + 06-24/06-25 unrevealed answers are pre-existing test pollution (confound the Home dashboard daily card; not the reveal feature).
- **Devices / accounts:** emulator-5554 = QA (`Y05AKO2IlTPMa0JQW1BiNIM0uzK2`) · emulator-5556 = Sam (`imDjjO…`) · paired, coupleId `Xal3Kw3gjSdn0niERYKJ`, both free (baseline restored).
- **Docs:** Playbook `ClaudeQAPlan.md` · Coverage `ClaudeQACoverage.md` · Ideas `Future.md` `## QA` · Branding `ClaudeBrandingReview.md`.
## Severity board
| Severity | Open | Fixed (pending 1 confirm) |
|---|---|---|
| P0 | 0 | 0 |
| P1 | **1** (A-201) | 0 |
| P2 | **0** | **1** (C-DARKART-001) |
| P3 | **2** (J-OBS, C-ART-EDGE-002) | **1** (C-ART-EDGE-001) |
## Issues — R12 (1 open P1 [A-201 date-match premium bypass] · 0 open P2 · 1×P2 fixed pending 1 confirm [C-DARKART-001] · 1 open P3 [J-OBS] · 1×P3 fixed pending 1 confirm [C-ART-EDGE-001])
> R11 fixed the two open art issues in the shared `BrandIllustration`/`EmptyState` helpers and verified both live
> on **both** decoupled theme directions, 0 FATAL. The 5 R10 P2 fixes were re-confirmed this round and **pruned** to the
> archived-ID line below (detail in git `9c84c36`). **Fixes for the two art issues are in the working tree (user commits).**
> Fix summary: C-DARKART-001 — `LocalAppInDarkTheme` CompositionLocal (set in `CloserTheme`) drives a config-overridden
> context (`createConfigurationContext` with `UI_MODE_NIGHT_*` from the in-app theme) so `-night` art follows the app's
> own theme, not the system. C-ART-EDGE-001 — tiled art feathers its 4 edges to transparent (`graphicsLayer{Offscreen}` +
> `drawWithContent` `BlendMode.DstIn` linear gradients) instead of a hard `clip` + `border`; `EmptyState` now routes its
> illustration through `BrandIllustration` so both fixes apply everywhere from one place.
| ID | Sev | Area | Description | Repro | Suggested fix | Status |
|---|---|---|---|---|---|---|
| A-201 | P1 | Premium / Date Match bypass (Pass A, R12) | **Premium date ideas are NOT gated — free users can view, swipe, like and match them with no paywall.** `DateIdea.isPremium` is documented as "requires an active premium entitlement," but `DateMatchRepositoryImpl.getDateIdeas()` returns `DateIdeaSeed.all` (premium ideas included, **no entitlement filter**), `DateMatchViewModel` loads them with no `CouplePremiumChecker`, and `DateMatchScreen` only renders a cosmetic `PremiumBadge()` — no lock overlay, no paywall on like/super-like. So the premium tier for Date Match is unenforced. **Escaped prior Pass A rounds** (which asserted "all gates use CouplePremiumChecker" — Date Match has NO gate). (Plan buckets "premium bypass" as P0; filed P1 as it's a content-tier subset, no security/data impact.) | 5554 (QA **free**): Play → Date Match → reject through deck → reach a **★ Premium** idea ("night camping getaway") → Like (heart) → **no paywall**, swipe accepted, deck advanced to the next premium idea ("Zipline canopy tour"); 0 FATAL, no `PERMISSION_DENIED`. | Gate premium date ideas through `CouplePremiumChecker`: either filter `isPremium` ideas out of a free couple's deck, OR intercept like/super-like on a premium idea → route to Paywall when neither partner is premium (couple-shared). Mirror the Desire Sync / Question-pack gate. | **Open (P1)** |
| C-DARKART-001 | P2 | Theme / dark-mode art (Pass C) | **Dark-mode illustrations didn't follow the IN-APP theme switch — only the system dark mode.** The in-app toggle (Settings → Appearance → Dark) swapped Compose colors via `CloserTheme(darkTheme=…)` but had no config `uiMode` override, so `painterResource` + the `drawable-night-nodpi/` variants resolved off the **system** `uiMode` → app-Dark on a light-mode phone showed **dark UI + light illustrations**. Affected all 12 `-night` illustrations. | 5554: `cmd uimode night no` (system light) → Settings → Appearance → **Dark** → before fix Security showed the **light** padlock tile on a dark screen. | **DONE:** `BrandIllustration` loads the drawable through `context.createConfigurationContext(cfg)` with `UI_MODE_NIGHT_*` set from `LocalAppInDarkTheme` (provided in `CloserTheme`). **Verified live R11 both directions:** 5554 system-light + app-Dark → **dark aubergine** art on dark screen (Security + Art-preview gallery); 5556 system-dark + app-Light → **light pastel** art on light screen; 0 FATAL, both apps alive. | **Fixed + verified live R11 (working tree; user commits)** |
| C-ART-EDGE-001 | P3 | Art / edge treatment (Pass C+H) | **Displayed illustrations had hard edges instead of fading into the screen**`BrandIllustration` hard-`clip`ped art to `RoundedCornerShape` + a hairline `border`, and `EmptyState` rendered raw `painterResource`, so the near-white tile read as a crisp rounded-rectangle boundary (esp. on dark). | Any art screen: hard tile edge/outline instead of feathering. | **DONE:** tiled art now feathers its 4 edges to transparent (`graphicsLayer{compositingStrategy=Offscreen}` + `drawWithContent` `BlendMode.DstIn` linear gradients, ~14% inset); `clip`+`border` removed; `EmptyState` routes through `BrandIllustration`. **Verified live R11:** Art-preview gallery + Security padlock melt softly into the surface on both themes; transparent art (`tile=false`) unaffected. | **Fixed + verified live R11 (working tree; user commits)** |
| C-ART-EDGE-002 | P3 | Art / hard edges on direct-call heroes (Pass C, R12) | **Hero illustrations rendered via direct `painterResource` (not the shared `BrandIllustration`) still show hard edges on dark theme** — the R11 C-ART-EDGE-001 feather fix only covered `BrandIllustration`/`EmptyState`. The Today "Weekend Side Quest" daily-question hero (light/pink art) renders as a **bright rounded-rect block with a hard bottom edge on the dark screen**. These direct-call heroes have **no `-night` variant** either. Likely same class: paywall couple art, onboarding, Home tonight-prompt/partner-activation, spin-wheel hero, pack art (all direct `painterResource(illustration_*)`). Matches the user's "all images should fade into the screen" request (only partially satisfied by C-ART-EDGE-001). | 5554 (dark): Today tab → daily question → "Weekend Side Quest" hero shows a hard bright edge against the dark card. | Route these heroes through `BrandIllustration` (gains feather + theme-variant), OR apply the same `featherEdges()` treatment at each call site; consider `tile`/`hero` variants. Verify each direct `painterResource(R.drawable.illustration_*)` site listed in the R12 grep. | **Open (P3)** |
| J-OBS | P3 | A11y / touch targets | A few conversation icon-buttons measure **~4245dp wide** (48dp tall) — single-axis marginal miss of the 48dp target; fully operable. Most controls are 48dp. | Pass J: uiautomator bounds on conversation → 23 clickables `<126px` wide. | Bump those icon-buttons to 48dp min (e.g. `Modifier.minimumInteractiveComponentSize()` / `size(48.dp)`). | **Open (P3, non-blocking)** |
## Resolved & confirmed (archived — full detail in git history)
A-001 · A-003 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · C-DS-001 · **C-HOME-001** · **C-NAV-001** · **C-NAV-002** · **C-NAV-003** · **C-PW-001** · **C-SEC-001** · D-001 · E-001 · E-002 · E-003 · **E-GAME-002** · **E-GAME-003** · E-OBS · F-OBS · F-RACE-001 · **I-001** · **I-002** — all fixed and re-verified (R11 pruned the 5 R10 P2 fixes — C-HOME-001 single Home card · C-NAV-002 `popUpTo(WHEEL_SESSION){inclusive}` present + R10-live · C-NAV-003 single app bar re-confirmed live · C-PW-001 dark paywall pills legible re-confirmed live · C-SEC-001 recovery row active for accepter re-confirmed live — all committed in `9c84c36`; E-GAME-003 `onGamePartFinished` deployed + committed `2cd0af6`) (E-GAME-002 confirmed live R10: `startNotifiedAt` set + partner_started_game queued to right partner + foreground banner + Join→joined active ToT at same Q1; commits 6e79cd9/38fdc6d) (commits in history; F-RACE-001 re-confirmed R8; **I-001** query→`whereIn(dayKeys)` + **I-002** Long-score→`Number.toInt()`, fixed `ab29f6b`, re-confirmed live R9: 0 outcomes denials/CCE). Pruned per the one-confirmation-round rule. (C-OBS / `outcomes` list / SubscriptionScreen per-user gate = investigated, **not bugs**.)
## Security cornerstone — clean (Pass D, deep dive, Round 7)
- **D1 at-rest:** chat text + `lastMessagePreview` + all 4 game-answer collections (ToT / How Well / Desire Sync / Wheel, both users) + Memory Lane capsules + date-swipe actions = `enc:v1:`. No plaintext content; only metadata in clear.
- **D2/D3 access:** non-member denied **all** reads/writes (raw Firestore REST → 403); real premium write `users/{uid}/entitlements/premium` denied (server-only → **no self-grant**); cross-couple denied.
- **D4 keys:** couple key phrase-wrapped (argon2id); recovery phrase server-blind; `encryptedRecoveryPhrase` wiped on acceptance; plaintext `inviteCode` not exploitable (invite readable only by inviter; no code-encrypted secret persisted).
- **Robustness:** malformed/abusive deep-link intents (unknown type, missing extras, injection/path-traversal) → 0 crash; killed-state cold-start chat deep-link → conversation loads.
## ⛔ "All notifications broken / app opens-and-closes" — ROOT CAUSE = splash crash (FIXED R10)
**The actual cause was NOT routing — it was a crash in the splash-screen exit animation on notification cold-starts.**
`MainActivity.onCreate` (added in **`95cad84`, 2026-06-25**) set `splashScreen.setOnExitAnimationListener { provider -> provider.iconView.animate()… }`.
On a **notification / PendingIntent cold-start** the OS hands the splash view over **without an icon** (`SplashScreenView: Icon: view: null`),
and `provider.iconView` throws an internal `NullPointerException` (`SplashScreenViewProvider$ViewImpl31.getIconView`) →
`onCreate` crashes → "Force finishing activity" → **the app opened and immediately closed on EVERY notification tap**
(chat, game-start, results — all of them, because they share the cold-start path). This is why it looked like "all
notifications broke again." Normal launcher cold-starts were fine (icon present), which masked it.
- **Why my earlier `am start` tests missed it:** shell `am start` uses a different splash transfer than the FCM
PendingIntent handover (the SysUILaunch remote transition), so it didn't hit the null-icon handover. Also `am
force-stop` can't receive FCM at all (stopped-package broadcast exclusion) — must use `am kill` to test killed-app push.
- **Fix (R10, working tree):** `MainActivity` wraps the icon scale in `runCatching` (best-effort) and the view fade in
`runCatching { … }.onFailure { provider.remove() }` so the splash is **always** removed and onCreate **never** crashes.
- **Verified live:** real FCM notification → killed (`am kill`) Closer2 → tapped the OS notification → cold-start logs
`Icon: view: null` then `remove starting view`, **0 FATAL, process stays alive, lands on Home** (was the crash).
Normal launcher cold-start still animates + works.
## Notification deep-link routing — SINGLE mechanism (do NOT reintroduce a second one)
**Invariant:** an app-posted notification carries the resolved route in **one** place — the `app_route` **extra**
and routing is `MainActivity.deepLinkRouteFromIntent``pendingDeepLink``AppNavigation` `navigateRoute`. Do **not**
also set an `ACTION_VIEW` + `closer://` **data Uri** on the notification intent: for routes that have a `navDeepLink`
(conversation / answer_reveal / daily_question / question_thread / home / play) the NavController auto-handles that Uri
**in addition** to `pendingDeepLink` → a race/duplicate nav. That dual path is what kept re-breaking notifications.
- **Why it broke "again" (root cause, traced via git):** `aaab768`/`1b9d8cf`/`b9b1560` built routing on the
`closer://` **data Uri** (NavController auto-handle) + a `pendingDeepLink` gated on **`currentRoute == HOME`**;
then `38fdc6d` added the `app_route` extra **on top** without removing the data Uri → two mechanisms for the same
tap. The HOME-only gate also meant a **warm** tap from any non-Home tab set `pendingDeepLink` but never consumed it.
- **Fix (R10, working tree):** `PartnerNotificationManager.showNotification` no longer sets `ACTION_VIEW`/data Uri —
`app_route` extra only. `AppNavigation` pendingDeepLink gate broadened from `== HOME` to `!in entryRoutes` (fires once
past onboarding, on any main screen). **Verified live (0 FATAL):** killed-app tap → chat opens the conversation; all
4 game **results** pushes (`partner_finished_game`) load the real per-session results (wheel "Here's how you each
answered" · This-or-That "5/5 in sync" · How Well "Perfect read 5/5" · Desire Sync "5 shared desires"); app_route-only
path (no Uri) loads results; **warm tap from Settings now routes** (was the stuck case).
## Round history (one line each)
- **R11 (2026-06-27) — confirmation round, FLAWLESS (0 open P0P2).** Fixed the last open P2 **C-DARKART-001** (dark-mode
art now follows the in-app theme: `LocalAppInDarkTheme` CompositionLocal in `CloserTheme``BrandIllustration` loads the
`-night` drawable via a `createConfigurationContext` whose `UI_MODE_NIGHT_*` comes from the app theme, not the system) and
the open P3 **C-ART-EDGE-001** (tiled art feathers its 4 edges to transparent via `graphicsLayer{Offscreen}` +
`BlendMode.DstIn` gradients instead of hard `clip`+`border`; `EmptyState` now routes through `BrandIllustration`). Verified
**live both decoupled theme directions** (5554 system-light+app-Dark → dark aubergine art; 5556 system-dark+app-Light →
light pastel art; both feathered), 0 FATAL, both apps alive. Re-confirmed + pruned the 5 R10 P2 fixes (C-HOME-001 single
Home card · C-NAV-002 wheel-back `popUpTo` present · C-NAV-003 single app bar live · C-PW-001 dark paywall pills legible
live · C-SEC-001 recovery row active for accepter live). Entrypoint launch-integrity smoke green on the fresh APK (launcher
+ notification cold-starts open & stay — splash-crash class clean). Art fixes in working tree; everything else committed
(`2cd0af6`).
- **E-GAME-003 (2026-06-27) — FIXED+VERIFIED+DEPLOYED: async-game first-finisher left the waiting partner un-notified.**
Async games (this_or_that/wheel/how_well/desire_sync) write answers to `couples/{c}/{gameType}/{sessionId}` and the
session only flips to `completed` when BOTH answer — so `onGameSessionUpdate` (watches the session doc) never fired on
a single finish, and the waiting partner got nothing ("Closer2 finished a game but the partner was never notified").
Fix = new Cloud Function **`onGamePartFinished`** (trigger on the answer doc; on exactly-1 answer, idempotently claim
`partFinishNotifiedAt` on the session + send `partner_completed_part` "X finished their part — your turn to play!").
Verified live: QA finished ToT part → session `partFinishNotifiedAt=true`, Sam queue got 1 `partner_completed_part`,
posted on Sam's device, tap → opened ToT, 0 FATAL. Deployed (`onGamePartFinished` created, `onGameSessionUpdate`
updated). Funcs source uncommitted (user commits).
- **R10 (2026-06-26) — FULL ClaudeQAPlan run AJ + fix phase.** Found 5 P2 in report-only passes, fixed + verified all live: C-HOME-001 (Home dup pending card), C-NAV-002 (wheel results→BACK re-entered finished session), C-NAV-003 (duplicate app bar on Wheel History/PartnerHome), C-PW-001 (dark paywall pills light-on-light), C-SEC-001 (Security read wrong recovery-phrase store → accepter couldn't view phrase; E2EE recovery itself sound). E-GAME-002 confirmed live (startNotifiedAt set + partner_started_game→right partner + foreground banner + Join→joined active ToT) → pruned. D1D7 security clean (non-member denied all raw-API reads/writes, no self-grant, secure-subdoc gate correct, argon2id+AAD=coupleId). Concurrency double-start→1 session. Perf jank 5.53% / a11y font-2.0 reflows — no regression. Build OK, both emulators reinstalled, 0 FATAL, content still `enc:v1:`. App fixes in working tree (user commits).
- **Notif→game fix + dark art + QA sweep (2026-06-26, uncommitted).** **E-GAME-001 (P1, FIXED+VERIFIED):** game notifications "led nowhere" — backgrounded/warm taps landed on Home (MainActivity was standard launch mode → `onNewIntent` never delivered the tap's extras → `pendingDeepLink` unset), and even when routed, the game screen showed *setup* instead of joining (one-shot `getActiveSessionForCouple` raced the post-push Firestore sync → returned stale-empty). Fixes: `AndroidManifest` `MainActivity launchMode=singleTop` + `QuestionSessionRepositoryImpl.getActiveSessionForCouple` now SERVER-first (cache fallback). **Verified live:** Sam backgrounded → taps partner_started_game → lands IN the active This-or-That (1/10), joined, no duplicate session; back-stack sane (game→back→Home→back→exit, C-NAV-001 holds). Generic across game types (shared routing + getActiveSession). **Dark-theme art:** 12 `_dark` variants → `drawable-night-nodpi/` (light names) so dark mode auto-swaps; verified live (Security shows the aubergine variant on dark; light unchanged). **QA sweep:** tabs both themes, deep-link back-stack, all 12 illustrations both themes — **0 FATAL**, baseline intact.
- **Brand art drop (2026-06-26) — wired + QA-swept, 0 issues.** All 11 generated illustrations (A1A12, source gitignored) wired into their screens via shared `EmptyState` + new `BrandIllustration` helper (commits `077a408`→`5868d06`). **Complete both-theme sweep:** in-context dark **and** light verified for Bucket List (A6), Quiet hours (A9), Security (A11), Delete account (A12) — all render as crisp rounded tiles, on-brand, no clipping/contrast issue; A1 (transparent), A3 (banner) + the empty-only states (A2/A4/A5/A8/A10, unreachable on the baseline couple) verified via the debug Art-preview gallery on both themes + the proven shared tile. **0 FATAL/ANR** both devices; baseline intact (0 sessions/outcomes). Process catch: 5556 was on a stale build mid-sweep → reinstalled current, both now on `768f511`. Details in `ClaudeBrandingReview.md`.
- **R9** — clean confirmation round (**0 new findings**): confirmed + pruned I-001/I-002 (0 outcomes denials/CCE on the fixed build); swept deferred Pass C deep/list screens (Answer History, Activity, Bucket List, Date Match/Matches — both themes) + Pass F network (offline cache render + clean reconnect). 0 open P0P2.
- **R8** — F-RACE-001 re-confirmed + pruned; Passes I (perf) + J (a11y) run; found+fixed+verified **I-001 & I-002** (outcomes read: query rules-denied + Long/Int parse CCE → "Your Progress" was silently dead). 0 open P0P2.
- **R7** — multi-angle security/concurrency deep dive → cornerstone fully clean; F-RACE-001 found + fixed + verified. 0 new open.
- **R6** — branding drop + Future.md backlog regression (white-keyhole icons/loader/splash, inclusive gender, copy, rate-limit split, results-push suppression, paywall retry/offline) → 0 new open.
- **R5** — Cloud Functions deployed (E-OBS channel fix, E-003 results routing) + new Pass G (account creation / fake-account abuse) clean → 0 open.
- **R1R4** — baseline Passes AF report-only; every P0P2 found was fixed + verified (see archived IDs).
## Operational constants
- **Execution mode:** autonomous run-to-completion — don't stop; fix blockers inline; cycle fix→re-QA until flawless. Don't hand back when context fills — re-read this run-state + coverage after any compaction. Commit before interruptible work; recover stuck sessions via the session-start ritual.
- **Standing authorization (user, 2026-06-24):** may `firebase deploy --only firestore:rules` + has admin access (Firestore reads/writes/seeds + entitlement toggles) — run without pausing. Only the macOS requirement for iOS (Parts 2/3) is a hard stop.
- **Hardening backlog → Future.md:** App Check not enforced on Firestore; `users/{uid}` update rule allows arbitrary non-`hasPremium` fields (tighten to a field allowlist).