Attention cues review fixes (#238)
- NEW lane: Gate A notice pinned below the rows (it scrolled off with >3 entries, same rule as the rail, #46); rows at text-xs for the wall. - Mobile chip moved off MapView's centred "Live" chip, which it covered. - Keyword cues re-resolve to the incident once correlation links the call, so the lane row is clickable and the pin pulses. - Enabling notifications sends one confirmation notification; a browser that grants permission but throws on construct (mobile Chrome) now shows "unsupported" instead of a switched-on control that never delivers. - useActiveIncidentsState: includeMetadataChanges so `loaded` flips on a metadata-only server sync (quiet org / offline start) instead of waiting for the first incident change and swallowing it into the baseline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
eb0faf2aea
commit
292c4fffd9
@@ -404,7 +404,16 @@ export function useAttention({ ready, resetKey, incidents, calls, keywords, visi
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ready, resetKey, incidents, calls, kwKey]);
|
||||
|
||||
const lane = useMemo(() => laneEntries(visible, seenAt, log, now, visibleCallIds), [visible, seenAt, log, now, visibleCallIds]);
|
||||
const pulse = useMemo(() => pulseIds(visibleIds, seenAt, log, now), [visibleIds, seenAt, log, now]);
|
||||
// A keyword usually fires on the transcript, before correlation links the
|
||||
// call. Re-resolve unlinked events against the calls as they are now, so
|
||||
// the lane row becomes the (clickable) incident and its pin pulses.
|
||||
const linkedLog = useMemo(() => {
|
||||
if (!log.some((e) => !e.incidentIds.length && e.callId)) return log;
|
||||
const links = new Map<string, string[]>();
|
||||
for (const c of calls) { const ids = callIncidentIds(c); if (ids.length) links.set(c.call_id, ids); }
|
||||
return log.map((e) => (e.incidentIds.length || !e.callId || !links.has(e.callId) ? e : { ...e, incidentIds: links.get(e.callId)! }));
|
||||
}, [log, calls]);
|
||||
const lane = useMemo(() => laneEntries(visible, seenAt, linkedLog, now, visibleCallIds), [visible, seenAt, linkedLog, now, visibleCallIds]);
|
||||
const pulse = useMemo(() => pulseIds(visibleIds, seenAt, linkedLog, now), [visibleIds, seenAt, linkedLog, now]);
|
||||
return { lane, pulseIds: pulse, now };
|
||||
}
|
||||
|
||||
@@ -157,7 +157,16 @@ export function useActiveIncidentsState(): { incidents: IncidentRecord[]; loaded
|
||||
}
|
||||
|
||||
const q = query(collection(db, "incidents"), where("org_id", "==", orgId), where("status", "==", "active"));
|
||||
unsubFirestore = onSnapshot(q, (snap) => {
|
||||
// includeMetadataChanges: the fromCache → server flip arrives as a
|
||||
// metadata-only snapshot when the data didn't change (e.g. an org with
|
||||
// nothing active, or a start while offline). Without it `loaded` would
|
||||
// stay false until some incident changed, and that change would be
|
||||
// swallowed into the baseline instead of cued.
|
||||
let first = true;
|
||||
unsubFirestore = onSnapshot(q, { includeMetadataChanges: true }, (snap) => {
|
||||
if (!snap.metadata.fromCache) setLoaded(true);
|
||||
if (!first && snap.docChanges().length === 0) return; // metadata only — no new array, no re-render storm
|
||||
first = false;
|
||||
setIncidents(snap.docs.map((d) => {
|
||||
const data = d.data();
|
||||
return {
|
||||
@@ -166,7 +175,6 @@ export function useActiveIncidentsState(): { incidents: IncidentRecord[]; loaded
|
||||
updated_at: toISO(data.updated_at),
|
||||
} as IncidentRecord;
|
||||
}));
|
||||
if (!snap.metadata.fromCache) setLoaded(true);
|
||||
}, (err: FirestoreError) => { console.error("useActiveIncidents:", err); });
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user