diff --git a/src/world_intel_mcp/cache.py b/src/world_intel_mcp/cache.py index 442114a..a260244 100644 --- a/src/world_intel_mcp/cache.py +++ b/src/world_intel_mcp/cache.py @@ -70,14 +70,21 @@ class Cache: return json.loads(row[0]) def set(self, key: str, value: Any, ttl_seconds: int) -> None: - """Store a value with TTL in seconds.""" - conn = self._get_conn() - now = time.time() - conn.execute( - "INSERT OR REPLACE INTO cache (key, value, expires_at, created_at) VALUES (?, ?, ?, ?)", - (key, json.dumps(value, default=str), now + ttl_seconds, now), - ) - conn.commit() + """Store a value with TTL in seconds. + + Write failures (readonly DB, locked) are logged and swallowed — + the cache is best-effort and must never crash the caller. + """ + try: + conn = self._get_conn() + now = time.time() + conn.execute( + "INSERT OR REPLACE INTO cache (key, value, expires_at, created_at) VALUES (?, ?, ?, ?)", + (key, json.dumps(value, default=str), now + ttl_seconds, now), + ) + conn.commit() + except sqlite3.OperationalError as exc: + logger.warning("Cache write failed for %s: %s", key, exc) def delete(self, key: str) -> None: """Delete a specific key.""" @@ -87,10 +94,14 @@ class Cache: def evict_expired(self) -> int: """Remove all expired entries. Returns count removed.""" - conn = self._get_conn() - cursor = conn.execute("DELETE FROM cache WHERE expires_at < ?", (time.time(),)) - conn.commit() - return cursor.rowcount + try: + conn = self._get_conn() + cursor = conn.execute("DELETE FROM cache WHERE expires_at < ?", (time.time(),)) + conn.commit() + return cursor.rowcount + except sqlite3.OperationalError as exc: + logger.warning("Cache evict failed: %s", exc) + return 0 def stats(self) -> dict[str, Any]: """Cache statistics.""" diff --git a/src/world_intel_mcp/dashboard/app.py b/src/world_intel_mcp/dashboard/app.py index 5f5f21f..8d2b765 100644 --- a/src/world_intel_mcp/dashboard/app.py +++ b/src/world_intel_mcp/dashboard/app.py @@ -46,8 +46,9 @@ from world_intel_mcp.analysis.alerts import fetch_alert_digest, fetch_weekly_tre from world_intel_mcp.analysis.posture import fetch_strategic_posture from world_intel_mcp.analysis.exposure import fetch_population_exposure from world_intel_mcp.sources.fleet import fetch_fleet_report -from world_intel_mcp.config.countries import INTEL_HOTSPOTS +from world_intel_mcp.config.countries import INTEL_HOTSPOTS, STRATEGIC_WATERWAYS from world_intel_mcp.config.geospatial import MILITARY_BASES, STRATEGIC_PORTS, PIPELINES, NUCLEAR_FACILITIES +from world_intel_mcp.sources.infrastructure import CABLE_CORRIDORS logger = logging.getLogger(__name__) @@ -173,6 +174,14 @@ async def _fetch_overview() -> dict: result["strategic_ports"] = {"ports": STRATEGIC_PORTS, "count": len(STRATEGIC_PORTS)} result["pipelines"] = {"pipelines": PIPELINES, "count": len(PIPELINES)} result["nuclear_facilities"] = {"facilities": NUCLEAR_FACILITIES, "count": len(NUCLEAR_FACILITIES)} + result["waterways"] = {"waterways": STRATEGIC_WATERWAYS, "count": len(STRATEGIC_WATERWAYS)} + result["cable_corridors"] = { + "corridors": [ + {"name": n, "lat_range": c["lat_range"], "lon_range": c["lon_range"], "cables": c["cables"]} + for n, c in CABLE_CORRIDORS.items() + ], + "count": len(CABLE_CORRIDORS), + } # Attach source health + timestamp result["source_health"] = _breaker.status() if _breaker else {} @@ -236,6 +245,14 @@ async def api_static(request): "strategic_ports": {"ports": STRATEGIC_PORTS, "count": len(STRATEGIC_PORTS)}, "pipelines": {"pipelines": PIPELINES, "count": len(PIPELINES)}, "nuclear_facilities": {"facilities": NUCLEAR_FACILITIES, "count": len(NUCLEAR_FACILITIES)}, + "waterways": {"waterways": STRATEGIC_WATERWAYS, "count": len(STRATEGIC_WATERWAYS)}, + "cable_corridors": { + "corridors": [ + {"name": n, "lat_range": c["lat_range"], "lon_range": c["lon_range"], "cables": c["cables"]} + for n, c in CABLE_CORRIDORS.items() + ], + "count": len(CABLE_CORRIDORS), + }, }, headers={"Access-Control-Allow-Origin": "*"}) diff --git a/src/world_intel_mcp/dashboard/index.html b/src/world_intel_mcp/dashboard/index.html index 6f9e00c..d4bade2 100644 --- a/src/world_intel_mcp/dashboard/index.html +++ b/src/world_intel_mcp/dashboard/index.html @@ -393,6 +393,14 @@ a { color: var(--accent); text-decoration: none; } .mk-nuke-fac.hot { animation: glow-gold 2s ease-in-out infinite; } .pipeline-line { cursor: pointer; transition: opacity 0.15s; } .pipeline-line:hover { opacity: 1 !important; stroke-width: 3 !important; } +.mk-waterway { + width: 10px; height: 10px; background: var(--accent); opacity: 0.8; + transform: rotate(45deg); border: 1.5px solid rgba(0,229,255,0.6); + cursor: pointer; transition: transform 0.15s, opacity 0.15s; +} +.mk-waterway:hover { transform: rotate(45deg) scale(1.5); opacity: 1; } +.cable-corridor { cursor: pointer; transition: fill-opacity 0.15s; } +.cable-corridor:hover { fill-opacity: 0.1 !important; stroke-opacity: 0.5 !important; } @keyframes glow-gold { 0%,100% { box-shadow: 0 0 4px rgba(240,184,64,0.3); } 50% { box-shadow: 0 0 16px rgba(240,184,64,0.7); } @@ -597,8 +605,8 @@ $('#drawerToggle').addEventListener('click', function() { }); // ════════════ DETAIL MODAL ════════════ -var COLORS = { earthquake: 'var(--red)', military: 'var(--blue)', conflict: 'var(--amber)', fire: 'var(--gold)', convergence: 'var(--purple)', nuclear: 'var(--green)', military_base: 'var(--teal)', port: 'var(--blue)', nuclear_facility: 'var(--gold)', pipeline: 'var(--purple)', exposure: '#e040fb' }; -var LABELS = { earthquake: 'EARTHQUAKE', military: 'MILITARY AIRCRAFT', conflict: 'CONFLICT EVENT', fire: 'WILDFIRE CLUSTER', convergence: 'SIGNAL CONVERGENCE', nuclear: 'NUCLEAR MONITOR', military_base: 'MILITARY BASE', port: 'STRATEGIC PORT', nuclear_facility: 'NUCLEAR FACILITY', pipeline: 'PIPELINE', exposure: 'POPULATION EXPOSURE' }; +var COLORS = { earthquake: 'var(--red)', military: 'var(--blue)', conflict: 'var(--amber)', fire: 'var(--gold)', convergence: 'var(--purple)', nuclear: 'var(--green)', military_base: 'var(--teal)', port: 'var(--blue)', nuclear_facility: 'var(--gold)', pipeline: 'var(--purple)', exposure: '#e040fb', waterway: 'var(--accent)', cable: 'var(--blue)' }; +var LABELS = { earthquake: 'EARTHQUAKE', military: 'MILITARY AIRCRAFT', conflict: 'CONFLICT EVENT', fire: 'WILDFIRE CLUSTER', convergence: 'SIGNAL CONVERGENCE', nuclear: 'NUCLEAR MONITOR', military_base: 'MILITARY BASE', port: 'STRATEGIC PORT', nuclear_facility: 'NUCLEAR FACILITY', pipeline: 'PIPELINE', exposure: 'POPULATION EXPOSURE', waterway: 'STRATEGIC WATERWAY', cable: 'CABLE CORRIDOR' }; function showDetail(type, d) { var title = '', subtitle = '', fields = [], link = ''; @@ -1155,6 +1163,31 @@ function updateMapInfra(data) { }); line.addTo(mapLayers.infra); }); + // Strategic waterways (diamond markers at chokepoints) + var wws = (data.waterways && data.waterways.waterways) || []; + wws.forEach(function(w) { + if (w.lat == null || w.lon == null) return; + total++; + var mk = L.marker([w.lat, w.lon], { + icon: L.divIcon({ className: 'mk-waterway', iconSize: [10, 10], iconAnchor: [5, 5] }) + }); + mk.bindTooltip('' + esc(w.name) + '
' + esc(w.throughput || '') + '', { className: 'mk-tip', direction: 'top', offset: [0, -6] }); + mk.addTo(mapLayers.infra); + }); + // Submarine cable corridors (shaded rectangles) + var cables = (data.cable_corridors && data.cable_corridors.corridors) || []; + cables.forEach(function(c) { + if (!c.lat_range || !c.lon_range) return; + total++; + var bounds = [[c.lat_range[0], c.lon_range[0]], [c.lat_range[1], c.lon_range[1]]]; + var rect = L.rectangle(bounds, { + color: '#4da8ff', weight: 1, opacity: 0.2, + fillColor: '#4da8ff', fillOpacity: 0.04, + dashArray: '4 3', className: 'cable-corridor' + }); + rect.bindTooltip('' + esc(c.name.replace(/_/g, ' ')) + '
' + (c.cables || []).join(', ') + '', { className: 'mk-tip', sticky: true }); + rect.addTo(mapLayers.infra); + }); $('#cntInfra').textContent = total; }