"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, MAY_BE_MERGED_KM, NEAR_CAMERA_KM, PRECIP_ECHO_DBZ, alertsAt, bboxAround, compass, contextFor, pathStops, sampleRadarDbz, stopNumbers, wantsWind, wxStyle, wxTime, type LatLon, type NearCamera, } from "@/lib/liveWall"; import { useWeatherAlertsIn, useWind, type Polled } from "@/lib/useWeather"; import type { WeatherAlertsResponse } from "@/lib/types"; 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"; } /** Minutes from `iso` to `endIso` (or now), as "42m" / "1h 5m". */ function elapsed(iso: string, endIso?: string | null): string { const end = endIso ? new Date(endIso).getTime() : Date.now(); const m = Math.max(0, Math.round((end - 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)
  • ); })} ); } // ── Weather (server-26#237) ────────────────────────────────────────────────── const NWS_CREDIT: CameraCredit = { attribution: "National Weather Service", attribution_url: "https://www.weather.gov/", disclaimer: null }; const METAR_CREDIT: CameraCredit = { attribution: "NOAA Aviation Weather Center (METAR)", attribution_url: "https://aviationweather.gov/", disclaimer: null }; /** * Why the NWS answer is unusable, in one line (same wording as the map's * banner) — or null when it is fine to read. */ function nwsProblem(state: Polled): string | null { const { data, error, loaded } = state; if (!loaded) return "checking…"; if (!data) return `could not reach server${error ? ` (${error})` : ""}`; if (data.error && !data.fetched_at) return "NWS feed error, no data yet"; return null; } function nwsFreshness(state: Polled): string { const d = state.data!; return `fetched ${fetchedLabel(d.fetched_at) ?? "—"}${d.error ? " · NWS feed error, showing the last good data" : ""}${state.error ? " · could not refresh" : ""}`; } /** Incident panel "NWS" lines: the warning polygons the incident's head point sits inside. */ function WeatherWarningContext({ head }: { head: LatLon | null }) { const box = useMemo(() => (head ? bboxAround([head], 1) : null), [head]); const state = useWeatherAlertsIn(box); const inside = useMemo(() => (head && state.data ? alertsAt(head, state.data.alerts) : []), [head, state.data]); const tag = NWS; if (!head) return
  • {tag}—
  • ; const problem = nwsProblem(state); if (problem) return
  • {tag}{problem}
  • ; const zoneOnly = state.data!.zone_only.length; return (
  • {tag} {inside.length ? ( {inside.map((a) => { const st = wxStyle(a.event); return ( inside {a.event}{wxTime(a.expires) ? ` · until ${wxTime(a.expires)}` : ""} ); })} ) : ( <>not inside any active NWS alert area )} {zoneOnly > 0 && · {zoneOnly} zone-based alert{zoneOnly !== 1 ? "s" : ""} in the served area can't be placed (Weather Warnings layer lists them)}
  • ); } /** Arrow pointing where the wind blows TO (METAR gives where it comes FROM). */ function WindArrow({ fromDeg }: { fromDeg: number }) { return ( ); } /** Incident panel "WIND" line for fire/hazmat incidents: nearest METAR station. */ function WindContext({ head }: { head: LatLon | null }) { const state = useWind(head); const tag = WIND; if (!head) return
  • {tag}— (no geocoded location)
  • ; if (!state.loaded) return
  • {tag}checking…
  • ; const d = state.data; if (!d) return
  • {tag}could not reach server{state.error ? ` (${state.error})` : ""}
  • ; if (!d.available || !d.wind) return
  • {tag}{d.reason ?? "no wind report"}
  • ; const w = d.wind; const calm = w.wind_kt === 0; const dir = calm ? "calm" : w.variable || w.wind_dir_deg == null ? "variable" : `from ${compass(w.wind_dir_deg)} (${Math.round(w.wind_dir_deg)}°)`; const obs = w.obs_time ? new Date(w.obs_time) : null; const obsLabel = obs && !isNaN(obs.getTime()) ? obs.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }) : null; return (
  • {tag} {!calm && !w.variable && w.wind_dir_deg != null && } {calm ? "Calm" : `${Math.round(w.wind_kt)} kt`} {w.gust_kt != null && !calm ? G {Math.round(w.gust_kt)} kt : null} {calm ? "" : ` ${dir}`} {" · "}{w.station}{w.station_name ? ` (${w.station_name})` : ""}, {w.distance_km.toFixed(1)} km away {obsLabel ? ` · observed ${obsLabel}` : ""} {w.stale && STALE}
  • ); } /** Region Feeds "NWS" row: active alerts over the wall's region, with fetch time or the error. */ function WeatherFeedRow({ 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]); const box = useMemo(() => bboxAround(pts, 10), [pts]); const state = useWeatherAlertsIn(box); const tag = NWS; if (!box) return
  • {tag}no region to check (no nodes or geocoded incidents)
  • ; const problem = nwsProblem(state); if (problem) return
  • {tag}{problem}
  • ; const d = state.data!; const warnings = d.alerts.filter((a) => wxStyle(a.event).kind === "warning").length; return (
  • {tag}{d.alerts.length ? `${d.alerts.length} active alert${d.alerts.length !== 1 ? "s" : ""} in region${warnings ? ` (${warnings} warning${warnings !== 1 ? "s" : ""})` : ""}` : "no active alert areas in region"} {d.zone_only.length ? ` · ${d.zone_only.length} zone-based in served area` : ""} · {nwsFreshness(state)} · Weather Warnings 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, IEM n0q, with valid time); 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); // A resolved incident opened from an #inc= link (server-26#275): nothing in // the panel may read as still running. Its duration freezes at resolved_at // (the correlator writes it; the frontend type doesn't declare it yet), and // the live camera / context feeds are labelled as "now", not incident time. const resolved = inc.status === "resolved"; const resolvedAt = (inc as IncidentRecord & { resolved_at?: string | null }).resolved_at ?? null; const resolvedAtOk = !!resolvedAt && !Number.isNaN(new Date(resolvedAt).getTime()); const ctx = useMemo(() => contextFor(points, aircraft, vessels, events), [points, aircraft, vessels, events]); const head = points.at(-1) ?? null; const radar = useRadarDbz(head); // Stop numbers come from the same pathStops() the map draws from, so the // spine numbers exactly what the map numbers (server-26#236): only movement // incidents get numbers, and a ghosted (impossible-speed) stop gets none. const path = useMemo(() => pathStops(inc, calls), [inc, calls]); const stopOf = useMemo(() => stopNumbers(path), [path]); const ghostIds = useMemo(() => new Set(path.stops.filter((s) => s.ghost).map((s) => s.call.call_id)), [path]); 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"} {resolved ? resolved : onAir ? ON AIR : active} {inc.incident_id.slice(0, 8)}

    {inc.title ?? "Incident"}

    {inc.location && {inc.location}} {!resolved ? {elapsed(inc.started_at)} elapsed : resolvedAtOk ? lasted {elapsed(inc.started_at, resolvedAt)} : null} {inc.call_ids?.length ?? 0} calls {path.mayBeMerged && ( may be merged · {path.lengthKm.toFixed(0)} km )}
    Open incident page →
    {inc.summary ?

    {inc.summary}

    :

    No AI summary yet.

    }

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

    {resolved &&

    Live views as of now, not from when this incident was active.

    } {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

    {resolved &&

    Current conditions near the location, not at incident time.

    }
      {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 < PRECIP_ECHO_DBZ ? "no precipitation echo within ~5 km" : `up to ~${Math.round(radar.dbz)} dBZ within ~5 km (approx., read from the radar layer's n0q tile)`}
    • {wantsWind(inc) && }
    {!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.

    )}

    {resolved ? "Last on scene" : "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 ghost = ghostIds.has(c.call_id); const text = c.transcript_corrected || c.transcript; const isPlaying = c.call_id === playingCallId; return (
    1. {stop ?? (ghost ? "?" : "·")}
      {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.

    )}
    ); }