feat: add strategic synthesis to dashboard map + drawer

- Strategic Posture section (top of drawer): composite score, 9 domain
  grid with color-coded scores, top 5 threats
- Fleet Report section: readiness score/level, waterway status table,
  aircraft count, active surges
- Population Exposure map layer: circle markers sized by population,
  colored by threat type (red=conflict, orange=quake, yellow=fire)
- Population Exposure drawer section: total exposed, by event type,
  city table with distance
- HUD pills: Posture score + Exposed population
- Layer toggle for Pop Exposure (purple, default on)
- Backend: exposure API now includes lat/lon for map rendering

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Marc Shade
2026-02-24 09:21:03 -05:00
co-authored by Claude Opus 4.6
parent 2969dfc950
commit de2cbeaa1a
3 changed files with 151 additions and 1 deletions
+2
View File
@@ -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"),
+6
View File
@@ -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(
+143 -1
View File
@@ -503,6 +503,12 @@ a { color: var(--accent); text-decoration: none; }
<span class="layer-count" id="cntInfra">0</span>
<div class="toggle on" style="--lc:var(--teal)"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="exposure">
<span class="layer-dot" style="background:#e040fb"></span>
<span class="layer-label">Pop Exposure</span>
<span class="layer-count" id="cntExposure">0</span>
<div class="toggle on" style="--lc:#e040fb"><div class="knob"></div></div>
</div>
</aside>
<!-- DATA DRAWER -->
@@ -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(
'<b>' + esc(c.city) + '</b> (' + popStr + ')<br>' +
'<span style="color:' + color + '">' + esc(c.nearest_event) + '</span> ' + 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('<div class="stat-pill"><span class="v crit">' + data.nuclear_monitor.critical_flags + '</span><span class="l">Nuke Flag</span></div>');
}
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('<div class="stat-pill"><span class="v' + psCls + '">' + ps.toFixed(0) + '</span><span class="l">Posture</span></div>');
}
if (data.population_exposure && !data.population_exposure.error && data.population_exposure.exposed_city_count > 0) {
pills.push('<div class="stat-pill"><span class="v warn">' + (data.population_exposure.total_exposed_population_formatted || '0') + '</span><span class="l">Exposed</span></div>');
}
$('#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 += '<div class="sh">STRATEGIC POSTURE</div>';
h += '<div style="display:flex;align-items:center;gap:12px;padding:6px 0">';
h += '<div style="font-size:2rem;font-weight:700;color:' + riskColor + '">' + compScore.toFixed(0) + '</div>';
h += '<div><div style="font-size:0.85rem;font-weight:600;color:' + riskColor + '">' + esc(riskLvl) + '</div>';
h += '<div class="dim" style="font-size:0.6rem">' + (sp.domains_assessed || 0) + ' domains assessed</div></div>';
h += '</div>';
var ds = sp.domain_scores || {};
h += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:2px">';
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 += '<div style="text-align:center;padding:4px 2px;border-radius:4px;background:rgba(255,255,255,0.02)">';
h += '<div style="font-size:0.75rem;font-weight:600;color:' + dColor + '">' + dScore.toFixed(0) + '</div>';
h += '<div class="dim" style="font-size:0.5rem;text-transform:uppercase;letter-spacing:0.5px">' + esc(dk) + '</div>';
h += '</div>';
}
}
h += '</div>';
var threats = sp.top_threats || [];
if (threats.length) {
h += '<div class="sub" style="margin-top:6px">Top Threats</div>';
threats.slice(0, 5).forEach(function(t) {
h += '<div style="display:flex;align-items:center;gap:6px;padding:2px 0;font-size:0.62rem">';
h += '<span class="dim" style="min-width:50px;text-transform:uppercase;letter-spacing:0.5px">' + esc(t.domain) + '</span>';
h += '<span style="color:var(--bright);flex:1">' + esc(t.signal) + '</span>';
h += '</div>';
});
}
}
// ── ALERTS ──
if (data.alert_digest && !data.alert_digest.error && data.alert_digest.alert_count > 0) {
h += '<div class="sh">ALERTS</div>';
@@ -1615,6 +1700,29 @@ function updateDrawer(data) {
}
}
// ── FLEET ──
if (data.fleet_report && !data.fleet_report.error) {
var fr = data.fleet_report;
h += '<div class="sh">FLEET</div>';
var fScore = fr.readiness_score || 0;
var fColor = fScore >= 70 ? 'var(--red)' : fScore >= 40 ? 'var(--amber)' : 'var(--green)';
h += '<div class="mini-row">';
h += '<div class="mini-box"><div class="v" style="color:' + fColor + '">' + fScore + '</div><div class="l">Readiness</div></div>';
h += '<div class="mini-box"><div class="v">' + esc(fr.readiness_level || '\u2014') + '</div><div class="l">Level</div></div>';
h += '<div class="mini-box"><div class="v">' + (fr.total_tracked_aircraft || 0) + '</div><div class="l">Aircraft</div></div>';
h += '<div class="mini-box"><div class="v">' + (fr.surge_count || 0) + '</div><div class="l">Surges</div></div>';
h += '</div>';
var wws = fr.waterway_summary || [];
if (wws.length) {
h += '<div class="sub">Waterways</div><table class="dtable"><thead><tr><th>Waterway</th><th>Status</th><th>Warnings</th></tr></thead><tbody>';
wws.forEach(function(w) {
var sCls = w.status === 'critical' ? 'down' : w.status === 'elevated' ? 'warn' : w.status === 'advisory' ? 'warn' : 'dim';
h += '<tr><td class="bright">' + esc(trunc(w.name || '?', 22)) + '</td><td class="' + sCls + '">' + esc(w.status || '\u2014') + '</td><td class="dim">' + (w.warning_count || 0) + '</td></tr>';
});
h += '</tbody></table>';
}
}
// ── 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 += '<div class="sh">POPULATION EXPOSURE</div>';
h += '<div class="mini-row">';
h += '<div class="mini-box"><div class="v warn">' + esc(pe.total_exposed_population_formatted || '0') + '</div><div class="l">Total Exposed</div></div>';
h += '<div class="mini-box"><div class="v">' + (pe.exposed_city_count || 0) + '</div><div class="l">Cities</div></div>';
h += '<div class="mini-box"><div class="v">' + (pe.events_analyzed || 0) + '</div><div class="l">Events</div></div>';
h += '<div class="mini-box"><div class="v">' + (pe.radius_km || 200) + 'km</div><div class="l">Radius</div></div>';
h += '</div>';
var byType = pe.by_event_type || {};
if (Object.keys(byType).length) {
h += '<div class="mini-row">';
for (var etk in byType) {
if (byType.hasOwnProperty(etk)) {
var etColor = etk === 'conflict' ? 'var(--red)' : etk === 'earthquake' ? 'var(--amber)' : '#ffea00';
h += '<div class="mini-box"><div class="v" style="color:' + etColor + '">' + esc(byType[etk]) + '</div><div class="l">' + esc(etk) + '</div></div>';
}
}
h += '</div>';
}
var exCities = pe.exposed_cities || [];
if (exCities.length) {
h += '<table class="dtable"><thead><tr><th>City</th><th>Pop</th><th>Threat</th><th>Dist</th></tr></thead><tbody>';
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 += '<tr><td class="bright">' + esc(c.city || '?') + '</td><td class="dim">' + popStr + '</td><td class="' + tColor + '">' + esc(c.nearest_event || '\u2014') + '</td><td class="dim">' + (c.distance_km || 0) + 'km</td></tr>';
});
h += '</tbody></table>';
}
}
// ── GEOSPATIAL ──
if (data.military_bases || data.strategic_ports || data.pipelines || data.nuclear_facilities) {
h += '<div class="sh">GEOSPATIAL</div>';
@@ -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);