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; }
@@ -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 += '
';
- 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 += '