+
+ {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"}
+
+
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
@@ -270,6 +388,7 @@ export function IncidentPanel({
{!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.
diff --git a/drb-frontend/components/map/OutageLayer.tsx b/drb-frontend/components/map/OutageLayer.tsx
new file mode 100644
index 0000000..63dd7a5
--- /dev/null
+++ b/drb-frontend/components/map/OutageLayer.tsx
@@ -0,0 +1,183 @@
+"use client";
+
+/**
+ * "Power Outages" map overlay (server-26#184). County-level areas from the
+ * admin-enabled, licence-gated outage providers, drawn at the county's label
+ * point and sized by meters out. The marker is a county, not an outage
+ * location, and every popup says so.
+ *
+ * Never a silently empty layer: while the overlay is on, a status chip always
+ * says what the layer is showing — loading, N counties, nothing reported in
+ * view (with the source's coverage caveat), no provider enabled, no licensed
+ * source covering this area, or a feed error.
+ */
+import Link from "next/link";
+import { useEffect, useMemo, useRef } from "react";
+import { createPortal } from "react-dom";
+import { Marker, Popup, useMap } from "react-leaflet";
+import L from "leaflet";
+import type { OutageArea, OutageProviderFeed } from "@/lib/types";
+import { useAuth } from "@/components/AuthProvider";
+import { SourceCredit, fetchedLabel } from "@/components/SourceCredit";
+import { useMapOutages, type OutagesState } from "@/lib/useOutages";
+import { countyName } from "@/lib/outageMatch";
+
+/** The outage glyph (lightning bolt in a ring), shared with the map legend. */
+export function outageGlyphSvg(size: number, stale = false): string {
+ const ring = stale
+ ? ``
+ : ``;
+ const bolt = ``;
+ return ``;
+}
+
+/** Marker diameter in px: grows with log10(meters out), 16–34. */
+function sizeFor(out: number): number {
+ return Math.round(Math.min(34, 16 + 4 * Math.log10(Math.max(1, out))));
+}
+
+function outageIcon(a: OutageArea): L.DivIcon {
+ const stale = a.stale;
+ const out = stale ? a.stale_customers_out : a.customers_out;
+ const s = sizeFor(out);
+ const label = out >= 1000 ? `${(out / 1000).toFixed(out >= 10_000 ? 0 : 1)}k` : String(out);
+ return L.divIcon({
+ className: "",
+ html: `
+ Stale upstream rows only: {a.stale_customers_out.toLocaleString()} meters on events whose own restoration
+ estimate passed more than 24 h ago. The source still lists them; they are not shown as live outages.
+
+ ) : (
+
+ {a.customers_out.toLocaleString()} meters/customers out (utility-reported)
+ {pct != null ? ` · ${pct < 0.1 ? "<0.1" : pct.toFixed(1)}% of ${a.customers_served!.toLocaleString()} served` : " · % served not known"}
+ {a.stale_customers_out > 0 && · plus {a.stale_customers_out.toLocaleString()} on stale rows}
+