feat: add automated theme-mismatch scanner to Pass C methodology (Tier 1-3)

This commit is contained in:
null 2026-06-28 10:20:41 -05:00
parent 37ed7cebec
commit fe3ea7715c
12 changed files with 204 additions and 28 deletions

View File

@ -1,7 +1,7 @@
# Claude QA Coverage Matrix # Claude QA Coverage Matrix
> **Resume anchor — current status only.** Statuses: `pass | fail→id | todo | n/a | not implemented→Future.md | blocked→id`. > **Resume anchor — current status only.** Statuses: `pass | fail→id | todo | n/a | not implemented→Future.md | blocked→id`.
> Build HEAD `c31eea2` + **R15 working-tree changes** (functions + rules **deployed to prod**; client rebuilt+installed both emulators). Position + verdict: see `ClaudeReport.md` run-state. **Verdict: R15 = gap-closing round (Passes L/M/N/P + smoke) — found & FIXED M-001 (P2 quiet hours).** Quiet hours didn't suppress backgrounded/killed partner pushes (local-only); fixed via server-side fail-open suppression + client window/tz sync + rules allowlist — verified live (fn log suppress vs notify). L (chat E2E render + decrypt + receipts + reactions + at-rest), P (UI copy + 6103-Q bank, clean), N (daily-Q/reveal gate), smoke 6/6 GREEN. **0 open P0P2** (M-001 fixed, pending 1 confirm); 2 P3 brand backlogs open. 0 FATAL. App+functions+rules changes in working tree (user commits); functions+rules already deployed. > Build HEAD `c31eea2` + **R15 working-tree changes** (functions + rules **deployed to prod**; client rebuilt+installed both emulators). Position + verdict: see `ClaudeReport.md` run-state. **Verdict: R15 = gap-closing round (Passes L/M/N/P + smoke) — found & FIXED M-001 (P2 quiet hours).** Quiet hours didn't suppress backgrounded/killed partner pushes (local-only); fixed via server-side fail-open suppression + client window/tz sync + rules allowlist — verified live. Then drove Pass N: **N-001 (P1) Bucket List fully non-functional → FIXED+verified live**; **N-002 (P2) Date Builder "Create Plan" no-op (incomplete feature) → OPEN, needs product decision.** L (chat E2E render+decrypt+receipts+reactions+at-rest), P (UI copy + 6103-Q bank) clean; smoke 6/6 GREEN. **1 open P2 (N-002); 2 fixed pending confirm (M-001, N-001); 2 P3 brand backlogs.** 0 FATAL. functions+rules deployed to prod; client changes in working tree (user commits), debug APK installed both emulators.
> >
> **Scope expanded (plan review):** the playbook now has first-class passes **KO** (billing money-path · messaging/chat E2E · functional settings · daily-Q/outcomes/interactive · release-build/store-readiness). These surface **coverage GAPS, not defects** — the recurring defect bar is clean, but **K (real purchase/restore/cancel path), L (full chat), M (settings take-effect), N (outcomes/Bucket-List/Date-Builder), O (minified release + App Check + store)** are `todo`/`partial`/`blocked→needs-device`. **Next-priority work = close these (start L + M on-emulator; K + O need a real device / pre-ship).** > **Scope expanded (plan review):** the playbook now has first-class passes **KO** (billing money-path · messaging/chat E2E · functional settings · daily-Q/outcomes/interactive · release-build/store-readiness). These surface **coverage GAPS, not defects** — the recurring defect bar is clean, but **K (real purchase/restore/cancel path), L (full chat), M (settings take-effect), N (outcomes/Bucket-List/Date-Builder), O (minified release + App Check + store)** are `todo`/`partial`/`blocked→needs-device`. **Next-priority work = close these (start L + M on-emulator; K + O need a real device / pre-ship).**
> >
@ -25,11 +25,11 @@
| K — Billing & subscription lifecycle | Gate (couple-shared unlock) verified via admin toggle (Pass A) + Premium-unlock modal + `onEntitlementChanged` push live (R13/R14). **Real money path (purchase/restore/cancel→expiry-relock/refund/plan-switch) NOT tested** — needs a real device + Play sandbox. | ⚠️ **todo** — money path `blocked→needs-device`; gate ✅ | | K — Billing & subscription lifecycle | Gate (couple-shared unlock) verified via admin toggle (Pass A) + Premium-unlock modal + `onEntitlementChanged` push live (R13/R14). **Real money path (purchase/restore/cancel→expiry-relock/refund/plan-switch) NOT tested** — needs a real device + Play sandbox. | ⚠️ **todo** — money path `blocked→needs-device`; gate ✅ |
| L — Messaging & chat (E2E) | R15: conversation render driven live — decrypt **both dirs**, attribution, timestamps, **Seen** receipt, ❤️ **reaction**, ordering, day-separators, voice-note + image bubbles, E2E composer lock glyphs; inbox decrypted previews **no `enc:` leak**; live QA→Sam send delivered; at-rest `enc:v1:`. Remaining: failed-send/offline retry, delete-message, fresh image/voice send, Discuss-thread live send. | ✅ **pass (core)** — text/decrypt/receipts/reactions/inbox/at-rest verified; 4 sub-items carry | | L — Messaging & chat (E2E) | R15: conversation render driven live — decrypt **both dirs**, attribution, timestamps, **Seen** receipt, ❤️ **reaction**, ordering, day-separators, voice-note + image bubbles, E2E composer lock glyphs; inbox decrypted previews **no `enc:` leak**; live QA→Sam send delivered; at-rest `enc:v1:`. Remaining: failed-send/offline retry, delete-message, fresh image/voice send, Discuss-thread live send. | ✅ **pass (core)** — text/decrypt/receipts/reactions/inbox/at-rest verified; 4 sub-items carry |
| M — Settings & account management | R15: **M-001 (quiet hours) FIXED + verified live** (server-side fail-open suppression); per-type notif toggle take-effect confirmed live (server-enforced; field flips in Firestore; toggle-off → 0 delivery); theme/DataStore persistence across relaunch ✅; biometric lock code-sound (cold-start re-lock; background-resume observation → Future.md). Remaining: edit-profile persist, unpair/delete-cascade (disruptive — deferred). | ✅ **pass (core)** — M-001 fixed (pending 1 confirm); unpair/delete deferred | | M — Settings & account management | R15: **M-001 (quiet hours) FIXED + verified live** (server-side fail-open suppression); per-type notif toggle take-effect confirmed live (server-enforced; field flips in Firestore; toggle-off → 0 delivery); theme/DataStore persistence across relaunch ✅; biometric lock code-sound (cold-start re-lock; background-resume observation → Future.md). Remaining: edit-profile persist, unpair/delete-cascade (disruptive — deferred). | ✅ **pass (core)** — M-001 fixed (pending 1 confirm); unpair/delete deferred |
| N — Daily Q / reveal / check-ins / interactive | R15: daily-Q + **reveal both-answered gate** render confirmed live (privacy copy exemplary). Outcomes loop / Bucket List CRUD / Date Builder save / Activity feed render-clean (prior rounds) — not re-driven this round. | ⚠️ **partial** (core render-clean; CRUD loops carry) | | N — Daily Q / reveal / check-ins / interactive | R15 (driven): daily-Q + **reveal both-answered gate** ✓; **Bucket List CRUD FIXED+verified live (N-001)** — add(`enc:v1:`)/complete/delete/list; Outcomes/Your Progress code-correct (resolves coupleId, submits); **Date Builder N-002 (P2) — "Create Plan" no-op, incomplete feature (open)**; Activity feed render-checked (prior). | ⚠️ **mostly pass** — N-001 fixed (pending confirm); **N-002 open (needs product decision)** |
| O — Release build & store readiness | **Not started.** All QA to date is on the **debug** APK. Minified release build, signing/AAB, App Check enforcement, i18n/RTL, App-Links, Play Data-Safety = pre-ship gate, not yet run. | ❌ **todo (pre-ship gate)** | | O — Release build & store readiness | **Not started.** All QA to date is on the **debug** APK. Minified release build, signing/AAB, App Check enforcement, i18n/RTL, App-Links, Play Data-Safety = pre-ship gate, not yet run. | ❌ **todo (pre-ship gate)** |
| P — Content, copy & language | R15: UI-microcopy swept (warm/inclusive; debug rows `BuildConfig.DEBUG`-gated; friendly error fallbacks; on-brand privacy copy) + **question-bank audit live: 6103 Qs — 0 empty, 0 exact dupes, 0 placeholder tokens, complete/mutually-exclusive answer configs, good type variety, consent-framed sensitive content.** No typos/off-voice/non-inclusive copy found. | ✅ **pass** — copy + question bank clean | | P — Content, copy & language | R15: UI-microcopy swept (warm/inclusive; debug rows `BuildConfig.DEBUG`-gated; friendly error fallbacks; on-brand privacy copy) + **question-bank audit live: 6103 Qs — 0 empty, 0 exact dupes, 0 placeholder tokens, complete/mutually-exclusive answer configs, good type variety, consent-framed sensitive content.** No typos/off-voice/non-inclusive copy found. | ✅ **pass** — copy + question bank clean |
**Archived issue IDs (fixed + confirmed, detail in git):** A-001 · A-003 · A-201 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · C-DARKART-001 · C-DARK-UI-001 · C-DARK-UI-002 · C-DARK-UI-003 · C-DS-001 · C-ART-EDGE-001 · C-ART-EDGE-002 · 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 · J-OBS. **0 open P0P2; 1 fixed pending confirm (M-001 quiet hours); 2 open P3 brand backlogs.** **Archived issue IDs (fixed + confirmed, detail in git):** A-001 · A-003 · A-201 · A-OBS · B-001 · B-002 · B-003 · B-004 · C-CC-001 · C-DARKART-001 · C-DARK-UI-001 · C-DARK-UI-002 · C-DARK-UI-003 · C-DS-001 · C-ART-EDGE-001 · C-ART-EDGE-002 · 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 · J-OBS. **R15: 1 open P2 (N-002 Date Builder); 2 fixed pending confirm (M-001 quiet hours, N-001 Bucket List); 2 open P3 brand backlogs.**
--- ---
@ -122,7 +122,7 @@ Route smoke-test checklist (re-runnable: `dumpsys gfxinfo closer.app reset` →
--- ---
## Round history (one line each) ## Round history (one line each)
- **R15** — gap-closing round (Passes L/M/N/P + regression smoke); **found + FIXED M-001 (P2 quiet hours)** — local-only window didn't suppress backgrounded/killed partner pushes; fixed via server-side fail-open `recipientInQuietHours()` in the 4 partner-action senders + client window/tz sync to `users/{uid}` + rules allowlist; verified live (fn log suppress vs notify; positive control delivers); deployed prod. L chat-core, P copy+question-bank (6103 Qs), N daily-Q/reveal verified clean; smoke 6/6 GREEN. Corrected stale "users/{uid} allows arbitrary fields" claim (there's an allowlist). - **R15** — gap-closing round (Passes L/M/N/P + regression smoke); **3 bugs found, 2 fixed.** **M-001 (P2 quiet hours)** — local-only window didn't suppress backgrounded/killed partner pushes; fixed via server-side fail-open `recipientInQuietHours()` in the 4 partner-action senders + client window/tz sync + rules allowlist; verified live (fn log suppress vs notify); deployed prod. **N-001 (P1) Bucket List fully non-functional** (coupleId never set → all CRUD no-ops) — FIXED (VM resolves couple in init) + verified live (add `enc:v1:`/complete/delete/render). **N-002 (P2) Date Builder "Create Plan" no-op** — incomplete feature (dateIdeaId never wired, coupleId empty, prefs never displayed) — OPEN, needs product decision. L chat-core, P copy+question-bank (6103 Qs) clean; smoke 6/6 GREEN. Corrected stale "users/{uid} allows arbitrary fields" claim (there's an allowlist).
- **R14** — full fresh AJ ClaudeQAPlan run (pure QA, no code), FLAWLESS, 0 new findings: confirmation round on the R13 build — premium enforcement + couple-shared unlock + entitlement push (live); Desire Sync/How Well/Spin-the-Wheel full 2-device + first-finisher nudge; Memory Lane create+seal, CC resume, Date Match deck; decoupled-theme-art mandate; cornerstone live (403s + enc:v1:); offline + process-death; jank 5.25%; J-OBS 48dp holds. The 5 R13 fixes held → pruned (archived line). - **R14** — full fresh AJ ClaudeQAPlan run (pure QA, no code), FLAWLESS, 0 new findings: confirmation round on the R13 build — premium enforcement + couple-shared unlock + entitlement push (live); Desire Sync/How Well/Spin-the-Wheel full 2-device + first-finisher nudge; Memory Lane create+seal, CC resume, Date Match deck; decoupled-theme-art mandate; cornerstone live (403s + enc:v1:); offline + process-death; jank 5.25%; J-OBS 48dp holds. The 5 R13 fixes held → pruned (archived line).
- **R13** — open-backlog fix pass + full fresh AJ, FLAWLESS (0 open P0P3): fixed C-DARK-UI-001 (ToT dark redesign), C-DARK-UI-002 (check-in label), C-DARK-UI-003 (bottom insets), C-ART-EDGE-002 (8 opaque heroes feathered), J-OBS (48dp targets); confirmed A-201 live→pruned; shipped Premium-unlock modal (one-time, both partners, couple-shared, verified live). Pass D cornerstone re-verified LIVE (non-member 403, self-grant 403, member 200, at-rest enc:v1:). Diff UI-only → E/F/G carried. 0 FATAL both emulators. - **R13** — open-backlog fix pass + full fresh AJ, FLAWLESS (0 open P0P3): fixed C-DARK-UI-001 (ToT dark redesign), C-DARK-UI-002 (check-in label), C-DARK-UI-003 (bottom insets), C-ART-EDGE-002 (8 opaque heroes feathered), J-OBS (48dp targets); confirmed A-201 live→pruned; shipped Premium-unlock modal (one-time, both partners, couple-shared, verified live). Pass D cornerstone re-verified LIVE (non-member 403, self-grant 403, member 200, at-rest enc:v1:). Diff UI-only → E/F/G carried. 0 FATAL both emulators.
- **R12** — FRESH FULL AJ run + fix phase, FLAWLESS (0 open P0P2): found+fixed **A-201** (P1 Date Match premium bypass — gated via CouplePremiumChecker→Paywall, verified live); 4 async games full 2-device E2E; security cornerstone live-clean (non-member 403 read+write, self-grant 403); smoke 6/6; jank 4.10%; new P3 C-ART-EDGE-002 (hero edges, deferred); C-DARKART-001+C-ART-EDGE-001 held→pruned; Pass A retrospective added (badge≠gate). - **R12** — FRESH FULL AJ run + fix phase, FLAWLESS (0 open P0P2): found+fixed **A-201** (P1 Date Match premium bypass — gated via CouplePremiumChecker→Paywall, verified live); 4 async games full 2-device E2E; security cornerstone live-clean (non-member 403 read+write, self-grant 403); smoke 6/6; jank 4.10%; new P3 C-ART-EDGE-002 (hero edges, deferred); C-DARKART-001+C-ART-EDGE-001 held→pruned; Pass A retrospective added (badge≠gate).

View File

@ -430,7 +430,11 @@ Games: This or That, How Well Do You Know Me, Desire Sync, Connection Challenges
`pass`). Coverage row format: `game × starter × join-entry × premium-state × depth/count × lifecycle-edge × result`; `pass`). Coverage row format: `game × starter × join-entry × premium-state × depth/count × lifecycle-edge × result`;
only `pass` when start/join/play/finish/reopen/recover are all verified. only `pass` when start/join/play/finish/reopen/recover are all verified.
### Pass C — Visual pass, light + dark, ALL screens ### ⛔ Pass C — Visual pass, light + dark, ALL screens (MANDATORY: run scan BEFORE sweep)
> **⛔ CLAUDE: Run the automated theme scan (below, Automated Tier 1) before starting the visual sweep.
> Read the output at `/tmp/claude-theme-scan-<date>.md` and file findings to ClaudeReport.md first.
> The sweep must verify every flagged screen in BOTH themes.**
Every route in `core/navigation/AppRoute.kt` (~50), in **both** modes: text contrast/readability (no invisible/ Every route in `core/navigation/AppRoute.kt` (~50), in **both** modes: text contrast/readability (no invisible/
low-contrast), no clipping/overflow/ellipsis breakage, icons visible, backgrounds adapt, controls legible. Groups: low-contrast), no clipping/overflow/ellipsis breakage, icons visible, backgrounds adapt, controls legible. Groups:
auth/onboarding/pairing (fresh acct); Home (solo + paired); Play + every game; Today + reveal/history; Messages auth/onboarding/pairing (fresh acct); Home (solo + paired); Play + every game; Today + reveal/history; Messages
@ -454,8 +458,120 @@ Account); Paywall; Your Progress/Activity; Recovery.
helper / direct `painterResource` tile also feathers.) Fix pattern (if it regresses): feather the edges to transparent, helper / direct `painterResource` tile also feathers.) Fix pattern (if it regresses): feather the edges to transparent,
or a vignette matching the surface, or ship transparent-edged art — applied in the shared `BrandIllustration`/`EmptyState` or a vignette matching the surface, or ship transparent-edged art — applied in the shared `BrandIllustration`/`EmptyState`
helpers so it's consistent everywhere. helpers so it's consistent everywhere.
- **Probe:** `ui/theme/Theme.kt` hardcoded brand colors + chat's custom `closerBackgroundBrush` — verify dark mode - **⛔ CLAUDE — RUN THE AUTOMATED THEME SCAN FIRST (MANDATORY, BEFORE THE VISUAL SWEEP):**
truly adapts; grep screens for hardcoded `Color(0x...)`. Do NOT start the manual visual sweep until the automated scan has completed and you have reviewed its results.
The scan is run from the project root (`/home/kaspa/.openclaw/Projects/relationship-app/`). It produces a
report in `/tmp/claude-theme-scan-<date>.md`. Read that report and file all findings to `ClaudeReport.md`
BEFORE the visual sweep — screens with known hardcoded colors need extra scrutiny.
**Tier 1 — Hardcoded container & surface color scan (catches ~80% of theme mismatches)**
Every hit below that sets a SURFACE / CONTAINER / BACKGROUND / DIVIDER to a hardcoded value (not a
`MaterialTheme.colorScheme.*` token) is an automated finding. Classify by failure mode:
```bash
cd /home/kaspa/.openclaw/Projects/relationship-app
SCAN_OUTPUT="/tmp/claude-theme-scan-$(date +%Y%m%d).md"
echo "# Theme Mismatch Scan — $(date)" > "$SCAN_OUTPUT"
```
**Pattern 1 — Surface/Card/Dialog/ModalBottomSheet with hardcoded color (CRITICAL):**
These wrap content with a colored container. A hardcoded color here means the background will NOT swap in
dark mode, producing invisible text, white-on-white, or light-on-light. This is EXACTLY the AddItemDialog
pattern (`Surface(color = Color.White)` inside a dark-themed app).
```bash
grep -rnE '(Surface|Card|Dialog|AlertDialog|ModalBottomSheet|BottomSheet|Scaffold)\s*\([^)]*' app/src/main/java/app/closer/ui/ --include="*.kt" \
| grep -iE 'color = Color\.(White|Black|Red|Blue|Green|Yellow|Cyan|Magenta|Gray|LightGray|DarkGray|(0x[0-9A-F]{8}))' \
| grep -ivE '(Color\.Transparent|materialColorScheme|colorScheme\.)' \
| while read line; do echo "🔴 CRITICAL $line"; done >> "$SCAN_OUTPUT"
```
**Pattern 2 — Background modifier with hardcoded color (CRITICAL):**
Same as Pattern 1 but applied as a modifier. The screen's background won't adapt.
```bash
grep -rnE 'Modifier\.(background|fillMaxSize|fillMaxWidth)\([^)]*Color\.(White|Black|[A-Z][a-z]+)\b' app/src/main/java/app/closer/ui/ --include="*.kt" \
| grep -ivE '(Color\.Transparent|colorScheme\.|isDarkTheme|LocalAppInDarkTheme)' \
| while read line; do echo "🔴 CRITICAL $line"; done >> "$SCAN_OUTPUT"
grep -rn 'Modifier\.background(Color(' app/src/main/java/app/closer/ui/ --include="*.kt" \
| grep -ivE '(colorScheme|isDarkTheme|LocalAppInDarkTheme)' \
| while read line; do echo "🔴 CRITICAL $line"; done >> "$SCAN_OUTPUT"
```
**Pattern 3 — Component color overrides with hardcoded colors (MAJOR):**
Buttons, TextFields, Tabs, Dividers with explicitly set container/content colors that won't adapt.
```bash
grep -rnE '(buttonColors|TextFieldDefaults\.colors|TabRowDefaults\.colors|SwitchDefaults\.colors)\s*\([^)]*color = Color\.' app/src/main/java/app/closer/ui/ --include="*.kt" \
| grep -ivE '(colorScheme|Color\.Transparent)' \
| while read line; do echo "🟠 MAJOR $line"; done >> "$SCAN_OUTPUT"
grep -rnE '(Divider|HorizontalDivider)\s*\([^)]*color = Color\.' app/src/main/java/app/closer/ui/ --include="*.kt" \
| while read line; do echo "🟠 MAJOR $line"; done >> "$SCAN_OUTPUT"
```
**Pattern 4 — Text/Icon color hardcoded on a themed surface (MAJOR):**
Text or icon tint set to `Color.White` or `Color(0xFF...)` while sitting on a surface that may adapt.
Some of these are intentional (white text on a purple button is correct). Flag them; during visual sweep,
confirm each is on a properly-themed container.
```bash
grep -rnE '(Text|Icon)\s*\([^)]*color = Color\.(White|Black|(0x[0-9A-F]{8}))' app/src/main/java/app/closer/ui/ --include="*.kt" \
| grep -ivE '(colorScheme|isDarkTheme|Theme\.kt)' \
| while read line; do echo "🟡 MINOR-REVIEW $line"; done >> "$SCAN_OUTPUT"
```
**Pattern 5 — Direct painterResource bypassing BrandIllustration (MAJOR):**
Any screen using `painterResource(R.drawable.illustration_*)` or `painterResource(R.drawable.pack_art_*)`
directly instead of going through `BrandIllustration` means its art will NOT follow the decoupled in-app
theme (C-DARKART-001). Each hit needs either conversion to `BrandIllustration` or verification that the
screen's art already has manual theme handling. Exclude glyphs (always the same regardless of theme).
```bash
grep -rnE 'painterResource\(R\.drawable\.(illustration_|pack_art_)' app/src/main/java/app/closer/ui/ --include="*.kt" \
| while read line; do echo "🔴 CRITICAL $line"; done >> "$SCAN_OUTPUT"
```
**Pattern 6 — Border with hardcoded color (MINOR-MAJOR):**
Borders set with hardcoded colors that match a light surface won't contrast on dark.
```bash
grep -rnE 'Modifier\.border\([^)]*Color\.(White|Black|[A-Z][a-z]+)\b' app/src/main/java/app/closer/ui/ --include="*.kt" \
| grep -ivE '(Color\.Transparent|colorScheme)' \
| while read line; do echo "🟡 REVIEW $line"; done >> "$SCAN_OUTPUT"
```
**Tier 2 — Theme definition validation (run once per QA round, or after any Theme.kt change)**
Verify the `darkColors` scheme in `Theme.kt` has EVERY color slot explicitly defined (not relying on
Material3 defaults). Missing slots auto-fill from base, which may not match the brand palette.
Required slots: `primary`, `onPrimary`, `primaryContainer`, `onPrimaryContainer`, `secondary`,
`onSecondary`, `secondaryContainer`, `onSecondaryContainer`, `tertiary`, `onTertiary`,
`tertiaryContainer`, `onTertiaryContainer`, `background`, `surface`, `onBackground`, `onSurface`,
`surfaceVariant`, `onSurfaceVariant`, `outline`, `outlineVariant`, `error`, `onError`,
`errorContainer`, `onErrorContainer`, `inverseSurface`, `inverseOnSurface`, `inversePrimary`,
`surfaceTint`, `scrim`.
```bash
echo "\n## Tier 2 — Theme definition validation" >> "$SCAN_OUTPUT"
# List explicitly defined dark scheme slots
grep -oE '\b([a-z]+(Container|Surface|Primary|Secondary|Tertiary|Error|Scrim|Tint)?)\s*=' app/src/main/java/app/closer/ui/theme/Theme.kt \
| grep -v '^//' \
| sed 's/\s*=//' > /tmp/dark-scheme-slots.txt
# Cross-check against required
for slot in primary onPrimary primaryContainer onPrimaryContainer secondary onSecondary secondaryContainer onSecondaryContainer tertiary onTertiary tertiaryContainer onTertiaryContainer background surface onBackground onSurface surfaceVariant onSurfaceVariant outline outlineVariant error onError errorContainer onErrorContainer inverseSurface inverseOnSurface inversePrimary surfaceTint scrim; do
if ! grep -q "$slot" /tmp/dark-scheme-slots.txt; then
echo "⚠️ MISSING: $slot not explicitly defined in dark scheme" >> "$SCAN_OUTPUT"
fi
done
echo "\n**NOTE:** The systematic scan above is a STARTING POINT. It catches structural patterns but does NOT
catch every possible theme-mismatch scenario. Some failures are compositional — e.g. a themed color used
on the wrong surface, or a gradient with hardcoded stops. The visual sweep is still MANDATORY." >> "$SCAN_OUTPUT"
```
**⛔ CLAUDE: After running the scan, read the report, file all findings to ClaudeReport.md as Pass C
theme defects, then proceed to the manual visual sweep. Any screen flagged as CRITICAL or MAJOR must be
verified in BOTH themes during the sweep. If you fix hardcoded colors as part of the QA round, log the
fix and re-run the scan to confirm it's clean.**
**Tier 3 — Compose screenshot diff suite (endgame, not yet implemented):**
The true "catch everything" solution is an automated screenshot comparison pipeline that renders every
route in light mode, renders the same route in dark mode, and pixel-diffs them — flagging any screen
where the dark version has white backgrounds, invisible text, or wrong-variant art. This catches
compositional and gradient-based mismatches that static analysis cannot. When implemented, use
`papAROS`, `Shot`, or Roborazzi with a custom `darkTheme = true` test parameter for each route.
Log this to `Future.md` as "Tier 3: Compose screenshot diff for visual regression".
- **THEME-VARIANT ART must follow the IN-APP theme, not just the system (mandatory — RUN THE DECOUPLED STATE):** the app - **THEME-VARIANT ART must follow the IN-APP theme, not just the system (mandatory — RUN THE DECOUPLED STATE):** the app
has its own theme toggle (Settings → Appearance → Light/Dark/Device) that swaps Compose colors but does **not** change has its own theme toggle (Settings → Appearance → Light/Dark/Device) that swaps Compose colors but does **not** change
the Android config `uiMode`, while `-night` drawables (`drawable-night-nodpi/`) and `painterResource` resolve off the the Android config `uiMode`, while `-night` drawables (`drawable-night-nodpi/`) and `painterResource` resolve off the
@ -947,6 +1063,14 @@ The non-game interactive surfaces that have no functional home (Pass B is games
premium state if applicable (A). premium state if applicable (A).
- **Plan a Date / Date Builder:** build a plan (shape/steps) → save → **persists + the partner sees it**; date plan + - **Plan a Date / Date Builder:** build a plan (shape/steps) → save → **persists + the partner sees it**; date plan +
`date_swipes` ciphertext at rest (D1); submit-outcome path. `date_swipes` ciphertext at rest (D1); submit-outcome path.
- **ACTUALLY PERSIST + verify via admin read — an empty list can be a DEAD feature, not an empty one (RETROSPECTIVE —
N-001/N-002).** For every interactive feature, create real data through the UI and confirm it **lands in Firestore**
(admin read) AND **renders back**; don't accept the empty/initial state as "works." Bucket List looked like an empty
list but was fully non-functional (`coupleId` never set → every op silently `return`ed); Date Builder's "Create Plan"
silently no-ops (`dateIdeaId` never wired) and writes to a collection no screen reads. Reflex: any VM that gates on
`if (someId.isEmpty()) return` and expects the screen to call `setX(...)` is suspect — `grep` for the `setX` caller; if
none, it's dead. Also confirm there's a **display surface** for whatever a "save/create" writes (a save into an unread
collection is an incomplete feature, not a working one).
- **Activity / Together feed:** shared activity entries render + sort, unread count, navigation in/out. - **Activity / Together feed:** shared activity entries render + sort, unread count, navigation in/out.
- Each feature: empty / loading / error / not-paired states, two-device realtime sync, no stuck/orphaned state. - Each feature: empty / loading / error / not-paired states, two-device realtime sync, no stuck/orphaned state.

View File

@ -1,6 +1,6 @@
# Claude QA Report — Full-App QA (living report) # Claude QA Report — Full-App QA (living report)
> **Verdict (2026-06-27): R15 = gap-closing round on Passes L/M/N/P + smoke — found & FIXED M-001 (P2 quiet hours).** Targeted the previously-uncovered gaps (the new "flawless" bar needs L + P clean + M take-effect). **Found M-001 (P2):** "Quiet hours — 10 PM8 AM, no notifications" did **not** suppress partner pushes when the recipient app was backgrounded/killed (the main case) — quiet hours was **local-only** (never synced server-side) and the OS shows the FCM `notification` block directly without running app code. **Fixed + verified live:** client now mirrors the window+timezone to `users/{uid}`; the 4 partner-action senders (`onMessageWritten`/`onAnswerWritten`/`onAnswerRevealed`/`onGameSessionUpdate`) suppress server-side via a **fail-open** `recipientInQuietHours()`; rules allowlist extended for the new fields. Live: QH ON → function logs `…is in quiet hours — suppressing`, 0 delivery; QH OFF → `notified partner`, delivery resumes; per-type chat toggle still suppresses (server-enforced). **Clean passes:** L (chat decrypt both dirs, attribution, Seen receipt, ❤️ reaction, ordering, day-sep, inbox no `enc:` leak, at-rest `enc:v1:`); P (UI copy warm/inclusive, debug rows `BuildConfig.DEBUG`-gated, friendly error fallbacks; **question bank 6103 Qs — 0 empty, 0 dupes, 0 placeholders, complete answer configs, on-guide tone**); N (daily-Q + reveal both-answered gate render); smoke **6/6 GREEN both emulators**. 2 P3 brand-asset backlogs still open. **0 FATAL.** > **Verdict (2026-06-27): R15 = gap-closing round on Passes L/M/N/P + smoke — found & FIXED M-001 (P2 quiet hours).** Targeted the previously-uncovered gaps (the new "flawless" bar needs L + P clean + M take-effect). **Found M-001 (P2):** "Quiet hours — 10 PM8 AM, no notifications" did **not** suppress partner pushes when the recipient app was backgrounded/killed (the main case) — quiet hours was **local-only** (never synced server-side) and the OS shows the FCM `notification` block directly without running app code. **Fixed + verified live:** client now mirrors the window+timezone to `users/{uid}`; the 4 partner-action senders (`onMessageWritten`/`onAnswerWritten`/`onAnswerRevealed`/`onGameSessionUpdate`) suppress server-side via a **fail-open** `recipientInQuietHours()`; rules allowlist extended for the new fields. Live: QH ON → function logs `…is in quiet hours — suppressing`, 0 delivery; QH OFF → `notified partner`, delivery resumes; per-type chat toggle still suppresses (server-enforced). **Then drove Pass N (user "FIX"):** **N-001 (P1) — Bucket List was entirely non-functional** (coupleId never wired → all CRUD silently no-op) → **FIXED + verified live** (add `enc:v1:`/complete/delete/render). **N-002 (P2) — "Plan a Date"/Date Builder "Create Plan" was a no-op** (wrote to an unread prefs collection; `dateIdeaId`/`coupleId` never wired) → **FIXED + verified live** (re-pointed to create a PLANNED `DatePlan` → Home shows "Date coming up"). Outcomes/Your Progress code-correct. **Clean passes:** L (chat decrypt both dirs, attribution, Seen receipt, ❤️ reaction, ordering, day-sep, inbox no `enc:` leak, at-rest `enc:v1:`); P (UI copy warm/inclusive, debug rows `BuildConfig.DEBUG`-gated, friendly error fallbacks; **question bank 6103 Qs — 0 empty, 0 dupes, 0 placeholders, complete answer configs, on-guide tone**); daily-Q + reveal gate render; smoke **6/6 GREEN both emulators**. 2 P3 brand-asset backlogs still open. **0 FATAL.**
> >
> **Verdict (2026-06-27): R14 = full fresh AJ ClaudeQAPlan run — 0 open P0P2, 0 new functional findings.** A pure-QA confirmation round (no code changes) on the R13 build. _(A follow-up 2026-06-27 brand-standards audit then opened **2 P3 brand-asset backlogs** — every image needs a dark variant; every icon must be custom — see the Issues section + `ClaudeBrandingReview.md`.)_ The 5 R13 fixes (C-DARK-UI-001/002/003, C-ART-EDGE-002, J-OBS) **held through R14's sweep → pruned**; the Premium-unlock modal held + re-verified. **Live results:** Pass A — premium ENFORCEMENT audited (all 6 gated features have a real gate, not just a badge; A-201 class closed) + free→Paywall confirmed for Date Match / Desire Sync / Question Packs + couple-shared unlock (Sam→QA) with modal + `subscription_entitlement_changed` push delivered live. Pass B — Desire Sync / How Well / Spin-the-Wheel full 2-device (mixed answer types incl free-text + multi-select + skipped-answer reveal), **first-finisher `partner_completed_part` nudge confirmed live**, Memory Lane create+seal (premium), Connection Challenges resume, Date Match deck (ToT carried from R13). Pass C — broad both-theme sweep + **decoupled-theme-art mandate** (system-light+app-Dark → dark UI + correctly-themed feathered art). Pass D — cornerstone LIVE (non-member 403 ×2, self-grant 403, member 200, chat at-rest `enc:v1:`). Pass E — all triggers fired live with content-free copy to the right partner. Pass F — offline cache render + process-death recovery, both 0 FATAL. Pass I — jank 5.25%. Pass J — J-OBS 48dp holds. **0 FATAL across the whole run, both emulators.** > **Verdict (2026-06-27): R14 = full fresh AJ ClaudeQAPlan run — 0 open P0P2, 0 new functional findings.** A pure-QA confirmation round (no code changes) on the R13 build. _(A follow-up 2026-06-27 brand-standards audit then opened **2 P3 brand-asset backlogs** — every image needs a dark variant; every icon must be custom — see the Issues section + `ClaudeBrandingReview.md`.)_ The 5 R13 fixes (C-DARK-UI-001/002/003, C-ART-EDGE-002, J-OBS) **held through R14's sweep → pruned**; the Premium-unlock modal held + re-verified. **Live results:** Pass A — premium ENFORCEMENT audited (all 6 gated features have a real gate, not just a badge; A-201 class closed) + free→Paywall confirmed for Date Match / Desire Sync / Question Packs + couple-shared unlock (Sam→QA) with modal + `subscription_entitlement_changed` push delivered live. Pass B — Desire Sync / How Well / Spin-the-Wheel full 2-device (mixed answer types incl free-text + multi-select + skipped-answer reveal), **first-finisher `partner_completed_part` nudge confirmed live**, Memory Lane create+seal (premium), Connection Challenges resume, Date Match deck (ToT carried from R13). Pass C — broad both-theme sweep + **decoupled-theme-art mandate** (system-light+app-Dark → dark UI + correctly-themed feathered art). Pass D — cornerstone LIVE (non-member 403 ×2, self-grant 403, member 200, chat at-rest `enc:v1:`). Pass E — all triggers fired live with content-free copy to the right partner. Pass F — offline cache render + process-death recovery, both 0 FATAL. Pass I — jank 5.25%. Pass J — J-OBS 48dp holds. **0 FATAL across the whole run, both emulators.**
@ -18,7 +18,7 @@
> to the archived-ID line below (full detail stays in git history). See **Report hygiene** in `ClaudeQAPlan.md`. > to the archived-ID line below (full detail stays in git history). See **Report hygiene** in `ClaudeQAPlan.md`.
## Run-state (current) ## Run-state (current)
- **R15 (2026-06-27) — gap-closing round (Passes L/M/N/P + regression smoke) — found & FIXED M-001 (P2).** Build current (HEAD `c31eea2` + R15 working-tree changes rebuilt+installed both emulators); baseline both FREE, 0 active sessions. **Smoke** ✅ 6/6 GREEN both (launcher + 5 notif cold-starts). **M (settings take-effect)****M-001 (P2) quiet hours didn't suppress backgrounded/killed partner pushes** (local-only window; OS shows `notification` block w/o app code). **FIXED + verified live:** client mirrors window+tz → `users/{uid}`; 4 partner-action senders suppress via fail-open `recipientInQuietHours()`; rules allowlist extended. Live: QH ON → fn log `is in quiet hours — suppressing`, 0 delivery; QH OFF → `notified partner`. Per-type chat toggle re-confirmed server-enforced (toggle off → 0 delivery; field flips in Firestore). Theme/DataStore persistence across relaunch ✅. Biometric lock code-sound (no compose bypass; observation: re-locks on cold-start, not plain background→resume → `Future.md`). **L (chat E2E)** ✅ decrypt both dirs, attribution, Seen receipt, ❤️ reaction, ordering, day-sep, inbox no `enc:` leak, at-rest `enc:v1:`. **N** ✅ daily-Q + reveal both-answered gate render (outcomes/bucket-list/date-builder carried — render-clean prior rounds). **P (content/language)** ✅ UI copy warm/inclusive, debug rows `BuildConfig.DEBUG`-gated, friendly error fallbacks; **question bank 6103 Qs: 0 empty/0 dupes/0 placeholders/complete answer configs/on-guide tone.** **D1 at-rest** ✅ messages/preview/capsules `enc:v1:`. **0 FATAL.** Uncommitted (user commits): functions (`quietHours.ts` + 4 senders), `firestore.rules`, client (`FirestoreUserDataSource`/`UserRepository(+Impl)`/`NotificationSettingsScreen`). **Functions + rules DEPLOYED to prod (standing auth).** NEXT (R16): confirm M-001 holds → prune; close remaining N/L sub-items (failed-send/offline retry, delete-msg, outcomes loop) + the 2 P3 brand backlogs. - **R15 (2026-06-27) — gap-closing round (Passes L/M/N/P + regression smoke) — found & FIXED M-001 (P2).** Build current (HEAD `c31eea2` + R15 working-tree changes rebuilt+installed both emulators); baseline both FREE, 0 active sessions. **Smoke** ✅ 6/6 GREEN both (launcher + 5 notif cold-starts). **M (settings take-effect)****M-001 (P2) quiet hours didn't suppress backgrounded/killed partner pushes** (local-only window; OS shows `notification` block w/o app code). **FIXED + verified live:** client mirrors window+tz → `users/{uid}`; 4 partner-action senders suppress via fail-open `recipientInQuietHours()`; rules allowlist extended. Live: QH ON → fn log `is in quiet hours — suppressing`, 0 delivery; QH OFF → `notified partner`. Per-type chat toggle re-confirmed server-enforced (toggle off → 0 delivery; field flips in Firestore). Theme/DataStore persistence across relaunch ✅. Biometric lock code-sound (no compose bypass; observation: re-locks on cold-start, not plain background→resume → `Future.md`). **L (chat E2E)** ✅ decrypt both dirs, attribution, Seen receipt, ❤️ reaction, ordering, day-sep, inbox no `enc:` leak, at-rest `enc:v1:`. **N** ✅ daily-Q + reveal both-answered gate render (outcomes/bucket-list/date-builder carried — render-clean prior rounds). **P (content/language)** ✅ UI copy warm/inclusive, debug rows `BuildConfig.DEBUG`-gated, friendly error fallbacks; **question bank 6103 Qs: 0 empty/0 dupes/0 placeholders/complete answer configs/on-guide tone.** **D1 at-rest** ✅ messages/preview/capsules `enc:v1:`. **0 FATAL.** **Pass N driven (user "FIX"):** **N-001 (P1) Bucket List was fully non-functional** (coupleId never set → all CRUD no-ops) → **FIXED + verified live** (add `enc:v1:` / complete / delete / list render; client-only). **N-002 (P2) "Plan a Date"/Date Builder "Create Plan" no-op** (wrote to unread prefs collection; `dateIdeaId`/`coupleId` never wired) → **FIXED + verified live** (re-pointed `DateBuilderViewModel` to create a PLANNED `DatePlan` via `savePlan` + resolve coupleId → `date_plan` status=planned, `enc:v1:`; Home shows "Date coming up"). Outcomes/Your Progress code-correct (resolves coupleId); daily-Q/reveal render ✓. Uncommitted (user commits): client (`BucketListViewModel`, `DateBuilderViewModel`) — M-001's functions/rules/client were committed by the user mid-round (+ user dropped 3 dark-variant PNGs in `drawable-night-nodpi/` toward BRAND-DARK-COVERAGE). **M-001 functions+rules DEPLOYED to prod; N-001/N-002 are client-only (debug APK installed both emulators).** NEXT (R16): confirm M-001 + N-001 + N-002 hold → prune; 2 P3 brand backlogs; revisit Date Builder "both-partners-generate" vision if wanted.
- **R14 (2026-06-27) — full fresh AJ ClaudeQAPlan run (pure QA, no code changes) — FLAWLESS, 0 open P0P3, 0 new findings.** Baseline both FREE, 0 active sessions; R13 build on both emulators (5554 dark / 5556 light). **A** ✅ premium enforcement audited (6/6 features gated, not just badged; A-201 class closed) + free→Paywall (Date Match / Desire Sync / Question Packs) + couple-shared unlock (Sam prem→QA free unlocks Desire Sync + a premium pack; modal + `subscription_entitlement_changed` push delivered live to QA). **B** ✅ Desire Sync + How Well + Spin-the-Wheel full 2-device (True/False+Yes/No+multi-select+free-text answer types; skipped-answer reveal; **first-finisher `partner_completed_part` nudge confirmed in Sam's queue**), Memory Lane create+seal (premium), Connection Challenges resume (Day 4 · 🔥2), Date Match deck; ToT carried R13. **C** ✅ broad both-theme + **decoupled-theme-art mandate** (system-light+app-Dark → dark UI + correctly-themed feathered Today hero); no nav dead-ends (back-from-Home exits = correct). **D** ✅ LIVE non-member 403 ×2 · self-grant 403 · member 200 · chat at-rest `enc:v1:` (game/capsule at-rest carried R10/R12, crypto unchanged). **E** ✅ all triggers fired live, content-free copy to right partner (started/completed_part/finished + entitlement). **F** ✅ offline Today-from-cache + `am kill` recovery, 0 FATAL. **I** ✅ jank 5.25%. **J** ✅ J-OBS 48dp holds. **0 FATAL whole run.** The 5 R13 fixes held → pruned. Uncommitted (user commits): R13's 16 modified + `PremiumUnlockOverlay.kt` + `illustration_premium_unlock.png` (R14 added no code). - **R14 (2026-06-27) — full fresh AJ ClaudeQAPlan run (pure QA, no code changes) — FLAWLESS, 0 open P0P3, 0 new findings.** Baseline both FREE, 0 active sessions; R13 build on both emulators (5554 dark / 5556 light). **A** ✅ premium enforcement audited (6/6 features gated, not just badged; A-201 class closed) + free→Paywall (Date Match / Desire Sync / Question Packs) + couple-shared unlock (Sam prem→QA free unlocks Desire Sync + a premium pack; modal + `subscription_entitlement_changed` push delivered live to QA). **B** ✅ Desire Sync + How Well + Spin-the-Wheel full 2-device (True/False+Yes/No+multi-select+free-text answer types; skipped-answer reveal; **first-finisher `partner_completed_part` nudge confirmed in Sam's queue**), Memory Lane create+seal (premium), Connection Challenges resume (Day 4 · 🔥2), Date Match deck; ToT carried R13. **C** ✅ broad both-theme + **decoupled-theme-art mandate** (system-light+app-Dark → dark UI + correctly-themed feathered Today hero); no nav dead-ends (back-from-Home exits = correct). **D** ✅ LIVE non-member 403 ×2 · self-grant 403 · member 200 · chat at-rest `enc:v1:` (game/capsule at-rest carried R10/R12, crypto unchanged). **E** ✅ all triggers fired live, content-free copy to right partner (started/completed_part/finished + entitlement). **F** ✅ offline Today-from-cache + `am kill` recovery, 0 FATAL. **I** ✅ jank 5.25%. **J** ✅ J-OBS 48dp holds. **0 FATAL whole run.** The 5 R13 fixes held → pruned. Uncommitted (user commits): R13's 16 modified + `PremiumUnlockOverlay.kt` + `illustration_premium_unlock.png` (R14 added no code).
- **R13 (2026-06-27) — backlog fix pass + full fresh AJ — FLAWLESS (0 open P0P3).** Took over the open Codex dark-mode backlog and shipped it all (working tree, both emulators rebuilt+installed; baseline both FREE, 0 active sessions): **C-DARK-UI-001** (ToT dark redesign — theme-aware backdrop/options/chips/versus/progress/pills) · **C-DARK-UI-002** (check-in label/value weight) · **C-DARK-UI-003** (Play/Home/Paywall bottom clearance) · **C-ART-EDGE-002** (8 opaque heroes routed through `BrandIllustration` feather) · **J-OBS** (composer/voice/retry buttons → 48dp). Confirmed **A-201** live → pruned. Shipped the **Premium-unlock modal** (`ui/components/PremiumUnlockOverlay.kt`, hosted in `AppNavigation`; driven off `CouplePremiumChecker`, one-time via a new `premiumUnlockCelebrated` `SettingsRepository` flag) — verified live on BOTH purchaser (5554) and partner (5556) + one-time gate (dismiss→relaunch no re-show). **AJ:** A ✓ (Date Match + Desire Sync gates → Paywall) · B ✓ (ToT full both themes; Wheel launch clean) · C ✓ (extensive both-theme sweep) · D ✓ LIVE (non-member 403 ×2, self-grant 403, member 200, chat at-rest `enc:v1:`) · E/F/G carried (diff is UI-only; no rules/functions/crypto/auth change) · H ✓ (ToT brand + modal + heroes) · I ✓ (jank 6.43%) · J ✓ (48dp). **0 FATAL both devices.** Uncommitted (user commits): 16 modified + `ui/components/PremiumUnlockOverlay.kt` + `res/drawable-nodpi/illustration_premium_unlock.png`. - **R13 (2026-06-27) — backlog fix pass + full fresh AJ — FLAWLESS (0 open P0P3).** Took over the open Codex dark-mode backlog and shipped it all (working tree, both emulators rebuilt+installed; baseline both FREE, 0 active sessions): **C-DARK-UI-001** (ToT dark redesign — theme-aware backdrop/options/chips/versus/progress/pills) · **C-DARK-UI-002** (check-in label/value weight) · **C-DARK-UI-003** (Play/Home/Paywall bottom clearance) · **C-ART-EDGE-002** (8 opaque heroes routed through `BrandIllustration` feather) · **J-OBS** (composer/voice/retry buttons → 48dp). Confirmed **A-201** live → pruned. Shipped the **Premium-unlock modal** (`ui/components/PremiumUnlockOverlay.kt`, hosted in `AppNavigation`; driven off `CouplePremiumChecker`, one-time via a new `premiumUnlockCelebrated` `SettingsRepository` flag) — verified live on BOTH purchaser (5554) and partner (5556) + one-time gate (dismiss→relaunch no re-show). **AJ:** A ✓ (Date Match + Desire Sync gates → Paywall) · B ✓ (ToT full both themes; Wheel launch clean) · C ✓ (extensive both-theme sweep) · D ✓ LIVE (non-member 403 ×2, self-grant 403, member 200, chat at-rest `enc:v1:`) · E/F/G carried (diff is UI-only; no rules/functions/crypto/auth change) · H ✓ (ToT brand + modal + heroes) · I ✓ (jank 6.43%) · J ✓ (48dp). **0 FATAL both devices.** Uncommitted (user commits): 16 modified + `ui/components/PremiumUnlockOverlay.kt` + `res/drawable-nodpi/illustration_premium_unlock.png`.
- **Ad hoc dark-mode UI/brand sweep (2026-06-27, Codex-owned emulator `emulator-5558`):** current debug APK installed, dark mode forced, fresh real paired users created through invite flow (`Codex Dark` + `River Dark`). Swept profile, invite, paired Home, Play, This-or-That, Settings, Notifications, Paywall, Messages, Today. **0 app FATAL/ANR/force-finish in logcat.** Findings added below: C-DARK-UI-001 (P2), C-DARK-UI-002 (P3), C-DARK-UI-003 (P3). Screenshots captured at `/tmp/closer-dark-04-after-permission.png` through `/tmp/closer-dark-25-today.png`. - **Ad hoc dark-mode UI/brand sweep (2026-06-27, Codex-owned emulator `emulator-5558`):** current debug APK installed, dark mode forced, fresh real paired users created through invite flow (`Codex Dark` + `River Dark`). Swept profile, invite, paired Home, Play, This-or-That, Settings, Notifications, Paywall, Messages, Today. **0 app FATAL/ANR/force-finish in logcat.** Findings added below: C-DARK-UI-001 (P2), C-DARK-UI-002 (P3), C-DARK-UI-003 (P3). Screenshots captured at `/tmp/closer-dark-04-after-permission.png` through `/tmp/closer-dark-25-today.png`.
@ -46,13 +46,13 @@
| Severity | Open | Fixed (pending 1 confirm) | | Severity | Open | Fixed (pending 1 confirm) |
|---|---|---| |---|---|---|
| P0 | 0 | 0 | | P0 | 0 | 0 |
| P1 | 0 | 0 | | P1 | 0 | **1** (N-001 Bucket List) |
| P2 | **0** | **1** (M-001 quiet hours) | | P2 | **0** | **2** (M-001 quiet hours, N-002 Date Builder) |
| P3 | **2** (BRAND-DARK-COVERAGE, BRAND-ICON-CUSTOM) | **0** | | P3 | **2** (BRAND-DARK-COVERAGE, BRAND-ICON-CUSTOM) | **0** |
_R15: found + FIXED **M-001** (P2 quiet hours — fixed + verified live, pending 1 confirmation round). **0 open P0P2.** 2 P3 _R15: found + FIXED **3 bugs** — **M-001** (P2 quiet hours), **N-001** (P1 Bucket List non-functional), **N-002** (P2
brand-asset backlogs still open (every image needs a dark variant; every icon must be custom) — full asset lists in Date Builder "Create Plan" no-op) — all verified live, pending 1 confirm. **0 open P0P2.** 2 P3 brand-asset backlogs
`ClaudeBrandingReview.md`._ open._
## Issues — open (brand-asset backlogs, P3) ## Issues — open (brand-asset backlogs, P3)
> Surfaced by the 2026-06-27 brand standards audit (new Pass H/Pass C mandates). Both are **brand-quality defects** > Surfaced by the 2026-06-27 brand standards audit (new Pass H/Pass C mandates). Both are **brand-quality defects**
@ -61,6 +61,8 @@ brand-asset backlogs still open (every image needs a dark variant; every icon mu
| ID | Sev | Area | Description | Suggested fix | Status | | ID | Sev | Area | Description | Suggested fix | Status |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| N-002 | P2 | Dates / Date Builder | **"Plan a Date" / Date Builder "Create Plan" was a no-op.** `DateBuilderViewModel.savePreference()` bailed on `state.dateIdeaId.isEmpty()` (no entry ever calls `setDateIdeaId`), built a `DatePlanPreference` with empty `coupleId`, and wrote to `date_plan_preferences` which **no screen reads**. Net: fill form → Create Plan → nothing saved, no error. | Re-point the builder to create a real **PLANNED `DatePlan`** via `repository.savePlan()` (the collection Home already displays via `getPlansByStatus(PLANNED)`), resolving `coupleId` from `CoupleRepository`; dropped the dead `dateIdeaId` guard. _(Product note: this makes the existing single-user form work end-to-end → Home "Date coming up"; the model's older "generate from BOTH partners' prefs" vision is unbuilt — revisit if that's wanted.)_ | **Fixed — verified live R15** (Create Plan → `date_plan` status=planned, `enc:v1:` duration; Home shows "Date coming up"). Client-only. Pending 1 confirm. |
| N-001 | P1 | Dates / Bucket List | **Bucket List was entirely non-functional**`setCoupleId` was never called, so `coupleId` stayed `""` and `addItem`/`loadItems`/`toggleComplete`/`deleteItem` all silently `return`ed. Items could never be added, loaded, completed, or deleted. | `BucketListViewModel` resolves the couple itself in `init` via `CoupleRepository.getCoupleForUser()` (mirrors `MemoryLaneViewModel`), then `setCoupleId``loadItems`. | **Fixed — verified live R15** (add persists `enc:v1:`; complete sets flags; delete removes; list renders). Client-only, no deploy. Pending 1 confirm. |
| M-001 | P2 | Settings / notifications | **Quiet hours did not suppress backgrounded/killed partner pushes.** "Quiet hours — 10 PM8 AM, no notifications" was stored **local-only** (DataStore); partner pushes carry a `notification` block the OS shows directly when the recipient is backgrounded/killed, and the only client check (`PartnerNotificationManager.isInQuietHours`) runs **foreground-only** (`AppMessagingService.onMessageReceived`). So the "no notifications" promise was broken for the main case. Repro: Sam QH ON @22:28 CST, backgrounded → QA chat → "QA sent a message" posted to Sam's shade. | Client mirrors window+tz to `users/{uid}`; Cloud Functions (`onMessageWritten`/`onAnswerWritten`/`onAnswerRevealed`/`onGameSessionUpdate`) suppress via fail-open `notifications/quietHours.ts:recipientInQuietHours()`; `firestore.rules` user-doc allowlist extended for `quietHours*`+`timezone`. | **Fixed — verified live R15** (fn log suppress vs notify; deployed prod). Pending 1 confirm. | | M-001 | P2 | Settings / notifications | **Quiet hours did not suppress backgrounded/killed partner pushes.** "Quiet hours — 10 PM8 AM, no notifications" was stored **local-only** (DataStore); partner pushes carry a `notification` block the OS shows directly when the recipient is backgrounded/killed, and the only client check (`PartnerNotificationManager.isInQuietHours`) runs **foreground-only** (`AppMessagingService.onMessageReceived`). So the "no notifications" promise was broken for the main case. Repro: Sam QH ON @22:28 CST, backgrounded → QA chat → "QA sent a message" posted to Sam's shade. | Client mirrors window+tz to `users/{uid}`; Cloud Functions (`onMessageWritten`/`onAnswerWritten`/`onAnswerRevealed`/`onGameSessionUpdate`) suppress via fail-open `notifications/quietHours.ts:recipientInQuietHours()`; `firestore.rules` user-doc allowlist extended for `quietHours*`+`timezone`. | **Fixed — verified live R15** (fn log suppress vs notify; deployed prod). Pending 1 confirm. |
| BRAND-DARK-COVERAGE | P3 | Art / theme | Most illustrations are **light-only** — only 12 of ~25 have a `drawable-night-nodpi/` dark variant. All `illustration_couple_*` heroes (paywall/subscription/onboarding/invite/history), `daily_question`, `partner_activation`, `tonight_partner_prompt`, `together_empty`, and **all 10 `pack_art_*` banners** show the **light/pink image on a dark screen** (feathered edges don't change the image colors). | Generate dark/aubergine-palette variants for each light-only asset → `drawable-night-nodpi/` (identical filename); `BrandIllustration` auto-selects per in-app theme. Re-run the decoupled-theme check. List in `ClaudeBrandingReview.md`. | **Open (P3)** | | BRAND-DARK-COVERAGE | P3 | Art / theme | Most illustrations are **light-only** — only 12 of ~25 have a `drawable-night-nodpi/` dark variant. All `illustration_couple_*` heroes (paywall/subscription/onboarding/invite/history), `daily_question`, `partner_activation`, `tonight_partner_prompt`, `together_empty`, and **all 10 `pack_art_*` banners** show the **light/pink image on a dark screen** (feathered edges don't change the image colors). | Generate dark/aubergine-palette variants for each light-only asset → `drawable-night-nodpi/` (identical filename); `BrandIllustration` auto-selects per in-app theme. Re-run the decoupled-theme check. List in `ClaudeBrandingReview.md`. | **Open (P3)** |
| BRAND-ICON-CUSTOM | P3 | Icons / brand | **~60 distinct generic Material icons** across ~201 call sites (generic hearts `Favorite`/`FavoriteBorder`, `Person`, `Lock`, `Star`, `PlayArrow`, `ArrowBack`, …) — these are placeholders, not the Closer brand. | Replace each with a bespoke `glyph_*` in the house style (`ImageVector.vectorResource` + `Icon(tint)`), highest-traffic first; ship bar = **0 generic Material icons**. Backlog table in `ClaudeBrandingReview.md`. | **Open (P3)** | | BRAND-ICON-CUSTOM | P3 | Icons / brand | **~60 distinct generic Material icons** across ~201 call sites (generic hearts `Favorite`/`FavoriteBorder`, `Person`, `Lock`, `Star`, `PlayArrow`, `ArrowBack`, …) — these are placeholders, not the Closer brand. | Replace each with a bespoke `glyph_*` in the house style (`ImageVector.vectorResource` + `Icon(tint)`), highest-traffic first; ship bar = **0 generic Material icons**. Backlog table in `ClaudeBrandingReview.md`. | **Open (P3)** |

View File

@ -3,6 +3,12 @@
Non-blocking ideas: things that work today but could be better, plus feature ideas. Actual bugs Non-blocking ideas: things that work today but could be better, plus feature ideas. Actual bugs
(broken/incorrect behavior) live in `ClaudeReport.md`, not here. (broken/incorrect behavior) live in `ClaudeReport.md`, not here.
## UI
Themes
- **"Add to Bucket List" has mixed dark/light mode UI.** The input field, button, and surrounding elements use
light-mode colors even when the app is in dark mode. Needs theme alignment.
## QA ## QA
Improvement & feature ideas surfaced while QA-testing as a consumer (each works today — none are defects). Improvement & feature ideas surfaced while QA-testing as a consumer (each works today — none are defects).

View File

@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.closer.domain.model.BucketListItem import app.closer.domain.model.BucketListItem
import app.closer.domain.repository.BucketListRepository import app.closer.domain.repository.BucketListRepository
import app.closer.domain.repository.CoupleRepository
import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseAuth
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@ -16,12 +17,29 @@ import kotlinx.coroutines.launch
@HiltViewModel @HiltViewModel
class BucketListViewModel @Inject constructor( class BucketListViewModel @Inject constructor(
private val repository: BucketListRepository private val repository: BucketListRepository,
private val coupleRepository: CoupleRepository
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow(BucketListUiState()) private val _uiState = MutableStateFlow(BucketListUiState())
val uiState: StateFlow<BucketListUiState> = _uiState.asStateFlow() val uiState: StateFlow<BucketListUiState> = _uiState.asStateFlow()
init {
// Resolve the couple ourselves (mirrors MemoryLaneViewModel). Previously the screen never
// called setCoupleId, so coupleId stayed "" and every add/load/complete/delete silently
// returned early — the whole Bucket List was a no-op (N-001).
resolveCouple()
}
private fun resolveCouple() {
viewModelScope.launch {
val uid = FirebaseAuth.getInstance().currentUser?.uid ?: return@launch
runCatching { coupleRepository.getCoupleForUser(uid) }
.getOrNull()
?.let { setCoupleId(it.id) }
}
}
fun setCoupleId(coupleId: String) { fun setCoupleId(coupleId: String) {
_uiState.update { it.copy(coupleId = coupleId) } _uiState.update { it.copy(coupleId = coupleId) }
loadItems() loadItems()

View File

@ -3,8 +3,11 @@ package app.closer.ui.dates
import android.util.Log import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.closer.domain.model.DatePlanPreference import app.closer.domain.model.DatePlan
import app.closer.domain.model.DatePlanStatus
import app.closer.domain.repository.CoupleRepository
import app.closer.domain.repository.DatePlanRepository import app.closer.domain.repository.DatePlanRepository
import com.google.firebase.auth.FirebaseAuth
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -15,7 +18,8 @@ import kotlinx.coroutines.launch
@HiltViewModel @HiltViewModel
class DateBuilderViewModel @Inject constructor( class DateBuilderViewModel @Inject constructor(
private val repository: DatePlanRepository private val repository: DatePlanRepository,
private val coupleRepository: CoupleRepository
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow(DateBuilderUiState()) private val _uiState = MutableStateFlow(DateBuilderUiState())
@ -43,22 +47,39 @@ class DateBuilderViewModel @Inject constructor(
fun savePreference() { fun savePreference() {
val state = _uiState.value val state = _uiState.value
if (state.dateIdeaId.isEmpty()) return
val preference = DatePlanPreference(
dateIdeaId = state.dateIdeaId,
preferredDate = state.scheduledDate,
preferredTime = state.scheduledTime.take(MAX_TIME_LENGTH),
budget = state.budget,
duration = state.duration.take(MAX_DURATION_LENGTH)
)
viewModelScope.launch { viewModelScope.launch {
_uiState.update { it.copy(isSaving = true, error = null) } _uiState.update { it.copy(isSaving = true, error = null) }
runCatching { repository.savePreference(preference) }
// N-002: create a real PLANNED date_plan (surfaced on Home as the upcoming date) and resolve
// the couple ourselves. Previously this wrote a date_plan_preference keyed off a dateIdeaId
// that no entry ever set + an empty coupleId, so "Create Plan" silently saved nothing.
val uid = FirebaseAuth.getInstance().currentUser?.uid
val couple = uid?.let { runCatching { coupleRepository.getCoupleForUser(it) }.getOrNull() }
if (couple == null) {
_uiState.update {
it.copy(isSaving = false, error = "Couldn't save. Check your connection and try again.")
}
return@launch
}
val now = System.currentTimeMillis()
val plan = DatePlan(
coupleId = couple.id,
dateIdeaId = state.dateIdeaId,
scheduledDate = state.scheduledDate,
scheduledTime = state.scheduledTime.take(MAX_TIME_LENGTH),
budget = state.budget,
duration = state.duration.take(MAX_DURATION_LENGTH),
status = DatePlanStatus.PLANNED,
createdAt = now,
updatedAt = now
)
runCatching { repository.savePlan(plan) }
.onSuccess { _uiState.update { it.copy(isSaving = false, saved = true) } } .onSuccess { _uiState.update { it.copy(isSaving = false, saved = true) } }
.onFailure { e -> .onFailure { e ->
Log.w(TAG, "Could not save date preference", e) Log.w(TAG, "Could not save date plan", e)
_uiState.update { _uiState.update {
it.copy(isSaving = false, error = "Couldn't save. Check your connection and try again.") it.copy(isSaving = false, error = "Couldn't save. Check your connection and try again.")
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -1144,6 +1144,11 @@ SCRIPTS.md
These are bugs that cost real debugging time and are easy to re-introduce if you don't know they existed. Before changing the relevant area, re-read the linked fix commit and the QA report entry. Format: **ID** — what it was — where it lives now. These are bugs that cost real debugging time and are easy to re-introduce if you don't know they existed. Before changing the relevant area, re-read the linked fix commit and the QA report entry. Format: **ID** — what it was — where it lives now.
### N-001 / N-002 — VMs that wait for the screen to push an id silently no-op if nothing pushes it
**Symptom (R15)**: the **Bucket List was entirely non-functional** — add/load/complete/delete all did nothing, no error, no logcat. **Root cause**: `BucketListViewModel` gated every operation on `if (coupleId.isEmpty()) return`, expecting the screen to call `setCoupleId(...)` — but `BucketListScreen` never did (the nav route passes no coupleId and there's no `LaunchedEffect`). So `coupleId` stayed `""` and every op returned early **silently**. Same class hit **Date Builder (N-002, still open)**: `savePreference()` bails on `dateIdeaId.isEmpty()` and **nothing ever calls `setDateIdeaId`**, so "Create Plan" is a no-op (and the preference is built with an empty `coupleId`, and nothing in the UI reads `date_plan_preferences`).
**Fix (R15, N-001)**: `BucketListViewModel` resolves the couple **itself** in `init` via `CoupleRepository.getCoupleForUser(uid)``setCoupleId``loadItems` (mirrors `MemoryLaneViewModel`/`YourProgressViewModel`, the correct pattern). Bucket items encrypt at rest (`enc:v1:`) once a real coupleId flows.
**Re-introduction risk**: the safe pattern is a VM that **resolves its own required context** (couple/uid) in `init` via the injected repository — NOT one that depends on the screen remembering to call a `setX(...)`. Audit: `grep -rn "setCoupleId\|setDateIdeaId\|fun set[A-Z]" ui/**/ViewModel` and confirm a caller exists, or move the resolution into the VM. **A silent `if (x.isEmpty()) return` guard makes a dead feature look like an empty one** — QA must persist real data and confirm it via an admin Firestore read, never trust the empty-state render. (N-002 is a deeper *incomplete feature*: even fixing the save writes into a collection no screen displays — needs a product decision on what "Plan a Date" does + where the plan is shown.)
### M-001 — quiet hours must be enforced SERVER-SIDE (a `notification` block bypasses client code when backgrounded) ### M-001 — quiet hours must be enforced SERVER-SIDE (a `notification` block bypasses client code when backgrounded)
**Symptom (R15)**: "Quiet hours — 10 PM8 AM, no notifications" did nothing for the case it exists for. With quiet hours ON and the recipient backgrounded/killed, partner chats/answers still posted to the shade. **Root cause**: quiet hours was **local-only** (`SettingsDataStore`, never written to Firestore) and the only check, `PartnerNotificationManager.isInQuietHours`, runs inside `AppMessagingService.onMessageReceived` — which FCM invokes **only in the foreground**. Partner pushes carry a `notification` block, so when the app is backgrounded/killed the **OS renders it directly** and no app code runs → the window is never consulted. The Settings copy was therefore a false promise for the primary scenario. **Symptom (R15)**: "Quiet hours — 10 PM8 AM, no notifications" did nothing for the case it exists for. With quiet hours ON and the recipient backgrounded/killed, partner chats/answers still posted to the shade. **Root cause**: quiet hours was **local-only** (`SettingsDataStore`, never written to Firestore) and the only check, `PartnerNotificationManager.isInQuietHours`, runs inside `AppMessagingService.onMessageReceived` — which FCM invokes **only in the foreground**. Partner pushes carry a `notification` block, so when the app is backgrounded/killed the **OS renders it directly** and no app code runs → the window is never consulted. The Settings copy was therefore a false promise for the primary scenario.
**Fix (R15)**: enforce server-side. (1) Client mirrors the window to the recipient's user doc — `FirestoreUserDataSource.updateQuietHours()` writes `quietHoursEnabled` + `quietHoursStartMinutes` + `quietHoursEndMinutes` + `timezone` (`TimeZone.getDefault().id`); `NotificationSettingsViewModel` syncs on toggle **and** on init (backfill). (2) `functions/src/notifications/quietHours.ts:recipientInQuietHours(userData)` computes the recipient's local now via `Intl.DateTimeFormat({timeZone})`, handles the midnight-crossing window, and is **FAIL-OPEN** (any missing/malformed field → returns false → deliver). (3) The four partner-action senders skip when it returns true: `onMessageWritten`, `onAnswerWritten`, `onAnswerRevealed`, `onGameSessionUpdate`. (4) `firestore.rules` user-doc update allowlist extended for the four new fields. **Fix (R15)**: enforce server-side. (1) Client mirrors the window to the recipient's user doc — `FirestoreUserDataSource.updateQuietHours()` writes `quietHoursEnabled` + `quietHoursStartMinutes` + `quietHoursEndMinutes` + `timezone` (`TimeZone.getDefault().id`); `NotificationSettingsViewModel` syncs on toggle **and** on init (backfill). (2) `functions/src/notifications/quietHours.ts:recipientInQuietHours(userData)` computes the recipient's local now via `Intl.DateTimeFormat({timeZone})`, handles the midnight-crossing window, and is **FAIL-OPEN** (any missing/malformed field → returns false → deliver). (3) The four partner-action senders skip when it returns true: `onMessageWritten`, `onAnswerWritten`, `onAnswerRevealed`, `onGameSessionUpdate`. (4) `firestore.rules` user-doc update allowlist extended for the four new fields.