diff --git a/app/static/index.html b/app/static/index.html
index d5944f1..0b91845 100644
--- a/app/static/index.html
+++ b/app/static/index.html
@@ -833,13 +833,22 @@ async function initMap() {
if (activeHls) { try { activeHls.destroy(); } catch (_) {} activeHls = null; }
});
updateHeatLegend();
- // Reload overlays when the user pans/zooms. Skip when a popup is open:
- // opening a camera popup auto-pans the map to fit it, and that moveend
- // must NOT rebuild the marker group under the open popup (which would
- // close it). Leaflet closes popups on manual drag, so a live popup here
- // means the move was autopan — safe to skip.
+ // Reload overlays when the user pans/zooms. A live popup makes the
+ // rebuild skip (so the popup's autopan doesn't destroy it), but user
+ // zoom/drag must close the popup FIRST — otherwise zooming out with a
+ // camera popup open leaves the stale, zoomed-in marker group on the
+ // map and the cameras "vanish" until a refresh.
+ //
+ // NOTE: `map._popup` is NOT a reliable open-check in Leaflet 1.9.4 —
+ // Map.closePopup() never nulls it, so once any popup has been opened
+ // it stays truthy forever. Track the real state via popupopen/close.
+ let camPopupOpen = false;
+ map.on('popupopen', () => { camPopupOpen = true; });
+ map.on('popupclose', () => { camPopupOpen = false; });
+ map.on('zoomstart', () => { if (camPopupOpen) map.closePopup(); });
+ map.on('dragstart', () => { if (camPopupOpen) map.closePopup(); });
map.on('moveend', () => {
- if (map._popup) return;
+ if (camPopupOpen) return; // only the popup's own autopan now
if (firesOn) loadFires();
if (camsOn) loadCams();
});