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 */}