Files
server-26/drb-frontend/components/LiveView.tsx
Logan Cusano bc636c00ce
Build & Deploy / Build & push images (push) Successful in 4m39s
Build & Deploy / Deploy to VM (push) Failing after 6m9s
Frontend redesign chunk 6: Live view
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.
2026-08-19 23:05:49 -04:00

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>
);
}