From 9aed9d3eea238a9d97042be3f84e800d84901214 Mon Sep 17 00:00:00 2001 From: csysp Date: Thu, 12 Mar 2026 18:26:43 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20add=20click-to-dismiss=20=C3=97=20butto?= =?UTF-8?q?n=20on=20global=20incidents=20popups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each alert bubble now has an × button in the top-right corner. Clicking it hides the alert for the session and clears its selection if it was active. - Dismissal keyed by stable content hash (title+coords) so dismissed state survives data.news array replacement on every 60s polling cycle - Button stopPropagation prevents accidental entity selection on dismiss - Single useState> — avoids naming collision with the react-map-gl `Map` import that caused the previous black-screen crash Co-Authored-By: Claude Opus 4.6 Former-commit-id: ce2dec52a9a40a581995323354414b278abdf443 --- frontend/src/components/MaplibreViewer.tsx | 59 +++++++++++++++++----- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/MaplibreViewer.tsx b/frontend/src/components/MaplibreViewer.tsx index 1ef5ea7..5b42c48 100644 --- a/frontend/src/components/MaplibreViewer.tsx +++ b/frontend/src/components/MaplibreViewer.tsx @@ -330,6 +330,11 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele const [shipClusters, setShipClusters] = useState([]); const [eqClusters, setEqClusters] = useState([]); + // Global Incidents popup: dismiss state + // Keys use stable content hash (title+coords) to survive data.news array replacement on refresh + // NOTE: Using Set (not Map) to avoid collision with the `Map` react-map-gl import + const [dismissedAlerts, setDismissedAlerts] = useState>(new Set()); + // --- Smooth interpolation: tick counter triggers GeoJSON recalc every second --- const [interpTick, setInterpTick] = useState(0); const dataTimestamp = useRef(Date.now()); @@ -1911,17 +1916,20 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele {/* Maplibre HTML Custom Markers for high-importance Threat Overlays (highest z-index) */} {activeLayers.global_incidents && spreadAlerts.map((n: any) => { const idx = n.originalIdx; + // Stable key: survives data.news array reorder/replacement across polling cycles + const alertKey = `${n.title || ''}_${(n.coords || []).join(',')}`; + if (dismissedAlerts.has(alertKey)) return null; + const count = n.cluster_count || 1; const score = n.risk_score || 0; - let riskColor = '#22c55e'; // Green (1-3) - if (score >= 9) riskColor = '#ef4444'; // Red (9-10) - else if (score >= 7) riskColor = '#f97316'; // Orange (7-8) - else if (score >= 4) riskColor = '#eab308'; // Yellow (4-6) - else if (score >= 1) riskColor = '#3b82f6'; // Blue (1-3) + let riskColor = '#22c55e'; // Green (0) + if (score >= 9) riskColor = '#ef4444'; // Red + else if (score >= 7) riskColor = '#f97316'; // Orange + else if (score >= 4) riskColor = '#eab308'; // Yellow + else if (score >= 1) riskColor = '#3b82f6'; // Blue // Hide alerts when any entity is selected (focus mode) - // For news: only show the selected alert. For all others: hide all alerts. let isVisible = viewState.zoom >= 1; if (selectedEntity) { if (selectedEntity.type === 'news') { @@ -1933,7 +1941,7 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele return (
- {/* Connector Line for scattered markers (Speech Bubble Line) */} + {/* Connector Line */} {n.showLine && isVisible && ( @@ -1961,7 +1969,7 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele backgroundColor: 'rgba(5, 5, 5, 0.95)', border: `1.5px solid ${riskColor}`, borderRadius: '4px', - padding: '5px 8px', + padding: '5px 24px 5px 8px', color: riskColor, fontFamily: 'monospace', fontSize: '9px', @@ -1970,10 +1978,11 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele boxShadow: `0 0 12px ${riskColor}60`, zIndex: 10, lineHeight: '1.2', - minWidth: '120px' + minWidth: '120px', + position: 'relative', }} > - {/* Bubble Tail / Triangle */} + {/* Bubble Tail */} {n.showLine && isVisible && (
0 ? `6px solid ${riskColor}` : 'none', left: '50%', @@ -1992,6 +2000,33 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele /> )} + {/* Dismiss button */} + +
!! ALERT LVL {score} !!