From 968134f8ee39d3116de1c8afe3d45351cfede014 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sun, 6 Sep 2026 23:43:22 -0400 Subject: [PATCH] frontend: fix map stacking + honest infra error states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From a live review of drb.cusano.net. MapView.tsx / globals.css: - The Leaflet map painted above the sticky Nav (z-40) and modal overlays, so on Live the account dropdown opened *behind* the map. Pin .leaflet-container to its own stacking context (position:relative; z-index:0) — keeps Leaflet's internal pane order, drops the whole map below app chrome. The map's own overlay UI (legend, rail, clock, fit-all) is outside .leaflet-container and unaffected. Chosen over raising Nav's z-index, which would float the sticky header over modal backdrops on ~7 pages. - Basemap: the "Dark" tile URL is already CARTO's keyless dark raster (so a prod "API KEY REQUIRED" watermark is a stale build or CARTO rate-limiting the origin, not this code). Add NEXT_PUBLIC_MAP_TILE_URL as a build-time override so a keyed style drops in without a code change; add the OSM attribution the keyless CARTO tiles require. incidents/page.tsx, alerts/page.tsx: - Both dumped raw Firestore "requires an index / PERMISSION_DENIED" strings (with a console.firebase URL) straight into the UI when the composite indexes aren't deployed (server-26 #13/#51). Collapse those known infra failures to a plain sentence; any other error passes through verbatim so a real bug still shows. alerts also now surfaces the events-query error at all — it was swallowed, showing a false "No alerts triggered yet." on a public-safety screen. onboarding/page.tsx: stale comment (/dashboard -> "/"). Untypechecked (no node/npm locally); presentational only — one string helper, one added error branch, a CSS rule, two tile-URL constants, a comment. next build in deploy.yml gates it. Co-Authored-By: Claude Sonnet 5 --- drb-frontend/app/alerts/page.tsx | 8 +++++++- drb-frontend/app/globals.css | 14 ++++++++++++++ drb-frontend/app/incidents/page.tsx | 13 ++++++++++++- drb-frontend/app/onboarding/page.tsx | 2 +- drb-frontend/components/MapView.tsx | 17 ++++++++++++++--- 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/drb-frontend/app/alerts/page.tsx b/drb-frontend/app/alerts/page.tsx index 58a3ddf..c034f65 100644 --- a/drb-frontend/app/alerts/page.tsx +++ b/drb-frontend/app/alerts/page.tsx @@ -186,7 +186,7 @@ function RulesTab({ isAdmin }: { isAdmin: boolean }) { export default function AlertsPage() { const { isAdmin } = useAuth(); - const { alerts, loading } = useAlerts(); + const { alerts, loading, error } = useAlerts(); const [tab, setTab] = useState<"events" | "rules">("events"); async function handleAcknowledge(id: string) { @@ -226,6 +226,12 @@ export default function AlertsPage() { {tab === "events" && ( loading ? (

Loading…

+ ) : error ? ( +

+ {/requires an index|PERMISSION_DENIED|insufficient permissions/i.test(error) + ? "Couldn't load alerts — a database index or security rule isn't deployed on the server yet (server-26 #13 / #51)." + : `Couldn't load alerts: ${error}`} +

) : alerts.length === 0 ? (

No alerts triggered yet.

) : ( diff --git a/drb-frontend/app/globals.css b/drb-frontend/app/globals.css index 76b77ab..4dbea4f 100644 --- a/drb-frontend/app/globals.css +++ b/drb-frontend/app/globals.css @@ -163,6 +163,20 @@ html:not(.dark) .border-indigo-800 { border-color: #a5b4fc !important; } animation: pulse-ring 1.8s ease-out infinite; } +/* ── Leaflet stacking fix ───────────────────────────────────────────────────── + * Leaflet's internal panes (z-index 200–700) and its zoom / layers controls + * (z-index 1000) otherwise paint above the sticky app Nav (z-40) and any modal + * overlay — on Live this put the account dropdown *behind* the map. Pinning the + * map container to its own low stacking context keeps Leaflet's internal layer + * order intact while dropping the whole map (tiles + controls) below the app + * chrome. The map's own overlay UI (legend, incident rail, clock, fit-all) sits + * outside .leaflet-container, so it is unaffected and still renders on top. + */ +.leaflet-container { + position: relative; + z-index: 0; +} + /* ── Form inputs ─────────────────────────────────────────────────────────── */ html:not(.dark) input:not([type="submit"]):not([type="button"]):not([type="reset"]), html:not(.dark) select, diff --git a/drb-frontend/app/incidents/page.tsx b/drb-frontend/app/incidents/page.tsx index b96de0f..d7e302a 100644 --- a/drb-frontend/app/incidents/page.tsx +++ b/drb-frontend/app/incidents/page.tsx @@ -28,6 +28,17 @@ const FILTER_THRESHOLD: Record = { all: -1, minor: 1, mo type SortMode = "recent" | "severity"; +// The Firestore client surfaces a missing composite index or an undeployed +// ruleset as a raw multi-line string with a console URL in it — not something +// to put in front of an operator. Collapse the known infra failures to a plain +// line; pass anything else straight through so a real bug still shows. +function friendlyIncidentsError(raw: string): string { + if (/requires an index|PERMISSION_DENIED|Missing or insufficient permissions|failed-precondition/i.test(raw)) { + return "Couldn't load incidents — the incidents database index isn't deployed on the server yet. This is a one-time backend deploy step (server-26 #13 / #51), not a problem with your data."; + } + return `Couldn't load incidents: ${raw}`; +} + function fmtTime(iso: string) { try { return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } catch { return iso; } } @@ -286,7 +297,7 @@ export default function IncidentsPage() { recorded yet" over the top of it told the operator the radio was quiet when the page had simply failed to load — server-26#13. */} {filtered.length === 0 && error && ( - + )} {filtered.length === 0 && !error && ( diff --git a/drb-frontend/app/onboarding/page.tsx b/drb-frontend/app/onboarding/page.tsx index 789d5a1..3ad9a73 100644 --- a/drb-frontend/app/onboarding/page.tsx +++ b/drb-frontend/app/onboarding/page.tsx @@ -43,7 +43,7 @@ export default function OnboardingPage() { // Firebase custom claims only show up in a *freshly fetched* ID token — // getIdTokenResult(true) inside refreshClaims forces that fetch, then // AuthProvider's own state (orgId) updates and the effect above - // redirects to /dashboard. + // redirects to "/" (Live). await refreshClaims(); } catch (err) { setError(err instanceof Error ? err.message : "Could not set up your organization. Try again."); diff --git a/drb-frontend/components/MapView.tsx b/drb-frontend/components/MapView.tsx index b047e68..13f0529 100644 --- a/drb-frontend/components/MapView.tsx +++ b/drb-frontend/components/MapView.tsx @@ -24,6 +24,17 @@ L.Icon.Default.mergeOptions({ shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png", }); +// ── 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. +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 = + '© OpenStreetMap contributors © CARTO'; + // ── Colour ──────────────────────────────────────────────────────────────────── // Severity is the only hue on this map — see UI_REDESIGN.md §2.3. Incident // type is carried by the glyph knocked out of the pin, never by colour, and @@ -540,14 +551,14 @@ export default function MapView({ nodes, activeCalls, incidents = [], calls = [] {/* Base layers */} -- 2.54.0