New components/LiveView.tsx renders the default landing at "/": full-bleed MapView (rail + legend from chunk 5) plus a new TimeScrubber strip below it — real call-density bars over the selected 1h/6h/24h/7d window, tinted by the worst severity in each bucket, playhead pinned to NOW. The playhead doesn't scrub yet; that needs `resolved_at` on incidents, which doesn't exist server-side (blocked chunk 13, in DEFERRED.md) — the density data itself is live, not a fixture. Distinguishes the two empty states UI_REDESIGN.md §4 calls out: a configured-but-quiet org (nodes online, zero active incidents) now shows "Listening — last check-in Xm ago" instead of rendering nothing, separate from the zero-node case (chunk 10's Activation screen). app/page.tsx's HomePage now renders LiveView directly for a signed-in, provisioned user instead of the chunk-4 interim redirect to /incidents. Per UI_REDESIGN.md chunk 6.
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Live — the default landing for every authenticated user (UI_REDESIGN.md
|
|
* §3, §5.1). Full-bleed map with the incident rail/legend MapView already
|
|
* renders (chunk 5), plus a time-density scrubber strip below it.
|
|
*/
|
|
import { useEffect, useState } from "react";
|
|
import dynamic from "next/dynamic";
|
|
import { useNodes } from "@/lib/useNodes";
|
|
import { useActiveCalls, useCalls } from "@/lib/useCalls";
|
|
import { useActiveIncidents } from "@/lib/useIncidents";
|
|
import { TimeScrubber } from "@/components/TimeScrubber";
|
|
|
|
const MapView = dynamic(() => import("@/components/MapView"), { ssr: false });
|
|
|
|
function timeAgo(date: Date): string {
|
|
const s = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
if (s < 60) return `${s}s ago`;
|
|
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
|
return `${Math.floor(s / 3600)}h ago`;
|
|
}
|
|
|
|
export function LiveView() {
|
|
const { nodes, loading: nodesLoading } = useNodes();
|
|
const activeCalls = useActiveCalls();
|
|
const activeIncidents = useActiveIncidents();
|
|
// Wide-ish recent window: feeds both the incident-path polyline (chunk 5)
|
|
// and the scrubber's density bars. Not a fixture — real recent calls.
|
|
const { calls: recentCalls, loading: callsLoading } = useCalls(500);
|
|
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!nodesLoading) setLastUpdated(new Date());
|
|
}, [nodes, activeCalls, activeIncidents, nodesLoading]);
|
|
|
|
const configuredButQuiet = !nodesLoading && nodes.length > 0 && activeIncidents.length === 0;
|
|
const mostRecentSeen = configuredButQuiet
|
|
? nodes
|
|
.map((n) => n.last_seen)
|
|
.filter((s): s is string => !!s)
|
|
.sort()
|
|
.at(-1)
|
|
: null;
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
<div className="relative flex-1 min-h-0">
|
|
{nodesLoading || callsLoading ? (
|
|
<div className="w-full h-full flex items-center justify-center text-ink-muted text-sm">
|
|
Loading map…
|
|
</div>
|
|
) : (
|
|
<MapView
|
|
nodes={nodes}
|
|
activeCalls={activeCalls}
|
|
incidents={activeIncidents}
|
|
calls={recentCalls}
|
|
lastUpdated={lastUpdated}
|
|
/>
|
|
)}
|
|
|
|
{/* Configured-and-quiet — distinct from "no nodes at all" (chunk 10's
|
|
Activation screen handles the zero-node case). A node that's
|
|
online but has heard nothing is NOT the same empty state as an
|
|
org with no equipment. */}
|
|
{configuredButQuiet && (
|
|
<div className="absolute top-12 left-1/2 -translate-x-1/2 z-[1001] pointer-events-none">
|
|
<span className="bg-surface/90 border border-line rounded-full px-3 py-1 text-xs text-ink-2 whitespace-nowrap">
|
|
● Listening{mostRecentSeen ? ` — last check-in ${timeAgo(new Date(mostRecentSeen))}` : ""}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<TimeScrubber calls={recentCalls} />
|
|
</div>
|
|
);
|
|
}
|