diff --git a/src/world_intel_mcp/analysis/exposure.py b/src/world_intel_mcp/analysis/exposure.py index 060afe3..0871426 100644 --- a/src/world_intel_mcp/analysis/exposure.py +++ b/src/world_intel_mcp/analysis/exposure.py @@ -54,6 +54,8 @@ def _find_exposed_cities( exposed[cname] = { "city": cname, "country": city["country"], + "lat": city["lat"], + "lon": city["lon"], "population": city["pop"], "distance_km": round(dist, 1), "nearest_event": event.get("type", "unknown"), diff --git a/src/world_intel_mcp/dashboard/app.py b/src/world_intel_mcp/dashboard/app.py index cca2db0..e656ffe 100644 --- a/src/world_intel_mcp/dashboard/app.py +++ b/src/world_intel_mcp/dashboard/app.py @@ -43,6 +43,9 @@ from world_intel_mcp.sources import ( service_status, ) from world_intel_mcp.analysis.alerts import fetch_alert_digest, fetch_weekly_trends +from world_intel_mcp.analysis.posture import fetch_strategic_posture +from world_intel_mcp.analysis.exposure import fetch_population_exposure +from world_intel_mcp.sources.fleet import fetch_fleet_report from world_intel_mcp.config.countries import INTEL_HOTSPOTS from world_intel_mcp.config.geospatial import MILITARY_BASES, STRATEGIC_PORTS, PIPELINES, NUCLEAR_FACILITIES @@ -108,6 +111,9 @@ async def _fetch_overview() -> dict: "alert_digest": fetch_alert_digest(fetcher), "weekly_trends": fetch_weekly_trends(fetcher), "service_status": service_status.fetch_service_status(fetcher), + "strategic_posture": fetch_strategic_posture(fetcher), + "fleet_report": fetch_fleet_report(fetcher), + "population_exposure": fetch_population_exposure(fetcher), } gathered = await asyncio.gather( diff --git a/src/world_intel_mcp/dashboard/index.html b/src/world_intel_mcp/dashboard/index.html index 59da5eb..8c980b6 100644 --- a/src/world_intel_mcp/dashboard/index.html +++ b/src/world_intel_mcp/dashboard/index.html @@ -503,6 +503,12 @@ a { color: var(--accent); text-decoration: none; } 0
+
+ + Pop Exposure + 0 +
+
@@ -549,7 +555,7 @@ function ago(ts) { if (!ts) return '\u2014'; var s = Math.floor((Date.now() - ne var latestData = null; var map = null; var mapLayers = {}; -var layerState = { quakes: true, military: true, conflict: true, fires: true, convergence: true, nuclear: true, infra: true }; +var layerState = { quakes: true, military: true, conflict: true, fires: true, convergence: true, nuclear: true, infra: true, exposure: true }; var drawerOpen = false; // ════════════ MAP INIT ════════════ @@ -564,6 +570,7 @@ function initMap() { mapLayers.convergence = L.layerGroup().addTo(map); mapLayers.nuclear = L.layerGroup().addTo(map); mapLayers.infra = L.layerGroup().addTo(map); + mapLayers.exposure = L.layerGroup().addTo(map); } // ════════════ LAYER TOGGLES ════════════ @@ -1116,6 +1123,38 @@ function updateMapInfra(data) { $('#cntInfra').textContent = total; } +function updateMapExposure(data) { + mapLayers.exposure.clearLayers(); + if (!data || data.error) { $('#cntExposure').textContent = '0'; return; } + var cities = data.exposed_cities || []; + var evtColors = { conflict: '#ff1744', earthquake: '#ff9100', wildfire: '#ffea00' }; + $('#cntExposure').textContent = cities.length; + cities.forEach(function(c) { + if (c.lat == null || c.lon == null) return; + var color = evtColors[c.nearest_event] || '#e040fb'; + var pop = c.population || 0; + var radius = Math.max(5, Math.min(22, Math.sqrt(pop / 400000))); + var circle = L.circleMarker([c.lat, c.lon], { + radius: radius, color: color, fillColor: color, fillOpacity: 0.3, + weight: 2, opacity: 0.7 + }); + var popStr = pop >= 1000000 ? (pop / 1000000).toFixed(1) + 'M' : (pop / 1000).toFixed(0) + 'K'; + circle.bindTooltip( + '' + esc(c.city) + ' (' + popStr + ')
' + + '' + esc(c.nearest_event) + ' ' + c.distance_km + 'km', + { className: 'mk-tip', direction: 'top' } + ); + circle.on('click', function() { + showDetail('exposure', { + city: c.city, country: c.country, population: popStr, + nearest_event: c.nearest_event, event_detail: c.event_detail, + distance_km: c.distance_km + }); + }); + circle.addTo(mapLayers.exposure); + }); +} + function updateAlertBanner(data) { var el = $('#alertBanner'); if (!data || data.error || !data.alerts || !data.alerts.length) { @@ -1187,6 +1226,14 @@ function updateHudStats(data) { if (data.nuclear_monitor && !data.nuclear_monitor.error && data.nuclear_monitor.critical_flags > 0) { pills.push('
' + data.nuclear_monitor.critical_flags + 'Nuke Flag
'); } + if (data.strategic_posture && !data.strategic_posture.error) { + var ps = data.strategic_posture.composite_score || 0; + var psCls = ps >= 55 ? ' crit' : ps >= 35 ? ' warn' : ''; + pills.push('
' + ps.toFixed(0) + 'Posture
'); + } + if (data.population_exposure && !data.population_exposure.error && data.population_exposure.exposed_city_count > 0) { + pills.push('
' + (data.population_exposure.total_exposed_population_formatted || '0') + 'Exposed
'); + } $('#hudStats').innerHTML = safe(pills.join('')); } @@ -1197,6 +1244,44 @@ function updateDrawer(data) { _drawerIdx = 0; var h = ''; + // ── STRATEGIC POSTURE ── + if (data.strategic_posture && !data.strategic_posture.error) { + var sp = data.strategic_posture; + var compScore = sp.composite_score || 0; + var riskLvl = sp.risk_level || 'UNKNOWN'; + var riskColor = riskLvl === 'CRITICAL' ? 'var(--red)' : riskLvl === 'HIGH' ? 'var(--amber)' : riskLvl === 'ELEVATED' ? '#ff9100' : 'var(--green)'; + h += '
STRATEGIC POSTURE
'; + h += '
'; + h += '
' + compScore.toFixed(0) + '
'; + h += '
' + esc(riskLvl) + '
'; + h += '
' + (sp.domains_assessed || 0) + ' domains assessed
'; + h += '
'; + var ds = sp.domain_scores || {}; + h += '
'; + for (var dk in ds) { + if (ds.hasOwnProperty(dk)) { + var dInfo = ds[dk]; + var dScore = dInfo.score || 0; + var dColor = dScore >= 55 ? 'var(--red)' : dScore >= 35 ? 'var(--amber)' : 'var(--green)'; + h += '
'; + h += '
' + dScore.toFixed(0) + '
'; + h += '
' + esc(dk) + '
'; + h += '
'; + } + } + h += '
'; + var threats = sp.top_threats || []; + if (threats.length) { + h += '
Top Threats
'; + threats.slice(0, 5).forEach(function(t) { + h += '
'; + h += '' + esc(t.domain) + ''; + h += '' + esc(t.signal) + ''; + h += '
'; + }); + } + } + // ── ALERTS ── if (data.alert_digest && !data.alert_digest.error && data.alert_digest.alert_count > 0) { h += '
ALERTS
'; @@ -1615,6 +1700,29 @@ function updateDrawer(data) { } } + // ── FLEET ── + if (data.fleet_report && !data.fleet_report.error) { + var fr = data.fleet_report; + h += '
FLEET
'; + var fScore = fr.readiness_score || 0; + var fColor = fScore >= 70 ? 'var(--red)' : fScore >= 40 ? 'var(--amber)' : 'var(--green)'; + h += '
'; + h += '
' + fScore + '
Readiness
'; + h += '
' + esc(fr.readiness_level || '\u2014') + '
Level
'; + h += '
' + (fr.total_tracked_aircraft || 0) + '
Aircraft
'; + h += '
' + (fr.surge_count || 0) + '
Surges
'; + h += '
'; + var wws = fr.waterway_summary || []; + if (wws.length) { + h += '
Waterways
'; + wws.forEach(function(w) { + var sCls = w.status === 'critical' ? 'down' : w.status === 'elevated' ? 'warn' : w.status === 'advisory' ? 'warn' : 'dim'; + h += ''; + }); + h += '
WaterwayStatusWarnings
' + esc(trunc(w.name || '?', 22)) + '' + esc(w.status || '\u2014') + '' + (w.warning_count || 0) + '
'; + } + } + // ── SOCIAL ── if (data.social_signals && !data.social_signals.error) { var soc = data.social_signals; @@ -1661,6 +1769,39 @@ function updateDrawer(data) { } } + // ── POPULATION EXPOSURE ── + if (data.population_exposure && !data.population_exposure.error && data.population_exposure.exposed_city_count > 0) { + var pe = data.population_exposure; + h += '
POPULATION EXPOSURE
'; + h += '
'; + h += '
' + esc(pe.total_exposed_population_formatted || '0') + '
Total Exposed
'; + h += '
' + (pe.exposed_city_count || 0) + '
Cities
'; + h += '
' + (pe.events_analyzed || 0) + '
Events
'; + h += '
' + (pe.radius_km || 200) + 'km
Radius
'; + h += '
'; + var byType = pe.by_event_type || {}; + if (Object.keys(byType).length) { + h += '
'; + for (var etk in byType) { + if (byType.hasOwnProperty(etk)) { + var etColor = etk === 'conflict' ? 'var(--red)' : etk === 'earthquake' ? 'var(--amber)' : '#ffea00'; + h += '
' + esc(byType[etk]) + '
' + esc(etk) + '
'; + } + } + h += '
'; + } + var exCities = pe.exposed_cities || []; + if (exCities.length) { + h += ''; + exCities.slice(0, 15).forEach(function(c) { + var popStr = (c.population || 0) >= 1000000 ? ((c.population / 1000000).toFixed(1) + 'M') : ((c.population / 1000).toFixed(0) + 'K'); + var tColor = c.nearest_event === 'conflict' ? 'down' : c.nearest_event === 'earthquake' ? 'warn' : 'warn'; + h += ''; + }); + h += '
CityPopThreatDist
' + esc(c.city || '?') + '' + popStr + '' + esc(c.nearest_event || '\u2014') + '' + (c.distance_km || 0) + 'km
'; + } + } + // ── GEOSPATIAL ── if (data.military_bases || data.strategic_ports || data.pipelines || data.nuclear_facilities) { h += '
GEOSPATIAL
'; @@ -1757,6 +1898,7 @@ function updateAll(data) { updateMapConvergence(data.signal_convergence); updateMapNuclear(data.nuclear_monitor); updateMapInfra(data); + updateMapExposure(data.population_exposure); // Alert banner updateAlertBanner(data.alert_digest);