frontend: fix map stacking + honest infra error states #108
@@ -186,7 +186,7 @@ function RulesTab({ isAdmin }: { isAdmin: boolean }) {
|
|||||||
|
|
||||||
export default function AlertsPage() {
|
export default function AlertsPage() {
|
||||||
const { isAdmin } = useAuth();
|
const { isAdmin } = useAuth();
|
||||||
const { alerts, loading } = useAlerts();
|
const { alerts, loading, error } = useAlerts();
|
||||||
const [tab, setTab] = useState<"events" | "rules">("events");
|
const [tab, setTab] = useState<"events" | "rules">("events");
|
||||||
|
|
||||||
async function handleAcknowledge(id: string) {
|
async function handleAcknowledge(id: string) {
|
||||||
@@ -226,6 +226,12 @@ export default function AlertsPage() {
|
|||||||
{tab === "events" && (
|
{tab === "events" && (
|
||||||
loading ? (
|
loading ? (
|
||||||
<p className="text-gray-500 text-sm font-mono">Loading…</p>
|
<p className="text-gray-500 text-sm font-mono">Loading…</p>
|
||||||
|
) : error ? (
|
||||||
|
<p className="text-red-400 text-sm font-mono">
|
||||||
|
{/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}`}
|
||||||
|
</p>
|
||||||
) : alerts.length === 0 ? (
|
) : alerts.length === 0 ? (
|
||||||
<p className="text-gray-600 text-sm font-mono">No alerts triggered yet.</p>
|
<p className="text-gray-600 text-sm font-mono">No alerts triggered yet.</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -163,6 +163,20 @@ html:not(.dark) .border-indigo-800 { border-color: #a5b4fc !important; }
|
|||||||
animation: pulse-ring 1.8s ease-out infinite;
|
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 ─────────────────────────────────────────────────────────── */
|
/* ── Form inputs ─────────────────────────────────────────────────────────── */
|
||||||
html:not(.dark) input:not([type="submit"]):not([type="button"]):not([type="reset"]),
|
html:not(.dark) input:not([type="submit"]):not([type="button"]):not([type="reset"]),
|
||||||
html:not(.dark) select,
|
html:not(.dark) select,
|
||||||
|
|||||||
@@ -28,6 +28,17 @@ const FILTER_THRESHOLD: Record<SeverityFilter, number> = { all: -1, minor: 1, mo
|
|||||||
|
|
||||||
type SortMode = "recent" | "severity";
|
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) {
|
function fmtTime(iso: string) {
|
||||||
try { return new Date(iso).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } catch { return iso; }
|
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
|
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. */}
|
quiet when the page had simply failed to load — server-26#13. */}
|
||||||
{filtered.length === 0 && error && (
|
{filtered.length === 0 && error && (
|
||||||
<ErrorBanner message={`Couldn't load incidents: ${error}`} />
|
<ErrorBanner message={friendlyIncidentsError(error)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{filtered.length === 0 && !error && (
|
{filtered.length === 0 && !error && (
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function OnboardingPage() {
|
|||||||
// Firebase custom claims only show up in a *freshly fetched* ID token —
|
// Firebase custom claims only show up in a *freshly fetched* ID token —
|
||||||
// getIdTokenResult(true) inside refreshClaims forces that fetch, then
|
// getIdTokenResult(true) inside refreshClaims forces that fetch, then
|
||||||
// AuthProvider's own state (orgId) updates and the effect above
|
// AuthProvider's own state (orgId) updates and the effect above
|
||||||
// redirects to /dashboard.
|
// redirects to "/" (Live).
|
||||||
await refreshClaims();
|
await refreshClaims();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Could not set up your organization. Try again.");
|
setError(err instanceof Error ? err.message : "Could not set up your organization. Try again.");
|
||||||
|
|||||||
@@ -24,6 +24,17 @@ L.Icon.Default.mergeOptions({
|
|||||||
shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
|
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 =
|
||||||
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/">CARTO</a>';
|
||||||
|
|
||||||
// ── Colour ────────────────────────────────────────────────────────────────────
|
// ── Colour ────────────────────────────────────────────────────────────────────
|
||||||
// Severity is the only hue on this map — see UI_REDESIGN.md §2.3. Incident
|
// 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
|
// 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 */}
|
{/* Base layers */}
|
||||||
<LayersControl.BaseLayer checked name="Dark">
|
<LayersControl.BaseLayer checked name="Dark">
|
||||||
<TileLayer
|
<TileLayer
|
||||||
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"
|
url={MAP_TILE_URL}
|
||||||
attribution='© <a href="https://carto.com/">CARTO</a>'
|
attribution={MAP_TILE_ATTRIBUTION}
|
||||||
/>
|
/>
|
||||||
</LayersControl.BaseLayer>
|
</LayersControl.BaseLayer>
|
||||||
<LayersControl.BaseLayer name="Light">
|
<LayersControl.BaseLayer name="Light">
|
||||||
<TileLayer
|
<TileLayer
|
||||||
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
|
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
|
||||||
attribution='© <a href="https://carto.com/">CARTO</a>'
|
attribution={MAP_TILE_ATTRIBUTION}
|
||||||
/>
|
/>
|
||||||
</LayersControl.BaseLayer>
|
</LayersControl.BaseLayer>
|
||||||
<LayersControl.BaseLayer name="Streets">
|
<LayersControl.BaseLayer name="Streets">
|
||||||
|
|||||||
Reference in New Issue
Block a user