diff --git a/routes/gsm_spy.py b/routes/gsm_spy.py index d217f40..51cebbd 100644 --- a/routes/gsm_spy.py +++ b/routes/gsm_spy.py @@ -1128,6 +1128,14 @@ def parse_grgsm_scanner_output(line: str) -> dict[str, Any] | None: fields[key.strip()] = value.strip() if 'ARFCN' in fields and 'CID' in fields: + cid = int(fields.get('CID', 0)) + mcc = int(fields.get('MCC', 0)) + + # Skip entries with no decoded cell identity (CID=0 means no cell info) + if cid == 0 or mcc == 0: + logger.debug(f"Skipping unresolved ARFCN (CID={cid}, MCC={mcc}): {line}") + return None + # Freq may have 'M' suffix (e.g. "925.2M") freq_str = fields.get('Freq', '0').rstrip('Mm') @@ -1135,9 +1143,9 @@ def parse_grgsm_scanner_output(line: str) -> dict[str, Any] | None: 'type': 'tower', 'arfcn': int(fields['ARFCN']), 'frequency': float(freq_str), - 'cid': int(fields.get('CID', 0)), + 'cid': cid, 'lac': int(fields.get('LAC', 0)), - 'mcc': int(fields.get('MCC', 0)), + 'mcc': mcc, 'mnc': int(fields.get('MNC', 0)), 'signal_strength': float(fields.get('Pwr', -999)), 'timestamp': datetime.now().isoformat() diff --git a/templates/gsm_spy_dashboard.html b/templates/gsm_spy_dashboard.html index 229f349..8d568ac 100644 --- a/templates/gsm_spy_dashboard.html +++ b/templates/gsm_spy_dashboard.html @@ -1229,7 +1229,7 @@
📡
-
Click a tower on the map
+
Select a tower from the list
@@ -1711,11 +1711,15 @@ console.log(`[GSM SPY] updateTower: key=${key} CID=${data.cid} signal=${data.signal_strength} lat=${data.lat} lon=${data.lon}`); towers[key] = data; + // Always update list and stats (regardless of coordinates) + updateTowersList(); + stats.totalTowers = Object.keys(towers).length; + stats.totalRogues = Object.values(towers).filter(t => t.rogue).length; + updateStatsDisplay(); + // Validate coordinates before creating map marker if (!data.lat || !data.lon || isNaN(parseFloat(data.lat)) || isNaN(parseFloat(data.lon))) { - console.log(`[GSM SPY] Tower ${data.cid} pending geocoding (status: ${data.status || 'unknown'}), updating list only`); - // Update towers list but skip map marker - updateTowersList(); + console.log(`[GSM SPY] Tower ${data.cid} pending geocoding (status: ${data.status || 'unknown'}), list updated`); return; } @@ -1755,14 +1759,6 @@ // Update icon if rogue status or selection changed marker.setIcon(createGSMMarkerIcon('tower', color, isSelected, data.rogue)); } - - // Update towers list - updateTowersList(); - - // Update stats - stats.totalTowers = Object.keys(towers).length; - stats.totalRogues = Object.values(towers).filter(t => t.rogue).length; - updateStatsDisplay(); } function drawSectorArc(key, tower) { @@ -1827,59 +1823,77 @@ } function selectTower(key) { - const prevSelected = selectedTowerKey; - selectedTowerKey = key; - const tower = towers[key]; + try { + console.log(`[GSM SPY] selectTower: ${key}`); + const prevSelected = selectedTowerKey; + selectedTowerKey = key; + const tower = towers[key]; - if (!tower) return; - - // Update marker icons for both previous and new selection - [prevSelected, key].forEach(towerKey => { - if (towerKey && towerMarkers[towerKey] && towers[towerKey]) { - const t = towers[towerKey]; - const color = t.rogue ? '#e25d5d' : '#38c180'; - const isSelected = towerKey === selectedTowerKey; - towerMarkers[towerKey].setIcon(createGSMMarkerIcon('tower', color, isSelected, t.rogue)); + if (!tower) { + console.warn(`[GSM SPY] Tower not found for key: ${key}`); + return; } - }); - // Update selected tower panel - const infoDiv = document.getElementById('selectedTowerInfo'); - infoDiv.innerHTML = ` -
-
- Cell ID - ${escapeHtml(tower.cid)} ${tower.rogue ? 'ROGUE' : ''} -
-
- MCC / MNC - ${escapeHtml(tower.mcc)} / ${escapeHtml(tower.mnc)} -
-
- LAC - ${escapeHtml(tower.lac)} -
-
- ARFCN - ${escapeHtml(tower.arfcn)} -
-
- Signal (dBm) - ${escapeHtml(tower.signal_strength || 'N/A')} -
-
- Location - ${tower.lat != null ? parseFloat(tower.lat).toFixed(6) + ', ' + parseFloat(tower.lon).toFixed(6) : 'Pending geocoding'} -
-
- First Seen - ${new Date(tower.timestamp).toLocaleTimeString()} -
-
- `; + // Update marker icons for both previous and new selection + [prevSelected, key].forEach(towerKey => { + if (towerKey && towerMarkers[towerKey] && towers[towerKey]) { + const t = towers[towerKey]; + const color = t.rogue ? '#e25d5d' : '#38c180'; + const isSelected = towerKey === selectedTowerKey; + towerMarkers[towerKey].setIcon(createGSMMarkerIcon('tower', color, isSelected, t.rogue)); + } + }); - // Update list selection - updateTowersList(); + // Build info rows + const signalVal = tower.signal_strength != null ? escapeHtml(tower.signal_strength) : 'N/A'; + const locationVal = tower.lat != null ? parseFloat(tower.lat).toFixed(6) + ', ' + parseFloat(tower.lon).toFixed(6) : 'Pending geocoding'; + const timeVal = tower.timestamp ? new Date(tower.timestamp).toLocaleTimeString() : 'Unknown'; + const operatorVal = tower.operator ? escapeHtml(tower.operator) : ''; + const freqVal = tower.frequency ? escapeHtml(tower.frequency) + ' MHz' : ''; + const statusVal = tower.status === 'pending' ? 'Pending geocoding' : (tower.source === 'cache' || tower.source === 'api' ? 'Resolved' : ''); + + // Update selected tower panel + const infoDiv = document.getElementById('selectedTowerInfo'); + infoDiv.innerHTML = ` +
+
+ Cell ID + ${escapeHtml(tower.cid)} ${tower.rogue ? 'ROGUE' : ''} +
+ ${operatorVal ? `
Operator${operatorVal}
` : ''} +
+ MCC / MNC + ${escapeHtml(tower.mcc)} / ${escapeHtml(tower.mnc)} +
+
+ LAC + ${escapeHtml(tower.lac)} +
+
+ ARFCN + ${escapeHtml(tower.arfcn)}${freqVal ? ' (' + freqVal + ')' : ''} +
+
+ Signal + ${signalVal} dBm +
+
+ Location + ${locationVal} +
+
+ First Seen + ${timeVal} +
+ ${statusVal ? `
Status${statusVal}
` : ''} +
+ `; + + // Update list selection + updateTowersList(); + } catch (error) { + console.error('[GSM SPY] selectTower error:', error); + } } function updateTowersList() { @@ -1895,15 +1909,18 @@ let html = ''; for (const [key, tower] of Object.entries(towers)) { const selected = key === selectedTowerKey ? 'selected' : ''; + const signalText = tower.signal_strength != null ? escapeHtml(tower.signal_strength) + ' dBm' : ''; + const operatorText = tower.operator ? escapeHtml(tower.operator) : ''; + const metaText = operatorText || (escapeHtml(tower.mcc) + '-' + escapeHtml(tower.mnc)); html += `
CID ${escapeHtml(tower.cid)} - ${escapeHtml(tower.mcc)}-${escapeHtml(tower.mnc)} + ${metaText} ${tower.rogue ? '' : ''}
- LAC ${escapeHtml(tower.lac)} | ARFCN ${escapeHtml(tower.arfcn)} | ${escapeHtml(tower.signal_strength || 'N/A')} dBm + LAC ${escapeHtml(tower.lac)} | ARFCN ${escapeHtml(tower.arfcn)}${signalText ? ' | ' + signalText : ''}${tower.lat != null ? ' | Located' : ''}
`; diff --git a/tests/test_gsm_spy.py b/tests/test_gsm_spy.py index 76c1bdf..0f110cb 100644 --- a/tests/test_gsm_spy.py +++ b/tests/test_gsm_spy.py @@ -71,6 +71,26 @@ class TestParseGrgsmScannerOutput: result = parse_grgsm_scanner_output(line) assert result is None + def test_cid_zero_filtered(self): + """Test that CID=0 entries (no decoded cell) are filtered out.""" + line = "ARFCN: 115, Freq: 925.0M, CID: 0, LAC: 0, MCC: 0, MNC: 0, Pwr: -100" + result = parse_grgsm_scanner_output(line) + assert result is None + + def test_mcc_zero_filtered(self): + """Test that MCC=0 entries (no decoded identity) are filtered out.""" + line = "ARFCN: 113, Freq: 924.6M, CID: 1234, LAC: 5678, MCC: 0, MNC: 0, Pwr: -90" + result = parse_grgsm_scanner_output(line) + assert result is None + + def test_valid_cid_nonzero(self): + """Test that valid non-zero CID/MCC entries pass through.""" + line = "ARFCN: 115, Freq: 925.0M, CID: 19088, LAC: 21864, MCC: 234, MNC: 10, Pwr: -58" + result = parse_grgsm_scanner_output(line) + assert result is not None + assert result['cid'] == 19088 + assert result['signal_strength'] == -58.0 + class TestParseTsharkOutput: """Tests for parse_tshark_output()."""