"use client"; /** * Live ops wall side panel — Level 1 (region) and Level 2 (incident) of the * approved mockup (design-canvas/live-ops-wall-mockup.html). */ import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import type { AircraftTrack, CallRecord, Camera, CameraCredit, CameraProviderFeed, IncidentRecord, Ny511Event, NodeRecord, VesselTrack, } from "@/lib/types"; import { isKnownSeverity, SEVERITY_COLORS, SEVERITY_LABEL } from "@/lib/severity"; import { useAuth } from "@/components/AuthProvider"; import { CameraFeed } from "@/components/CameraFeed"; import { MachineOutputNotice } from "@/components/ui/MachineOutputNotice"; import { SourceCredit, fetchedLabel } from "@/components/SourceCredit"; import { InlinePlayer } from "@/components/InlinePlayer"; import { canPlayAudio, hasAudio, playIncident, stop as stopAudio, useAudio } from "@/lib/audioQueue"; import { CONTEXT_KM, NEAR_CAMERA_KM, bboxAround, contextFor, sampleRadarDbz, type LatLon, type NearCamera } from "@/lib/liveWall"; import { useOutagesIn, type OutagesState } from "@/lib/useOutages"; import { OUTAGE_POINT_KM, countyName, isUtilityHazard, matchOutages, staleCountiesAt } from "@/lib/outageMatch"; function sevColor(s: string | null | undefined) { return isKnownSeverity(s) ? SEVERITY_COLORS[s] : "var(--ink-muted)"; } function sevLabel(s: string | null | undefined) { return isKnownSeverity(s) ? SEVERITY_LABEL[s] : "Unknown"; } function elapsed(iso: string): string { const m = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 60_000)); return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h ${m % 60}m`; } function hhmm(iso: string): string { return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false }); } const H3 = "text-[11px] uppercase tracking-wide text-ink-muted font-medium mb-1.5"; // Spine rows rendered before "show all loaded" (server-26#239). const SPINE_CAP = 30; const SEC = "px-4 py-3 border-b border-line"; export function CameraTile({ cam, feed, km, onOpen }: { cam: Camera; feed?: CameraProviderFeed; km?: number; onOpen: () => void }) { return ( ); } function NoCameras({ servingCount, radiusKm }: { servingCount: number; radiusKm: number }) { // Only admins can open /admin (everyone else is bounced to /), so only they get the link. const { isAdmin } = useAuth(); return (

{servingCount !== 0 ? `No public camera within ${radiusKm} km.` : isAdmin ? <>No camera provider is enabled. Turn one on under Admin → Camera providers. : "No camera provider is enabled. A platform admin can turn them on."}

); } // ── Power outages (server-26#184) ──────────────────────────────────────────── /** * Why the outage answer is empty or unusable, in one line — or null when the * data is fine to read. Shared by the region Feeds row and the incident panel. */ function OutageProblem({ state, where }: { state: OutagesState; where: string }) { const { isAdmin } = useAuth(); const { data, error, loaded } = state; if (!loaded) return <>checking…; if (!data) return <>could not reach server{error ? ` (${error})` : ""}; if (data.status === "none_enabled") { return isAdmin ? <>no outage source enabled (Admin → Outage providers) : <>no outage source enabled — a platform admin can turn one on; } if (data.status === "no_source") return <>no licensed outage source covers {where}; if (data.status === "error") { const bad = data.providers.filter((p) => p.covers_bbox && p.error); return <>feed error{bad.length ? `: ${bad.map((p) => `${p.name} (${p.error})`).join("; ")}` : ""}; } return null; } function outageIsUsable(state: OutagesState): boolean { return !!state.data && state.data.status === "ok"; } /** Incident panel "OUTAGE" lines: the cross-match, with source credit and fetch time. */ function OutageContext({ inc, points }: { inc: IncidentRecord; points: LatLon[] }) { // 1 km pad: county data is matched by the county bbox containing a point, so // any county the incident touches intersects this box. const box = useMemo(() => bboxAround(points, 1), [points]); const state = useOutagesIn(box); const hazard = isUtilityHazard(inc); const matches = useMemo( () => (state.data ? matchOutages(inc, points, state.data.areas, state.data.providers) : []), [inc, points, state.data], ); const staleHere = useMemo(() => (state.data ? staleCountiesAt(points, state.data.areas) : []), [points, state.data]); const tag = OUTAGE; if (!points.length) return
  • {tag}—
  • ; if (!outageIsUsable(state)) return
  • {tag}
  • ; const covering = state.data!.providers.filter((p) => p.covers_bbox); const refreshNote = state.error ? " · could not refresh, showing the last answer" : ""; const feedErr = covering.find((p) => p.error); return ( <> {hazard && (
  • {matches.length ? matches[0].kind === "point" ? `⚡ Utility-hazard call within ${OUTAGE_POINT_KM} km of a reported outage` : "⚡ Utility-hazard call in a county with reported outages" : "⚡ Utility-hazard call · no reported outage matched"}
  • )} {matches.map((m) => (
  • {tag}{m.text} {m.area.etr ? ` · est. restoration ${new Date(m.area.etr).toLocaleString([], { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" })}` : ""}
  • ))} {!matches.length && (
  • {tag} {feedErr ? `${feedErr.name} feed error${feedErr.fetched_at ? `, last good data ${fetchedLabel(feedErr.fetched_at)}` : ", no data yet"} — none matched` : `none reported here by ${covering.map((p) => p.name).join(", ") || "the enabled source"}`} {staleHere.length > 0 && ` (${staleHere.map(countyName).join(", ")}: stale upstream rows only, not shown as live)`} {covering.map((p) => ( ))}
  • )} ); } /** Region Feeds "OUTAGE" row: provider status for the wall's region. */ function OutageFeedRow({ incidents, nodes }: { incidents: IncidentRecord[]; nodes: NodeRecord[] }) { const pts = useMemo(() => { const inc = incidents.filter((i) => i.location_coords).map((i) => [i.location_coords!.lat, i.location_coords!.lng] as LatLon); return inc.length ? inc : nodes.map((n) => [n.lat, n.lon] as LatLon); }, [incidents, nodes]); // Counties are large; pad the region so a county just outside the pins still counts. const box = useMemo(() => bboxAround(pts, 10), [pts]); const state = useOutagesIn(box); const tag = OUTAGE; if (!box) return
  • {tag}no region to check (no nodes or geocoded incidents)
  • ; if (!outageIsUsable(state)) return
  • {tag}
  • ; const { areas, providers } = state.data!; return ( <> {providers.filter((p) => p.covers_bbox).map((p) => { const mine = areas.filter((a) => a.provider === p.id); const live = mine.filter((a) => !a.stale && a.customers_out > 0); const out = live.reduce((t, a) => t + a.customers_out, 0); const stale = mine.filter((a) => a.stale).length; return (
  • {tag}{p.name}: {live.length ? `${live.length} count${live.length !== 1 ? "ies" : "y"} with outages, ${out.toLocaleString()} meters/customers out` : "no outages reported in region"} {stale ? ` · ${stale} stale` : ""}{p.error ? " · feed error" : ""}{p.fetched_at ? ` · fetched ${fetchedLabel(p.fetched_at)}` : ""} {state.error ? " · could not refresh" : ""} · Power Outages layer (map menu)
  • ); })} ); } // ── Level 1: region ────────────────────────────────────────────────────────── export function RegionPanel({ incidents, calls, onAirCount, nodes, wallCameras, feeds, servingCount, aircraftCount, vesselCount, events, eventsOff, pinnedIds, onOpenCamera, }: { incidents: IncidentRecord[]; calls: CallRecord[]; onAirCount: number; nodes: NodeRecord[]; wallCameras: { cam: Camera; km?: number }[]; feeds: Map; servingCount: number; aircraftCount: number; vesselCount: number; events: Ny511Event[]; eventsOff: boolean; pinnedIds: Set; onOpenCamera: (c: Camera) => void; }) { const calls6h = useMemo(() => { const cutoff = Date.now() - 6 * 3600_000; return calls.filter((c) => new Date(c.started_at).getTime() >= cutoff).length; }, [calls]); const providers = Array.from(feeds.values()); return (

    Region

    {[[incidents.length, "active incidents"], [calls6h, "calls · last 6 h"], [onAirCount, "on air now"]].map(([v, l]) => (
    {v}
    {l}
    ))}

    Camera wall · pinned + nearest to major/moderate incidents

    {wallCameras.length ? (
    {wallCameras.map(({ cam, km }) => (
    onOpenCamera(cam)} /> {pinnedIds.has(cam.id) && PINNED}
    ))}
    ) : ( )}

    Off-screen and hidden-tab tiles stop fetching; at most two live streams play at once.

    Feeds

    • ADS-B{aircraftCount} aircraft heard by your nodes
    • AIS{vesselCount} vessels / aids to navigation
    • 511NY{eventsOff ? "road events off (Admin → Camera providers)" : `${events.length} road events in region`}
    • {providers.map((p) => (
    • CAM{p.name}: {p.total_in_bbox} in region{p.error ? " · feed error" : ""}{p.fetched_at ? ` · list ${fetchedLabel(p.fetched_at)}` : ""}
    • ))}
    • NEXRADWeather Radar layer (map menu); sampled per incident

    Nodes

      {nodes.map((n) => (
    • {n.name} {n.status}
    • ))}

    Region → click an incident for its path, summary, units and nearest cameras → click a camera to open it. Esc steps back up. Bookmark a view with #inc=…&cam=…&tour.

    ); } // ── Level 2: incident ──────────────────────────────────────────────────────── function useRadarDbz(head: LatLon | null): { dbz: number | null; done: boolean } { const [state, setState] = useState<{ dbz: number | null; done: boolean }>({ dbz: null, done: false }); const key = head ? `${head[0].toFixed(3)},${head[1].toFixed(3)}` : ""; useEffect(() => { if (!head) return; let cancelled = false; setState({ dbz: null, done: false }); sampleRadarDbz(head[0], head[1]).then((dbz) => { if (!cancelled) setState({ dbz, done: true }); }); return () => { cancelled = true; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [key]); return state; } export function IncidentPanel({ inc, calls, points, near, feeds, servingCount, onAir, aircraft, vessels, events, eventsCredit, onOpenCamera, }: { inc: IncidentRecord; calls: CallRecord[]; points: LatLon[]; near: NearCamera[]; feeds: Map; servingCount: number; onAir: boolean; aircraft: AircraftTrack[]; vessels: VesselTrack[]; events: Ny511Event[]; eventsCredit: { credit?: CameraCredit; fetched_at: number | null }; onOpenCamera: (c: Camera) => void; }) { const color = sevColor(inc.severity); const ctx = useMemo(() => contextFor(points, aircraft, vessels, events), [points, aircraft, vessels, events]); const radar = useRadarDbz(points.at(-1) ?? null); // Stop numbers share the index with the map path: geocoded calls, oldest first. const stopOf = useMemo(() => { const m = new Map(); calls.filter((c) => c.location_coords).slice().sort((a, b) => a.started_at.localeCompare(b.started_at)) .forEach((c, i) => m.set(c.call_id, i + 1)); return m; }, [calls]); const spine = useMemo(() => calls.slice().sort((a, b) => b.started_at.localeCompare(a.started_at)), [calls]); // "Play incident" (server-26#233): every loaded call with a recording, oldest first. const playable = useMemo(() => spine.filter(hasAudio).map((c) => c.call_id).reverse(), [spine]); const auth = useAuth(); const audioOk = canPlayAudio(auth); const playingCallId = useAudio((s) => s.playingCallId); const queueLen = useAudio((s) => s.queue.length); const playingHere = !!playingCallId && spine.some((c) => c.call_id === playingCallId); // Truncation is said out loud, not silent (server-26#239): the spine shows // what this page has loaded, which can be less than the incident holds. const [showAll, setShowAll] = useState(false); useEffect(() => { setShowAll(false); }, [inc.incident_id]); const totalCalls = Math.max(inc.call_ids?.length ?? 0, calls.length); const outsideWindow = Math.max(0, (inc.call_ids?.length ?? 0) - calls.length); const shown = showAll ? spine : spine.slice(0, SPINE_CAP); const unitsOn = inc.units_active?.length ? inc.units_active : inc.units ?? []; const cleared = inc.units_cleared ?? []; return (
    {sevLabel(inc.severity)} {inc.type ?? "other"} {onAir ? ON AIR : active} {inc.incident_id.slice(0, 8)}

    {inc.title ?? "Incident"}

    {inc.location && {inc.location}} {elapsed(inc.started_at)} elapsed {inc.call_ids?.length ?? 0} calls
    Open incident page →
    {inc.summary ?

    {inc.summary}

    :

    No AI summary yet.

    }

    Nearest cameras · auto-surfaced{points.length > 1 ? " (weighted to the path's head)" : ""}

    {near.length ? (
    {near.map(({ cam, km }) => ( onOpenCamera(cam)} /> ))}
    ) : points.length ? ( ) : (

    This incident has no geocoded location, so no cameras can be matched.

    )}
    Camera signals (detections as incident evidence) · future — blocked on server-26#199 and #200.

    Situational context · fused layers

      {ctx.aircraft.map(({ a, km }) => (
    • ADS-B {a.callsign || a.icao}{a.altitude_ft != null ? ` · ${Math.round(a.altitude_ft).toLocaleString()} ft` : ""} · {km.toFixed(1)} km
    • ))} {ctx.vessels.map(({ v, km }) => (
    • AIS {v.name || v.mmsi}{v.speed_kt != null ? ` · ${Math.round(v.speed_kt)} kt` : ""} · {km.toFixed(1)} km
    • ))} {ctx.road.map(({ e, km }) => (
    • 511NY {/* DAA: NYSDOT's text as published, with its update time and source credit. */} {e.description} · {km.toFixed(1)} km
    • ))}
    • NEXRAD {!points.length ? "—" : !radar.done ? "sampling…" : radar.dbz == null ? "radar sample unavailable" : radar.dbz === 0 ? "no precipitation echo within ~5 km" : `up to ~${radar.dbz} dBZ within ~5 km (approx., from the radar tile)`}
    {!ctx.aircraft.length && !ctx.vessels.length && !ctx.road.length && (

    No aircraft within {CONTEXT_KM.aircraft} km, vessels within {CONTEXT_KM.vessel} km, or road events within {CONTEXT_KM.road} km.

    )}

    On scene

    {unitsOn.length ? unitsOn.map((u) => {u}) : —}

    Cleared

    {cleared.length ? cleared.map((u) => {u}) : —}

    Call spine · newest first

    {audioOk && playable.length > 0 && ( playingHere ? ( ) : ( ) )}
    {spine.length > SPINE_CAP || outsideWindow > 0 ? (

    {showAll || spine.length <= SPINE_CAP ? `Showing all ${spine.length} loaded of ${totalCalls}` : `Showing latest ${SPINE_CAP} of ${totalCalls}`} {outsideWindow > 0 && ` · ${outsideWindow} older call${outsideWindow !== 1 ? "s" : ""} outside the loaded window`} {spine.length > SPINE_CAP && ( )}

    ) : null} {spine.length ? (
      {shown.map((c) => { const stop = stopOf.get(c.call_id); const text = c.transcript_corrected || c.transcript; const isPlaying = c.call_id === playingCallId; return (
    1. {stop ?? "·"}
      {hhmm(c.started_at)} {c.talkgroup_name && {c.talkgroup_name}}

      {text || no transcript}

      {isPlaying && hasAudio(c) &&
      }
      {hasAudio(c) && !isPlaying && }
    2. ); })}
    ) : (

    No calls for this incident in the loaded window.

    )}
    ); }