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 += '
' + byPri.high + '
High
'; + 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 += ''; + healthItems.slice(0, 12).forEach(function(item) { + var isHigh = item.high_concern; + h += '' + esc(trunc(item.title || '?', 40)) + ''; + }); + h += '
OutbreakOrgAge
' + esc(item.organization || '\u2014') + '' + ago(item.published) + '
'; + } + } + + // ── 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 += ''; + 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 += ''; + }); + h += '
CountryTypeDateRisk
' + esc(e.country || '?') + '' + esc(e.election_type || '\u2014') + '' + esc(e.date || '\u2014') + '' + + '
' + + '
'; + } + } + + // ── 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 += ''; + shipQuotes.forEach(function(q) { + h += ''; + }); + h += '
SymPrice%
' + esc(q.symbol || '?') + '' + fmtNum(q.price) + '' + fmtPct(q.change_pct) + '%
'; + } + } + + // ── 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 += ''; + posts.slice(0, 10).forEach(function(p) { + h += ''; + }); + h += '
PostScoreSub
' + esc(trunc(p.title || '?', 35)) + '' + fmtBigPlain(p.score || 0) + '' + esc(p.subreddit || '\u2014') + '
'; + } + } + + // ── 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 += ''; + 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 += ''; + }); + h += '
SiteStatusEventsConcern
' + esc(s.name || '?') + '' + esc(s.status || '\u2014') + '' + (s.events_detected || 0) + '' + esc(concern) + '
'; + } + } + + // ── 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
'; + anomalies.slice(0, 10).forEach(function(a) { + var dev = a.z_score || a.deviation || 0; + h += ''; + }); + h += '
MetricRegionDev
' + esc(a.event_type || a.metric || '?') + '' + esc(a.region || '\u2014') + '' + (dev > 0 ? '+' : '') + fmtNum(dev, 1) + '\u03C3
'; + } + var trends = wt.trends || []; + if (trends.length) { + h += '
Most Volatile
'; + trends.slice(0, 8).forEach(function(t) { + h += ''; + }); + h += '
MetricCV%Mean
' + esc(t.metric || '?') + '' + fmtNum(t.volatility_cv, 1) + '' + fmtNum(t.mean, 1) + '
'; + } + } + $('#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);