diff --git a/ROADMAP.md b/ROADMAP.md index 1a6a238..013bf66 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Benchmark**: [koala73/worldmonitor](https://github.com/koala73/worldmonitor) **Updated**: 2026-03-08 -**Current tools**: 109 (108 intel + 1 status) +**Current tools**: 110 (109 intel + 1 status) --- @@ -20,11 +20,11 @@ | Area | Finding | Status | Action | |------|---------|--------|--------| -| MCP tool parity | 109 tools declared in `TOOLS`; 109 routed in `_dispatch()` | :white_check_mark: | Keep as an invariant | +| MCP tool parity | 110 tools declared in `TOOLS`; 110 routed in `_dispatch()` | :white_check_mark: | Keep as an invariant | | Optional vector runtime | Missing `qdrant-client` / `fastembed` previously surfaced as runtime failures | :white_check_mark: Fixed | Vector features now degrade cleanly and report availability | | Base-environment test run | `pytest -q` fails collection without dev extras because `respx` is not installed | :yellow_circle: | Run `pip install -e ".[dev]"` before full-suite validation | -| Core verification | 77 infrastructure/vector tests pass in the base environment | :white_check_mark: | `test_cache.py`, `test_analysis.py`, `test_vector_store.py` | -| Documentation drift | Prior roadmap documented 89 tools while the codebase exposed 109 | :white_check_mark: Updated below | Keep roadmap synced with phase increments | +| Core verification | 127 infrastructure/report/vector tests pass in the base environment | :white_check_mark: | `test_reports.py`, `test_cache.py`, `test_analysis.py`, `test_vector_store.py` | +| Documentation drift | Prior roadmap documented 89 tools while the codebase exposed 110 | :white_check_mark: Updated below | Keep roadmap synced with phase increments | | Maintainability | `src/world_intel_mcp/server.py` is ~2.5k lines and remains the main refactor target | :yellow_circle: | Split tool registry and dispatch by domain | ### Implemented Addendum Missing From Prior Roadmap @@ -59,6 +59,12 @@ | `intel_domain_summary` | Per-category summary of stored intelligence | :white_check_mark: | | `intel_trend_detection` | Recent-vs-baseline activity surge/drop detection | :white_check_mark: | +#### Intelligence Reports (1 tool) + +| Tool | Purpose | Status | +|------|---------|--------| +| `intel_generate_report` | Generate PDF/HTML multi-domain intelligence reports with HTML fallback when `.[pdf]` is unavailable | :white_check_mark: | + --- ## 1. Data Sources — Complete Inventory @@ -207,6 +213,56 @@ | `intel_news_clusters` | Jaccard similarity clustering | :white_check_mark: | | `intel_keyword_spikes` | Welford's algorithm spike detection | :white_check_mark: | +### Extended Geospatial (10 tools) +| Tool | WM Equivalent | Status | +|------|---------------|--------| +| `intel_undersea_cables` | 34 cables with landing points | :white_check_mark: | +| `intel_ai_datacenters` | 48 global AI/cloud clusters | :white_check_mark: | +| `intel_spaceports` | 27 launch facilities | :white_check_mark: | +| `intel_critical_minerals` | 27 deposit types | :white_check_mark: | +| `intel_stock_exchanges` | 82 global exchanges | :white_check_mark: | +| `intel_trade_routes` | 19 maritime chokepoints/routes | :white_check_mark: | +| `intel_cloud_regions` | 28 AWS/Azure/GCP regions | :white_check_mark: | +| `intel_financial_centers` | 20 GFCI-ranked cities | :white_check_mark: | +| `intel_nuclear_facilities` | 24 power/enrichment/research | :white_check_mark: | + +### Financial Intelligence (12 tools) +| Tool | Purpose | Status | +|------|---------|--------| +| `intel_forex_rates` | Latest FX rates by base + symbol filters | :white_check_mark: | +| `intel_forex_timeseries` | Historical FX series with configurable lookback | :white_check_mark: | +| `intel_major_crosses` | Major crosses + DXY proxy snapshot | :white_check_mark: | +| `intel_yield_curve` | Treasury curve + inversion analysis | :white_check_mark: | +| `intel_bond_indices` | Bond ETF summary (AGG, TLT, HYG, LQD, TIP) | :white_check_mark: | +| `intel_earnings_calendar` | Upcoming earnings calendar | :white_check_mark: | +| `intel_earnings_surprise` | Historical earnings surprise analysis | :white_check_mark: | +| `intel_sec_filings` | Full-text SEC EDGAR filing search | :white_check_mark: | +| `intel_company_filings` | Company-specific 10-K / 10-Q / 8-K retrieval | :white_check_mark: | +| `intel_recent_8k` | Recent material-event 8-K stream | :white_check_mark: | +| `intel_company_profile` | Composite company enrichment profile | :white_check_mark: | +| `intel_macro_composite` | Weighted market regime / macro composite score | :white_check_mark: | + +### Vector Intelligence (5 tools) +| Tool | Purpose | Status | +|------|---------|--------| +| `intel_semantic_search` | Natural-language search across accumulated intelligence | :white_check_mark: | +| `intel_similar_events` | Similarity search against historical events | :white_check_mark: | +| `intel_timeline` | Chronological timeline from vector store history | :white_check_mark: | +| `intel_vector_stats` | Qdrant collection statistics | :white_check_mark: | +| `intel_collect` | On-demand collection cycle for vector population | :white_check_mark: | + +### Cross-Domain Analytics (3 tools) +| Tool | Purpose | Status | +|------|---------|--------| +| `intel_cross_correlate` | Cross-category correlation for a topic/query | :white_check_mark: | +| `intel_domain_summary` | Per-category summary of stored intelligence | :white_check_mark: | +| `intel_trend_detection` | Recent-vs-baseline activity surge/drop detection | :white_check_mark: | + +### Reports (1 tool) +| Tool | Purpose | Status | +|------|---------|--------| +| `intel_generate_report` | PDF/HTML multi-domain intelligence reports | :white_check_mark: | + ### System (1 tool) | Tool | Purpose | Status | |------|---------|--------| @@ -361,17 +417,22 @@ Qdrant-backed semantic retrieval added across all fetched intelligence, plus tim Added historical cross-category correlation, stored-data summarization, and recent-vs-baseline trend detection on top of the vector archive for early-warning and activity-shift analysis. +### Phase 17: Intelligence Reports (+1 = 110 tools) +`intel_generate_report` + +Added PDF/HTML intelligence report generation over the existing multi-domain data collection stack. PDF output remains optional behind `.[pdf]`, with HTML fallback available when WeasyPrint is not installed. + --- ## Summary | Category | Current | Notes | |----------|---------|-------| -| Total MCP tools | 109 | 108 intelligence tools + `intel_status` | -| Tool parity | 109 / 109 | `TOOLS` and `_dispatch()` are aligned | +| Total MCP tools | 110 | 109 intelligence tools + `intel_status` | +| Tool parity | 110 / 110 | `TOOLS` and `_dispatch()` are aligned | | Static datasets | 18 | Bases, ports, pipelines, nuclear, cables, datacenters, spaceports, minerals, exchanges, trade routes, cloud regions, financial centers | | RSS feeds | 119 | 24 categories | -| Tests in repo | 186 | Full suite requires `.[dev]`; 77 core tests validated in the base environment | +| Tests in repo | 344 | 11 test files; full suite requires `.[dev]` | | Primary remaining gap | Architecture | `server.py` monolith remains the main refactor target | -**Bottom line**: 109 tools across 30+ domains, with the roadmap now aligned to the live MCP registry. The main remaining gaps are full-environment test bootstrapping (`.[dev]`) and continued modularization of the monolithic `server.py` tool registry/dispatcher. +**Bottom line**: 110 tools across 30+ domains, with the roadmap now aligned to the live MCP registry. The main remaining gaps are full-environment test bootstrapping (`.[dev]`) and continued modularization of the monolithic `server.py` tool registry/dispatcher. diff --git a/src/world_intel_mcp/dashboard/index.html b/src/world_intel_mcp/dashboard/index.html index a5d411d..3122735 100644 --- a/src/world_intel_mcp/dashboard/index.html +++ b/src/world_intel_mcp/dashboard/index.html @@ -656,85 +656,85 @@ a { color: var(--accent); text-decoration: none; }
Earthquakes - 0 +
Military - 0 +
Conflict - 0 +
Wildfires - 0 +
Convergence - 0 +
Nuclear Sites - 0 +
Infrastructure - 0 +
Pop Exposure - 0 +
Air Traffic - 0 +
Road Traffic - 0 +
Nav Warnings - 0 +
Webcams - 0 +
Climate - 0 +
News - 0 +
@@ -822,6 +822,67 @@ var layerState = { quakes: true, military: true, conflict: true, fires: true, co // Restore saved layer toggles from localStorage try { var _saved = JSON.parse(localStorage.getItem('phoenix-layers')); if (_saved) { for (var _k in _saved) { if (layerState.hasOwnProperty(_k)) layerState[_k] = _saved[_k]; } } } catch(e) {} var drawerOpen = false; +var latestStaticData = null; +var latestVectorData = null; +var DASHBOARD_CACHE_KEY = 'phoenix-dashboard-cache-v1'; +var STATIC_CACHE_KEY = 'phoenix-static-cache-v1'; +var VECTOR_CACHE_KEY = 'phoenix-vector-cache-v1'; +var INFRA_SNAPSHOT_KEYS = ['military_bases', 'strategic_ports', 'pipelines', 'nuclear_facilities', 'trade_routes', 'waterways', 'cable_corridors']; +function _readCacheEnvelope(key) { + try { + var raw = localStorage.getItem(key); + return raw ? JSON.parse(raw) : null; + } catch(e) { return null; } +} +function _writeCacheEnvelope(key, payload) { + try { localStorage.setItem(key, JSON.stringify({ saved_at: new Date().toISOString(), payload: payload })); } catch(e) {} +} +function _extractCachePayload(entry) { + if (!entry) return null; + return entry.payload || entry; +} +function _mergeStaticData(data) { + if (!data || !latestStaticData) return data; + var merged = Object.assign({}, data); + INFRA_SNAPSHOT_KEYS.forEach(function(key) { + if (merged[key] == null && latestStaticData[key] != null) merged[key] = latestStaticData[key]; + }); + return merged; +} +function _hideLoading() { + var loading = document.getElementById('loading'); + if (loading) loading.classList.add('gone'); +} +function _restoreCachedDashboard() { + var cachedStatic = _extractCachePayload(_readCacheEnvelope(STATIC_CACHE_KEY)); + if (cachedStatic) latestStaticData = cachedStatic; + var cachedData = _extractCachePayload(_readCacheEnvelope(DASHBOARD_CACHE_KEY)); + if (!cachedData) { + if (latestStaticData) { + try { updateMapInfra(latestStaticData); } catch(e) { console.warn('cached static restore failed:', e); } + } + return false; + } + try { + updateAll(cachedData); + _hideLoading(); + return true; + } catch(e) { + console.warn('dashboard cache restore failed:', e); + return false; + } +} +function _restoreCachedVector() { + var cachedVector = _extractCachePayload(_readCacheEnvelope(VECTOR_CACHE_KEY)); + if (!cachedVector) return false; + try { + updateVectorPanel(cachedVector); + return true; + } catch(e) { + console.warn('vector cache restore failed:', e); + return false; + } +} // ════════════ MAP INIT ════════════ function initMap() { @@ -1055,11 +1116,11 @@ function showDetail(type, d) { ['Source', d.feed_name || '\u2014'], ['Category', d.category || '\u2014'], ['Published', d.published ? new Date(d.published).toLocaleString() : '\u2014'], - ['Summary', trunc(d.summary || '', 200)], + ['Summary', trunc(d.summary || d.description || '', 200)], ['Tier', d.source_tier || '\u2014'], ['Region', d._geo_country || '\u2014'] ]; - link = d.link || ''; + link = d.link || d.url || ''; } else if (type === 'navwarn') { title = 'NAVAREA ' + (d.navarea || '?') + ' \u2014 ' + (d.id || ''); subtitle = d.status || ''; @@ -1245,7 +1306,7 @@ function showDrawerDetail(id) { ]; } else if (type === 'navwarning') { title = (d.navArea || d.area || 'Nav Warning'); - subtitle = ''; + subtitle = d.status || ''; fields = [ ['Area', d.navArea || d.area || '\u2014'], ['Number', d.number || d.id || '\u2014'], @@ -1447,7 +1508,8 @@ function _toggleLayerByIndex(idx) { var toggle = row.querySelector('.toggle'); toggle.classList.toggle('on'); layerState[key] = toggle.classList.contains('on'); - if (layerState[key]) { mapLayers[key].addTo(map); } else { map.removeLayer(mapLayers[key]); } + if (layerState[key]) { mapLayers[key].addTo(map); } + else { map.removeLayer(mapLayers[key]); } try { localStorage.setItem('phoenix-layers', JSON.stringify(layerState)); } catch(e) {} } @@ -1457,7 +1519,8 @@ function _toggleAllLayers() { _layerKeys.forEach(function(key) { if (!mapLayers[key]) return; layerState[key] = _allLayersOn; - if (_allLayersOn) { mapLayers[key].addTo(map); } else { map.removeLayer(mapLayers[key]); } + if (_allLayersOn) { mapLayers[key].addTo(map); } + else { map.removeLayer(mapLayers[key]); } var row = document.querySelector('.layer-row[data-layer="' + key + '"]'); if (row) { var t = row.querySelector('.toggle'); if (t) { if (_allLayersOn) t.classList.add('on'); else t.classList.remove('on'); } } }); @@ -1738,9 +1801,7 @@ function updateMapInfra(data) { var mk = L.marker([tr.lat, tr.lon], { icon: L.divIcon({ className: iconCls, iconSize: [8, 8], iconAnchor: [4, 4] }) }); - mk.bindTooltip('' + esc(tr.name) + '
' + - '' + esc(tr.type) + ' · ' + (tr.oil_flow_mbd || 0) + ' mbd oil · ~' + (tr.daily_vessel_transits || 0) + ' vessels/day', - { className: 'mk-tip', direction: 'top', offset: [0, -5] }); + mk.bindTooltip('' + esc(tr.name) + ' ' + esc(tr.type) + ' · ' + (tr.oil_flow_mbd || 0) + ' mbd oil · ~' + (tr.daily_vessel_transits || 0) + ' vessels/day', { className: 'mk-tip', direction: 'top', offset: [0, -5] }); mk.on('click', function() { showDetail('trade_route', { name: tr.name, type: tr.type, oil_flow_mbd: tr.oil_flow_mbd, @@ -1771,11 +1832,10 @@ 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 color = COLORS[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], { @@ -2107,7 +2167,7 @@ function updateDrawer(data) { h += '
'; if (byPri.critical) h += '
' + byPri.critical + '
Critical
'; if (byPri.high) h += '
' + byPri.high + '
High
'; - if (byPri.medium) h += '
' + byPri.medium + '
Medium
'; + if (byPri.medium) h += '
' + byPri.medium + '
Medium
'; h += '
'; var alertItems = data.alert_digest.alerts || []; alertItems.forEach(function(a) { @@ -2222,10 +2282,9 @@ function updateDrawer(data) { h += '
'; h += ''; threats.slice(0, 15).forEach(function(t) { - var sev = (t.severity || '').toLowerCase(); - var sc = sev === 'critical' ? 'down' : sev === 'high' ? 'warn' : 'dim'; + var sevCls = t.severity === 'critical' ? 'down' : t.severity === 'high' ? 'warn' : 'dim'; var did = _storeItem('cyber', t); - h += ''; + h += ''; }); h += '
IOCTypeSev
' + esc(trunc(t.indicator || t.url || t.ioc || '?', 30)) + '' + esc(t.threat_type || t.type || '\u2014') + '' + esc(t.severity || '\u2014') + '
' + esc(trunc(t.indicator || t.url || t.ioc || '?', 30)) + '' + esc(t.threat_type || t.type || '\u2014') + '' + esc(t.severity || '\u2014') + '
'; } @@ -2280,7 +2339,7 @@ function updateDrawer(data) { h += '
Risk Scores
'; countries.slice(0, 10).forEach(function(c) { var rl = (c.risk_level || '').toLowerCase(); - var rc = rl === 'extreme' || rl === 'critical' ? 'down' : rl === 'high' ? 'warn' : rl === 'elevated' ? 'warn' : 'dim'; + var rc = rl === 'extreme' || rl === 'critical' ? 'down' : rl === 'high' ? 'high' : rl === 'elevated' ? 'high' : 'dim'; h += ''; }); h += '
CountryScoreLevel
' + esc(c.country || '?') + '' + fmtNum(c.risk_score, 1) + '' + esc(c.risk_level || '\u2014') + '
'; @@ -2364,44 +2423,30 @@ function updateDrawer(data) { h += '
' + fmtBigPlain(totals.total_idps || 0) + '
IDPs
'; h += '
' + fmtBigPlain(totals.grand_total || 0) + '
Total
'; h += ''; - var origins = data.displacement.by_origin || []; - if (origins.length) { - h += ''; - origins.slice(0, 8).forEach(function(o) { - h += ''; - }); - h += '
OriginRefugeesIDPs
' + esc(o.country_name || o.country || '?') + '' + fmtBigPlain(o.refugees || 0) + '' + fmtBigPlain(o.internally_displaced || o.idps || 0) + '
'; + var byType = data.displacement.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 += '
'; } - } - if (data.signal_convergence && !data.signal_convergence.error) { - var hs = data.signal_convergence.hotspots || []; - if (hs.length) { - h += '
Signal Convergence
'; - hs.slice(0, 10).forEach(function(hp) { - var score = hp.convergence_score || 0; - var sigs = hp.signals || {}; - var sigStr = Object.entries(sigs).map(function(e) { return e[0] + ':' + e[1]; }).join(', '); - h += ''; - }); - h += '
RegionScoreSignals
' + esc(hp.name || '?') + '' + fmtNum(score, 1) + '' + esc(trunc(sigStr, 25)) + '
'; - } - } - if (data.climate_anomalies && !data.climate_anomalies.error) { - var src = data.climate_anomalies.anomalies || data.climate_anomalies.zones || data.climate_anomalies.data || []; - var anomalies = Array.isArray(src) ? src : (typeof src === 'object' ? Object.values(src) : []); - if (anomalies.length) { - h += '
Climate Anomalies
'; - anomalies.slice(0, 8).forEach(function(a) { - var temp = a.temp_anomaly_c != null ? a.temp_anomaly_c : (a.temperature_anomaly || a.temp_anomaly || a.temp_deviation); - var precip = a.precip_anomaly_pct != null ? a.precip_anomaly_pct : (a.precipitation_anomaly || a.precip_anomaly || a.precip_deviation); - h += ''; + var exCities = data.exposed_cities || []; + if (exCities.length) { + h += '
ZoneTempPrecip
' + esc(a.zone || a.name || a.region || '?') + '' + (temp != null ? (temp > 0 ? '+' : '') + fmtNum(temp, 1) + '\u00B0C' : '\u2014') + '' + (precip != null ? (precip > 0 ? '+' : '') + fmtNum(precip, 0) + '%' : '\u2014') + '
'; + 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
'; } } // ── AGI WATCH ── - h += '
AGI WATCH
'; if (data.ai_watch && !data.ai_watch.error) { var aiw = data.ai_watch; var labTrend = aiw.lab_trending || []; @@ -2699,7 +2744,7 @@ function updateDrawer(data) { h += '
'; var avgCls = tf.global_avg_congestion >= 40 ? ' crit' : tf.global_avg_congestion >= 20 ? ' warn' : ''; h += '
' + fmtNum(tf.global_avg_congestion, 0) + '%
Avg Congestion
'; - h += '
' + tf.count + '
Cities
'; + h += '
' + (tf.count || 0) + '
Cities
'; h += '
'; h += ''; (tf.cities || []).forEach(function(c) { @@ -2746,7 +2791,7 @@ function updateDrawer(data) { } (uf.ships || []).slice(0, 15).forEach(function(s) { var cls = s.type === 'Aircraft Carrier' ? 'crit' : 'bright'; - h += '
' + esc(s.name) + ' (' + esc(s.hull_number) + ') ' + esc(s.region) + '
'; + h += '
' + esc(s.name) + ' ' + (s.hull_number || '') + ' ' + esc(s.region) + '
'; }); } @@ -2821,10 +2866,10 @@ function updateDrawer(data) { try { var _collapsed = JSON.parse(localStorage.getItem('phoenix-drawer-collapsed')) || []; if (_collapsed.length) { - $$('#drawerBody .sh').forEach(function(sh) { - if (_collapsed.indexOf(sh.textContent.trim()) >= 0) { - sh.classList.add('collapsed'); - var next = sh.nextElementSibling; + $$('#drawerBody .sh').forEach(function(s) { + if (_collapsed.indexOf(s.textContent.trim()) >= 0) { + s.classList.add('collapsed'); + var next = s.nextElementSibling; while (next && !next.classList.contains('sh')) { next.style.display = 'none'; next = next.nextElementSibling; } } }); @@ -2855,8 +2900,11 @@ function updateTicker(data) { function refreshDashboard(data) { updateAll(data); } function updateAll(data) { + if (!data) return; + data = _mergeStaticData(data); latestData = data; window._lastSSEData = data; + _writeCacheEnvelope(DASHBOARD_CACHE_KEY, data); // Map layers (each wrapped to prevent cascade failures) var conflictData = (data.acled_events && !data.acled_events.error && data.acled_events.count > 0) ? data.acled_events @@ -2982,7 +3030,7 @@ $('#hudStats').addEventListener('click', function(e) { } try { var bounds = mapLayers[layerKey].getBounds(); - if (bounds && bounds.isValid()) map.fitBounds(bounds, { padding: [50, 50], maxZoom: 6 }); + if (bounds && bounds.isValid()) map.fitBounds(bounds, { padding: [50, 50], maxZoom: bounds.getNorthEast().lat === bounds.getSouthWest().lat ? 10 : 6 }); } catch(e2) {} }); @@ -3061,103 +3109,17 @@ function performSearch(q) { setTimeout(function() { $('#searchInput').style.borderColor = ''; }, 2000); } -// Mobile: collapsible layer panel -$('#layers .panel-title').addEventListener('click', function() { - $('#layers').classList.toggle('layers-open'); -}); - -// Auto-position layer panel + alert banner below topbar -function _syncLayersTop() { - var tb = $('#topBar'); - if (tb) { - var h = tb.offsetHeight; - var bannerTop = h + 16; - document.documentElement.style.setProperty('--layers-top', bannerTop + 'px'); - // Push layers below the alert banner if it's showing - var banner = $('#alertBanner'); - var bannerH = (banner && banner.classList.contains('show')) ? banner.offsetHeight + 4 : 0; - $('#layers').style.top = (bannerTop + bannerH) + 'px'; - } -} -_syncLayersTop(); -window.addEventListener('resize', _syncLayersTop); - -// Show loading indicator until SSE data arrives -$('#hudStats').innerHTML = '
LOADING LIVE FEEDS
'; -$('#drawerBody').innerHTML = '
LOADING INTELLIGENCE FEEDS
'; - -// ═══════════ REGIONAL PRESETS ═══════════ -var REGION_VIEWS = { - global: { center: [20, 0], zoom: 3 }, - americas: { center: [10, -80], zoom: 4 }, - europe: { center: [50, 15], zoom: 5 }, - mena: { center: [28, 40], zoom: 5 }, - asia: { center: [20, 110], zoom: 4 }, - africa: { center: [0, 25], zoom: 4 }, -}; -$$('#regionPresets .rgn-btn').forEach(function(btn) { - btn.addEventListener('click', function() { - var region = btn.dataset.region; - var view = REGION_VIEWS[region]; - if (!view) return; - $$('#regionPresets .rgn-btn').forEach(function(b) { b.classList.remove('active'); }); - btn.classList.add('active'); - map.flyTo(view.center, view.zoom, { duration: 1.2 }); - try { localStorage.setItem('phoenix-region', region); } catch(e) {} - }); -}); -// Restore saved region -try { - var savedRegion = localStorage.getItem('phoenix-region'); - if (savedRegion && REGION_VIEWS[savedRegion]) { - $$('#regionPresets .rgn-btn').forEach(function(b) { - b.classList.toggle('active', b.dataset.region === savedRegion); - }); - var sv = REGION_VIEWS[savedRegion]; - map.setView(sv.center, sv.zoom); - } -} catch(e) {} - -// ═══════════ TIME WINDOW FILTER ═══════════ -var _timeFilterHours = 24; -$$('#timeFilter .rgn-btn').forEach(function(btn) { - btn.addEventListener('click', function() { - _timeFilterHours = parseInt(btn.dataset.hours) || 0; - $$('#timeFilter .rgn-btn').forEach(function(b) { b.classList.remove('active'); }); - btn.classList.add('active'); - try { localStorage.setItem('phoenix-time-filter', String(_timeFilterHours)); } catch(e) {} - // Re-render with current data if available - if (window._lastSSEData) refreshDashboard(window._lastSSEData); - }); -}); -// Restore saved time filter -try { - var savedTime = localStorage.getItem('phoenix-time-filter'); - if (savedTime !== null) { - _timeFilterHours = parseInt(savedTime) || 0; - $$('#timeFilter .rgn-btn').forEach(function(b) { - b.classList.toggle('active', b.dataset.hours === savedTime); - }); - } -} catch(e) {} - -// Time filter helper: check if an ISO timestamp is within the active window -function _withinTimeWindow(isoTimestamp) { - if (_timeFilterHours === 0) return true; // "All" = no filter - if (!isoTimestamp) return true; // No timestamp = show - try { - var ts = new Date(isoTimestamp).getTime(); - var cutoff = Date.now() - (_timeFilterHours * 3600000); - return ts >= cutoff; - } catch(e) { return true; } -} - // Load static geospatial data immediately (bases, ports, nuclear facilities). fetch('/api/static') .then(function(r) { return r.json(); }) .then(function(data) { - try { updateMapInfra(data); } catch(e) { console.warn('static infra failed:', e); } - document.getElementById('loading').classList.add('gone'); + latestStaticData = data; + _writeCacheEnvelope(STATIC_CACHE_KEY, data); + if (window._lastSSEData) { + refreshDashboard(window._lastSSEData); + } else { + try { updateMapInfra(data); } catch(e) { console.warn('static infra failed:', e); } + } }) .catch(function(e) { console.warn('static fetch failed:', e); }); @@ -3171,6 +3133,8 @@ function updateVectorPanel(vd) { panel.classList.add('hidden'); return; } + latestVectorData = vd; + _writeCacheEnvelope(VECTOR_CACHE_KEY, vd); panel.classList.remove('hidden'); // Points count in header
CityCong%Speed