Files
server-26/drb-frontend/components/MapView.tsx
T
Logan ba43796c51 Updates, big updates
incident_correlator.py — full rewrite: always runs on every call, fetches all active incidents cross-type, fast path collects all talkgroup matches and disambiguates by unit/vehicle overlap → location proximity → embedding, new location proximity path, slow path requires location corroboration, "Auto:" stripped from titles, "auto-generated" tag added, units/vehicles now accumulated on update
intelligence.py — resolved field in GPT schema, returned as 5th value
upload.py — both pipelines unpack 5-tuple, always call correlate, auto-resolve on resolved=True
summarizer.py — stale sweep runs each tick, resolves incidents idle for 90+ minutes
config.py — correlation_window_hours=2, embedding_similarity_threshold=0.93, location_proximity_km=0.5, incident_auto_resolve_minutes=90
2026-04-19 22:53:53 -04:00

145 lines
4.8 KiB
TypeScript

"use client";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import L from "leaflet";
import type { NodeRecord, CallRecord, IncidentRecord } from "@/lib/types";
// Fix Leaflet default icon paths broken by webpack
delete (L.Icon.Default.prototype as unknown as Record<string, unknown>)._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png",
iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png",
shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
});
const nodeIcon = (status: string) =>
L.divIcon({
className: "",
html: `<div style="
width:14px;height:14px;border-radius:50%;
background:${status === "online" || status === "recording" ? "#4ade80" : status === "unconfigured" ? "#818cf8" : "#6b7280"};
border:2px solid #111827;
box-shadow:0 0 6px ${status === "recording" ? "#fb923c" : "transparent"};
"></div>`,
iconSize: [14, 14],
iconAnchor: [7, 7],
});
const INCIDENT_COLORS: Record<string, string> = {
fire: "#ef4444",
police: "#3b82f6",
ems: "#eab308",
accident: "#f97316",
other: "#6b7280",
};
const incidentIcon = (type: string | null) => {
const color = INCIDENT_COLORS[type ?? "other"] ?? INCIDENT_COLORS.other;
return L.divIcon({
className: "",
html: `<div style="
width:16px;height:16px;border-radius:3px;
background:${color};border:2px solid #111827;
display:flex;align-items:center;justify-content:center;
font-size:9px;color:#111827;font-weight:bold;line-height:1;
">!</div>`,
iconSize: [16, 16],
iconAnchor: [8, 8],
});
};
interface Props {
nodes: NodeRecord[];
activeCalls: CallRecord[];
incidents?: IncidentRecord[];
}
export default function MapView({ nodes, activeCalls, incidents = [] }: Props) {
const activeByNode = Object.fromEntries(
activeCalls.map((c) => [c.node_id, c])
);
// Only show incidents that have been geocoded (location_coords set by the server).
const plottedIncidents = incidents.flatMap((inc) =>
inc.location_coords
? [{ inc, pos: [inc.location_coords.lat, inc.location_coords.lng] as [number, number] }]
: []
);
const center: [number, number] =
nodes.length > 0
? [nodes[0].lat, nodes[0].lon]
: plottedIncidents.length > 0
? plottedIncidents[0].pos
: [39.5, -98.35];
const zoom =
nodes.length > 0
? 10
: plottedIncidents.length > 0
? 14
: 4;
return (
<MapContainer
center={center}
zoom={zoom}
className="w-full h-full rounded-lg"
style={{ background: "#111827" }}
>
<TileLayer
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
attribution='&copy; <a href="https://carto.com/">CARTO</a>'
/>
{/* Node markers */}
{nodes.map((node) => (
<Marker
key={node.node_id}
position={[node.lat, node.lon]}
icon={nodeIcon(node.status)}
>
<Popup className="font-mono">
<div className="text-gray-900">
<p className="font-bold">{node.name}</p>
<p className="text-xs text-gray-500">{node.node_id}</p>
<p className="text-xs mt-1 capitalize">{node.status}</p>
{activeByNode[node.node_id] && (
<p className="text-xs text-orange-600 mt-1">
TG {activeByNode[node.node_id].talkgroup_id ?? "—"}{" "}
{activeByNode[node.node_id].talkgroup_name}
</p>
)}
</div>
</Popup>
</Marker>
))}
{/* Incident markers — positioned at the node covering the incident's system */}
{plottedIncidents.map(({ inc, pos }) => (
<Marker
key={inc.incident_id}
position={pos}
icon={incidentIcon(inc.type)}
>
<Popup className="font-mono">
<div className="text-gray-900">
<p className="font-bold">{inc.title ?? "Incident"}</p>
<p className="text-xs capitalize" style={{ color: INCIDENT_COLORS[inc.type ?? "other"] ?? INCIDENT_COLORS.other }}>
{inc.type ?? "other"}
</p>
<p className="text-xs mt-1 capitalize">{inc.status}</p>
{inc.location && <p className="text-xs text-gray-600 mt-1">{inc.location}</p>}
<p className="text-xs text-gray-500">{inc.call_ids.length} call{inc.call_ids.length !== 1 ? "s" : ""}</p>
{inc.summary && <p className="text-xs mt-1">{inc.summary}</p>}
<a href={`/incidents/${inc.incident_id}`} className="text-xs text-blue-600 hover:underline mt-1 block">
View incident
</a>
</div>
</Popup>
</Marker>
))}
</MapContainer>
);
}