fix: enrich conflict zone fallback with country names, actors, and dates

When both ACLED and UCDP fail, the dashboard falls back to static
INTEL_HOTSPOTS data. Previously the fallback events had empty fields
(country, location, actors, date, source, notes). Now maps ISO codes
to country names via TIER1_COUNTRIES, populates all fields the
frontend modal expects, and includes escalation severity labels.
This commit is contained in:
Marc Shade
2026-03-08 09:02:32 -04:00
parent b01a77721b
commit a43d07cb3e
+116 -42
View File
@@ -11,10 +11,11 @@ from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv from dotenv import load_dotenv
load_dotenv(Path(__file__).resolve().parents[3] / ".env", override=False) load_dotenv(Path(__file__).resolve().parents[3] / ".env", override=False)
from starlette.applications import Starlette from starlette.applications import Starlette
from starlette.responses import HTMLResponse, JSONResponse, Response, StreamingResponse from starlette.responses import HTMLResponse, JSONResponse, StreamingResponse
from starlette.routing import Route from starlette.routing import Route
from world_intel_mcp.cache import Cache from world_intel_mcp.cache import Cache
@@ -53,10 +54,23 @@ from world_intel_mcp.analysis.exposure import fetch_population_exposure
from world_intel_mcp.analysis.situation import fetch_situation_brief from world_intel_mcp.analysis.situation import fetch_situation_brief
from world_intel_mcp.sources.fleet import fetch_fleet_report from world_intel_mcp.sources.fleet import fetch_fleet_report
from world_intel_mcp.sources.usni_fleet import fetch_usni_fleet from world_intel_mcp.sources.usni_fleet import fetch_usni_fleet
from world_intel_mcp.config.countries import INTEL_HOTSPOTS, STRATEGIC_WATERWAYS from world_intel_mcp.config.countries import (
from world_intel_mcp.config.geospatial import MILITARY_BASES, STRATEGIC_PORTS, PIPELINES, NUCLEAR_FACILITIES INTEL_HOTSPOTS,
STRATEGIC_WATERWAYS,
TIER1_COUNTRIES,
)
from world_intel_mcp.config.geospatial import (
MILITARY_BASES,
STRATEGIC_PORTS,
PIPELINES,
NUCLEAR_FACILITIES,
)
from world_intel_mcp.sources.infrastructure import CABLE_CORRIDORS from world_intel_mcp.sources.infrastructure import CABLE_CORRIDORS
from world_intel_mcp.config.trade_routes import TRADE_ROUTES, CLOUD_REGIONS, FINANCIAL_CENTERS from world_intel_mcp.config.trade_routes import (
TRADE_ROUTES,
CLOUD_REGIONS,
FINANCIAL_CENTERS,
)
from world_intel_mcp.sources.central_banks import fetch_central_bank_rates from world_intel_mcp.sources.central_banks import fetch_central_bank_rates
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -82,6 +96,7 @@ def _ensure_fetcher() -> Fetcher:
# Data fetching # Data fetching
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
async def _fetch_overview() -> dict: async def _fetch_overview() -> dict:
"""Fetch all dashboard domains in parallel, return unified dict.""" """Fetch all dashboard domains in parallel, return unified dict."""
fetcher = _ensure_fetcher() fetcher = _ensure_fetcher()
@@ -163,22 +178,42 @@ async def _fetch_overview() -> dict:
acled_ok = not acled.get("error") and (acled.get("count") or 0) > 0 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 ucdp_ok = not ucdp.get("error") and (ucdp.get("count") or 0) > 0
if not acled_ok and not ucdp_ok: if not acled_ok and not ucdp_ok:
escalation_labels = {1: "low", 2: "low", 3: "moderate", 4: "high", 5: "critical"} escalation_labels = {
hotspot_events = [ 1: "low",
{ 2: "low",
"latitude": h["lat"], 3: "moderate",
"longitude": h["lon"], 4: "high",
"country": name.replace("_", " ").title(), 5: "critical",
"event_type": "conflict zone", }
"type_of_violence_label": "active hotspot", # Map ISO codes to country names for richer display
"fatalities": 0, _iso_names = {c: info["name"] for c, info in TIER1_COUNTRIES.items()}
"best": 0, hotspot_events = []
"escalation": h["baseline_escalation"], for name, h in INTEL_HOTSPOTS.items():
"severity": escalation_labels.get(h["baseline_escalation"], "unknown"), assoc = h.get("associated_countries", [])
"associated_countries": h.get("associated_countries", []), country_name = _iso_names.get(assoc[0], "") if assoc else ""
} actors = [_iso_names.get(c, c) for c in assoc]
for name, h in INTEL_HOTSPOTS.items() esc = h["baseline_escalation"]
] hotspot_events.append(
{
"latitude": h["lat"],
"longitude": h["lon"],
"country": country_name or name.replace("_", " ").title(),
"location": name.replace("_", " ").title(),
"event_type": "Active Hotspot",
"type_of_violence_label": "active hotspot",
"fatalities": 0,
"best": 0,
"escalation": esc,
"severity": escalation_labels.get(esc, "unknown"),
"event_date": datetime.now(timezone.utc).strftime("%Y-%m-%d"),
"actor1": actors[0] if len(actors) > 0 else "\u2014",
"actor2": actors[1] if len(actors) > 1 else "\u2014",
"source": "Intel Hotspot Database",
"notes": f"Escalation level {esc}/5 ({escalation_labels.get(esc, 'unknown')}). "
f"Monitored hotspot involving {', '.join(actors)}.",
"associated_countries": assoc,
}
)
result["conflict_zones"] = { result["conflict_zones"] = {
"events": hotspot_events, "events": hotspot_events,
"count": len(hotspot_events), "count": len(hotspot_events),
@@ -187,16 +222,33 @@ async def _fetch_overview() -> dict:
# Static geospatial datasets (no API calls) # Static geospatial datasets (no API calls)
result["military_bases"] = {"bases": MILITARY_BASES, "count": len(MILITARY_BASES)} result["military_bases"] = {"bases": MILITARY_BASES, "count": len(MILITARY_BASES)}
result["strategic_ports"] = {"ports": STRATEGIC_PORTS, "count": len(STRATEGIC_PORTS)} result["strategic_ports"] = {
"ports": STRATEGIC_PORTS,
"count": len(STRATEGIC_PORTS),
}
result["pipelines"] = {"pipelines": PIPELINES, "count": len(PIPELINES)} result["pipelines"] = {"pipelines": PIPELINES, "count": len(PIPELINES)}
result["nuclear_facilities"] = {"facilities": NUCLEAR_FACILITIES, "count": len(NUCLEAR_FACILITIES)} result["nuclear_facilities"] = {
result["waterways"] = {"waterways": STRATEGIC_WATERWAYS, "count": len(STRATEGIC_WATERWAYS)} "facilities": NUCLEAR_FACILITIES,
"count": len(NUCLEAR_FACILITIES),
}
result["waterways"] = {
"waterways": STRATEGIC_WATERWAYS,
"count": len(STRATEGIC_WATERWAYS),
}
result["trade_routes"] = {"routes": TRADE_ROUTES, "count": len(TRADE_ROUTES)} result["trade_routes"] = {"routes": TRADE_ROUTES, "count": len(TRADE_ROUTES)}
result["cloud_regions"] = {"regions": CLOUD_REGIONS, "count": len(CLOUD_REGIONS)} result["cloud_regions"] = {"regions": CLOUD_REGIONS, "count": len(CLOUD_REGIONS)}
result["financial_centers"] = {"centers": FINANCIAL_CENTERS, "count": len(FINANCIAL_CENTERS)} result["financial_centers"] = {
"centers": FINANCIAL_CENTERS,
"count": len(FINANCIAL_CENTERS),
}
result["cable_corridors"] = { result["cable_corridors"] = {
"corridors": [ "corridors": [
{"name": n, "lat_range": c["lat_range"], "lon_range": c["lon_range"], "cables": c["cables"]} {
"name": n,
"lat_range": c["lat_range"],
"lon_range": c["lon_range"],
"cables": c["cables"],
}
for n, c in CABLE_CORRIDORS.items() for n, c in CABLE_CORRIDORS.items()
], ],
"count": len(CABLE_CORRIDORS), "count": len(CABLE_CORRIDORS),
@@ -205,7 +257,8 @@ async def _fetch_overview() -> dict:
# AI situational brief (runs after main gather so it has all data) # AI situational brief (runs after main gather so it has all data)
try: try:
result["situation_brief"] = await asyncio.wait_for( result["situation_brief"] = await asyncio.wait_for(
fetch_situation_brief(result), timeout=35.0, fetch_situation_brief(result),
timeout=35.0,
) )
except Exception as exc: except Exception as exc:
logger.warning("Situation brief failed: %s", exc) logger.warning("Situation brief failed: %s", exc)
@@ -224,6 +277,7 @@ async def _fetch_overview() -> dict:
# Routes # Routes
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
async def index(request): async def index(request):
"""Serve the dashboard HTML page (reloads on each request during dev).""" """Serve the dashboard HTML page (reloads on each request during dev)."""
html_path = Path(__file__).parent / "index.html" html_path = Path(__file__).parent / "index.html"
@@ -269,23 +323,43 @@ async def api_static(request):
The dashboard fetches this on boot so the infrastructure layer The dashboard fetches this on boot so the infrastructure layer
populates immediately without waiting for the full SSE gather. populates immediately without waiting for the full SSE gather.
""" """
return JSONResponse({ return JSONResponse(
"military_bases": {"bases": MILITARY_BASES, "count": len(MILITARY_BASES)}, {
"strategic_ports": {"ports": STRATEGIC_PORTS, "count": len(STRATEGIC_PORTS)}, "military_bases": {"bases": MILITARY_BASES, "count": len(MILITARY_BASES)},
"pipelines": {"pipelines": PIPELINES, "count": len(PIPELINES)}, "strategic_ports": {
"nuclear_facilities": {"facilities": NUCLEAR_FACILITIES, "count": len(NUCLEAR_FACILITIES)}, "ports": STRATEGIC_PORTS,
"waterways": {"waterways": STRATEGIC_WATERWAYS, "count": len(STRATEGIC_WATERWAYS)}, "count": len(STRATEGIC_PORTS),
"cable_corridors": { },
"corridors": [ "pipelines": {"pipelines": PIPELINES, "count": len(PIPELINES)},
{"name": n, "lat_range": c["lat_range"], "lon_range": c["lon_range"], "cables": c["cables"]} "nuclear_facilities": {
for n, c in CABLE_CORRIDORS.items() "facilities": NUCLEAR_FACILITIES,
], "count": len(NUCLEAR_FACILITIES),
"count": len(CABLE_CORRIDORS), },
"waterways": {
"waterways": STRATEGIC_WATERWAYS,
"count": len(STRATEGIC_WATERWAYS),
},
"cable_corridors": {
"corridors": [
{
"name": n,
"lat_range": c["lat_range"],
"lon_range": c["lon_range"],
"cables": c["cables"],
}
for n, c in CABLE_CORRIDORS.items()
],
"count": len(CABLE_CORRIDORS),
},
"trade_routes": {"routes": TRADE_ROUTES, "count": len(TRADE_ROUTES)},
"cloud_regions": {"regions": CLOUD_REGIONS, "count": len(CLOUD_REGIONS)},
"financial_centers": {
"centers": FINANCIAL_CENTERS,
"count": len(FINANCIAL_CENTERS),
},
}, },
"trade_routes": {"routes": TRADE_ROUTES, "count": len(TRADE_ROUTES)}, headers={"Access-Control-Allow-Origin": "*"},
"cloud_regions": {"regions": CLOUD_REGIONS, "count": len(CLOUD_REGIONS)}, )
"financial_centers": {"centers": FINANCIAL_CENTERS, "count": len(FINANCIAL_CENTERS)},
}, headers={"Access-Control-Allow-Origin": "*"})
async def api_health(request): async def api_health(request):