Live wall attention cues: NEW lane, pin pulse, opt-in tone and notifications (#238)
- lib/attention.ts: pure snapshot diff (Major opened, severity escalated, pursuit path gained a stop, priority keyword in a new transcript) with a silent baseline on page open; calls baseline separately on their first non-empty snapshot. Diffed over all active incidents so widening a filter is never "new"; emitted, laned and pulsed only for the FILTERED view. - components/live/NewLane.tsx: "NEW · last 15 min" overlay above the rail (desktop) / compact chip (mobile), Gate A notice included. - components/live/AttentionControls.tsx: opt-in Web Audio tone (enabling click unlocks audio) and Web Notifications while the tab is hidden, body carries the machine-output notice; prefs + keywords in localStorage. - LiveView wires it and passes pulseIncidentIds to MapView (2 min pulse). - useIncidents: useActiveIncidentsState exposes a loaded flag (first server snapshot) so the baseline never sees an empty pre-load list. - MachineOutputNotice exports MACHINE_OUTPUT_NOTICE_TEXT (same wording). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1d6e79ba25
commit
eb0faf2aea
@@ -128,15 +128,24 @@ export function useIncident(incidentId: string | null) {
|
||||
return { incident, loading: loading || (!!incidentId && settledId !== incidentId) };
|
||||
}
|
||||
|
||||
export function useActiveIncidents() {
|
||||
/**
|
||||
* Active incidents plus whether the first SERVER snapshot has landed
|
||||
* (server-26#238). `loaded` is what lets the wall tell "nothing active" from
|
||||
* "not loaded yet" — attention cues baseline on it, so opening the page never
|
||||
* fires a cue for every incident already open. Reset on sign-out / org change.
|
||||
*/
|
||||
export function useActiveIncidentsState(): { incidents: IncidentRecord[]; loaded: boolean } {
|
||||
const [incidents, setIncidents] = useState<IncidentRecord[]>([]);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const { orgId } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
let unsubFirestore: (() => void) | undefined;
|
||||
setLoaded(false);
|
||||
|
||||
const unsubAuth = onAuthStateChanged(auth, (user) => {
|
||||
if (unsubFirestore) { unsubFirestore(); unsubFirestore = undefined; }
|
||||
setLoaded(false);
|
||||
|
||||
if (!user) {
|
||||
setIncidents([]);
|
||||
@@ -157,6 +166,7 @@ export function useActiveIncidents() {
|
||||
updated_at: toISO(data.updated_at),
|
||||
} as IncidentRecord;
|
||||
}));
|
||||
if (!snap.metadata.fromCache) setLoaded(true);
|
||||
}, (err: FirestoreError) => { console.error("useActiveIncidents:", err); });
|
||||
});
|
||||
|
||||
@@ -166,5 +176,9 @@ export function useActiveIncidents() {
|
||||
};
|
||||
}, [orgId]);
|
||||
|
||||
return incidents;
|
||||
return { incidents, loaded };
|
||||
}
|
||||
|
||||
export function useActiveIncidents() {
|
||||
return useActiveIncidentsState().incidents;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user