Merge pull request 'map: remove stray clock, unstack incident rail, OSM tile fallback (#118)' (#122) from fix/118-map-overlays into main
Build & Deploy / Deploy to VM (push) Canceled after 0s
Build & Deploy / Report a failed deploy (push) Canceled after 0s
Build & Deploy / Build & push images (push) Canceled after 1m31s

This commit was merged in pull request #122.
This commit is contained in:
2026-09-07 19:06:30 -04:00
+16 -29
View File
@@ -25,15 +25,15 @@ L.Icon.Default.mergeOptions({
});
// ── Basemap tiles ─────────────────────────────────────────────────────────────
// Default is CARTO's keyless dark raster basemap — no token, fits the dark UI.
// Overridable via NEXT_PUBLIC_MAP_TILE_URL so a keyed style (a CARTO account
// style, MapTiler, Mapbox, …) can be dropped in for prod without a code change.
// Whatever is supplied must use Leaflet's {s}/{z}/{x}/{y}{r} placeholder scheme.
// Prod sets NEXT_PUBLIC_MAP_TILE_URL to a keyed style (a CARTO account style,
// MapTiler, Mapbox, …). The in-code fallback is plain OpenStreetMap so the map
// still renders if that var is missing — CARTO's keyless CDN has proven flaky.
// Whatever is supplied must use Leaflet's {s}/{z}/{x}/{y}{r} placeholder scheme;
// the {z}/{x}/{y} tokens below are substituted by Leaflet at runtime.
const MAP_TILE_URL =
process.env.NEXT_PUBLIC_MAP_TILE_URL ||
"https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png";
const MAP_TILE_ATTRIBUTION =
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/">CARTO</a>';
"https://tile.openstreetmap.org/{z}/{x}/{y}.png";
const MAP_TILE_ATTRIBUTION = "&copy; OpenStreetMap contributors";
// ── Colour ────────────────────────────────────────────────────────────────────
// Severity is the only hue on this map — see UI_REDESIGN.md §2.3. Incident
@@ -459,9 +459,6 @@ export default function MapView({ nodes, activeCalls, incidents = [], calls = []
const [drawerOpen, setDrawerOpen] = useState(false);
const [agoClock, setAgoClock] = useState(0);
const [radarEpoch, setRadarEpoch] = useState(() => Date.now());
const [clockStr, setClockStr] = useState(() =>
new Date().toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" })
);
useEffect(() => {
const id = setInterval(() => setAgoClock((t: number) => t + 1), 10_000);
@@ -474,15 +471,6 @@ export default function MapView({ nodes, activeCalls, incidents = [], calls = []
return () => clearInterval(id);
}, []);
// Live clock for TOC situational awareness
useEffect(() => {
const id = setInterval(() =>
setClockStr(new Date().toLocaleTimeString([], { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" })),
1000
);
return () => clearInterval(id);
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
const ago = useMemo(() => (lastUpdated ? timeAgo(lastUpdated) : null), [lastUpdated, agoClock]);
@@ -623,13 +611,8 @@ export default function MapView({ nodes, activeCalls, incidents = [], calls = []
)}
</div>
{/* ── Clock — bottom-left for TOC situational awareness ───────────────── */}
<div className="absolute bottom-8 left-3 z-[1001] bg-surface/90 border border-line rounded-lg px-3 py-2 pointer-events-none">
<span className="text-ink text-sm font-mono tabular-nums">{clockStr}</span>
</div>
{/* ── Legend — shape-first, both themes. Never a bare colour swatch. ──── */}
<div className="absolute bottom-8 right-3 z-[1001] bg-surface/90 border border-line rounded-lg px-3 py-2.5 text-xs pointer-events-none space-y-2">
<div className="absolute bottom-8 right-3 z-[1001] bg-surface/90 border border-line rounded-lg px-3 py-2.5 text-xs pointer-events-none space-y-2 max-h-[calc(100%-4rem)] overflow-y-auto">
<div className="space-y-1">
<p className="text-ink-muted font-medium text-[10px] uppercase tracking-wide">Severity</p>
{(["major", "moderate", "minor", "routine"] as Severity[]).map((sev) => (
@@ -673,15 +656,19 @@ export default function MapView({ nodes, activeCalls, incidents = [], calls = []
{/* ── Incident overlay panel ───────────────────────────────────────────── */}
{incidents.length > 0 && (
<>
{/* Desktop: left sidebar — starts below zoom controls + fit-all button */}
<div className="absolute top-[8rem] left-3 bottom-[4.5rem] z-[1001] hidden md:flex flex-col w-56 gap-1.5">
{/* Desktop: left sidebar — offset below the zoom stack + fit-all button
so it never overlaps the Leaflet +/- controls (#118). Height is
capped and the list scrolls on its own, so the rail never reaches
the bottom-right legend. pointer-events are off on the wrapper and
back on for the cards, so the map still pans in the gaps. */}
<div className="absolute top-[9.5rem] left-3 z-[1001] hidden md:flex flex-col w-56 gap-1.5 max-h-[calc(100%-12rem)] pointer-events-none">
{/* Gate A / A2 (server-26#46) — the rail's titles, locations and
unit counts are pipeline output. Pinned above the scroll area
so it cannot be scrolled off the screen it qualifies. */}
<div className="bg-surface/90 backdrop-blur-sm border border-line rounded-lg px-2 py-1.5 shrink-0">
<div className="bg-surface/90 backdrop-blur-sm border border-line rounded-lg px-2 py-1.5 shrink-0 pointer-events-auto">
<MachineOutputNotice variant="inline" className="text-[10px] leading-snug items-start" />
</div>
<div className="flex flex-col gap-1.5 overflow-y-auto">
<div className="flex flex-col gap-1.5 overflow-y-auto min-h-0 pointer-events-auto">
{incidents.map((inc) => {
const color = severityColor(inc.severity);
const age = inc.started_at ? timeAgo(new Date(inc.started_at)) : null;