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 += `
- 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()."""