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."""
def generate():
"""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()
while True:
try:
# Check if scanner is still running
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'})
break
# Try to get data from queue
try:
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)
last_keepalive = time.time()
except queue.Empty:
@@ -538,20 +547,18 @@ def stream():
last_keepalive = time.time()
except GeneratorExit:
logger.info("SSE stream: client disconnected (GeneratorExit)")
break
except Exception as e:
logger.error(f"Error in GSM stream: {e}")
yield format_sse({'type': 'error', 'message': str(e)})
break
return Response(
generate(),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'X-Accel-Buffering': 'no'
}
)
response = Response(generate(), mimetype='text/event-stream')
response.headers['Cache-Control'] = 'no-cache'
response.headers['X-Accel-Buffering'] = 'no'
response.headers['Connection'] = 'keep-alive'
return response
@gsm_spy_bp.route('/status')
+24 -13
View File
@@ -1627,21 +1627,25 @@
eventSource.close();
}
console.log('[GSM SPY] Opening EventSource to /gsm_spy/stream');
eventSource = new EventSource('/gsm_spy/stream');
eventSource.onopen = function() {
console.log('[GSM SPY] EventSource connected');
};
eventSource.onmessage = function(e) {
try {
console.log('[GSM SPY] SSE raw:', e.data.substring(0, 200));
const data = JSON.parse(e.data);
if (data.type === 'keepalive') {
return;
}
if (data.type === 'tower') {
updateTower(data);
} else if (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}`);
console.log('[GSM SPY] SSE event type:', data.type, 'keys:', Object.keys(data).join(','));
if (data.type === 'tower' || data.type === 'tower_update') {
updateTower(data);
} else if (data.type === 'device') {
updateDevice(data);
@@ -1649,16 +1653,20 @@
addRogueAlert(data);
} else if (data.type === 'stats') {
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) {
console.error('[GSM SPY] Error parsing event:', error);
console.error('[GSM SPY] Error parsing event:', error, 'raw:', e.data);
}
};
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) {
console.log('[GSM SPY] EventSource closed, reconnecting...');
console.log('[GSM SPY] EventSource closed, reconnecting in 3s...');
setTimeout(startEventStream, 3000);
}
};
@@ -1700,11 +1708,12 @@
// ============================================
function updateTower(data) {
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;
// Validate coordinates before creating map marker
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
updateTowersList();
return;
@@ -1856,11 +1865,11 @@
</div>
<div class="tower-info-row">
<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 class="tower-info-row">
<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 class="tower-info-row">
<span class="tower-info-label">First Seen</span>
@@ -1875,8 +1884,10 @@
function updateTowersList() {
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>';
return;
}
@@ -1892,7 +1903,7 @@
${tower.rogue ? '<span class="rogue-indicator"></span>' : ''}
</div>
<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>
`;