Live wall: selected incident loads its own calls; resolved deep links; scrubber marks unloaded time (#239)
- LiveView: the selected incident merges useCallsByIncident (org_id + incident_ids CONTAINS, existing index, no orderBy/limit) into the calls fed to IncidentPanel and MapView, so a long incident keeps its full path and spine past the 500-call window. Stale rows from the previous selection are re-checked against the current id before merging. - #inc= not in the active set is fetched with useIncident: a resolved one is selected, drawn on the map and shown under a "Resolved HH:MM" banner; a missing or other-org id shows "Incident not found". Auto-tour stays on active incidents. - TimeScrubber: new loadedSince prop (oldest call when the window came back full). The axis before it is hatched and labelled "not loaded" instead of reading as a quiet stretch. - useCalls returns hasMore (full page, counted before the duplicate_of filter); useIncident resets loading on a new id so a lookup doesn't flash "not found". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6e462720a1
commit
a267fdf982
@@ -26,12 +26,20 @@ const BUCKETS = 48;
|
||||
* `incidents` (optional) adds one band per active incident from its start to
|
||||
* its last update, tinted by severity — click a band to drill into it (live
|
||||
* ops wall). The playhead still does not move; see the header.
|
||||
*
|
||||
* `loadedSince` (server-26#239): ISO time of the oldest call the caller's
|
||||
* window loaded when that window came back FULL (null = everything in range
|
||||
* was loaded). The axis before it is hatched and labelled "not loaded", so a
|
||||
* 7d view never reads as a quiet week for time nobody fetched.
|
||||
*/
|
||||
export function TimeScrubber({ calls, incidents = [], selectedIncidentId = null, onSelectIncident }: {
|
||||
export function TimeScrubber({ calls, incidents = [], selectedIncidentId = null, onSelectIncident, loadedSince = null, windowSize }: {
|
||||
calls: CallRecord[];
|
||||
incidents?: IncidentRecord[];
|
||||
selectedIncidentId?: string | null;
|
||||
onSelectIncident?: (id: string) => void;
|
||||
loadedSince?: string | null;
|
||||
/** Size of the caller's call window, for the "not loaded" tooltip. */
|
||||
windowSize?: number;
|
||||
}) {
|
||||
const [rangeIdx, setRangeIdx] = useState(2); // default 24h
|
||||
|
||||
@@ -56,6 +64,24 @@ export function TimeScrubber({ calls, incidents = [], selectedIncidentId = null,
|
||||
|
||||
const maxCount = Math.max(1, ...buckets.map((b) => b.count));
|
||||
|
||||
// Share of the axis (0-100) before the loaded window starts; 0 = all loaded.
|
||||
const unloaded = useMemo(() => {
|
||||
const t = loadedSince ? new Date(loadedSince).getTime() : NaN;
|
||||
if (Number.isNaN(t)) return { pct: 0, firstLoaded: 0, since: null as Date | null };
|
||||
const start = Date.now() - range.ms;
|
||||
if (t <= start) return { pct: 0, firstLoaded: 0, since: null as Date | null };
|
||||
const frac = Math.min(1, (t - start) / range.ms);
|
||||
// Buckets wholly before the window are unknown, not empty: drawn blank.
|
||||
return { pct: frac * 100, firstLoaded: Math.floor(frac * BUCKETS), since: new Date(t) };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [loadedSince, rangeIdx, calls]);
|
||||
const sinceLabel = unloaded.since
|
||||
? unloaded.since.toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false })
|
||||
: "";
|
||||
const unloadedTitle = unloaded.since
|
||||
? `Not loaded: only the latest ${windowSize ?? "few hundred"} calls are on this page (back to ${sinceLabel}). Earlier activity is unknown, not quiet.`
|
||||
: "";
|
||||
|
||||
const bands = useMemo(() => {
|
||||
const now = Date.now();
|
||||
const start = now - range.ms;
|
||||
@@ -88,7 +114,28 @@ export function TimeScrubber({ calls, incidents = [], selectedIncidentId = null,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{unloaded.pct > 0 && (
|
||||
<div
|
||||
role="img"
|
||||
aria-label={unloadedTitle}
|
||||
title={unloadedTitle}
|
||||
className="absolute left-0 top-0 bottom-0 z-[1] flex items-center justify-center overflow-hidden rounded-sm border border-dashed border-line"
|
||||
style={{
|
||||
width: `${unloaded.pct}%`,
|
||||
background: "repeating-linear-gradient(135deg, var(--line-strong) 0 2px, transparent 2px 7px)",
|
||||
}}
|
||||
>
|
||||
{unloaded.pct >= 8 && (
|
||||
<span className="px-1.5 rounded bg-surface/90 text-[10px] font-mono uppercase tracking-wide text-ink-muted whitespace-nowrap">
|
||||
not loaded
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{buckets.map((b, i) => {
|
||||
if (i < unloaded.firstLoaded) {
|
||||
return <div key={i} aria-hidden="true" style={{ flex: 1, minWidth: 2 }} />;
|
||||
}
|
||||
const heightPct = b.count === 0 ? 4 : Math.max(10, (b.count / maxCount) * 100);
|
||||
const color =
|
||||
b.worstRank === 3
|
||||
|
||||
Reference in New Issue
Block a user