docs: fix ROADMAP tool inventory (110/110) and test count (344)

This commit is contained in:
Marc Shade
2026-03-08 13:17:06 -04:00
parent 346e26deb1
commit 75d29d2ae7
2 changed files with 193 additions and 168 deletions
+69 -8
View File
@@ -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.
+124 -160
View File
@@ -656,85 +656,85 @@ a { color: var(--accent); text-decoration: none; }
<div class="layer-row" data-layer="quakes">
<span class="layer-dot" style="background:var(--red)"></span>
<span class="layer-label">Earthquakes</span>
<span class="layer-count" id="cntQuakes">0</span>
<span class="layer-count" id="cntQuakes"></span>
<div class="toggle on" style="--lc:var(--red)"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="military">
<span class="layer-dot" style="background:var(--blue)"></span>
<span class="layer-label">Military</span>
<span class="layer-count" id="cntMil">0</span>
<span class="layer-count" id="cntMil"></span>
<div class="toggle on" style="--lc:var(--blue)"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="conflict">
<span class="layer-dot" style="background:var(--amber)"></span>
<span class="layer-label">Conflict</span>
<span class="layer-count" id="cntConflict">0</span>
<span class="layer-count" id="cntConflict"></span>
<div class="toggle on" style="--lc:var(--amber)"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="fires">
<span class="layer-dot" style="background:var(--gold)"></span>
<span class="layer-label">Wildfires</span>
<span class="layer-count" id="cntFires">0</span>
<span class="layer-count" id="cntFires"></span>
<div class="toggle on" style="--lc:var(--gold)"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="convergence">
<span class="layer-dot" style="background:var(--purple)"></span>
<span class="layer-label">Convergence</span>
<span class="layer-count" id="cntConv">0</span>
<span class="layer-count" id="cntConv"></span>
<div class="toggle on" style="--lc:var(--purple)"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="nuclear">
<span class="layer-dot" style="background:var(--green)"></span>
<span class="layer-label">Nuclear Sites</span>
<span class="layer-count" id="cntNuclear">0</span>
<span class="layer-count" id="cntNuclear"></span>
<div class="toggle on" style="--lc:var(--green)"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="infra">
<span class="layer-dot" style="background:var(--teal)"></span>
<span class="layer-label">Infrastructure</span>
<span class="layer-count" id="cntInfra">0</span>
<span class="layer-count" id="cntInfra"></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>
<span class="layer-count" id="cntExposure"></span>
<div class="toggle on" style="--lc:#e040fb"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="airtraffic">
<span class="layer-dot" style="background:#00c8ff"></span>
<span class="layer-label">Air Traffic</span>
<span class="layer-count" id="cntAirTraffic">0</span>
<span class="layer-count" id="cntAirTraffic"></span>
<div class="toggle on" style="--lc:#00c8ff"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="traffic">
<span class="layer-dot" style="background:#e0e0e0"></span>
<span class="layer-label">Road Traffic</span>
<span class="layer-count" id="cntTraffic">0</span>
<span class="layer-count" id="cntTraffic"></span>
<div class="toggle on" style="--lc:#ffa000"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="navwarnings">
<span class="layer-dot" style="background:#ffc832"></span>
<span class="layer-label">Nav Warnings</span>
<span class="layer-count" id="cntNavWarn">0</span>
<span class="layer-count" id="cntNavWarn"></span>
<div class="toggle on" style="--lc:#ffc832"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="webcams">
<span class="layer-dot" style="background:#909090"></span>
<span class="layer-label">Webcams</span>
<span class="layer-count" id="cntCams">0</span>
<span class="layer-count" id="cntCams"></span>
<div class="toggle on" style="--lc:#78dcff"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="climate">
<span class="layer-dot" style="background:#ff6b6b"></span>
<span class="layer-label">Climate</span>
<span class="layer-count" id="cntClimate">0</span>
<span class="layer-count" id="cntClimate"></span>
<div class="toggle on" style="--lc:#ff6b6b"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="news">
<span class="layer-dot" style="background:var(--accent)"></span>
<span class="layer-label">News</span>
<span class="layer-count" id="cntNews">0</span>
<span class="layer-count" id="cntNews"></span>
<div class="toggle on" style="--lc:var(--accent)"><div class="knob"></div></div>
</div>
<div style="border-top:1px solid rgba(255,255,255,0.08);margin-top:4px;padding-top:6px">
@@ -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('<b>' + esc(tr.name) + '</b><br>' +
'<span style="opacity:0.7">' + esc(tr.type) + ' · ' + (tr.oil_flow_mbd || 0) + ' mbd oil · ~' + (tr.daily_vessel_transits || 0) + ' vessels/day</span>',
{ className: 'mk-tip', direction: 'top', offset: [0, -5] });
mk.bindTooltip('<b>' + esc(tr.name) + '</b> <span style="opacity:0.7">' + esc(tr.type) + ' · ' + (tr.oil_flow_mbd || 0) + ' mbd oil · ~' + (tr.daily_vessel_transits || 0) + ' vessels/day</span>', { 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 += '<div class="mini-row">';
if (byPri.critical) h += '<div class="mini-box"><div class="v" style="color:var(--red)">' + byPri.critical + '</div><div class="l">Critical</div></div>';
if (byPri.high) h += '<div class="mini-box"><div class="v" style="color:var(--amber)">' + byPri.high + '</div><div class="l">High</div></div>';
if (byPri.medium) h += '<div class="mini-box"><div class="v" style="color:var(--blue)">' + byPri.medium + '</div><div class="l">Medium</div></div>';
if (byPri.medium) h += '<div class="mini-box"><div class="v">' + byPri.medium + '</div><div class="l">Medium</div></div>';
h += '</div>';
var alertItems = data.alert_digest.alerts || [];
alertItems.forEach(function(a) {
@@ -2222,10 +2282,9 @@ function updateDrawer(data) {
h += '</div>';
h += '<table class="dtable"><thead><tr><th>IOC</th><th>Type</th><th>Sev</th></tr></thead><tbody>';
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 += '<tr data-click="' + did + '"><td>' + esc(trunc(t.indicator || t.url || t.ioc || '?', 30)) + '</td><td class="dim">' + esc(t.threat_type || t.type || '\u2014') + '</td><td class="' + sc + '">' + esc(t.severity || '\u2014') + '</td></tr>';
h += '<tr data-click="' + did + '"><td>' + esc(trunc(t.indicator || t.url || t.ioc || '?', 30)) + '</td><td class="dim">' + esc(t.threat_type || t.type || '\u2014') + '</td><td class="' + sevCls + '">' + esc(t.severity || '\u2014') + '</td></tr>';
});
h += '</tbody></table>';
}
@@ -2280,7 +2339,7 @@ function updateDrawer(data) {
h += '<div class="sub">Risk Scores</div><table class="dtable"><thead><tr><th>Country</th><th>Score</th><th>Level</th></tr></thead><tbody>';
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 += '<tr><td class="bright">' + esc(c.country || '?') + '</td><td>' + fmtNum(c.risk_score, 1) + '</td><td class="' + rc + '">' + esc(c.risk_level || '\u2014') + '</td></tr>';
});
h += '</tbody></table>';
@@ -2364,44 +2423,30 @@ function updateDrawer(data) {
h += '<div class="mini-box"><div class="v">' + fmtBigPlain(totals.total_idps || 0) + '</div><div class="l">IDPs</div></div>';
h += '<div class="mini-box"><div class="v warn">' + fmtBigPlain(totals.grand_total || 0) + '</div><div class="l">Total</div></div>';
h += '</div>';
var origins = data.displacement.by_origin || [];
if (origins.length) {
h += '<table class="dtable"><thead><tr><th>Origin</th><th>Refugees</th><th>IDPs</th></tr></thead><tbody>';
origins.slice(0, 8).forEach(function(o) {
h += '<tr><td class="bright">' + esc(o.country_name || o.country || '?') + '</td><td>' + fmtBigPlain(o.refugees || 0) + '</td><td>' + fmtBigPlain(o.internally_displaced || o.idps || 0) + '</td></tr>';
});
h += '</tbody></table>';
var byType = data.displacement.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>';
}
}
if (data.signal_convergence && !data.signal_convergence.error) {
var hs = data.signal_convergence.hotspots || [];
if (hs.length) {
h += '<div class="sub">Signal Convergence</div><table class="dtable"><thead><tr><th>Region</th><th>Score</th><th>Signals</th></tr></thead><tbody>';
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 += '<tr><td class="bright">' + esc(hp.name || '?') + '</td><td class="' + (score >= 7 ? 'down' : score >= 4 ? 'warn' : 'dim') + '">' + fmtNum(score, 1) + '</td><td class="dim">' + esc(trunc(sigStr, 25)) + '</td></tr>';
});
h += '</tbody></table>';
}
}
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 += '<div class="sub">Climate Anomalies</div><table class="dtable"><thead><tr><th>Zone</th><th>Temp</th><th>Precip</th></tr></thead><tbody>';
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 += '<tr><td>' + esc(a.zone || a.name || a.region || '?') + '</td><td class="' + (temp > 0 ? 'warn' : 'up') + '">' + (temp != null ? (temp > 0 ? '+' : '') + fmtNum(temp, 1) + '\u00B0C' : '\u2014') + '</td><td class="' + (precip > 50 ? 'warn' : precip < -50 ? 'down' : 'dim') + '">' + (precip != null ? (precip > 0 ? '+' : '') + fmtNum(precip, 0) + '%' : '\u2014') + '</td></tr>';
var exCities = data.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>';
}
}
// ── AGI WATCH ──
h += '<div class="sh">AGI WATCH</div>';
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 += '<div class="mini-row">';
var avgCls = tf.global_avg_congestion >= 40 ? ' crit' : tf.global_avg_congestion >= 20 ? ' warn' : '';
h += '<div class="mini-box"><div class="v' + avgCls + '">' + fmtNum(tf.global_avg_congestion, 0) + '%</div><div class="l">Avg Congestion</div></div>';
h += '<div class="mini-box"><div class="v">' + tf.count + '</div><div class="l">Cities</div></div>';
h += '<div class="mini-box"><div class="v">' + (tf.count || 0) + '</div><div class="l">Cities</div></div>';
h += '</div>';
h += '<table class="dtable"><thead><tr><th>City</th><th>Cong%</th><th>Speed</th></tr></thead><tbody>';
(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 += '<div style="font-size:0.65rem;padding:1px 0"><span class="' + cls + '">' + esc(s.name) + '</span> <span class="dim">(' + esc(s.hull_number) + ') ' + esc(s.region) + '</span></div>';
h += '<div style="font-size:0.65rem;padding:1px 0"><span class="' + cls + '">' + esc(s.name) + '</span> <span class="dim">' + (s.hull_number || '') + ' ' + esc(s.region) + '</span></div>';
});
}
@@ -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 = '<div class="stat-pill"><span class="l" style="animation:pulse-dot 2s ease-in-out infinite">LOADING LIVE FEEDS</span></div>';
$('#drawerBody').innerHTML = '<div style="padding:30px;text-align:center"><div class="load-ring" style="width:28px;height:28px;margin:0 auto 10px"></div><div class="dim" style="font-size:0.6rem;letter-spacing:1.5px">LOADING INTELLIGENCE FEEDS</div></div>';
// ═══════════ 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