From 2bd9209b0520f430b415c0cc5b968436fd3d751e Mon Sep 17 00:00:00 2001 From: "Victor \"David\" Medina" Date: Sat, 27 Jun 2026 18:55:55 -0400 Subject: [PATCH] =?UTF-8?q?feat(C-F2):=20brief=20continuity=20=E2=80=94=20?= =?UTF-8?q?the=20morning=20brief=20remembers=20overnight?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The brief no longer reads as a flat list with no memory. A slim, STATIC continuity line at the top of /focus shows real carry-over since the owner's last visit — 'N awaiting reply · N snoozed, coming back' — so the owner sees the loop is alive between sessions (the day 2-5 retention signal). Honest + safe by construction: counts only (no synthesized $), reuses the existing recovery_items states (sent = awaiting reply, snoozed = coming back) via two cheap tenant-scoped count queries added to the P2-S1 readiness block. ANTI-THEATER: the line is STATIC and renders ONLY when there's real carry-over (no clutter on an empty board). Scope: ships the brief-continuity half of F2. The 'show fewer like this' owner-pref half (a write-path + preference store) is deferred to a browser-tested cycle. tsc: 0 errors. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/(shell)/focus/FocusModeClient.tsx | 26 +++++++++++++ app/(shell)/focus/page.tsx | 53 +++++++++++++++++++-------- 2 files changed, 63 insertions(+), 16 deletions(-) diff --git a/app/(shell)/focus/FocusModeClient.tsx b/app/(shell)/focus/FocusModeClient.tsx index 0d824d61..b9b95456 100644 --- a/app/(shell)/focus/FocusModeClient.tsx +++ b/app/(shell)/focus/FocusModeClient.tsx @@ -169,6 +169,8 @@ interface FocusModeClientProps { founderReadiness?: FounderReadiness | null; /** Owner-facing First-Light readiness (ALL owners) — drives the momentum strip. */ firstLightReadiness?: FirstLightReadiness | null; + /** F2: brief continuity — carry-over since the owner's last visit (counts only). */ + briefContinuity?: { snoozedCount: number; awaitingReplyCount: number } | null; } type ActionDecision = "approved" | "skipped" | "dismissed"; @@ -269,6 +271,7 @@ export function FocusModeClient({ isFounder = false, founderReadiness = null, firstLightReadiness = null, + briefContinuity = null, }: FocusModeClientProps) { const router = useRouter(); const copy = getVerticalCopy(vertical); @@ -926,6 +929,29 @@ export function FocusModeClient({ {!isFounder && firstLightReadiness && ( )} + {/* F2: brief continuity — the brief "remembers" overnight. Static, counts only, + renders ONLY when there's real carry-over (no clutter on an empty board). */} + {briefContinuity && + (briefContinuity.awaitingReplyCount > 0 || briefContinuity.snoozedCount > 0) && ( +
+ Since you were last here + {briefContinuity.awaitingReplyCount > 0 && ( + + + {briefContinuity.awaitingReplyCount} awaiting reply + + )} + {briefContinuity.snoozedCount > 0 && ( + + + {briefContinuity.snoozedCount} snoozed, coming back + + )} +
+ )} {simpleView ? ( <> {/* Simple mode (reframe v2): greeting + the Single-Action Queue ONLY - ONE diff --git a/app/(shell)/focus/page.tsx b/app/(shell)/focus/page.tsx index be550a6b..3af3704a 100644 --- a/app/(shell)/focus/page.tsx +++ b/app/(shell)/focus/page.tsx @@ -248,29 +248,49 @@ export default async function FocusPage({ searchParams }: { searchParams: Search let clientCount = 0; let pendingRecoveryCount = 0; let approvedCount = 0; + let snoozedCount = 0; + let awaitingReplyCount = 0; if (profile?.tenant_id) { - const [clientCountRes, pendingCountRes, approvedCountRes] = await Promise.all([ - supabase - .from("pulse_clients") - .select("id", { count: "exact", head: true }) - .eq("tenant_id", profile.tenant_id), - supabase - .from("recovery_items") - .select("id", { count: "exact", head: true }) - .eq("tenant_id", profile.tenant_id) - .not("status", "in", '("approved","dismissed")'), - supabase - .from("recovery_items") - .select("id", { count: "exact", head: true }) - .eq("tenant_id", profile.tenant_id) - .eq("status", "approved"), - ]); + const [clientCountRes, pendingCountRes, approvedCountRes, snoozedCountRes, sentCountRes] = + await Promise.all([ + supabase + .from("pulse_clients") + .select("id", { count: "exact", head: true }) + .eq("tenant_id", profile.tenant_id), + supabase + .from("recovery_items") + .select("id", { count: "exact", head: true }) + .eq("tenant_id", profile.tenant_id) + .not("status", "in", '("approved","dismissed")'), + supabase + .from("recovery_items") + .select("id", { count: "exact", head: true }) + .eq("tenant_id", profile.tenant_id) + .eq("status", "approved"), + // F2: brief continuity — items the owner snoozed (coming back) ... + supabase + .from("recovery_items") + .select("id", { count: "exact", head: true }) + .eq("tenant_id", profile.tenant_id) + .eq("status", "snoozed"), + // ... and items already sent, awaiting a client reply. + supabase + .from("recovery_items") + .select("id", { count: "exact", head: true }) + .eq("tenant_id", profile.tenant_id) + .eq("status", "sent"), + ]); clientCount = clientCountRes.count ?? 0; pendingRecoveryCount = pendingCountRes.count ?? 0; approvedCount = approvedCountRes.count ?? 0; + snoozedCount = snoozedCountRes.count ?? 0; + awaitingReplyCount = sentCountRes.count ?? 0; } const firstLightAchieved = lifetimeRecoveredCents > 0; const firstLightReadiness = { clientCount, pendingRecoveryCount, approvedCount, firstLightAchieved }; + // F2: brief continuity — the brief "remembers" overnight so the owner sees carry-over, + // not just a flat list (counts only, honest). + const briefContinuity = { snoozedCount, awaitingReplyCount }; // Founder panel keeps its exact shape + founder-only gating. const founderReadiness: | { pulseConnected: boolean; clientCount: number; pendingRecoveryCount: number; firstLightAchieved: boolean } @@ -425,6 +445,7 @@ export default async function FocusPage({ searchParams }: { searchParams: Search isFounder={isFounder} founderReadiness={founderReadiness} firstLightReadiness={firstLightReadiness} + briefContinuity={briefContinuity} /> );