From 6b7f817aa64abd1be613636bfeaaf9b4886ea9da Mon Sep 17 00:00:00 2001 From: Smittix Date: Sun, 8 Feb 2026 18:41:30 +0000 Subject: [PATCH] Add live monitoring status overlay with heartbeat updates Backend: monitor_thread sends periodic monitor_heartbeat events (every 5s) with elapsed time, packet count, and device count so the frontend knows monitoring is active. Frontend: new monitoring overlay replaces scan progress bar when auto-monitor starts. Shows pulsing green indicator, ARFCN being monitored, live elapsed timer, packet/device counts, and "Listening..."/"Capturing" activity state. Co-Authored-By: Claude Opus 4.6 --- routes/gsm_spy.py | 21 ++++ templates/gsm_spy_dashboard.html | 159 ++++++++++++++++++++++++++++++- 2 files changed, 179 insertions(+), 1 deletion(-) diff --git a/routes/gsm_spy.py b/routes/gsm_spy.py index 1634417..fea0834 100644 --- a/routes/gsm_spy.py +++ b/routes/gsm_spy.py @@ -1628,6 +1628,10 @@ def monitor_thread(process): stderr_thread = threading.Thread(target=read_stderr, daemon=True) stderr_thread.start() + monitor_start_time = time.time() + packets_captured = 0 + last_heartbeat = time.time() + try: while app_module.gsm_spy_monitor_process: # Check if process died @@ -1635,6 +1639,21 @@ def monitor_thread(process): logger.info(f"Monitor process exited (code: {process.returncode})") break + # Send periodic heartbeat so frontend knows monitor is alive + now = time.time() + if now - last_heartbeat >= 5: + last_heartbeat = now + elapsed = int(now - monitor_start_time) + try: + app_module.gsm_spy_queue.put_nowait({ + 'type': 'monitor_heartbeat', + 'elapsed': elapsed, + 'packets': packets_captured, + 'devices': len(app_module.gsm_spy_devices) + }) + except queue.Full: + pass + # Get output from queue with timeout try: msg_type, line = output_queue_local.get(timeout=1.0) @@ -1646,6 +1665,8 @@ def monitor_thread(process): parsed = parse_tshark_output(line) if parsed: + packets_captured += 1 + # Store in DataStore key = parsed.get('tmsi') or parsed.get('imsi') or str(time.time()) app_module.gsm_spy_devices[key] = parsed diff --git a/templates/gsm_spy_dashboard.html b/templates/gsm_spy_dashboard.html index dc673ad..ff05a7c 100644 --- a/templates/gsm_spy_dashboard.html +++ b/templates/gsm_spy_dashboard.html @@ -525,6 +525,91 @@ box-shadow: 0 0 8px rgba(0, 229, 255, 0.4); } + /* Monitor Status Overlay */ + .monitor-status-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 1000; + background: linear-gradient(180deg, rgba(10, 14, 20, 0.95) 0%, rgba(10, 14, 20, 0.85) 100%); + border-bottom: 1px solid var(--accent-green, #4caf50); + backdrop-filter: blur(8px); + padding: 10px 16px; + } + + .monitor-status-inner { + max-width: 600px; + margin: 0 auto; + } + + .monitor-status-row { + display: flex; + align-items: center; + gap: 10px; + font-family: var(--font-mono); + font-size: 12px; + } + + .monitor-pulse { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--accent-green, #4caf50); + box-shadow: 0 0 6px var(--accent-green, #4caf50); + animation: pulse-glow 2s ease-in-out infinite; + } + + @keyframes pulse-glow { + 0%, 100% { opacity: 1; box-shadow: 0 0 6px var(--accent-green, #4caf50); } + 50% { opacity: 0.5; box-shadow: 0 0 12px var(--accent-green, #4caf50); } + } + + .monitor-label { + color: var(--accent-green, #4caf50); + font-weight: 700; + letter-spacing: 1.5px; + font-size: 11px; + } + + .monitor-arfcn { + color: var(--text-primary); + font-weight: 600; + } + + .monitor-elapsed { + margin-left: auto; + color: var(--text-secondary); + } + + .monitor-stats-row { + display: flex; + align-items: center; + gap: 8px; + margin-top: 5px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--text-secondary); + padding-left: 18px; + } + + .monitor-stat-sep { + width: 3px; + height: 3px; + border-radius: 50%; + background: var(--text-secondary); + opacity: 0.5; + } + + .monitor-listening { + animation: listening-fade 2.5s ease-in-out infinite; + } + + @keyframes listening-fade { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.3; } + } + /* Right Sidebar */ .right-sidebar { background: var(--bg-panel); @@ -1293,6 +1378,23 @@ +
@@ -1667,6 +1769,7 @@ eventSource = null; } document.getElementById('scanProgress').style.display = 'none'; + hideMonitorStatus(); console.log('[GSM SPY] Scanner stopped'); }) .catch(error => { @@ -1740,13 +1843,16 @@ updateScanStatus('Scan #' + data.scan + ' complete (' + data.towers_found + ' towers, ' + data.duration + 's)'); document.getElementById('scanProgressBar').style.width = '100%'; } else if (data.type === 'auto_monitor_started') { - updateScanStatus('Monitoring ARFCN ' + data.arfcn + ' for devices...'); + showMonitorStatus(data.arfcn); console.log('[GSM SPY] Auto-monitor started on ARFCN', data.arfcn); + } else if (data.type === 'monitor_heartbeat') { + updateMonitorStatus(data.elapsed, data.packets, data.devices); } else if (data.type === 'error') { console.error('[GSM SPY] Server error:', data.message); updateScanStatus('Error: ' + data.message); } else if (data.type === 'disconnected') { console.warn('[GSM SPY] Server disconnected stream'); + hideMonitorStatus(); } } catch (error) { console.error('[GSM SPY] Error parsing event:', error, 'raw:', e.data); @@ -2002,6 +2108,57 @@ statusText.textContent = message; } + let monitorStartTime = null; + let monitorTimerInterval = null; + + function showMonitorStatus(arfcn) { + // Hide scan progress, show monitor status + document.getElementById('scanProgress').style.display = 'none'; + const overlay = document.getElementById('monitorStatus'); + overlay.style.display = 'block'; + document.getElementById('monitorArfcn').textContent = 'ARFCN ' + arfcn; + document.getElementById('monitorPackets').textContent = '0'; + document.getElementById('monitorDevices').textContent = '0'; + document.getElementById('monitorActivity').textContent = 'Listening...'; + + // Start local elapsed timer for smooth updates between heartbeats + monitorStartTime = Date.now(); + if (monitorTimerInterval) clearInterval(monitorTimerInterval); + monitorTimerInterval = setInterval(function() { + const elapsed = Math.floor((Date.now() - monitorStartTime) / 1000); + document.getElementById('monitorElapsed').textContent = formatElapsed(elapsed); + }, 1000); + } + + function updateMonitorStatus(elapsed, packets, devices) { + const overlay = document.getElementById('monitorStatus'); + if (overlay.style.display === 'none') return; + document.getElementById('monitorElapsed').textContent = formatElapsed(elapsed); + document.getElementById('monitorPackets').textContent = packets; + document.getElementById('monitorDevices').textContent = devices; + // Sync local timer with server elapsed + monitorStartTime = Date.now() - (elapsed * 1000); + // Flash activity indicator on heartbeat + const activity = document.getElementById('monitorActivity'); + activity.textContent = packets > 0 ? 'Capturing' : 'Listening...'; + activity.style.color = packets > 0 ? 'var(--accent-green, #4caf50)' : ''; + } + + function hideMonitorStatus() { + document.getElementById('monitorStatus').style.display = 'none'; + if (monitorTimerInterval) { + clearInterval(monitorTimerInterval); + monitorTimerInterval = null; + } + monitorStartTime = null; + } + + function formatElapsed(seconds) { + const m = Math.floor(seconds / 60); + const s = seconds % 60; + return String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); + } + function updateTowersList() { const listDiv = document.getElementById('towersList'); const towerCount = Object.keys(towers).length;