Fix GSM Spy frontend: SSE state replay, field name mismatch, crash fix

- Send all existing towers on SSE connect (fixes data loss on reconnect)
- Fix tower.signal -> tower.signal_strength field name in frontend
- Fix TypeError crash in selectTower when tower has no coordinates
- Add Connection: keep-alive header to SSE response
- Add comprehensive console.log debugging for SSE data flow
- Handle error/disconnected SSE event types in frontend

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-08 16:57:39 +00:00
parent 33953fcf2b
commit 7cb2efca30
2 changed files with 40 additions and 22 deletions
+16 -9
View File
@@ -516,19 +516,28 @@ def stream():
"""SSE stream for real-time GSM updates.""" """SSE stream for real-time GSM updates."""
def generate(): def generate():
"""Generate SSE events.""" """Generate SSE events."""
logger.info("SSE stream connected - client subscribed")
# Send current state on connect (handles reconnects and late-joining clients)
existing_towers = dict(app_module.gsm_spy_towers.items())
logger.info(f"SSE sending {len(existing_towers)} existing towers on connect")
for key, tower_data in existing_towers.items():
yield format_sse(tower_data)
last_keepalive = time.time() last_keepalive = time.time()
while True: while True:
try: try:
# Check if scanner is still running # Check if scanner is still running
if not app_module.gsm_spy_scanner_running and not app_module.gsm_spy_monitor_process: if not app_module.gsm_spy_scanner_running and not app_module.gsm_spy_monitor_process:
logger.info("SSE stream: scanner stopped, sending disconnect")
yield format_sse({'type': 'disconnected'}) yield format_sse({'type': 'disconnected'})
break break
# Try to get data from queue # Try to get data from queue
try: try:
data = app_module.gsm_spy_queue.get(timeout=1) data = app_module.gsm_spy_queue.get(timeout=1)
logger.info(f"SSE sending: type={data.get('type', '?')}") logger.info(f"SSE sending: type={data.get('type', '?')} keys={list(data.keys())}")
yield format_sse(data) yield format_sse(data)
last_keepalive = time.time() last_keepalive = time.time()
except queue.Empty: except queue.Empty:
@@ -538,20 +547,18 @@ def stream():
last_keepalive = time.time() last_keepalive = time.time()
except GeneratorExit: except GeneratorExit:
logger.info("SSE stream: client disconnected (GeneratorExit)")
break break
except Exception as e: except Exception as e:
logger.error(f"Error in GSM stream: {e}") logger.error(f"Error in GSM stream: {e}")
yield format_sse({'type': 'error', 'message': str(e)}) yield format_sse({'type': 'error', 'message': str(e)})
break break
return Response( response = Response(generate(), mimetype='text/event-stream')
generate(), response.headers['Cache-Control'] = 'no-cache'
mimetype='text/event-stream', response.headers['X-Accel-Buffering'] = 'no'
headers={ response.headers['Connection'] = 'keep-alive'
'Cache-Control': 'no-cache', return response
'X-Accel-Buffering': 'no'
}
)
@gsm_spy_bp.route('/status') @gsm_spy_bp.route('/status')
+24 -13
View File
@@ -1627,21 +1627,25 @@
eventSource.close(); eventSource.close();
} }
console.log('[GSM SPY] Opening EventSource to /gsm_spy/stream');
eventSource = new EventSource('/gsm_spy/stream'); eventSource = new EventSource('/gsm_spy/stream');
eventSource.onopen = function() {
console.log('[GSM SPY] EventSource connected');
};
eventSource.onmessage = function(e) { eventSource.onmessage = function(e) {
try { try {
console.log('[GSM SPY] SSE raw:', e.data.substring(0, 200));
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
if (data.type === 'keepalive') { if (data.type === 'keepalive') {
return; return;
} }
if (data.type === 'tower') { console.log('[GSM SPY] SSE event type:', data.type, 'keys:', Object.keys(data).join(','));
updateTower(data);
} else if (data.type === 'tower_update') { if (data.type === 'tower' || data.type === 'tower_update') {
// Background geocoding resolved coordinates for a tower
console.log(`Tower coordinates resolved via API: MCC=${data.mcc} MNC=${data.mnc} LAC=${data.lac} CID=${data.cid}`);
updateTower(data); updateTower(data);
} else if (data.type === 'device') { } else if (data.type === 'device') {
updateDevice(data); updateDevice(data);
@@ -1649,16 +1653,20 @@
addRogueAlert(data); addRogueAlert(data);
} else if (data.type === 'stats') { } else if (data.type === 'stats') {
updateStats(data); updateStats(data);
} else if (data.type === 'error') {
console.error('[GSM SPY] Server error:', data.message);
} else if (data.type === 'disconnected') {
console.warn('[GSM SPY] Server disconnected stream');
} }
} catch (error) { } catch (error) {
console.error('[GSM SPY] Error parsing event:', error); console.error('[GSM SPY] Error parsing event:', error, 'raw:', e.data);
} }
}; };
eventSource.onerror = function(e) { eventSource.onerror = function(e) {
console.error('[GSM SPY] EventSource error:', e); console.error('[GSM SPY] EventSource error, readyState:', eventSource.readyState);
if (eventSource.readyState === EventSource.CLOSED) { if (eventSource.readyState === EventSource.CLOSED) {
console.log('[GSM SPY] EventSource closed, reconnecting...'); console.log('[GSM SPY] EventSource closed, reconnecting in 3s...');
setTimeout(startEventStream, 3000); setTimeout(startEventStream, 3000);
} }
}; };
@@ -1700,11 +1708,12 @@
// ============================================ // ============================================
function updateTower(data) { function updateTower(data) {
const key = `${data.mcc}-${data.mnc}-${data.lac}-${data.cid}`; const key = `${data.mcc}-${data.mnc}-${data.lac}-${data.cid}`;
console.log(`[GSM SPY] updateTower: key=${key} CID=${data.cid} signal=${data.signal_strength} lat=${data.lat} lon=${data.lon}`);
towers[key] = data; towers[key] = data;
// Validate coordinates before creating map marker // Validate coordinates before creating map marker
if (!data.lat || !data.lon || isNaN(parseFloat(data.lat)) || isNaN(parseFloat(data.lon))) { if (!data.lat || !data.lon || isNaN(parseFloat(data.lat)) || isNaN(parseFloat(data.lon))) {
console.log(`Tower ${data.cid} pending geocoding (status: ${data.status || 'unknown'})`); console.log(`[GSM SPY] Tower ${data.cid} pending geocoding (status: ${data.status || 'unknown'}), updating list only`);
// Update towers list but skip map marker // Update towers list but skip map marker
updateTowersList(); updateTowersList();
return; return;
@@ -1856,11 +1865,11 @@
</div> </div>
<div class="tower-info-row"> <div class="tower-info-row">
<span class="tower-info-label">Signal (dBm)</span> <span class="tower-info-label">Signal (dBm)</span>
<span class="tower-info-value">${escapeHtml(tower.signal || 'N/A')}</span> <span class="tower-info-value">${escapeHtml(tower.signal_strength || 'N/A')}</span>
</div> </div>
<div class="tower-info-row"> <div class="tower-info-row">
<span class="tower-info-label">Location</span> <span class="tower-info-label">Location</span>
<span class="tower-info-value">${tower.lat.toFixed(6)}, ${tower.lon.toFixed(6)}</span> <span class="tower-info-value">${tower.lat != null ? parseFloat(tower.lat).toFixed(6) + ', ' + parseFloat(tower.lon).toFixed(6) : 'Pending geocoding'}</span>
</div> </div>
<div class="tower-info-row"> <div class="tower-info-row">
<span class="tower-info-label">First Seen</span> <span class="tower-info-label">First Seen</span>
@@ -1875,8 +1884,10 @@
function updateTowersList() { function updateTowersList() {
const listDiv = document.getElementById('towersList'); const listDiv = document.getElementById('towersList');
const towerCount = Object.keys(towers).length;
console.log(`[GSM SPY] updateTowersList: ${towerCount} towers, listDiv exists: ${!!listDiv}`);
if (Object.keys(towers).length === 0) { if (towerCount === 0) {
listDiv.innerHTML = '<div class="no-data"><div>No towers detected</div></div>'; listDiv.innerHTML = '<div class="no-data"><div>No towers detected</div></div>';
return; return;
} }
@@ -1892,7 +1903,7 @@
${tower.rogue ? '<span class="rogue-indicator"></span>' : ''} ${tower.rogue ? '<span class="rogue-indicator"></span>' : ''}
</div> </div>
<div class="list-item-details"> <div class="list-item-details">
LAC ${escapeHtml(tower.lac)} | ARFCN ${escapeHtml(tower.arfcn)} | ${escapeHtml(tower.signal || 'N/A')} dBm LAC ${escapeHtml(tower.lac)} | ARFCN ${escapeHtml(tower.arfcn)} | ${escapeHtml(tower.signal_strength || 'N/A')} dBm
</div> </div>
</div> </div>
`; `;