mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
fix(coach): make the launcher's dot mean a message you can read
The Coach button showed its unread dot over an empty conversation, and opening the Coach neither showed anything nor cleared the dot. Two paths fed one indicator. The nightly sweep flipped a "remind me" reminder from active to due and stopped there — no conversation, no message, and `surfaced` had no writer at all. The launcher lit its dot off a second query that asked only whether anything was due. So the dot promised content that was never written, and because opening the Coach clears the message-based signal and not the reminder one, the only way out was to resolve the reminder in Settings, which nothing on the badge suggested. A reminder minted for a plan review put a user who had never typed in the Coach in exactly that state. The sweep now writes the reminder into the user's Coach thread as a real assistant message and marks it surfaced in the same transaction: no message, no status change, and nothing to point at. The badge derives from that message like every other proactive one, so opening the Coach shows the note and clears the dot. The launcher's second query is gone. Reminders an older build left stuck on due are picked up by the same pass, so an indicator that has been sitting on an empty conversation resolves into the message it always implied. No provider call and no push: the body is a deterministic localized template around the user's own note text, six languages. Proven end to end against Postgres — the row exists, the badge reads that row, opening clears it, a second tick writes nothing, and a failed write leaves the reminder untouched with the badge dark. All five fail against the previous sweep. The second indicator in the report, under the bottom bar's overflow entry, is the active-route marker for that group and not a badge. Refs #715
This commit is contained in:
1 parent
eb5c8a91fb
commit
82490870e5
11 files changed
+594
-112
No files matched your search
@@ -122,27 +122,20 @@ export function LayoutCoachFab() {
|
||||
},
|
||||
});
|
||||
|
||||
// v1.22 (M4) — surface a due/surfaced Coach reminder on the FAB. Additive:
|
||||
// the dot also lights when the daily sweep has flipped a "remind me" reminder
|
||||
// to due, independent of the nudge seen-stamp (resolving the reminder in the
|
||||
// ledger clears it). Same cheap, gated, stale-while-revalidate read shape.
|
||||
const { data: dueReminders } = useQuery({
|
||||
queryKey: queryKeys.coachReminders("due,surfaced"),
|
||||
queryFn: async () => {
|
||||
const data = await apiGet<{ reminders?: unknown[] } | undefined>(
|
||||
"/api/coach/reminders?status=due,surfaced",
|
||||
);
|
||||
return data?.reminders ?? [];
|
||||
},
|
||||
enabled: coachAvailable && !onCoachPage,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
const hasDueReminders = (dueReminders?.length ?? 0) > 0;
|
||||
|
||||
const nudgedAt = status?.nudgedAt ?? null;
|
||||
// ONE source for the dot: an assistant message the user has not opened yet.
|
||||
//
|
||||
// v1.22 added a second arm — a separate "any due reminder?" query — and that
|
||||
// is the arm that produced a badge over an empty Coach. The sweep flipped a
|
||||
// reminder to `due` without writing anything into a conversation, so the dot
|
||||
// promised a message that did not exist; opening the Coach showed the empty
|
||||
// state and did not clear the dot either, because only resolving the reminder
|
||||
// in Settings could. The sweep now writes the reminder into the thread as a
|
||||
// real assistant message (`coach-reminder-sweep.ts`), which the nudge status
|
||||
// already reports — so the reminder still lights the dot, through the message
|
||||
// it actually wrote, and opening the Coach clears it like any other.
|
||||
const unread = isNudgeUnread(status, seenStamp);
|
||||
// The visual dot lights for either an unread nudge OR a due reminder.
|
||||
const showDot = unread || hasDueReminders;
|
||||
const showDot = unread;
|
||||
|
||||
// The unread dot is visual-only (`aria-hidden`) and the swapped
|
||||
// `aria-label` is not announced on mutation — a screen-reader user
|
||||
|
||||
Reference in new issue
Block a user