Building a Live Incident Dashboard with Python and Leaflet
A regional emergency operations center stands up a Leaflet wall display to track a moving brush fire: engines push GPS pings, a 9-1-1 plotter drops new ignitions, and three mutual-aid agencies edit the same incident layer. At twenty updates a second the map repaints cleanly. Then the wind shifts, call volume spikes, a cellular sector congests, and the dashboard built on naive HTTP polling falls thirty seconds behind — markers stack on stale coordinates, the strike-team leader is looking at a perimeter that no longer exists, and an evacuation order goes out against a map that lied. This page solves that one narrow failure: a Python-fed Leaflet dashboard that stays live and honest under surge, consuming the normalized stream produced by the WebSocket and MQTT live incident feed bridge instead of polling, and degrading to a logged, audited fallback rather than to a frozen tile.
Root Cause and Operational Impact
The freeze is rarely the network’s fault and almost never Leaflet’s rendering core. Three conditions turn a working dashboard into a hazard the moment the rate climbs:
- Request-cycle latency compounds. Stateless HTTP polling re-opens a TCP/TLS handshake, re-authenticates, and re-serializes the full feature set on every tick. At surge volume the poll interval and the round-trip time stack, so the displayed state is always one or two intervals stale — and “two intervals” during a fast-moving incident is a perimeter that has already crossed a road.
- Unbounded DOM mutation. Pushing every inbound feature straight into the map as a fresh
L.Markerforces a layout reflow per message. A few hundred markers a second saturates the main thread; the browser stops painting, input handlers queue, and the operator cannot pan or zoom while the event is at its worst. - No degraded mode. When the persistent stream drops in an RF-shadowed canyon or a congested cell, a dashboard with no fallback simply stops updating — and shows no signal that it has stopped. A blank-but-still map is more dangerous than an error, because command staff keep trusting it.
In an after-action report this reads as “UI was slow.” During the incident it is a positional-truth failure: a unit dispatched to a marker that is ninety seconds behind the fire, a structure count under-reported into the NIMS (National Incident Management System) ICS-209 situation report, or an agency’s own units vanishing from the common operating picture. The dashboard must hold its latency budget under surge and announce when it has degraded, never fail silently to a stale tile.
Tiered Resolution Strategy
Work the dashboard from the definitive real-time path down to a safe default that is always visible and always logged:
- Definitive fix — diff-driven WebSocket updates onto a canvas renderer. Consume the normalized GeoJSON stream from the broker, where each message already carries a
diff_type(create,update,delete). Apply only the diff to an existingL.MarkerClusterGroupbacked byL.Canvas, never a full layer rebuild. The map mutates a single feature per message instead of repainting the world. - Coalesce updates to the frame budget. Buffer inbound messages and flush them once per
requestAnimationFrame(≈60 Hz ceiling, throttled to 1 Hz per feature), so a burst of a thousand pings becomes one batched render rather than a thousand reflows. - Deduplicate before render. Hash on
incident_idplussequence_numberso an at-least-once MQTT redelivery (QoS 1) does not paint a duplicate marker or resurrect a deleted one. - Safe default with an audit flag. If the WebSocket drops, run exponential-backoff reconnect while transparently switching to a delta-polling HTTP endpoint, raise a visible “DEGRADED — polling fallback” banner, and stamp the served snapshot with
fallback_mode=trueand a sync timestamp. A flagged, visibly-degraded map is recoverable; a silently frozen one is not.
Production Python Implementation
The broker below ingests validated telemetry, tags each feature with a diff_type so the client can apply minimal updates, and exposes a delta-poll fallback for clients that lose the socket. It uses full type hints, explicit exception boundaries, structured logging (no print), and emits an audit record whenever a payload is quarantined or a client is served in degraded mode. It assumes coordinates have already passed real-time geocoding and location normalization and conform to the broker’s GeoJSON schema.
import hashlib
import json
import logging
from datetime import datetime, timezone
from typing import Any, Literal
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from pydantic import BaseModel, ValidationError, field_validator
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(levelname)-8s | %(funcName)s | %(message)s",
)
logger = logging.getLogger("incident_dashboard_broker")
DiffType = Literal["create", "update", "delete"]
class IncidentFeature(BaseModel):
"""Minimal validated incident payload bound for the Leaflet client."""
incident_id: str
sequence_number: int
diff_type: DiffType
lat: float
lon: float
severity: int
timestamp: str
@field_validator("lat")
@classmethod
def _check_lat(cls, v: float) -> float:
if not (-90.0 <= v <= 90.0):
raise ValueError("Latitude outside WGS 84 / EPSG:4326 bounds (-90..90)")
return v
@field_validator("lon")
@classmethod
def _check_lon(cls, v: float) -> float:
if not (-180.0 <= v <= 180.0):
raise ValueError("Longitude outside WGS 84 / EPSG:4326 bounds (-180..180)")
return v
# Latest authoritative state per incident, used to serve the delta-poll fallback.
incident_cache: dict[str, dict[str, Any]] = {}
ws_clients: set[Any] = set()
_seen: set[str] = set()
def _dedup_key(feature: IncidentFeature) -> str:
"""Stable hash so an at-least-once (QoS 1) redelivery renders only once."""
raw = f"{feature.incident_id}:{feature.sequence_number}:{feature.diff_type}"
return hashlib.sha1(raw.encode("utf-8")).hexdigest()
async def _audit(event: str, **fields: Any) -> None:
"""Emit an immutable audit record for after-action review."""
record = {"event": event, "audit_ts": datetime.now(timezone.utc).isoformat(), **fields}
logger.info("AUDIT %s", json.dumps(record, separators=(",", ":")))
async def process_telemetry(raw_message: str) -> None:
"""Validate, dedupe, cache, and fan out one inbound telemetry message."""
try:
feature = IncidentFeature(**json.loads(raw_message))
except (json.JSONDecodeError, ValidationError) as exc:
# Quarantine malformed payloads to the dead-letter queue, never the map.
await _audit("payload_quarantined", error=str(exc), payload=raw_message[:200])
return
key = _dedup_key(feature)
if key in _seen:
return # idempotent redelivery; suppress duplicate render
_seen.add(key)
# Cache authoritative state so a degraded client can resync via delta poll.
if feature.diff_type == "delete":
incident_cache.pop(feature.incident_id, None)
else:
incident_cache[feature.incident_id] = feature.model_dump(mode="json")
await _broadcast(feature.model_dump(mode="json"))
async def _broadcast(payload: dict[str, Any]) -> None:
"""Fan out to connected Leaflet clients, pruning dead sockets as we go."""
if not ws_clients:
return
dropped: set[Any] = set()
for client in ws_clients:
try:
await client.send_json(payload)
except Exception: # noqa: BLE001 — any send failure means a dead socket
dropped.add(client)
ws_clients.difference_update(dropped)
app = FastAPI()
@app.get("/api/incidents/fallback")
async def fallback_snapshot(last_sync: str | None = None) -> JSONResponse:
"""Delta-poll endpoint for clients that lost the WebSocket.
Returns only features updated since the caller's last successful sync and
flags the response so the dashboard can render a visible DEGRADED state.
"""
delta = {
iid: feat
for iid, feat in incident_cache.items()
if last_sync is None or feat["timestamp"] > last_sync
}
await _audit("degraded_poll_served", since=last_sync, feature_count=len(delta))
return JSONResponse(
content={
"fallback_mode": True,
"server_ts": datetime.now(timezone.utc).isoformat(),
"features": delta,
}
)
On the browser side, the client applies the diff_type against an L.MarkerClusterGroup on a canvas renderer and coalesces a burst of messages into a single animation frame:
const renderer = L.canvas({ padding: 0.5 }); // canvas, not SVG, under surge
const cluster = L.markerClusterGroup({ chunkedLoading: true });
map.addLayer(cluster);
const markers = new Map(); // incident_id -> L.Marker
let pending = []; // batched diffs, flushed per frame
let frameQueued = false;
function flush() {
frameQueued = false;
for (const f of pending) {
if (f.diff_type === "delete") {
const m = markers.get(f.incident_id);
if (m) { cluster.removeLayer(m); markers.delete(f.incident_id); }
} else {
let m = markers.get(f.incident_id);
if (!m) {
m = L.circleMarker([f.lat, f.lon], { renderer });
markers.set(f.incident_id, m);
cluster.addLayer(m);
} else {
m.setLatLng([f.lat, f.lon]); // update in place, no full-layer rebuild
}
}
}
pending = [];
}
function onMessage(feature) {
pending.push(feature);
if (!frameQueued) { frameQueued = true; requestAnimationFrame(flush); }
}
Validation Checklist
Verify each item before the dashboard fronts a live operational feed:
- The client subscribes to the WebSocket stream and never polls the full feature set on a timer while the socket is healthy.
- Every inbound message carries
diff_type; the client applies the diff in place and never rebuilds the whole layer per update. - Markers render on an
L.Canvasrenderer (notL.SVG) and high-density points useL.MarkerClusterGroup. - Inbound messages are coalesced into a single
requestAnimationFrameflush and throttled to at most 1 Hz per feature. - Redelivered messages are deduplicated on
incident_id+sequence_numberso a QoS 1 retry paints exactly once. - A dropped socket triggers exponential-backoff reconnect and switches to the
/api/incidents/fallbackdelta poll within one interval. - Degraded mode raises a visible “DEGRADED — polling fallback” banner; the map never silently stops updating.
- Every quarantined payload and every degraded poll emits an audit record carrying an
audit_tsfor after-action review.
Edge Cases and Gotchas
Axis-order inversion. Leaflet expects [lat, lng]; GeoJSON and most broker payloads carry [lon, lat]. Feeding raw GeoJSON coordinate pairs straight into L.circleMarker silently lands every unit in the wrong hemisphere. Destructure explicitly (f.lat, f.lon) and spot-check one known fixture before go-live.
Null-island drift. A failed upstream transform pulls coordinates toward (0, 0). Any feature near the equator/prime-meridian intersection should be treated as a failed normalization and dropped from the map rather than plotted off the West African coast, where it also skews any auto-fit bounds the operator triggers.
Offline device quirks. Field tablets that lose connectivity keep their last sessionStorage snapshot; on reconnect they must replay the delta poll with their stored last_sync rather than assuming the live stream is already current, or they will miss every delete that fired while they were dark.
Agency-specific datum anomalies. One mutual-aid agency publishing in NAD27 while the feed standard is NAD83(2011) offsets its markers by tens of metres near sector edges — enough to misassign a structure across a jurisdiction line. Resolve datum shifts during normalization upstream; never let the dashboard “correct” coordinates after the fact. Where several agencies edit the same incident concurrently, pair this dashboard with conflict resolution in multi-agency edits so a fast render never displays a state that lost the merge, and gate inbound features with automated attribute validation rules before they ever reach the broadcast loop.