Set QT_QPA_PLATFORM=offscreen for both grgsm_livemon and
grgsm_scanner to prevent SIGABRT when no X11 display is available.
grgsm_livemon uses GNU Radio which loads Qt plugins — without a
display, Qt aborts with "could not connect to display".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Repositioned progress indicator from right sidebar to a full-width
overlay at the top of the map panel
- Added animated spinning icon, glowing progress bar, blurred backdrop
- Centered layout with max-width constraint for readability
- Progress bar and status text more visible during active scans
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add pre-flight checks (shutil.which) for grgsm_livemon and tshark
- Capture stderr when grgsm_livemon exits immediately (exit code 1)
- Start background stderr reader thread for ongoing livemon diagnostics
- Add idle_count grace period in SSE stream to handle scanner→monitor
transition without premature disconnect
- Forward monitor failure errors to SSE for frontend display
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The scanner and monitor are mutually exclusive (both need the SDR).
Previously auto-monitor tried to start mid-scan (causing device
conflicts) and required 3 towers (rarely achieved with weak signals).
Now after the first scan completes:
- If any towers were found, automatically stop scanner and start
grgsm_livemon + tshark on the strongest tower's ARFCN
- SDR handoff is clean (scanner process has already exited)
- If monitor fails to start, scanner loop resumes
- Scanner thread's finally block preserves SDR allocation when
monitor has taken over
- Frontend shows "Monitoring ARFCN X for devices..." status
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Forward scanner progress (%) and status to SSE stream
- Show progress bar and scan status in TRACKED TOWERS panel
- Send scan_complete event with tower count and duration
- Fix Europe BAND_CONFIG: only EGSM900 is recommended (GSM850/GSM800
are rarely used in Europe and waste scan time)
- DCS1800 available but not recommended (RTL-SDR sensitivity is lower)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CID=0 with valid MCC/MNC means the scanner found the cell but didn't
decode System Information 3/4 (which carries the Cell ID). These are
still valid towers worth displaying. Only filter when MCC=0 AND MNC=0
(truly unidentified signals).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend:
- Filter out CID=0 and MCC=0 entries (ARFCNs with no decoded cell identity)
Frontend:
- Move stats update before coordinate check so towers always counted
- Fix signal_strength display using null check instead of || (0 is falsy)
- Show operator name, frequency, and status in tower detail panel
- Show "Located" indicator in tower list for geocoded towers
- Fix selectTower crash when tower has no coordinates
- Update placeholder text to "Select a tower from the list"
- Add try/catch to selectTower for error resilience
Tests:
- Add tests for CID=0 and MCC=0 filtering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
grgsm_scanner is a Python/GNU Radio script, so stdbuf has no effect.
Setting PYTHONUNBUFFERED=1 in the subprocess env forces Python to
flush stdout on every write, enabling real-time scan output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
grgsm_scanner fully buffers stdout when piped, so scan results never
reach Python until the buffer fills or process exits. Wrapping with
stdbuf -oL forces line-buffered output for real-time data streaming.
Also increased scan timeout from 120s to 300s since scanning 4 bands
legitimately takes 2-3 minutes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The ptrkrysik/gr-gsm repo uses SWIG which is incompatible with
GNU Radio 3.10+. The bkerler fork supports modern GNU Radio and
builds successfully on current Ubuntu/Debian systems.
Updated all references in Dockerfile, setup.sh, dependencies.py,
and error messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
apt-packaged gr-gsm aborts with SIGABRT (-6) due to duplicate FSM
registration in libosmocore. Setting this env var suppresses the
fatal assertion, allowing grgsm_scanner to run normally.
Applied to both scanner and livemon subprocess spawns.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
grgsm_scanner exits in <300ms with osmo_fsm assertion error due to
libosmocore incompatibility. Added crash detection: if process exits
in <5s with non-zero code, counts as crash. After 3 crashes, stops
retrying and sends error to SSE stream so the UI can display it.
Also drains remaining queue items after process exits and logs exit
code and scan duration for diagnostics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Global LOG_LEVEL defaults to WARNING, silencing all INFO/DEBUG logs.
GSM Spy needs verbose logging for scanner diagnostics. Override the
module logger level to DEBUG so scanner output is always visible.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gsm_spy.py used logging.getLogger() directly which returns a bare
logger with no handler. The parent 'intercept' logger has
propagate=False, so all GSM Spy logs were silently dropped.
Now uses utils.logging.get_logger() which adds a stderr handler
and sets the log level, matching all other route modules.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
grgsm_scanner (like many GNU Radio tools) writes scan results to
stderr, not stdout. The stderr reader was only logging at debug
level and discarding lines. Now feeds stderr into the parse queue.
Also added info-level logging for all scanner output lines to aid
debugging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Parser expected pipe-delimited table rows but grgsm_scanner outputs
comma-separated key-value pairs like:
ARFCN: 975, Freq: 925.2M, CID: 13522, LAC: 38722, MCC: 262, MNC: 1, Pwr: -58
This was the root cause of no data appearing in GSM Spy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
stop_scanner() cleared gsm_spy_active_device without calling
release_sdr_device(), so the device stayed claimed in the registry.
The scanner thread's finally block then saw None and skipped release.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
UI was sending GSM900 but backend REGIONAL_BANDS expects EGSM900
for Europe and Asia regions, causing validation rejection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GSM Spy was failing with FileNotFoundError because grgsm_scanner
wasn't installed. These tools are now installed automatically by
setup.sh (both Debian and macOS) and included in the Dockerfile,
matching how other tools like multimon-ng and ffmpeg are handled.
- setup.sh: Remove ask_yes_no prompts for gr-gsm and tshark, install
unconditionally; add check_recommended tier for final summary
- Dockerfile: Add tshark to apt layer, add gr-gsm RUN layer with
apt-then-source-build fallback, preseed debconf for tshark
- gsm_spy.py: Add shutil.which pre-check in start_scanner route,
catch FileNotFoundError in scanner_thread to stop retry loop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- kill_all() now resets gsm_spy_scanner_running and related state so
the scanner thread stops after killall
- scanner_thread sets flag to False instead of None on exit
- Restore alert_rules, alert_events, recording_sessions tables and
wifi_clients column removed by PR in database.py
- Escape all server-sourced values in analysis modals with escapeHtml()
- Reset gsm_towers_found/gsm_devices_tracked on stop to prevent
counter drift across sessions
- Replace raw terminate/kill with safe_terminate() in scanner_thread
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The function-strip CSS was never linked in index.html, causing all
strip items to render as unstyled stacked elements instead of a
horizontal flex bar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move waterfall controls from the sidebar into a function-strip bar inside
#listeningPostVisuals so they sit directly above the waterfall canvas.
Also fix the "SDR device in use" error when clicking a waterfall frequency
to listen — the WebSocket waterfall's device claim wasn't being released
before the audio start request because the backend cleanup hadn't finished.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Zooming caused "I/Q capture process exited immediately" because the client
closed the WebSocket and opened a new one, racing with the old rtl_sdr
process releasing the USB device. Now zoom/retune sends a start command on
the existing WebSocket, and the server adds a USB release delay plus retry
loop when restarting capture within the same connection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The FFT pipeline produces power values in the ~0-60 dB range for
normalized IQ data, but quantize_to_uint8 used a hardcoded range
of -90 to -20 dB. Every bin saturated to 255, producing a uniform
yellow waterfall with no signal differentiation.
Now auto-scales to the actual min/max of each frame so the full
colour palette is always used.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After a WebSocket handler exits, flask-sock returns a Response to
Werkzeug which writes "HTTP/1.1 200 OK..." on the still-open socket.
Browsers see these HTTP bytes as a malformed WebSocket frame, causing
"Invalid frame header".
Now the handler explicitly closes the raw TCP socket after the
WebSocket close handshake, so Werkzeug's write harmlessly fails.
Applied to both waterfall and audio WebSocket handlers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
simple-websocket 1.1.0's receive(timeout=N) returns None on timeout
instead of raising TimeoutError. The handler treated None as
"connection closed" and broke out of the loop, causing Werkzeug to
write its HTTP 200 response on the still-open WebSocket socket.
The browser saw those HTTP bytes as an invalid WebSocket frame.
Now checks ws.connected to distinguish timeout (None + connected)
from actual close (None + not connected).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
agents.js syncLocalModeStates() expects these endpoints to check
whether each mode is running locally. Both were missing, causing
404 errors on mode switch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The before_request require_login hook was returning a 302 redirect
for WebSocket upgrade requests, which browsers report as "Invalid
frame header". WebSocket requests don't always carry session cookies
reliably. Allow /ws/ paths through the login check since the page
that initiates these connections already requires authentication.
Also keeps the prior fix: serialize WebSocket sends through a queue
to avoid concurrent read/write on the non-thread-safe simple-websocket.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The fft_reader thread was calling ws.send() concurrently with
ws.receive() in the main loop. simple-websocket is not thread-safe
for simultaneous read/write, corrupting frame headers. Now the reader
thread enqueues frames and only the main loop touches the WebSocket.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace the batch rtl_power SSE pipeline with continuous I/Q streaming
via WebSocket for smooth ~25fps waterfall display. The server captures
raw I/Q samples (rtl_sdr/rx_sdr), computes Hann-windowed FFT, and
sends compact binary frames (1035 bytes vs ~15KB JSON, 93% reduction).
Client falls back to existing SSE path if WebSocket is unavailable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Waterfall clicks now auto-select the correct modulation for the frequency
band (e.g., WFM for FM broadcast, AM for airband) instead of using whatever
modulation was last selected. Adds a hover tooltip showing frequency and
suggested modulation. Fixes the kill-all notification to show a clean
"All processes stopped" message instead of listing "bluetooth_scanner".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a phosphor-persistence waveform scope showing audio RMS/peak
levels during ISS SSTV and General SSTV decoding, matching the
existing pager scope pattern with a purple color scheme.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enable -M level on rtl_433 to include RSSI/SNR in decoded JSON, extract
signal levels and push scope events to the SSE stream. Renders a green-
themed canvas oscilloscope showing signal strength pulses on packet decode
with amber SNR indicator and decay between packets.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tap the rtl_fm → multimon-ng audio pipeline via a relay thread to extract
RMS/peak amplitude levels and render a 60fps canvas oscilloscope during
pager decoding, giving visual feedback of RF activity before messages are
fully decoded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reverts IQ pipeline and removes syncWaterfallToFrequency calls from
pager, sensor, rtlamr, DMR, SSTV, and SSTV general modes. Waterfall
is now exclusive to listening post mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace rtl_fm/rtl_433 with rtl_sdr for raw IQ capture when available,
enabling a Python IQ processor to compute FFT for the waterfall while
simultaneously feeding decoded data to multimon-ng (pager) or rtl_433
(sensor). Falls back to the legacy pipeline when rtl_sdr is unavailable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>