221 lines
68 KiB
Markdown
221 lines
68 KiB
Markdown
# 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 PM–8 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 A–J ClaudeQAPlan run — 0 open P0–P2, 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): R13 = fixed the entire open backlog + full fresh A–J — FLAWLESS (0 open P0–P3).** Took over and **fixed all 3 open Codex dark-mode findings** (C-DARK-UI-001 P2 This-or-That redesign; C-DARK-UI-002 P3 check-in label/value; C-DARK-UI-003 P3 bottom-inset clipping) plus the 2 carried P3s (C-ART-EDGE-002 direct-call hero feathering; J-OBS 48dp touch targets), and **confirmed A-201 (P1) live → pruned**. Also shipped the **branding Premium-unlock modal** (`illustration_premium_unlock`, one-time, shown to BOTH partners on couple-shared activation). All verified live on both emulators (5554 dark / 5556 light), **0 FATAL**. Full fresh A–J run clean: Pass D security cornerstone re-verified LIVE (non-member 403, self-grant 403, member 200, chat at-rest `enc:v1:`); A premium gates → Paywall (Date Match + Desire Sync); B ToT full both themes + Wheel launch; I jank 6.43% (perf-safe); J 48dp confirmed. Diff is **UI-only** (no rules/functions/crypto change) → E/F/G carried. All app changes in the working tree — user commits.
|
||
|
||
> **Verdict addendum (2026-06-27): ad hoc DARK-MODE UI/brand review on dedicated Codex emulator COMPLETE.** Built + installed the current debug APK on my own `CloserCodexQA` emulator (`emulator-5558`), forced system dark mode, created a fresh real paired couple through the app invite flow, and swept profile/onboarding, unpaired invite, paired Home, Play, This-or-That, Settings, Notifications, Paywall, Messages, and Today. **Button text is generally readable** across profile/Home/Settings/Notifications/Messages/Paywall, but the sweep found **1 open P2**: This-or-That active gameplay has low-contrast dark option text and an off-brand diagonal/circle backdrop crossing the prompt. Also found **2 open P3s**: first-launch check-in modal label/value collision and recurring bottom-inset clipping on scroll content near nav/gesture areas. Logs checked after navigation/game entry: **0 app FATAL/ANR/force-finish**; only uiautomator/system noise plus a non-crashing BillingClient unbind warning.
|
||
|
||
> **Verdict (2026-06-27): R11 confirmation round COMPLETE — FLAWLESS (0 open P0–P2). 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 (A–J + fix phase). 0 open P0–P2; 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 (D1–D7). [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)
|
||
- **R18b (2026-06-28) — Future.md review → found+fixed a P0 (user: "review Future.md and do fixes if needed. verify bugs and why").** **O-ONBOARD-001 (P0) — onboarding CRASHES on the final slide for EVERY fresh install** (and the login/signup screen too). **Verified live before/after on `emulator-5558` (fresh, API 34):** old build → onboarding slide-3 `CtaSlide` → `FATAL EXCEPTION: java.lang.IllegalArgumentException: Only VectorDrawables and rasterized asset types are supported` at `PainterResources…loadVectorResource` ← `OnboardingScreen.kt:246`; fixed build → CtaSlide renders the logo + "Create account", and the signup screen (AuthLogoMark) renders too — full onboarding→signup reachable, 0 FATAL. **Why (root cause, git-confirmed):** `ic_launcher_foreground.xml` was a `<vector>` until commit **334cb07 "brand: update app icon"** which swapped it to a **`<bitmap>`** wrapper; `painterResource` routes any `.xml` drawable through the VectorDrawable loader, which throws on a `<bitmap>` root. The two Compose call sites — `OnboardingScreen.kt` `CtaSlide` + `AuthVisuals.kt` `AuthLogoMark` — weren't updated. **Regression invisible to recurring QA** because 5554/5556 are past onboarding + logged-in (signed up before 334cb07); every fresh install since crashes. (Future.md's root-cause guess — background/aapt quirk — was wrong.) **Fix:** both sites now `painterResource(R.drawable.closer_launcher_foreground)` (the raster the `<bitmap>` wraps; same pattern `LoadingState.kt:146` already uses); the `<bitmap>` XML stays for the real adaptive launcher icon. Scanned the whole app — **no other `painterResource`-on-non-vector-XML remains** (only `ic_launcher_foreground`/`_monochrome` are `<bitmap>`; monochrome isn't used via painterResource). Also fixed the remaining BucketList **add-FAB** hardcoded `Color(0xFFB98AF4)` → `MaterialTheme.colorScheme.primary` (closes the Future.md "BucketList mixed dark/light" item — dialog was R16, FAB was the leftover; verified live light). **Build clean; 205 unit + 24 functions green; all 3 emulators on the fixed APK.** **Regression guard ADDED + proven:** `scripts/painter-xml-scan.sh` flags any `painterResource(R.drawable.X)` where `X` is a non-`<vector>` XML drawable (the exact crash class); demonstrated it catches the bug when reintroduced (exit 1) and passes clean on the fix (exit 0); wired into the plan's cheap-gates (step 3). Uncommitted (user commits): `OnboardingScreen.kt`, `AuthVisuals.kt`, `BucketListScreen.kt`, `scripts/painter-xml-scan.sh`, `ClaudeQAPlan.md`, `Future.md`, `ClaudeReport.md`, `ClaudeQACoverage.md`. NEXT: prune O-ONBOARD-001 after 1 confirm; the instrumented onboarding→signup smoke (androidTest, currently 0) remains a `Future.md` idea (would have caught this too).
|
||
- **R18 (2026-06-28) — continuing full run (user: "why are you stopping?" → don't hand back at checkpoints).** Both emulators online (5554=Dark, 5556=Light, both reset to Device-default after testing); package is `closer.app` (launcher `closer.app/app.closer.MainActivity`). **C-DARKART-002 FIXED + verified live across all 4 theme/art states** (see Severity-board R18 note + the issue row): `MainActivity` now drives `AppCompatDelegate.setDefaultNightMode` from `ThemeMode` (sync initial read → no flicker loop; `LaunchedEffect` for runtime toggles), so every `painterResource` + BrandIllustration follows the in-app theme via the real Configuration uiMode. The previously-broken **pack-art banners now render DARK** in decoupled in-app-Dark + system-light, and the Today hero does too; symmetric in-app-Light + system-dark → light; both coupled states correct. **C-DARKART-001 re-confirmed.** The test-suite gate also caught **TEST-002** (flaky `MemoryCapsuleGenerator` determinism test — un-injected `System.currentTimeMillis()` clock violated the documented "pure" contract; **no production caller yet** so zero runtime impact, but it intermittently reddened the suite) → fixed by injecting `createdAtMillis`. **Build clean; 205 unit + 24 functions green.** Uncommitted (user commits): `MainActivity.kt`, `MemoryCapsuleGenerator.kt` (+ its test), `ClaudeReport.md`. DONE this round: **Pass A ✅ / B ✅ / E ✅ / L ✅ / P ✅** + **M-001 confirmed** (recommend prune). NEXT: prune C-DARKART-002 / TEST-002 / P-GRAMMAR-001 / M-001 after this confirm round; resolve **O-AGE-001** (P2 pre-ship age gate — product call); P3 backlogs (BRAND-DARK-COVERAGE, BRAND-ICON-CUSTOM, C-ORIENT-001); optional deeper re-runs of F/G/I (last full sweep R12). Board: **0 open P0/P1 · 1 open P2 (O-AGE-001) · 3 open P3**. **Pass A ✅ (R18, live, Sam free on 5556 — both members confirmed free via admin read):** premium gate enforced across **two distinct surfaces** — Desire Sync (game) → Paywall, premium **Boundaries** pack → Paywall; negative control: **Mixed** "Communication" pack opens and a **free prompt is accessible** (answer composer shown), so the gate isn't over-broad; **Free** filter shows a graceful "Nothing in free yet" empty state (catalog note: no fully-free packs). Paywall billing plans don't load on the non-GMS emulator ("Couldn't load plans / Try again") — expected Pass K env limit, degrades gracefully (no crash).
|
||
- **R17 (2026-06-28) — continuing full run (user: "complete full run, don't stop").** Both emulators on R16 build (5554=Dark, 5556=Light); HEAD `8b7bbc2` + working-tree fixes. **Theme fixes confirmed LIVE (dark, 5554):** C-THEME-005 (Wheel-History lock → surfaceVariant/primary), C-THEME-008/009 (Date-Match heart→primaryContainer + count badge→error) — joining 001/002 from R16. **NEW finding C-DARKART-002 (P2):** dark-variant art doesn't render in the decoupled in-app-Dark + system-light state — pack art (`QuestionPackLibraryScreen:223` via `packArtworkRes`) + ~7 literal `painterResource` sites resolve `-night` off SYSTEM uiMode, not the in-app theme; **proven live** (in-app-Dark + system `auto` → light pack art; system night=yes → correct dark art). The BRAND-DARK-COVERAGE batch art is correct but only shows under system-night. **Pass D1 at-rest = CLEAN (admin read R17):** messages `text`, `lastMessagePreview`, Memory Lane capsule content+title, **all 4 game answers (this_or_that/desire_sync/how_well/wheel) + date_swipe `action`** all `enc:v1:`; only metadata in clear. **Pass D3 = CLEAN (live raw-API R17):** minted non-member token → couple doc / messages / capsules / desire_sync reads + premium self-grant all **DENIED 403** (`scratchpad/d3_negative.js`). **C-DARKART-002 fully diagnosed** (routing through BrandIllustration is insufficient — `createConfigurationContext` doesn't resolve `-night` for these resources; recommended fix = sync config uiMode to in-app theme; my probe edit reverted; tree clean; build+units green; theme-scan CRIT still 0). NEXT (R18): **C-DARKART-002 fix** (uiMode-sync, architectural) + re-verify C-DARKART-001 holds; **M-001** quiet-hours backgrounded-push re-test → prune; live-confirm C-THEME-004 + light-side spot-check; then Pass A (premium gate) / B (a game) / E (full notif) / L / P. Cornerstones D1+D3 ✅ this round.
|
||
- **R16 (2026-06-28) — full ClaudeQAPlan run STARTED.** Session-start: both emulators online (5554/5556), HEAD `8b7bbc2`, working tree carries a **dark-variant art batch** (pack_art_*_dark, together_empty/tonight_partner_prompt night variants → BRAND-DARK-COVERAGE progress) + my doc edits; baseline TBD. **Cheap gates (new step 3):** functions tests **24/24 ✅**; Android unit tests **found 5 failures → FIXED → 205 ✅** (**TEST-001**, test-vs-code drift: (a) `PartnerNotificationManagerTest` stubbed `isInQuietHours(any())` but the method's default `now: Calendar` param pinned a stale instant that never matched the call-time clock → stub now `any(), any()`; (b) `CloserBrandCopyTest` ≤64 cap predated the intentional 150-char flagship `primaryMessage` — `BrandMessageRotator` wraps it `maxLines=3` — → cap now applies to short slogans only, flagship bounded `1..160`. Both **test-side only**; production correct: quiet hours verified live R15, flagship is committed design `6d74c6a`). **theme-scan** 🔴9/🟠8/🟡32 (the 9 CRITICAL = C-THEME-001..009 already filed). **wiring-scan** 🔴0/🟠20/🟡35 (🔴0 = Pass N DoD met). **Done so far:** rebuilt+installed both; **smoke 5554 = 6/6 PASS, 0 blocked** (launcher + 5 notif cold-starts open & stay), 5556 in progress. **Theme triage — of the 9 filed C-THEME, 3 are NOT real shipped defects:** C-THEME-003 = `@Preview`-only (`WheelRevealPreview`) → false positive; theme-scan now **excludes @Preview** composables. C-THEME-006/007 = dead unused `PlaceholderScreen.kt` (replaced by the real dashboard; 0 source refs) → **file deleted**. **The other 6 are real → FIXED** (theme tokens): BucketList (badge→primaryContainer; AddItemDialog surface→surface + Cancel→secondaryContainer + Add→primary + CategoryChips→primary/surfaceVariant — also closes the Future.md "mixed dialog" note), DateMatch (heart→primaryContainer, count badge→error/onError), WheelHistory (lock→surfaceVariant/primary), QuestionThread (waiting banner→surfaceVariant). **theme-scan CRITICAL 9→0; build + unit tests green.** **R16 RESULT:** smoke **6/6 both (0 blocked)**; theme-scan **CRITICAL 9→0** (3 reclassified, 6 fixed); **C-THEME-001/002 + N-001 + N-002 verified LIVE (dark, 5554)**; units **205** + functions **24** green; dead `PlaceholderScreen.kt` deleted; theme-scan now excludes `@Preview`. **N-001/N-002 pruned.** Open now: **O-AGE-001** (P2 pre-ship) + 3 P3. **NEXT (R17):** live-confirm the 4 remaining C-THEME fixes (004/005/008/009) in both themes + the 6 in LIGHT on 5556; re-test **M-001** quiet-hours (backgrounded-push) to prune it; then resume passes A–N+P — esp. a live both-theme sweep of the new **dark-art batch** (BRAND-DARK-COVERAGE) + the D/E cornerstones. Uncommitted (user commits): unit-test fixes, 4 theme-fixed screens + BucketList/DateMatch, `scripts/theme-scan.sh`, **deleted** `PlaceholderScreen.kt`, dark-art batch, QA docs.
|
||
- **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 A–J ClaudeQAPlan run (pure QA, no code changes) — FLAWLESS, 0 open P0–P3, 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 A–J — FLAWLESS (0 open P0–P3).** 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). **A–J:** 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`.
|
||
`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 ✅ (smoke 6/6 + Pass B triggers) | F ✅ (concurrency+process-death+offline) | G ✅ (security half live) | H (finding C-ART-EDGE-002) | I ✅ (jank 4.10%) | J (J-OBS P3) | **FIX PHASE ✅ — A-201 (P1) FIXED+VERIFIED LIVE** (Date Match LOVE/MAYBE on premium idea → Paywall via CouplePremiumChecker; SKIP passes; 0 FATAL). C-DARKART-001+C-ART-EDGE-001 held → PRUNED. | **R12 COMPLETE — FLAWLESS: 0 open P0–P2.** Remaining: 2 non-blocking P3s (C-ART-EDGE-002 hero edges, J-OBS touch targets). | NEXT (R13): confirm A-201 holds → prune; optional P3 polish. Uncommitted (user commits): R11 art files + `ui/dates/DateMatchViewModel.kt` + `ui/dates/DateMatchScreen.kt` (A-201). 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 P0–P2 | 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 + 1–5 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 — `app/.../ui/theme/Theme.kt`, `app/.../ui/components/BrandIllustration.kt`, `app/.../ui/components/EmptyState.kt`; **+ R12 A-201 fix — `app/.../ui/dates/DateMatchViewModel.kt` (CouplePremiumChecker gate + `paywallRequired` event) + `app/.../ui/dates/DateMatchScreen.kt` (navigates to paywall).** Everything else committed in `2cd0af6`. Build installed both emulators.
|
||
- **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 + 1–5 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 | **1** (**O-ONBOARD-001** onboarding/auth crash on every fresh install — fixed + verified live before/after on 5558) |
|
||
| P1 | 0 | 0 |
|
||
| P2 | **1** (O-AGE-001 pre-ship) | **9** (6× C-THEME fixed, M-001 quiet hours, TEST-001 unit suite, **C-DARKART-002** decoupled dark-art — fixed+verified R18) |
|
||
| P3 | **2** (BRAND-DARK-COVERAGE [assets present; Pass-C spot-check pending], C-ORIENT-001) | **4** (TEST-002 flaky capsule-determinism test; P-GRAMMAR-001 13 stress-Q agreement errors — asset fix; BucketList FAB hardcoded→`primary`; **BRAND-ICON-CUSTOM** — all 187 Material icons → Closer glyphs) |
|
||
|
||
_R16: ran the new **cheap gates** → found+fixed **TEST-001** (unit suite was silently red, 5 failures) → **205 unit + 24
|
||
functions green**. **Entrypoint smoke 6/6 on BOTH emulators, 0 blocked.** Theme triage: of the 9 filed C-THEME, **3 were
|
||
not real shipped defects** (C-THEME-003 `@Preview` false positive [theme-scan now excludes @Preview]; C-THEME-006/007 dead
|
||
`PlaceholderScreen` [deleted]); **6 real → FIXED** → **theme-scan CRITICAL 9→0**, build+units green; **C-THEME-001/002
|
||
verified LIVE (dark)**. Confirmed + **pruned** R15 fixes **N-001** (P1, live add/delete) + **N-002** (P2, Home "Date coming
|
||
up"). **M-001 carried** (not re-tested this round). Remaining open: **1 P2 pre-ship** (O-AGE-001) + **3 P3** (2 brand
|
||
backlogs + C-ORIENT-001). 4 C-THEME fixes (004/005/008/009) pending live confirm next round._
|
||
|
||
_R18 (2026-06-28): **C-DARKART-002 FIXED + verified live (all 4 theme/art states).** Root fix = drive the real
|
||
Configuration uiMode from the in-app theme via `AppCompatDelegate.setDefaultNightMode` in `MainActivity` (read initial
|
||
theme synchronously to avoid a placeholder→real recreation flicker loop; `LaunchedEffect` handles runtime toggles). This
|
||
makes **every** `painterResource` site + BrandIllustration follow the in-app theme at once, retiring the unreliable
|
||
per-site `createConfigurationContext` hack. Verified: coupled-dark→dark, coupled-light→light, **decoupled in-app-Dark +
|
||
system-light → dark (Today hero AND the previously-broken pack-art banners)**, decoupled in-app-Light + system-dark →
|
||
light. C-DARKART-001 re-confirmed (holds). Running the new test-suite gate also caught **TEST-002**: `MemoryCapsuleGenerator`
|
||
documents itself "pure/deterministic" but every factory stamped `createdAt = System.currentTimeMillis()`, so the
|
||
`same input produces identical capsules` test was **flaky** across ms boundaries (passed alone, failed in the full suite).
|
||
Fixed by injecting `createdAtMillis` (defaults to wall clock; tests pin it) — honors the contract; **no production caller
|
||
exists yet** (Memory Lane not wired in), so zero runtime impact, but it was undermining the suite gate. **205 unit + 24
|
||
functions green; build clean.**_
|
||
|
||
_R18 Pass A/B/P (live, 5556 Sam free): **Pass A ✅** premium gate enforced on 2 surfaces (Desire Sync + Boundaries pack
|
||
→ Paywall), free content reachable (Mixed pack prompt opens), graceful empty/billing states. **Pass B ✅** Wheel
|
||
playthrough end-to-end (spin→Stress→start→MC + free-text answers→1/2/3 progression→End→async "answers are in, waiting
|
||
for partner" reveal-gate); 0 FATAL either device. **Pass P → P-GRAMMAR-001 (P3, FIXED in asset):** the in-game wheel
|
||
question surfaced a subject-verb agreement error; bank scan found **13 questions** (all `stress` category, from one
|
||
template family of 35) where **plural subjects were substituted into a singular "{subject} is …" frame** — "low energy
|
||
days/busy weeks/health worries/burnout signs/unexpected problems **is** affecting you / **is** starting to show up".
|
||
Fixed the 13 rows in `app/src/main/assets/database/app.db` (data-only `UPDATE … REPLACE(' is ',' are ')` on exact IDs
|
||
stress_021/022/031/032/041/042/061/062/066/067/195/199/203; backup `app.db.bak_pgrammar`; verified 0 remaining). Room's
|
||
identity hash is schema-based so the edit is safe; **root/durable fix belongs in the content generator** (build_db.py —
|
||
NOT run per standing constraint) so regeneration doesn't reintroduce it (pluralize the verb or curate subjects).
|
||
**Caveat:** running emulators copied the old asset on first launch and won't re-copy without a data wipe — source asset
|
||
is corrected for new installs, verified via sqlite. Recommend a broader template-grammar audit of the bank (the prior
|
||
completeness scan checked empties/dupes/placeholders, not agreement)._
|
||
|
||
_R18 Pass L (live E2E, 5556 Sam → 5554 QA): **✅** sent a uniquely-tagged message; **received + rendered in plaintext on
|
||
the partner device** (E2E decrypt works), **`enc:v1:` at rest** (admin read: text=enc✓(79)), **at-rest leak check passes**
|
||
(marker absent from all message docs — `scratchpad/msg_atrest.js`), **"Seen" read receipt** present, and the chat renders
|
||
correctly in **dark** on 5554 (incidental C-theme confirm). Cross-checks D1 (message at-rest) on fresh live data._
|
||
|
||
_R18 M-001 + Pass E (live): **M-001 → confirmed (recommend prune).** Toggling Quiet Hours on 5554 (QA) writes the client
|
||
mirror to `users/{QA}` correctly — `quietHoursEnabled:true`, `quietHoursStartMinutes:1320` (10 PM), `quietHoursEndMinutes:480`
|
||
(8 AM), `timezone:"America/Chicago"` — the exact fields the deployed `recipientInQuietHours()` reads; toggling off →
|
||
`enabled:false`. The client-mirror half (the R15 fix) is intact; the server-suppression half is deployed + was live-verified
|
||
R15. The full clock-windowed suppression was NOT re-run this round (emulator clock ~4 PM is outside the 10 PM–8 AM window;
|
||
doing it would need either clock manipulation or a per-occurrence-gated production write that the auto-classifier denied —
|
||
not worked around). Restored QH to baseline (off). **Pass E ✅ (real backgrounded delivery + privacy + deep-link):**
|
||
backgrounded QA → Sam sends chat → FCM `notification` delivered (`channel=partner_activity`, importance high, vis=PRIVATE)
|
||
with title **"Sam sent a message"** / body **"Tap to read and reply."** — **no message content in the push** (E2E ciphertext
|
||
stays server-blind; privacy-safe, cross-checks D6). Clean Home-baseline re-test: **tapping the notification deep-links to the
|
||
correct conversation** with messages decrypted. (A first attempt resumed at the prior screen — re-tested from Home and it
|
||
routed correctly, so that was a test artifact, not a bug.)_
|
||
|
||
## Issues — open (Pass C theme defects + brand-asset backlogs)
|
||
> Surfaced by the 2026-06-27 brand standards audit (new Pass H/Pass C mandates) and the 2026-06-28 theme-scan run. Brand-quality defects (light-only art, generic icons) and Pass C theme defects (hardcoded surface/background colors) both live here; asset lists + prompts are in `ClaudeBrandingReview.md`.
|
||
>
|
||
> **R16 reclassified (NOT real shipped defects, removed from open):** **C-THEME-003** = `WheelCompleteScreen.kt:507` is inside `@Preview fun WheelRevealPreview()` — design-time only, never shipped → **false positive**; `scripts/theme-scan.sh` now **excludes `@Preview`** composables so it won't re-file. **C-THEME-006 / C-THEME-007** = `PlaceholderScreen.kt` (`SignalChip`/`PreviewPanel`) had **0 source references** (replaced by the real dashboard per `docs/qa/private-mvp-checklist.md`) → **dead code, file deleted**.
|
||
|
||
| ID | Sev | Area | Description | Suggested fix | Status |
|
||
|---|---|---|---|---|---|
|
||
| C-DARKART-002 | P2 | Visual / theme-variant art (Pass C/H) | **Dark-variant art does NOT render in the decoupled in-app-Dark + system-light state** (a common, supported config: user sets in-app **Dark** on a light/`auto`-system phone). Pack-art banners (`QuestionPackLibraryScreen.kt:223` via `packArtworkRes`) + ~7 literal `painterResource(R.drawable.illustration_/pack_art_)` sites (SpinWheel, AnswerReveal, Home, PlayHub, + debug ArtPreview) load via **raw `painterResource`**, which resolves the `-night` qualifier off the **system** uiMode, not the in-app theme — so the new `drawable-night-nodpi/` dark variants (BRAND-DARK-COVERAGE batch) only show when system=night. **Verified live R17 (5554), deterministic decoupled test (system night=no + in-app=Dark):** Question-Packs list banners render **LIGHT on dark**; forcing system night=yes → correct dark aubergine art (variants exist + packaged). **Routing through `BrandIllustration` is INSUFFICIENT — proven:** temporarily routed the pack-art `Image`→`BrandIllustration(tile=false)`, rebuilt+installed, `am kill` fresh cold-start in the decoupled state → **still light** (reverted the change). So `createConfigurationContext(uiMode=NIGHT_YES)` + `getDrawable` does **not** resolve the `-night` variant for these `-nodpi` resources (Android drawable-cache/resolution gotcha). NB: the Today daily-question hero (also BrandIllustration) *did* show dark in the same state — so the mechanism is **unreliable/asset-dependent**, which also casts doubt on whether C-DARKART-001's BrandIllustration fix truly holds for all art (re-verify). | **Comprehensive fix (recommended): sync the Activity config `uiMode` to the in-app `ThemeMode`** (e.g. `AppCompatDelegate.setDefaultNightMode` or apply a `Configuration` override in `MainActivity`) so **all** resource resolution — `painterResource` AND BrandIllustration — follows the in-app theme and the decoupled state disappears. This replaces the per-site BrandIllustration hack. Verify both decoupled directions + that CloserTheme color-scheme derivation (explicit `darkTheme` from ThemeMode) isn't double-applied. | **FIXED (R18)** — implemented the recommended uiMode-sync: `MainActivity` drives `AppCompatDelegate.setDefaultNightMode` from `ThemeMode` (initial read synchronous to avoid a placeholder→real recreation flicker loop; `LaunchedEffect` for runtime toggles). **Verified live all 4 states:** coupled-dark→dark, coupled-light→light, **decoupled in-app-Dark + system-light → DARK (Today hero AND pack-art banners — the previously-broken sites)**, decoupled in-app-Light + system-dark → light. Replaces the per-site BrandIllustration hack (now redundant-but-harmless). Pending 1 confirm. |
|
||
| C-THEME-001 | P2 | Dates / Bucket List | **AddItemDialog used a hardcoded light surface** (`Surface(color = Color.White)`, `BucketListScreen.kt`) → light dialog on dark. | → `MaterialTheme.colorScheme.surface`; also themed the whole dialog (Cancel→`secondaryContainer`, Add→`primary`, CategoryChips→`primary`/`surfaceVariant`) — closes the Future.md "mixed dark/light dialog" note. | **Fixed — verified LIVE R16 (dark): dialog surface dark, fields/chips/buttons readable.** Pending 1 confirm. |
|
||
| C-THEME-002 | P2 | Dates / Bucket List | **CategoryBadge + category filter chips used hardcoded light colors** (`Color(0xFFF3E8FF)`; ternary `Color(0xFFFFF8FC)` — the latter evaded the scanner's `color = Color(` regex). | → badge `primaryContainer`/`onPrimaryContainer`; chips `primary`/`surfaceVariant`. | **Fixed — verified LIVE R16 (dark): item-card "Adventure" badge + All/Adventure filter chips readable.** Pending 1 confirm. |
|
||
| C-THEME-004 | P2 | Questions / Discussion thread | **WaitingPhase "Your answer is saved" banner used `Color.White.copy(alpha=0.78f)`** (`QuestionThreadScreen.kt`). | → `surfaceVariant.copy(alpha=0.78f)` (children already use `onSurface`/`onSurfaceVariant`). | **Fixed R16** (theme-scan 0, build+units green) — pending live confirm. |
|
||
| C-THEME-005 | P2 | Wheel / History | **History premium-lock icon used `Color(0xFFF8F1FF)` bg + `Color(0xFFB98AF4)` tint** (`WheelHistoryScreen.kt`). | → bg `surfaceVariant`, tint `primary`. | **Fixed R16** (theme-scan 0, build+units green) — pending live confirm. |
|
||
| C-THEME-008 | P2 | Dates / Date Match | **"View matches" heart button used `Color(0xFFF3E8FF)` bg + `Color(0xFF56306F)` tint** (`DateMatchScreen.kt`). | → bg `primaryContainer`, tint `onPrimaryContainer`. | **Fixed R16** (theme-scan 0, build+units green) — pending live confirm. |
|
||
| C-THEME-009 | P2 | Dates / Date Match | **Match-count badge used `Color(0xFF8D2D35)` + `Color.White` text** (`DateMatchScreen.kt`). | → `error`/`onError` (semantic count badge, adapts both themes). | **Fixed R16** (theme-scan 0, build+units green) — pending live confirm. |
|
||
| O-AGE-001 | P2 | Release / store readiness (Pass O) | **No age gate / age verification despite adult-intimacy content.** Sign-up collects only email+password+confirm; Create Profile collects name+gender; `domain/model/User.kt` has **no DOB/age field**; the only "birthday" in-app is the *partner's* relationship special-date (`SpecialDatesSection`), not age. Yet the app ships sexual/intimacy content (Desire Sync). Google Play content-rating + sexual-content policy generally require an accurate maturity rating and may require an age gate. _(Static finding — 2026-06-28 QA-plan gap review; confirm against current Play policy + intended content rating.)_ | Add an 18+/age-appropriate gate where required + complete the Play content/maturity questionnaire to match actual content. **Pre-ship gate** (does not block per-round flawless). | **Open (pre-ship)** |
|
||
| C-ORIENT-001 | P3 | Visual / config (Pass C/O) | **App not portrait-locked; landscape layout unverified.** `AndroidManifest.xml` declares no `screenOrientation`, so `MainActivity` rotates to landscape — but no round has verified the landscape (or tablet/large-screen, minSdk 26 / targetSdk 35) layout renders correctly. _(Static finding — 2026-06-28 QA-plan gap review.)_ | Decide: **lock portrait** in the manifest if landscape isn't a supported experience, **or** certify the landscape layout (Pass C orientation check). | **Open** |
|
||
| M-001 | P2 | Settings / notifications | **Quiet hours did not suppress backgrounded/killed partner pushes.** "Quiet hours — 10 PM–8 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. |
|
||
| TEST-001 | P2 | QA infra / unit tests | **Unit suite was silently RED (5 failures) — the regression net was non-functional, undetected until R16 ran it for the first time** (test-vs-code drift). (a) `PartnerNotificationManagerTest` (4×) stubbed `quietHoursManager.isInQuietHours(any())`, but the method's default `now: Calendar = Calendar.getInstance()` param made the stub pin the instant captured at stub time → never matched the SUT's call-time clock → `MockKException`. (b) `CloserBrandCopyTest` asserted every privacy message `≤64` chars, which predated the **intentional** 150-char flagship `primaryMessage` (commit `6d74c6a`; `BrandMessageRotator` wraps it at `maxLines=3`). | **Test-side only** (production correct: quiet hours verified live R15; flagship is committed design). Stub → `isInQuietHours(any(), any())`; brand test caps short slogans `≤64` + flagship `1..160`. | **Fixed — verified R16** (`./gradlew testDebugUnitTest` 205 ✅, functions 24 ✅). 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-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`. | **FIXED (R18, 2026-06-28)** — all 187 `Icons.*` call sites (~49 distinct) swapped to `glyph_*` via central `ui/components/CloserGlyphs.kt` (+ `CategoryGlyph.kt`); **0 `Icons.*` / 0 material-icon imports remain**; build + 205 tests green; verified live both themes. Pending 1 confirm. |
|
||
|
||
## Resolved & confirmed (archived — full detail in git history)
|
||
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** · **N-001** · **N-002** — all fixed and re-verified (R16 pruned **N-001** [Bucket List non-functional → CRUD works; confirmed live add/delete] + **N-002** [Date Builder no-op → Home "Date coming up"; confirmed live]) (R14 pruned the 5 R13 fixes — **C-DARK-UI-001** ToT dark redesign · **C-DARK-UI-002** check-in label/value · **C-DARK-UI-003** bottom-inset clearance · **C-ART-EDGE-002** 8 opaque heroes feathered · **J-OBS** 48dp touch targets — held through R14's full A–J sweep; in working tree) (R13 pruned **A-201** [Date-Match premium ideas ungated → now gated to Paywall via `CouplePremiumChecker`] — fixed R12, confirmed live R13; in working tree) (R12 pruned C-DARKART-001 [in-app-theme `-night` art] + C-ART-EDGE-001 [feathered edges] — fixed R11, held through R12 visual sweep; in working tree) (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)
|
||
- **R17 re-verified (live, admin + raw-API):** **D1 at-rest** — messages `text`, `lastMessagePreview`, Memory Lane capsules (content+title), all 4 game answers (this_or_that/desire_sync/how_well/wheel), date_swipe `action` → all `enc:v1:`; only metadata clear. **D3 negative access** — minted non-member token → raw Firestore REST: couple doc / messages / capsules / desire_sync reads + premium self-grant **all DENIED 403**. Scripts: `scratchpad/d1_atrest.js`, `d1_probe3.js`, `d3_negative.js`.
|
||
- **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)
|
||
- **R14 (2026-06-27) — full fresh A–J run (pure QA, no code), FLAWLESS, 0 new findings.** Confirmation round on the R13 build: A premium enforcement audit + couple-shared unlock + entitlement push live; B 3 async games full 2-device + first-finisher nudge + Memory Lane/CC/Date Match core; C decoupled-theme-art mandate; D cornerstone live (403s + enc:v1:); E triggers/copy live; F offline + process-death; I jank 5.25%; J 48dp holds. 0 FATAL both emulators. The 5 R13 fixes held → pruned to the archived line.
|
||
- **R13 (2026-06-27) — open-backlog fix pass + full fresh A–J, FLAWLESS (0 open P0–P3).** Fixed all 5 carried/open UI items (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, and shipped the branding **Premium-unlock modal** (one-time, both partners, couple-shared). A–J: D security cornerstone re-verified LIVE (non-member 403, self-grant 403, at-rest `enc:v1:`); premium gates → Paywall; ToT both themes; jank 6.43%. Diff UI-only → E/F/G carried. 0 FATAL both emulators. App changes in working tree (user commits).
|
||
- **R12 (2026-06-27) — FRESH FULL A–J run + fix phase, FLAWLESS (0 open P0–P2).** Found **A-201 (P1): Date Match
|
||
premium ideas ungated** (free users could like/match ★Premium ideas — `getDateIdeas()=all`, no checker, badge only;
|
||
escaped prior Pass A rounds) → **fixed + verified live** (gated LOVE/MAYBE via `CouplePremiumChecker`→Paywall, SKIP
|
||
passes). Pass B: all 4 async games full 2-device E2E (ToT/Wheel/HowWell/DesireSync) + first-finisher nudge + C-NAV-002
|
||
+ Ready=Start re-verified live. Pass D LIVE clean: non-member 403 (read+write), self-grant→403, game answers enc:v1:.
|
||
Pass E smoke 6/6. Pass I jank 4.10% (art change perf-safe). New P3 C-ART-EDGE-002 (direct-call hero hard edges,
|
||
deferred). C-DARKART-001+C-ART-EDGE-001 (R11) held → pruned. Retrospective added to Pass A (badge≠gate; try to USE
|
||
premium content as a free user). Fixes in working tree (user commits).
|
||
- **R11 (2026-06-27) — confirmation round, FLAWLESS (0 open P0–P2).** 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 A–J + 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. D1–D7 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 (A1–A12, 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 P0–P2.
|
||
- **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 P0–P2.
|
||
- **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.
|
||
- **R1–R4** — baseline Passes A–F report-only; every P0–P2 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. _(Correction R15: the `users/{uid}` update rule is NOT open — it enforces a **field allowlist** (`firestore.rules` ~L198, `hasOnly([...])`); R15 extended it for `quietHours*`+`timezone`. Keep that list in sync with `FirestoreUserDataSource` when adding a client-written field.)_
|