From dd3c4f6951a1e2f7a51c5330ef189e36c71eecda Mon Sep 17 00:00:00 2001 From: Marc Shade Date: Mon, 23 Feb 2026 22:25:05 -0500 Subject: [PATCH] fix: conflict zones always visible via INTEL_HOTSPOTS fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When both ACLED (no API key) and UCDP (timeout/empty) fail, the dashboard now falls back to 22 pre-coded conflict hotspots from INTEL_HOTSPOTS with severity levels (critical/high/moderate/low). Frontend updated with 3-tier fallback: ACLED → UCDP → conflict_zones. Marker sizing uses escalation level for hotspots, tooltips show severity. Co-Authored-By: Claude Opus 4.6 --- src/world_intel_mcp/dashboard/app.py | 30 ++++++++++++++++++++++++ src/world_intel_mcp/dashboard/index.html | 29 +++++++++++++++++++---- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/world_intel_mcp/dashboard/app.py b/src/world_intel_mcp/dashboard/app.py index 06f703c..89fc1df 100644 --- a/src/world_intel_mcp/dashboard/app.py +++ b/src/world_intel_mcp/dashboard/app.py @@ -42,6 +42,7 @@ from world_intel_mcp.sources import ( nuclear, ) from world_intel_mcp.analysis.alerts import fetch_alert_digest, fetch_weekly_trends +from world_intel_mcp.config.countries import INTEL_HOTSPOTS logger = logging.getLogger(__name__) @@ -119,6 +120,35 @@ async def _fetch_overview() -> dict: else: result[name] = data + # Conflict zone fallback: when both ACLED and UCDP fail, provide + # static hotspot data so the conflict layer is never empty. + acled = result.get("acled_events", {}) + ucdp = result.get("ucdp_events", {}) + acled_ok = not acled.get("error") and (acled.get("count") or 0) > 0 + ucdp_ok = not ucdp.get("error") and (ucdp.get("count") or 0) > 0 + if not acled_ok and not ucdp_ok: + escalation_labels = {1: "low", 2: "low", 3: "moderate", 4: "high", 5: "critical"} + hotspot_events = [ + { + "latitude": h["lat"], + "longitude": h["lon"], + "country": name.replace("_", " ").title(), + "event_type": "conflict zone", + "type_of_violence_label": "active hotspot", + "fatalities": 0, + "best": 0, + "escalation": h["baseline_escalation"], + "severity": escalation_labels.get(h["baseline_escalation"], "unknown"), + "associated_countries": h.get("associated_countries", []), + } + for name, h in INTEL_HOTSPOTS.items() + ] + result["conflict_zones"] = { + "events": hotspot_events, + "count": len(hotspot_events), + "source": "intel-hotspots", + } + # Attach source health + timestamp result["source_health"] = _breaker.status() if _breaker else {} result["cache_stats"] = _cache.stats() if _cache else {} diff --git a/src/world_intel_mcp/dashboard/index.html b/src/world_intel_mcp/dashboard/index.html index cec2454..3cf83cc 100644 --- a/src/world_intel_mcp/dashboard/index.html +++ b/src/world_intel_mcp/dashboard/index.html @@ -700,12 +700,16 @@ function updateMapConflict(data) { var lat = e.latitude || e.lat, lon = e.longitude || e.lon; if (lat == null || lon == null) return; var fat = e.fatalities || e.best || 0; - var size = Math.max(8, Math.min(18, fat / 2 + 8)); + var esc_level = e.escalation || 0; + var size = fat > 0 ? Math.max(8, Math.min(18, fat / 2 + 8)) : Math.max(8, esc_level * 3 + 6); var label = e.event_type || e.type_of_violence_label || '?'; var mk = L.marker([Number(lat), Number(lon)], { icon: L.divIcon({ className: 'mk-conflict', iconSize: [size, size], iconAnchor: [size/2, size/2] }) }); - mk.bindTooltip(label + ' \u2014 ' + (e.country || '') + (fat > 0 ? ' (' + fat + ' fatal)' : ''), { className: 'mk-tip', direction: 'top', offset: [0, -size/2] }); + var tip = label + ' \u2014 ' + (e.country || ''); + if (fat > 0) tip += ' (' + fat + ' fatal)'; + else if (e.severity) tip += ' [' + e.severity + ']'; + mk.bindTooltip(tip, { className: 'mk-tip', direction: 'top', offset: [0, -size/2] }); mk.on('click', function() { showDetail('conflict', e); }); mk.addTo(mapLayers.conflict); }); @@ -811,7 +815,9 @@ function updateHudStats(data) { if (data.military_flights && !data.military_flights.error) { pills.push('
' + (data.military_flights.count || 0) + 'Aircraft
'); } - var conflictSrc = (data.acled_events && !data.acled_events.error) ? data.acled_events : data.ucdp_events; + var conflictSrc = (data.acled_events && !data.acled_events.error && data.acled_events.count > 0) ? data.acled_events + : (data.ucdp_events && !data.ucdp_events.error && data.ucdp_events.count > 0) ? data.ucdp_events + : data.conflict_zones; if (conflictSrc && !conflictSrc.error) { var ac = conflictSrc.count || 0; pills.push('
' + ac + 'Conflict
'); @@ -1084,6 +1090,18 @@ function updateDrawer(data) { h += ''; } } + if (data.conflict_zones && !data.conflict_zones.error) { + var czones = data.conflict_zones.events || []; + if (czones.length) { + h += '
Active Conflict Zones (' + (data.conflict_zones.count || czones.length) + ')
'; + czones.forEach(function(e) { + var sev = e.severity || 'unknown'; + var cls = sev === 'critical' ? 'down' : sev === 'high' ? 'warn' : 'dim'; + h += ''; + }); + h += '
ZoneSeverity
' + esc(trunc(e.country || '?', 25)) + '' + esc(sev) + '
'; + } + } if (data.displacement && !data.displacement.error) { var totals = data.displacement.global_totals || {}; h += '
Global Displacement
'; @@ -1343,7 +1361,10 @@ function updateAll(data) { // Map layers updateMapQuakes(data.earthquakes); updateMapMilitary(data.military_flights); - updateMapConflict((data.acled_events && !data.acled_events.error) ? data.acled_events : data.ucdp_events); + var conflictData = (data.acled_events && !data.acled_events.error && data.acled_events.count > 0) ? data.acled_events + : (data.ucdp_events && !data.ucdp_events.error && data.ucdp_events.count > 0) ? data.ucdp_events + : data.conflict_zones; + updateMapConflict(conflictData); updateMapFires(data.wildfires); updateMapConvergence(data.signal_convergence); updateMapNuclear(data.nuclear_monitor);