diff --git a/src/world_intel_mcp/dashboard/app.py b/src/world_intel_mcp/dashboard/app.py
index 6b1412d..06f703c 100644
--- a/src/world_intel_mcp/dashboard/app.py
+++ b/src/world_intel_mcp/dashboard/app.py
@@ -35,7 +35,13 @@ from world_intel_mcp.sources import (
intelligence,
space_weather,
ai_watch,
+ health,
+ elections,
+ shipping,
+ social,
+ nuclear,
)
+from world_intel_mcp.analysis.alerts import fetch_alert_digest, fetch_weekly_trends
logger = logging.getLogger(__name__)
@@ -91,6 +97,13 @@ async def _fetch_overview() -> dict:
"signal_convergence": intelligence.fetch_signal_convergence(fetcher),
"space_weather": space_weather.fetch_space_weather(fetcher),
"ai_watch": ai_watch.fetch_ai_watch(fetcher),
+ "disease_outbreaks": health.fetch_disease_outbreaks(fetcher),
+ "election_calendar": elections.fetch_election_calendar(fetcher),
+ "shipping_index": shipping.fetch_shipping_index(fetcher),
+ "social_signals": social.fetch_social_signals(fetcher),
+ "nuclear_monitor": nuclear.fetch_nuclear_monitor(fetcher),
+ "alert_digest": fetch_alert_digest(fetcher),
+ "weekly_trends": fetch_weekly_trends(fetcher),
}
gathered = await asyncio.gather(
diff --git a/src/world_intel_mcp/dashboard/index.html b/src/world_intel_mcp/dashboard/index.html
index 90d29cf..f13d7b8 100644
--- a/src/world_intel_mcp/dashboard/index.html
+++ b/src/world_intel_mcp/dashboard/index.html
@@ -274,6 +274,43 @@ a { color: var(--accent); text-decoration: none; }
.ticker-item .src { color: var(--dim); margin-left: 6px; }
.ticker-item .sep { color: rgba(0,229,255,0.2); margin: 0 4px; }
+/* ═══════════════ ALERT BANNER ═══════════════ */
+#alertBanner {
+ position: fixed; top: 54px; left: 10px; right: 360px; z-index: 45;
+ max-height: 0; overflow: hidden; transition: max-height 0.4s var(--ease);
+}
+#alertBanner.show { max-height: 120px; }
+#alertBanner.show + #layers { top: 108px; }
+.alert-strip {
+ display: flex; align-items: center; gap: 10px;
+ padding: 6px 14px; border-radius: 8px; margin-bottom: 4px;
+ font-family: var(--mono); font-size: 0.68rem;
+ background: rgba(255,59,59,0.06); border: 1px solid rgba(255,59,59,0.12);
+}
+.alert-strip.critical { background: rgba(255,59,59,0.1); border-color: rgba(255,59,59,0.2); }
+.alert-strip.high { background: rgba(255,159,28,0.08); border-color: rgba(255,159,28,0.15); }
+.alert-strip.medium { background: rgba(77,168,255,0.06); border-color: rgba(77,168,255,0.1); }
+.alert-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; animation: pulse-dot 2s ease-in-out infinite; }
+.alert-strip.critical .alert-dot { background: var(--red); box-shadow: 0 0 8px var(--red); }
+.alert-strip.high .alert-dot { background: var(--amber); box-shadow: 0 0 6px var(--amber); }
+.alert-strip.medium .alert-dot { background: var(--blue); box-shadow: 0 0 4px var(--blue); }
+.alert-msg { flex: 1; color: var(--bright); }
+.alert-domain { color: var(--dim); font-size: 0.58rem; text-transform: uppercase; letter-spacing: 0.5px; flex-shrink: 0; }
+
+/* ═══════════════ RISK BAR ═══════════════ */
+.risk-bar { position: relative; height: 6px; background: rgba(255,255,255,0.04); border-radius: 3px; margin: 2px 0; overflow: hidden; }
+.risk-fill { height: 100%; border-radius: 3px; transition: width 0.5s var(--ease); }
+.risk-fill.crit { background: linear-gradient(90deg, var(--red), #ff6666); }
+.risk-fill.high { background: linear-gradient(90deg, var(--amber), #ffcc66); }
+.risk-fill.med { background: linear-gradient(90deg, var(--blue), #80c4ff); }
+.risk-fill.low { background: linear-gradient(90deg, var(--green), #66e6b3); }
+
+/* ═══════════════ GAUGE ═══════════════ */
+.gauge-row { display: flex; align-items: center; gap: 8px; margin: 6px 0 10px; }
+.gauge-track { flex: 1; height: 8px; background: rgba(255,255,255,0.04); border-radius: 4px; overflow: hidden; }
+.gauge-fill { height: 100%; border-radius: 4px; transition: width 0.6s var(--ease), background 0.3s; }
+.gauge-label { font-family: var(--mono); font-size: 0.75rem; font-weight: 600; min-width: 32px; text-align: right; }
+
/* ═══════════════ MAP MARKERS ═══════════════ */
.mk-quake {
border-radius: 50%;
@@ -315,6 +352,19 @@ a { color: var(--accent); text-decoration: none; }
cursor: pointer; transition: border-color 0.2s, background 0.2s;
}
.mk-conv:hover { border-color: rgba(167,139,250,0.6); background: rgba(167,139,250,0.1); }
+.mk-nuclear {
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(52,211,153,0.8) 15%, rgba(52,211,153,0) 70%);
+ border: 2px solid rgba(52,211,153,0.5);
+ cursor: pointer; transition: transform 0.15s;
+ box-shadow: 0 0 6px rgba(52,211,153,0.3);
+}
+.mk-nuclear:hover { transform: scale(1.3); }
+.mk-nuclear.crit { animation: glow-green 1.8s ease-in-out infinite; }
+@keyframes glow-green {
+ 0%,100% { box-shadow: 0 0 4px rgba(52,211,153,0.3); }
+ 50% { box-shadow: 0 0 22px rgba(52,211,153,0.8); }
+}
/* Leaflet tooltip override */
.leaflet-tooltip.mk-tip {
@@ -370,6 +420,9 @@ a { color: var(--accent); text-decoration: none; }
+
+
+
@@ -449,7 +508,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 };
+var layerState = { quakes: true, military: true, conflict: true, fires: true, convergence: true, nuclear: true };
var drawerOpen = false;
// ════════════ MAP INIT ════════════
@@ -462,6 +521,7 @@ function initMap() {
mapLayers.conflict = L.layerGroup().addTo(map);
mapLayers.fires = L.layerGroup().addTo(map);
mapLayers.convergence = L.layerGroup().addTo(map);
+ mapLayers.nuclear = L.layerGroup().addTo(map);
}
// ════════════ LAYER TOGGLES ════════════
@@ -486,8 +546,8 @@ $('#drawerToggle').addEventListener('click', function() {
});
// ════════════ DETAIL MODAL ════════════
-var COLORS = { earthquake: 'var(--red)', military: 'var(--blue)', conflict: 'var(--amber)', fire: 'var(--gold)', convergence: 'var(--purple)' };
-var LABELS = { earthquake: 'EARTHQUAKE', military: 'MILITARY AIRCRAFT', conflict: 'CONFLICT EVENT', fire: 'WILDFIRE CLUSTER', convergence: 'SIGNAL CONVERGENCE' };
+var COLORS = { earthquake: 'var(--red)', military: 'var(--blue)', conflict: 'var(--amber)', fire: 'var(--gold)', convergence: 'var(--purple)', nuclear: 'var(--green)' };
+var LABELS = { earthquake: 'EARTHQUAKE', military: 'MILITARY AIRCRAFT', conflict: 'CONFLICT EVENT', fire: 'WILDFIRE CLUSTER', convergence: 'SIGNAL CONVERGENCE', nuclear: 'NUCLEAR MONITOR' };
function showDetail(type, d) {
var title = '', subtitle = '', fields = [], link = '';
@@ -560,6 +620,20 @@ function showDetail(type, d) {
}
}
if (d.lat != null) fields.push(['Coordinates', d.lat.toFixed(3) + ', ' + d.lon.toFixed(3)]);
+ } else if (type === 'nuclear') {
+ var cs = d.concern_score || 0;
+ title = (d.site || 'Unknown Site') + ' \u2014 M' + (d.magnitude || 0).toFixed(1);
+ subtitle = d.site_country || '';
+ fields = [
+ ['Concern Score', cs.toFixed(1), cs >= 70 ? 'crit' : cs >= 50 ? 'high' : cs >= 30 ? 'high' : ''],
+ ['Concern Level', d.concern_level || '\u2014', d.concern_level === 'critical' ? 'crit' : d.concern_level === 'high' ? 'high' : ''],
+ ['Magnitude', (d.magnitude || 0).toFixed(1)],
+ ['Depth', d.depth_km != null ? d.depth_km + ' km' : '\u2014', d.depth_km <= 5 ? 'high' : ''],
+ ['Distance to Site', d.distance_km != null ? d.distance_km + ' km' : '\u2014'],
+ ['Location', d.place || '\u2014'],
+ ['Time', d.time || '\u2014'],
+ ['Coordinates', d.latitude != null ? d.latitude.toFixed(3) + ', ' + d.longitude.toFixed(3) : '\u2014']
+ ];
}
var hdr = '' + esc(LABELS[type] || type) + '
' +
@@ -679,6 +753,52 @@ function updateMapConvergence(data) {
});
}
+function updateMapNuclear(data) {
+ mapLayers.nuclear.clearLayers();
+ if (!data || data.error) { $('#cntNuclear').textContent = '0'; return; }
+ var sites = data.sites || [];
+ var flagged = 0;
+ sites.forEach(function(site) {
+ if (site.lat == null || site.lon == null) return;
+ var evts = site.events_detected || 0;
+ flagged += evts;
+ var highest = site.highest_concern;
+ var isCrit = highest && highest.concern_level === 'critical';
+ var size = evts > 0 ? Math.max(14, Math.min(24, evts * 3 + 10)) : 10;
+ var mk = L.marker([site.lat, site.lon], {
+ icon: L.divIcon({ className: 'mk-nuclear' + (isCrit ? ' crit' : ''), iconSize: [size, size], iconAnchor: [size/2, size/2] })
+ });
+ var tip = esc(site.name) + ' (' + esc(site.status) + ')';
+ if (evts > 0) tip += ' \u2014 ' + evts + ' events';
+ mk.bindTooltip(tip, { className: 'mk-tip', direction: 'top', offset: [0, -size/2] });
+ mk.on('click', function() {
+ if (highest) { showDetail('nuclear', highest); }
+ });
+ mk.addTo(mapLayers.nuclear);
+ });
+ $('#cntNuclear').textContent = flagged;
+}
+
+function updateAlertBanner(data) {
+ var el = $('#alertBanner');
+ if (!data || data.error || !data.alerts || !data.alerts.length) {
+ el.classList.remove('show');
+ el.textContent = '';
+ return;
+ }
+ var alerts = data.alerts.slice(0, 3);
+ var html = alerts.map(function(a) {
+ var p = a.priority || 'medium';
+ return '' +
+ '' +
+ '' + esc(a.message || '') + '' +
+ '' + esc(a.domain || '') + '' +
+ '
';
+ }).join('');
+ el.innerHTML = safe(html);
+ el.classList.add('show');
+}
+
// ════════════ TOP BAR STATS ════════════
function updateHudStats(data) {
@@ -711,6 +831,22 @@ function updateHudStats(data) {
if (data.ai_watch && !data.ai_watch.error) {
pills.push('' + (data.ai_watch.count || 0) + 'AI Papers
');
}
+ if (data.alert_digest && !data.alert_digest.error && data.alert_digest.alert_count > 0) {
+ var ac = data.alert_digest.alert_count;
+ var hasCrit = (data.alert_digest.by_priority || {}).critical > 0;
+ pills.push('' + ac + 'Alerts
');
+ }
+ if (data.shipping_index && !data.shipping_index.error) {
+ var ss = data.shipping_index.stress_score || 0;
+ pills.push('' + ss + 'Ship Stress
');
+ }
+ if (data.disease_outbreaks && !data.disease_outbreaks.error) {
+ var hc = data.disease_outbreaks.high_concern_count || 0;
+ if (hc > 0) pills.push('' + hc + 'Outbreak
');
+ }
+ if (data.nuclear_monitor && !data.nuclear_monitor.error && data.nuclear_monitor.critical_flags > 0) {
+ pills.push('' + data.nuclear_monitor.critical_flags + 'Nuke Flag
');
+ }
$('#hudStats').innerHTML = safe(pills.join(''));
}
@@ -719,6 +855,26 @@ function updateHudStats(data) {
function updateDrawer(data) {
var h = '';
+ // ── ALERTS ──
+ if (data.alert_digest && !data.alert_digest.error && data.alert_digest.alert_count > 0) {
+ h += 'ALERTS
';
+ var byPri = data.alert_digest.by_priority || {};
+ h += '';
+ if (byPri.critical) h += '
' + byPri.critical + '
Critical
';
+ if (byPri.high) h += '
';
+ if (byPri.medium) h += '
' + byPri.medium + '
Medium
';
+ h += '
';
+ var alertItems = data.alert_digest.alerts || [];
+ alertItems.forEach(function(a) {
+ var pc = a.priority === 'critical' ? 'down' : a.priority === 'high' ? 'warn' : 'dim';
+ h += '';
+ h += '';
+ h += '' + esc(a.message || '') + '';
+ h += '' + esc(a.domain || '') + '';
+ h += '
';
+ });
+ }
+
// ── MARKETS ──
h += 'MARKETS
';
if (data.market_quotes && !data.market_quotes.error) {
@@ -1003,6 +1159,159 @@ function updateDrawer(data) {
h += 'Loading AI feeds...
';
}
+ // ── HEALTH ──
+ if (data.disease_outbreaks && !data.disease_outbreaks.error) {
+ var dob = data.disease_outbreaks;
+ h += 'HEALTH
';
+ h += '';
+ h += '
' + (dob.count || 0) + '
Alerts
';
+ h += '
' + (dob.high_concern_count || 0) + '
High Concern
';
+ h += '
';
+ var byOrg = dob.by_organization || {};
+ if (Object.keys(byOrg).length) {
+ h += '';
+ for (var orgK in byOrg) {
+ if (byOrg.hasOwnProperty(orgK)) {
+ h += '
' + byOrg[orgK] + '
' + esc(orgK) + '
';
+ }
+ }
+ h += '
';
+ }
+ var healthItems = dob.items || [];
+ if (healthItems.length) {
+ h += '| Outbreak | Org | Age |
';
+ healthItems.slice(0, 12).forEach(function(item) {
+ var isHigh = item.high_concern;
+ h += '| ' + esc(trunc(item.title || '?', 40)) + ' | ' + esc(item.organization || '\u2014') + ' | ' + ago(item.published) + ' |
';
+ });
+ h += '
';
+ }
+ }
+
+ // ── ELECTIONS ──
+ if (data.election_calendar && !data.election_calendar.error) {
+ var ecal = data.election_calendar;
+ var elections = ecal.elections || [];
+ if (elections.length) {
+ h += 'ELECTIONS
';
+ var hiRisk = ecal.highest_risk;
+ if (hiRisk) {
+ h += '' + fmtNum(hiRisk.risk_score || 0, 0) + '
Top Risk
';
+ h += '
' + esc(hiRisk.country || '?') + '
' + esc(hiRisk.election_type || '') + '
';
+ }
+ h += '| Country | Type | Date | Risk |
';
+ elections.slice(0, 12).forEach(function(e) {
+ var rs = e.risk_score || 0;
+ var rCls = rs >= 80 ? 'crit' : rs >= 50 ? 'high' : rs >= 30 ? 'high' : '';
+ var riskPct = Math.min(100, rs);
+ h += '| ' + esc(e.country || '?') + ' | ' + esc(e.election_type || '\u2014') + ' | ' + esc(e.date || '\u2014') + ' | ' +
+ '' +
+ ' |
';
+ });
+ h += '
';
+ }
+ }
+
+ // ── SHIPPING ──
+ if (data.shipping_index && !data.shipping_index.error) {
+ var ship = data.shipping_index;
+ h += 'SHIPPING
';
+ var stress = ship.stress_score || 0;
+ var stressColor = stress >= 60 ? 'var(--red)' : stress >= 30 ? 'var(--amber)' : 'var(--green)';
+ h += '';
+ h += '
Stress';
+ h += '
';
+ h += '
' + stress + '';
+ h += '
';
+ h += '' + esc(ship.assessment || '\u2014') + '
Assessment
';
+ var sigs = ship.signals || [];
+ if (sigs.length) {
+ h += '';
+ sigs.forEach(function(s) { h += '' + esc(s) + ''; });
+ h += '
';
+ }
+ var shipQuotes = ship.quotes || [];
+ if (shipQuotes.length) {
+ h += '| Sym | Price | % |
';
+ shipQuotes.forEach(function(q) {
+ h += '| ' + esc(q.symbol || '?') + ' | ' + fmtNum(q.price) + ' | ' + fmtPct(q.change_pct) + '% |
';
+ });
+ h += '
';
+ }
+ }
+
+ // ── SOCIAL ──
+ if (data.social_signals && !data.social_signals.error) {
+ var soc = data.social_signals;
+ h += 'SOCIAL
';
+ var vel = soc.velocity_metrics || {};
+ if (Object.keys(vel).length) {
+ h += '';
+ for (var vk in vel) {
+ if (vel.hasOwnProperty(vk)) {
+ h += '
' + (typeof vel[vk] === 'number' ? fmtNum(vel[vk], 0) : esc(String(vel[vk]))) + '
' + esc(vk.replace(/_/g, ' ')) + '
';
+ }
+ }
+ h += '
';
+ }
+ var posts = soc.posts || [];
+ if (posts.length) {
+ h += '| Post | Score | Sub |
';
+ posts.slice(0, 10).forEach(function(p) {
+ h += '| ' + esc(trunc(p.title || '?', 35)) + ' | ' + fmtBigPlain(p.score || 0) + ' | ' + esc(p.subreddit || '\u2014') + ' |
';
+ });
+ h += '
';
+ }
+ }
+
+ // ── NUCLEAR ──
+ if (data.nuclear_monitor && !data.nuclear_monitor.error) {
+ var nuc = data.nuclear_monitor;
+ h += 'NUCLEAR MONITOR
';
+ h += '';
+ h += '
' + (nuc.total_flagged_events || 0) + '
Flagged
';
+ h += '
' + (nuc.critical_flags || 0) + '
Critical
';
+ h += '
' + ((nuc.sites || []).length) + '
Sites
';
+ h += '
';
+ var nucSites = nuc.sites || [];
+ if (nucSites.length) {
+ h += '| Site | Status | Events | Concern |
';
+ nucSites.forEach(function(s) {
+ var concern = s.highest_concern ? s.highest_concern.concern_level : 'none';
+ var cc = concern === 'critical' ? 'down' : concern === 'high' ? 'warn' : concern === 'elevated' ? 'warn' : 'dim';
+ h += '| ' + esc(s.name || '?') + ' | ' + esc(s.status || '\u2014') + ' | ' + (s.events_detected || 0) + ' | ' + esc(concern) + ' |
';
+ });
+ h += '
';
+ }
+ }
+
+ // ── TRENDS ──
+ if (data.weekly_trends && !data.weekly_trends.error) {
+ var wt = data.weekly_trends;
+ h += 'TRENDS
';
+ h += '';
+ h += '
' + (wt.trend_count || 0) + '
Metrics
';
+ h += '
' + (wt.current_anomaly_count || 0) + '
Anomalies
';
+ h += '
';
+ var anomalies = wt.current_anomalies || [];
+ if (anomalies.length) {
+ h += 'Active Anomalies
| Metric | Region | Dev |
';
+ anomalies.slice(0, 10).forEach(function(a) {
+ var dev = a.z_score || a.deviation || 0;
+ h += '| ' + esc(a.event_type || a.metric || '?') + ' | ' + esc(a.region || '\u2014') + ' | ' + (dev > 0 ? '+' : '') + fmtNum(dev, 1) + '\u03C3 |
';
+ });
+ h += '
';
+ }
+ var trends = wt.trends || [];
+ if (trends.length) {
+ h += 'Most Volatile
| Metric | CV% | Mean |
';
+ trends.slice(0, 8).forEach(function(t) {
+ h += '| ' + esc(t.metric || '?') + ' | ' + fmtNum(t.volatility_cv, 1) + ' | ' + fmtNum(t.mean, 1) + ' |
';
+ });
+ h += '
';
+ }
+ }
+
$('#drawerBody').innerHTML = safe(h);
}
@@ -1035,6 +1344,10 @@ function updateAll(data) {
updateMapConflict(data.acled_events);
updateMapFires(data.wildfires);
updateMapConvergence(data.signal_convergence);
+ updateMapNuclear(data.nuclear_monitor);
+
+ // Alert banner
+ updateAlertBanner(data.alert_digest);
// HUD
updateHudStats(data);