From 9bf87546f55d2ad6efc60967d690f66c2d1e4069 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 23:14:33 +0000 Subject: [PATCH 1/4] Add timestamp markers to AIS track display - Shows circle markers at regular intervals along vessel track - Hover to see timestamp, speed, and course - Green marker for track START, red for LATEST position - ~10 markers max to avoid clutter - Properly cleans up markers when panel closes --- static/index.html | 65 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/static/index.html b/static/index.html index d058701..88b0dc1 100644 --- a/static/index.html +++ b/static/index.html @@ -1628,12 +1628,60 @@ api(`/osint?vessel_id=${id}`) ]); - // Draw track - if (trackLine) map.removeLayer(trackLine); + // Draw track with timestamp markers + if (trackLine) { + if (Array.isArray(trackLine)) { + trackLine.forEach(layer => map.removeLayer(layer)); + } else { + map.removeLayer(trackLine); + } + } if (track?.length > 1) { - trackLine = L.polyline(track.map(p => [p.latitude, p.longitude]), { - color: getThreatColor(v.threat_level), weight: 2, opacity: 0.7, dashArray: '6' + const trackColor = getThreatColor(v.threat_level); + const line = L.polyline(track.map(p => [p.latitude, p.longitude]), { + color: trackColor, weight: 2, opacity: 0.7, dashArray: '6' }).addTo(map); + + // Add timestamp markers along the track + const markers = []; + const markerInterval = Math.max(1, Math.floor(track.length / 10)); // ~10 markers max + + track.forEach((p, idx) => { + // Show markers at regular intervals, plus first and last + const isEndpoint = idx === 0 || idx === track.length - 1; + const isInterval = idx % markerInterval === 0; + + if (isEndpoint || isInterval) { + const ts = p.timestamp ? new Date(p.timestamp) : null; + const timeStr = ts ? ts.toLocaleString('en-GB', { + day: '2-digit', month: 'short', year: '2-digit', + hour: '2-digit', minute: '2-digit' + }) : 'Unknown'; + + const size = isEndpoint ? 8 : 5; + const marker = L.circleMarker([p.latitude, p.longitude], { + radius: size, + color: trackColor, + fillColor: isEndpoint ? (idx === 0 ? '#27ae60' : '#e74c3c') : trackColor, + fillOpacity: 0.9, + weight: 1 + }).addTo(map); + + const label = idx === 0 ? 'START' : (idx === track.length - 1 ? 'LATEST' : ''); + marker.bindTooltip(` +
+ ${label ? `${label}
` : ''} + ${timeStr}
+ ${p.speed_knots ? `Speed: ${p.speed_knots.toFixed(1)} kts
` : ''} + ${p.course ? `Course: ${p.course.toFixed(0)}°` : ''} +
+ `, { permanent: false, direction: 'top' }); + + markers.push(marker); + } + }); + + trackLine = [line, ...markers]; } renderVesselPanel(v, events, osint); @@ -1868,6 +1916,15 @@ function closeVesselPanel() { document.getElementById('vessel-panel').classList.remove('active'); selectedVessel = null; + // Clear track line and markers + if (trackLine) { + if (Array.isArray(trackLine)) { + trackLine.forEach(layer => map.removeLayer(layer)); + } else { + map.removeLayer(trackLine); + } + trackLine = null; + } } // Search From ffbc9b114844975a8f1b37e9171f6c461e6802e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 23:18:46 +0000 Subject: [PATCH 2/4] Add UI for entering GFW API token - Added reload_token() and save_token() to gfw_integration.py - Token is dynamically reloaded if not set (picks up config file changes) - UI shows input field when GFW not configured - Save button calls /api/gfw/configure endpoint - Shows success/error status after saving --- gfw_integration.py | 32 ++++++++++++++++++++++++++++++++ server.py | 1 + static/index.html | 46 +++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 74 insertions(+), 5 deletions(-) diff --git a/gfw_integration.py b/gfw_integration.py index 236b30a..0ae66c3 100644 --- a/gfw_integration.py +++ b/gfw_integration.py @@ -671,11 +671,43 @@ def get_gfw_client() -> GFWClient: return _client +def reload_token() -> str: + """Reload GFW token from config file.""" + global GFW_TOKEN + token = os.environ.get("GFW_API_TOKEN", "") + if not token and os.path.exists(CONFIG_PATH): + try: + with open(CONFIG_PATH) as f: + config = json.load(f) + token = config.get('api_token', '') + except: + pass + GFW_TOKEN = token + return token + + def is_configured() -> bool: """Check if GFW API token is configured.""" + global GFW_TOKEN + # Reload from file if not set (in case config was updated) + if not GFW_TOKEN: + reload_token() return bool(GFW_TOKEN) +def save_token(token: str) -> bool: + """Save GFW API token to config file.""" + global GFW_TOKEN + try: + with open(CONFIG_PATH, 'w') as f: + json.dump({'api_token': token}, f) + GFW_TOKEN = token + return True + except Exception as e: + print(f"Error saving GFW token: {e}") + return False + + def search_vessel(query: str = None, mmsi: str = None, imo: str = None, name: str = None) -> dict: """Search for vessel identity.""" diff --git a/server.py b/server.py index adfba21..188ee4f 100644 --- a/server.py +++ b/server.py @@ -151,6 +151,7 @@ get_dark_fleet_indicators as gfw_get_dark_fleet_indicators, check_sts_zone as gfw_check_sts_zone, save_token as gfw_save_token, + reload_token as gfw_reload_token, get_sar_detections as gfw_get_sar_detections, find_dark_vessels as gfw_find_dark_vessels ) diff --git a/static/index.html b/static/index.html index 88b0dc1..514a9d7 100644 --- a/static/index.html +++ b/static/index.html @@ -3659,12 +3659,18 @@

${asset.name}

const status = await api('/gfw/status'); if (!status.configured) { display.innerHTML = ` -
+
GFW API not configured
- - Get free API token - + +
+ + + Get free token + +
+
`; return; @@ -3677,6 +3683,36 @@

${asset.name}

} } + async function saveGFWToken() { + const input = document.getElementById('gfw-token-input'); + const status = document.getElementById('gfw-save-status'); + const token = input?.value?.trim(); + + if (!token) { + status.innerHTML = 'Please enter a token'; + return; + } + + status.innerHTML = 'Saving...'; + + try { + const result = await fetch('/api/gfw/configure', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({token: token}) + }).then(r => r.json()); + + if (result.success) { + status.innerHTML = '✓ Token saved! Reload the page to use GFW.'; + input.style.borderColor = '#27ae60'; + } else { + status.innerHTML = `${result.error || 'Failed to save'}`; + } + } catch(e) { + status.innerHTML = 'Error saving token'; + } + } + function renderGFWData(data) { const display = document.getElementById('gfw-display'); if (!display) return; From eb762e3a2da24677b087f7224aa7abb041f510ad Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 23:28:47 +0000 Subject: [PATCH 3/4] Add infrastructure legend panel with cable type colors - Shows legend in bottom-left when infrastructure layer is active - Color-coded: Purple=telecom, Yellow=power, Orange=gas - Shows protection zone indicator - Displays count of loaded assets - Toggle visibility with infrastructure button or close button --- static/index.html | 61 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/static/index.html b/static/index.html index 514a9d7..725490a 100644 --- a/static/index.html +++ b/static/index.html @@ -720,6 +720,47 @@
+ +
+
+ 🔌 Infrastructure + +
+
+
+
+ Telecom/Fiber Cable +
+
+
+ Power Cable (HVDC) +
+
+
+ Gas Pipeline +
+
+
+ Protection Zone +
+
+ 0 assets loaded
+ Click cable for details +
+
+
+