fix: conflict zones always visible via INTEL_HOTSPOTS fallback
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 {}
|
||||
|
||||
@@ -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('<div class="stat-pill"><span class="v">' + (data.military_flights.count || 0) + '</span><span class="l">Aircraft</span></div>');
|
||||
}
|
||||
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('<div class="stat-pill"><span class="v' + (ac > 100 ? ' warn' : '') + '">' + ac + '</span><span class="l">Conflict</span></div>');
|
||||
@@ -1084,6 +1090,18 @@ function updateDrawer(data) {
|
||||
h += '</tbody></table>';
|
||||
}
|
||||
}
|
||||
if (data.conflict_zones && !data.conflict_zones.error) {
|
||||
var czones = data.conflict_zones.events || [];
|
||||
if (czones.length) {
|
||||
h += '<div class="sub">Active Conflict Zones (' + (data.conflict_zones.count || czones.length) + ')</div><table class="dtable"><thead><tr><th>Zone</th><th>Severity</th></tr></thead><tbody>';
|
||||
czones.forEach(function(e) {
|
||||
var sev = e.severity || 'unknown';
|
||||
var cls = sev === 'critical' ? 'down' : sev === 'high' ? 'warn' : 'dim';
|
||||
h += '<tr><td>' + esc(trunc(e.country || '?', 25)) + '</td><td class="' + cls + '">' + esc(sev) + '</td></tr>';
|
||||
});
|
||||
h += '</tbody></table>';
|
||||
}
|
||||
}
|
||||
if (data.displacement && !data.displacement.error) {
|
||||
var totals = data.displacement.global_totals || {};
|
||||
h += '<div class="sub">Global Displacement</div><div class="mini-row">';
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user