feat: waterway diamonds, cable corridor zones, cache resilience
- Render 9 strategic waterway chokepoints as cyan diamond markers (Hormuz, Malacca, Suez, Panama, Bab-el-Mandeb, Taiwan, Gibraltar, GIUK Gap, Bosphorus) with throughput tooltips - Render 6 submarine cable corridors as dashed blue rectangles (transatlantic N/S, transpacific, asia-europe, red sea, med) - Add waterways + cable_corridors to /api/static and SSE payloads - Make cache.set() and evict_expired() swallow sqlite3 write errors instead of crashing callers (fixes "readonly database" in dashboard) - Infrastructure layer: 134 → 173 items Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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."""
|
||||
|
||||
@@ -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": "*"})
|
||||
|
||||
|
||||
|
||||
@@ -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('<b>' + esc(w.name) + '</b><br><span style="opacity:0.7">' + esc(w.throughput || '') + '</span>', { 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('<b>' + esc(c.name.replace(/_/g, ' ')) + '</b><br><span style="opacity:0.7">' + (c.cables || []).join(', ') + '</span>', { className: 'mk-tip', sticky: true });
|
||||
rect.addTo(mapLayers.infra);
|
||||
});
|
||||
$('#cntInfra').textContent = total;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user