diff --git a/app.py b/app.py index 14ccae0..37115d7 100644 --- a/app.py +++ b/app.py @@ -183,7 +183,7 @@ deauth_detector_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE) deauth_detector_lock = threading.Lock() # GSM Spy -gsm_spy_process = None +gsm_spy_scanner_running = False # Flag: scanner thread active gsm_spy_livemon_process = None # For grgsm_livemon process gsm_spy_monitor_process = None # For tshark monitoring process gsm_spy_queue = queue.Queue(maxsize=QUEUE_MAX_SIZE) diff --git a/config.py b/config.py index 25efa72..4da4826 100644 --- a/config.py +++ b/config.py @@ -201,7 +201,7 @@ ADMIN_USERNAME = _get_env('ADMIN_USERNAME', 'admin') ADMIN_PASSWORD = _get_env('ADMIN_PASSWORD', 'admin') # GSM Spy settings -GSM_OPENCELLID_API_KEY = _get_env('GSM_OPENCELLID_API_KEY', 'pk.68c92ecb85886de7b50ed5a4c73f9504') +GSM_OPENCELLID_API_KEY = _get_env('GSM_OPENCELLID_API_KEY', '') GSM_OPENCELLID_API_URL = _get_env('GSM_OPENCELLID_API_URL', 'https://opencellid.org/cell/get') GSM_API_DAILY_LIMIT = _get_env_int('GSM_API_DAILY_LIMIT', 1000) GSM_TA_METERS_PER_UNIT = _get_env_int('GSM_TA_METERS_PER_UNIT', 554) diff --git a/routes/gsm_spy.py b/routes/gsm_spy.py index afef89e..1091b87 100644 --- a/routes/gsm_spy.py +++ b/routes/gsm_spy.py @@ -20,6 +20,7 @@ import app as app_module import config from config import SHARED_OBSERVER_LOCATION_ENABLED from utils.database import get_db +from utils.process import register_process, safe_terminate, unregister_process from utils.sse import format_sse from utils.validation import validate_device_index @@ -207,6 +208,82 @@ def arfcn_to_frequency(arfcn): raise ValueError(f"ARFCN {arfcn} not found in any known GSM band") +def validate_band_names(bands: list[str], region: str) -> tuple[list[str], str | None]: + """Validate band names against REGIONAL_BANDS whitelist. + + Args: + bands: List of band names from user input + region: Region name (Americas, Europe, Asia) + + Returns: + Tuple of (validated_bands, error_message) + """ + if not bands: + return [], None + + region_bands = REGIONAL_BANDS.get(region) + if not region_bands: + return [], f"Invalid region: {region}" + + valid_band_names = set(region_bands.keys()) + invalid_bands = [b for b in bands if b not in valid_band_names] + + if invalid_bands: + return [], (f"Invalid bands for {region}: {', '.join(invalid_bands)}. " + f"Valid bands: {', '.join(sorted(valid_band_names))}") + + return bands, None + + +def _start_monitoring_processes(arfcn: int, device_index: int) -> tuple[subprocess.Popen, subprocess.Popen]: + """Start grgsm_livemon and tshark processes for monitoring an ARFCN. + + Returns: + Tuple of (grgsm_process, tshark_process) + """ + frequency_hz = arfcn_to_frequency(arfcn) + frequency_mhz = frequency_hz / 1e6 + + # Start grgsm_livemon + grgsm_cmd = [ + 'grgsm_livemon', + '--args', f'rtl={device_index}', + '-f', f'{frequency_mhz}M' + ] + grgsm_proc = subprocess.Popen( + grgsm_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + register_process(grgsm_proc) + logger.info(f"Started grgsm_livemon (PID: {grgsm_proc.pid})") + + time.sleep(2) # Wait for grgsm_livemon to start + + # Start tshark + tshark_cmd = [ + 'tshark', '-i', 'lo', + '-Y', 'gsm_a.rr.timing_advance || gsm_a.tmsi || gsm_a.imsi', + '-T', 'fields', + '-e', 'gsm_a.rr.timing_advance', + '-e', 'gsm_a.tmsi', + '-e', 'gsm_a.imsi', + '-e', 'gsm_a.lac', + '-e', 'gsm_a.cellid' + ] + tshark_proc = subprocess.Popen( + tshark_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + bufsize=1 + ) + register_process(tshark_proc) + logger.info(f"Started tshark (PID: {tshark_proc.pid})") + + return grgsm_proc, tshark_proc + + @gsm_spy_bp.route('/dashboard') def dashboard(): """Render GSM Spy dashboard.""" @@ -222,7 +299,7 @@ def start_scanner(): global gsm_towers_found, gsm_connected with app_module.gsm_spy_lock: - if app_module.gsm_spy_process: + if app_module.gsm_spy_scanner_running: return jsonify({'error': 'Scanner already running'}), 400 data = request.get_json() or {} @@ -246,7 +323,14 @@ def start_scanner(): }), 409 # If no bands selected, use all bands for the region (backwards compatibility) - if not selected_bands: + if selected_bands: + validated_bands, error = validate_band_names(selected_bands, region) + if error: + from app import release_sdr_device + release_sdr_device(device_index) + return jsonify({'error': error}), 400 + selected_bands = validated_bands + else: region_bands = REGIONAL_BANDS.get(region, REGIONAL_BANDS['Americas']) selected_bands = list(region_bands.keys()) logger.warning(f"No bands specified, using all bands for {region}: {selected_bands}") @@ -271,7 +355,11 @@ def start_scanner(): # Set a flag to indicate scanner should run app_module.gsm_spy_active_device = device_index app_module.gsm_spy_region = region - app_module.gsm_spy_process = True # Use as flag initially + app_module.gsm_spy_scanner_running = True # Use as flag initially + + # Reset counters for new session + gsm_towers_found = 0 + gsm_devices_tracked = 0 # Start geocoding worker (if not already running) start_geocoding_worker() @@ -317,53 +405,15 @@ def start_monitor(): if not arfcn: return jsonify({'error': 'ARFCN required'}), 400 + # Validate device index try: - # Convert ARFCN to frequency - frequency_hz = arfcn_to_frequency(arfcn) - frequency_mhz = frequency_hz / 1e6 - - # grgsm_livemon --args="rtl=0" -f 925.8M | tshark -i lo -Y "..." - grgsm_cmd = [ - 'grgsm_livemon', - '--args', f'rtl={device_index}', - '-f', f'{frequency_mhz}M' - ] - - tshark_cmd = [ - 'tshark', - '-i', 'lo', - '-Y', 'gsm_a.rr.timing_advance || gsm_a.tmsi || gsm_a.imsi', - '-T', 'fields', - '-e', 'gsm_a.rr.timing_advance', - '-e', 'gsm_a.tmsi', - '-e', 'gsm_a.imsi', - '-e', 'gsm_a.lac', - '-e', 'gsm_a.cellid' - ] - - logger.info(f"Starting GSM monitor: {' '.join(grgsm_cmd)} | {' '.join(tshark_cmd)}") - - # Start grgsm_livemon (outputs to UDP port 4729 by default) - grgsm_proc = subprocess.Popen( - grgsm_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) - logger.info(f"Started grgsm_livemon (PID: {grgsm_proc.pid})") - - # Give grgsm_livemon time to initialize and start sending UDP packets - time.sleep(2) - - # Start tshark (captures from loopback interface where UDP packets arrive) - tshark_proc = subprocess.Popen( - tshark_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - bufsize=1 - ) - logger.info(f"Started tshark (PID: {tshark_proc.pid})") + device_index = validate_device_index(device_index) + except ValueError as e: + return jsonify({'error': str(e)}), 400 + try: + # Start monitoring processes + grgsm_proc, tshark_proc = _start_monitoring_processes(arfcn, device_index) app_module.gsm_spy_livemon_process = grgsm_proc app_module.gsm_spy_monitor_process = tshark_proc app_module.gsm_spy_selected_arfcn = arfcn @@ -398,32 +448,22 @@ def stop_scanner(): killed = [] # Stop scanner (now just a flag, thread will see it and exit) - if app_module.gsm_spy_process: - app_module.gsm_spy_process = None + if app_module.gsm_spy_scanner_running: + app_module.gsm_spy_scanner_running = False killed.append('scanner') + # Terminate livemon process if app_module.gsm_spy_livemon_process: - try: - app_module.gsm_spy_livemon_process.terminate() - app_module.gsm_spy_livemon_process.wait(timeout=5) + unregister_process(app_module.gsm_spy_livemon_process) + if safe_terminate(app_module.gsm_spy_livemon_process, timeout=5): killed.append('livemon') - except Exception: - try: - app_module.gsm_spy_livemon_process.kill() - except Exception: - pass app_module.gsm_spy_livemon_process = None + # Terminate monitor process if app_module.gsm_spy_monitor_process: - try: - app_module.gsm_spy_monitor_process.terminate() - app_module.gsm_spy_monitor_process.wait(timeout=5) + unregister_process(app_module.gsm_spy_monitor_process) + if safe_terminate(app_module.gsm_spy_monitor_process, timeout=5): killed.append('monitor') - except Exception: - try: - app_module.gsm_spy_monitor_process.kill() - except Exception: - pass app_module.gsm_spy_monitor_process = None # Release SDR device @@ -449,7 +489,7 @@ def stream(): while True: try: # Check if scanner is still running - if not app_module.gsm_spy_process and not app_module.gsm_spy_monitor_process: + if not app_module.gsm_spy_scanner_running and not app_module.gsm_spy_monitor_process: yield format_sse({'type': 'disconnected'}) break @@ -486,7 +526,7 @@ def status(): """Get current GSM Spy status.""" api_usage = get_api_usage_today() return jsonify({ - 'running': app_module.gsm_spy_process is not None, + 'running': app_module.gsm_spy_scanner_running is not None, 'monitoring': app_module.gsm_spy_monitor_process is not None, 'towers_found': gsm_towers_found, 'devices_tracked': gsm_devices_tracked, @@ -1122,52 +1162,8 @@ def auto_start_monitor(tower_data): device_index = app_module.gsm_spy_active_device or 0 - # Convert ARFCN to frequency - frequency_hz = arfcn_to_frequency(arfcn) - frequency_mhz = frequency_hz / 1e6 - - # Start grgsm_livemon - grgsm_cmd = [ - 'grgsm_livemon', - '--args', f'rtl={device_index}', - '-f', f'{frequency_mhz}M' - ] - - tshark_cmd = [ - 'tshark', - '-i', 'lo', - '-Y', 'gsm_a.rr.timing_advance || gsm_a.tmsi || gsm_a.imsi', - '-T', 'fields', - '-e', 'gsm_a.rr.timing_advance', - '-e', 'gsm_a.tmsi', - '-e', 'gsm_a.imsi', - '-e', 'gsm_a.lac', - '-e', 'gsm_a.cellid' - ] - - logger.info(f"Starting auto-monitor: {' '.join(grgsm_cmd)} | {' '.join(tshark_cmd)}") - - # Start grgsm_livemon (outputs to UDP port 4729 by default) - grgsm_proc = subprocess.Popen( - grgsm_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) - logger.info(f"Started grgsm_livemon for auto-monitor (PID: {grgsm_proc.pid})") - - # Give grgsm_livemon time to initialize and start sending UDP packets - time.sleep(2) - - # Start tshark (captures from loopback interface where UDP packets arrive) - tshark_proc = subprocess.Popen( - tshark_cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - bufsize=1 - ) - logger.info(f"Started tshark for auto-monitor (PID: {tshark_proc.pid})") - + # Start monitoring processes + grgsm_proc, tshark_proc = _start_monitoring_processes(arfcn, device_index) app_module.gsm_spy_livemon_process = grgsm_proc app_module.gsm_spy_monitor_process = tshark_proc app_module.gsm_spy_selected_arfcn = arfcn @@ -1210,7 +1206,7 @@ def scanner_thread(cmd, device_index): process = None try: - while app_module.gsm_spy_process: # Flag check + while app_module.gsm_spy_scanner_running: # Flag check scan_count += 1 logger.info(f"Starting GSM scan #{scan_count}") @@ -1240,7 +1236,7 @@ def scanner_thread(cmd, device_index): last_output = time.time() scan_timeout = 120 # 2 minute maximum per scan - while app_module.gsm_spy_process: + while app_module.gsm_spy_scanner_running: # Check if process died if process.poll() is not None: logger.info(f"Scanner exited (code: {process.returncode})") @@ -1325,13 +1321,13 @@ def scanner_thread(cmd, device_index): pass # Check if should continue - if not app_module.gsm_spy_process: + if not app_module.gsm_spy_scanner_running: break # Wait between scans with responsive flag checking logger.info("Waiting 5 seconds before next scan") for i in range(5): - if not app_module.gsm_spy_process: + if not app_module.gsm_spy_scanner_running: break time.sleep(1) @@ -1355,7 +1351,7 @@ def scanner_thread(cmd, device_index): # Reset global state with app_module.gsm_spy_lock: - app_module.gsm_spy_process = None + app_module.gsm_spy_scanner_running = None if app_module.gsm_spy_active_device is not None: from app import release_sdr_device release_sdr_device(app_module.gsm_spy_active_device) diff --git a/static/css/gsm_spy_dashboard.css b/static/css/gsm_spy_dashboard.css deleted file mode 100644 index ef362f1..0000000 --- a/static/css/gsm_spy_dashboard.css +++ /dev/null @@ -1,715 +0,0 @@ -/* GSM SPY Dashboard Styles */ - -:root { - --font-mono: 'IBM Plex Mono', 'JetBrains Mono', 'Courier New', monospace; - --bg-dark: #0b1118; - --bg-panel: #101823; - --bg-panel-hover: #1a2331; - --border-color: #263246; - --accent-green: #38c180; - --accent-cyan: #4aa3ff; - --accent-red: #e25d5d; - --accent-yellow: #ffa500; - --text-primary: #e8e8e8; - --text-secondary: #888; - --text-dim: #555; -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - padding: 0; - font-family: var(--font-mono); - background: var(--bg-dark); - color: var(--text-primary); - overflow: hidden; - font-size: 12px; -} - -/* Radar background and scanline */ -.radar-bg { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px), - linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px); - background-size: 50px 50px; - pointer-events: none; - z-index: 0; -} - -.scanline { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 2px; - background: var(--accent-cyan); - opacity: 0.3; - animation: scan 3s linear infinite; - pointer-events: none; - z-index: 1; -} - -@keyframes scan { - from { transform: translateY(0); } - to { transform: translateY(100vh); } -} - -/* Header */ -.header { - position: fixed; - top: 0; - left: 0; - right: 0; - height: 60px; - background: var(--bg-panel); - border-bottom: 1px solid var(--border-color); - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 20px; - z-index: 100; -} - -.logo { - font-size: 24px; - font-weight: 700; - color: var(--accent-cyan); - letter-spacing: 2px; -} - -.status-bar { - display: flex; - gap: 15px; - align-items: center; -} - -.status-indicator { - display: flex; - align-items: center; - gap: 8px; - font-size: 11px; -} - -.status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--text-dim); -} - -.status-dot.active { - background: var(--accent-green); - animation: pulse-dot 2s ease-in-out infinite; -} - -.status-dot.error { - background: var(--accent-red); -} - -@keyframes pulse-dot { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -/* Stats strip */ -.stats-strip { - position: fixed; - top: 60px; - left: 0; - right: 0; - height: 50px; - background: var(--bg-panel); - border-bottom: 1px solid var(--border-color); - display: flex; - gap: 20px; - padding: 0 20px; - align-items: center; - z-index: 99; -} - -.strip-stat { - display: flex; - flex-direction: column; - align-items: center; -} - -.strip-value { - font-size: 20px; - font-weight: 700; - color: var(--accent-green); - line-height: 1.2; -} - -.strip-label { - font-size: 9px; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -/* Dashboard layout */ -.dashboard { - position: fixed; - top: 110px; - bottom: 80px; - left: 0; - right: 0; - display: grid; - grid-template-columns: 280px 1fr 300px; - gap: 10px; - padding: 10px; -} - -/* Sidebar panels */ -.left-sidebar, .right-sidebar { - display: flex; - flex-direction: column; - gap: 10px; - overflow-y: auto; -} - -.panel { - background: var(--bg-panel); - border: 1px solid var(--border-color); - border-radius: 4px; - overflow: hidden; - display: flex; - flex-direction: column; -} - -.panel-header { - padding: 10px 12px; - font-size: 11px; - font-weight: 700; - border-bottom: 1px solid var(--border-color); - color: var(--accent-cyan); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.panel-content { - padding: 12px; -} - -/* Signal source panel */ -.signal-source select, -.region-selector select { - width: 100%; - background: var(--bg-dark); - color: var(--text-primary); - border: 1px solid var(--border-color); - border-radius: 3px; - padding: 8px; - font-family: var(--font-mono); - font-size: 11px; -} - -.region-selector { - margin-top: 10px; -} - -.region-selector label { - display: block; - margin-bottom: 5px; - font-size: 10px; - color: var(--text-secondary); -} - -.band-info { - margin-top: 8px; - padding: 8px; - background: var(--bg-dark); - border-radius: 3px; - font-size: 10px; - color: var(--text-secondary); -} - -/* Selected tower info */ -.selected-info { - padding: 12px; - font-size: 11px; -} - -.selected-info.empty { - color: var(--text-dim); - text-align: center; - padding: 20px; -} - -.selected-info > div { - margin-bottom: 8px; -} - -.selected-info strong { - color: var(--accent-cyan); -} - -/* Tower and device lists */ -.tower-list, .device-list, .alert-list { - max-height: 300px; - overflow-y: auto; -} - -.tower-item, .device-item, .alert-item { - padding: 10px 12px; - border-bottom: 1px solid var(--border-color); - cursor: pointer; - transition: background 0.2s; - font-size: 11px; -} - -.tower-item:hover, .device-item:hover { - background: var(--bg-panel-hover); -} - -.tower-item:last-child, .device-item:last-child, .alert-item:last-child { - border-bottom: none; -} - -.tower-item.rogue { - border-left: 3px solid var(--accent-red); -} - -.tower-item-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 5px; -} - -.tower-cid { - font-weight: 700; - color: var(--accent-cyan); -} - -.tower-signal { - font-size: 10px; - color: var(--text-secondary); -} - -.tower-operator { - font-size: 10px; - color: var(--text-dim); -} - -.device-item-id { - font-weight: 700; - color: var(--accent-green); - margin-bottom: 5px; -} - -.device-ta { - font-size: 10px; - color: var(--text-secondary); -} - -.alert-item { - background: rgba(226, 93, 93, 0.1); - border-left: 3px solid var(--accent-red); - cursor: default; -} - -.alert-item strong { - color: var(--accent-red); -} - -.alert-item small { - display: block; - margin-top: 5px; - color: var(--text-dim); - font-size: 9px; -} - -/* Map container */ -.map-container { - position: relative; - border: 1px solid var(--border-color); - border-radius: 4px; - overflow: hidden; -} - -#gsmMap { - width: 100%; - height: 100%; - background: var(--bg-dark); -} - -/* Map markers - Vector Icons */ -.gsm-marker { - background: transparent !important; - border: none !important; - position: relative; -} - -.gsm-marker svg { - display: block; - transition: filter 0.2s ease; -} - -/* Selection ring for selected towers */ -.selection-ring { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 40px; - height: 40px; - border: 2px solid rgba(255,255,255,0.6); - border-radius: 50%; - animation: selection-pulse 2s ease-in-out infinite; - pointer-events: none; -} - -@keyframes selection-pulse { - 0%, 100% { - transform: translate(-50%, -50%) scale(1); - opacity: 0.6; - } - 50% { - transform: translate(-50%, -50%) scale(1.3); - opacity: 0.2; - } -} - -/* Rogue tower pulse ring */ -.rogue-pulse-ring { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 30px; - height: 30px; - border: 2px solid var(--accent-red); - border-radius: 50%; - animation: rogue-pulse 1.5s ease-out infinite; - pointer-events: none; -} - -@keyframes rogue-pulse { - 0% { - transform: translate(-50%, -50%) scale(0.8); - opacity: 0.8; - } - 100% { - transform: translate(-50%, -50%) scale(2); - opacity: 0; - } -} - -/* Device marker animations */ -.gsm-device { - animation: device-fade-in 0.3s ease-out; -} - -@keyframes device-fade-in { - 0% { - opacity: 0; - transform: scale(0.5); - } - 100% { - opacity: 1; - transform: scale(1); - } -} - -.device-fade-out { - animation: device-fade-out 1s ease-out forwards; -} - -@keyframes device-fade-out { - 0% { - opacity: 1; - transform: scale(1); - } - 100% { - opacity: 0; - transform: scale(0.3); - } -} - -/* Legacy circle marker support (fallback) */ -.tower-marker { - width: 20px; - height: 20px; - border-radius: 50%; - background: var(--accent-green); - border: 2px solid white; - box-shadow: 0 0 8px rgba(56, 195, 128, 0.6); -} - -.tower-marker.rogue { - background: var(--accent-red); - box-shadow: 0 0 8px rgba(226, 93, 93, 0.8); - animation: blink 1s infinite; -} - -@keyframes blink { - 0%, 50% { opacity: 1; } - 51%, 100% { opacity: 0.3; } -} - -.device-blip { - animation: pulse-blip 5s ease-out forwards; -} - -@keyframes pulse-blip { - 0% { - opacity: 1; - transform: scale(1); - } - 100% { - opacity: 0; - transform: scale(3); - } -} - -/* Controls bar */ -.controls-bar { - position: fixed; - bottom: 0; - left: 0; - right: 0; - height: 80px; - background: var(--bg-panel); - border-top: 1px solid var(--border-color); - display: flex; - gap: 20px; - padding: 15px 20px; - align-items: center; - z-index: 99; -} - -.control-group { - display: flex; - flex-direction: column; - gap: 5px; -} - -.control-group-label { - font-size: 9px; - color: var(--text-secondary); - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.control-group-items { - display: flex; - gap: 10px; - align-items: center; -} - -/* Input fields */ -input[type="text"], input[type="number"], select { - background: var(--bg-dark); - color: var(--text-primary); - border: 1px solid var(--border-color); - border-radius: 3px; - padding: 8px 10px; - font-family: var(--font-mono); - font-size: 11px; - min-width: 120px; -} - -input[type="text"]:focus, input[type="number"]:focus, select:focus { - outline: none; - border-color: var(--accent-cyan); -} - -/* Buttons */ -button { - background: var(--accent-cyan); - color: white; - border: none; - padding: 8px 16px; - border-radius: 4px; - cursor: pointer; - font-family: var(--font-mono); - font-size: 12px; - font-weight: 600; - transition: all 0.2s; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -button:hover { - opacity: 0.8; - transform: translateY(-1px); -} - -button:active { - transform: translateY(0); -} - -button.active { - background: var(--accent-red); - animation: pulse-btn 2s ease-in-out infinite; -} - -@keyframes pulse-btn { - 0%, 100% { box-shadow: 0 0 0 0 rgba(226, 93, 93, 0.7); } - 50% { box-shadow: 0 0 0 10px rgba(226, 93, 93, 0); } -} - -button:disabled { - background: var(--text-dim); - cursor: not-allowed; - opacity: 0.5; -} - -/* GPS indicator */ -.gps-indicator { - display: inline-flex; - align-items: center; - gap: 5px; - padding: 6px 12px; - background: var(--bg-dark); - border: 1px solid var(--border-color); - border-radius: 3px; - font-size: 10px; - color: var(--text-secondary); -} - -.gps-indicator::before { - content: ''; - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--text-dim); -} - -.gps-indicator.active::before { - background: var(--accent-green); - animation: pulse-dot 2s ease-in-out infinite; -} - -/* Scrollbar styling */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: var(--bg-dark); -} - -::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 4px; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--text-dim); -} - -/* Empty state */ -.empty-state { - padding: 30px 20px; - text-align: center; - color: var(--text-dim); - font-size: 11px; -} - -/* Responsive adjustments */ -@media (max-width: 1400px) { - .dashboard { - grid-template-columns: 250px 1fr 280px; - } -} - -@media (max-width: 1024px) { - .dashboard { - grid-template-columns: 1fr; - grid-template-rows: auto 1fr auto; - } - - .left-sidebar, .right-sidebar { - flex-direction: row; - overflow-x: auto; - overflow-y: visible; - } - - .panel { - min-width: 250px; - } -} - -/* Utility classes */ -.text-success { color: var(--accent-green); } -.text-danger { color: var(--accent-red); } -.text-warning { color: var(--accent-yellow); } -.text-info { color: var(--accent-cyan); } -.text-muted { color: var(--text-secondary); } - -.mt-1 { margin-top: 8px; } -.mt-2 { margin-top: 16px; } -.mb-1 { margin-bottom: 8px; } -.mb-2 { margin-bottom: 16px; } - -/* Advanced Analysis Results Panel */ -.analysis-results { - border-top: 1px solid var(--border-color); - padding: 12px; - max-height: 300px; - overflow-y: auto; -} - -.analysis-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; - font-size: 11px; - font-weight: 700; - color: var(--accent-cyan); - text-transform: uppercase; -} - -.analysis-content { - font-size: 10px; - line-height: 1.6; -} - -.analysis-stat { - display: flex; - justify-content: space-between; - padding: 6px 0; - border-bottom: 1px solid rgba(255,255,255,0.05); -} - -.analysis-stat:last-child { - border-bottom: none; -} - -.analysis-stat-label { - color: var(--text-secondary); -} - -.analysis-stat-value { - color: var(--accent-green); - font-weight: 600; -} - -.analysis-device-item { - padding: 8px; - margin: 6px 0; - background: var(--bg-dark); - border-radius: 3px; - border-left: 3px solid var(--accent-cyan); -} - -.analysis-warning { - color: var(--accent-yellow); - font-size: 10px; - padding: 8px; - background: rgba(255, 165, 0, 0.1); - border-radius: 3px; - margin-top: 8px; -} diff --git a/templates/gsm_spy_dashboard.html b/templates/gsm_spy_dashboard.html index 4382547..dca7ae4 100644 --- a/templates/gsm_spy_dashboard.html +++ b/templates/gsm_spy_dashboard.html @@ -1657,11 +1657,13 @@ } // ============================================ - // GSM ICON DEFINITIONS + // GSM ICON DEFINITIONS - High Quality Vector Icons // ============================================ const GSM_ICONS = { - tower: 'M12 2L11 3v5h2V3l-1-1zm-1 6v2H9v2h2v2H9v2h2v2H9v2h6v-2h-2v-2h2v-2h-2v-2h2v-2h-2V8h-2zm-3 4H6v8h2v-8zm8 0h-2v8h2v-8zM5 14H3v6h2v-6zm14 0h-2v6h2v-6z', - device: 'M7 2v20h10V2H7zm2 2h6v12H9V4zm0 14h6v2H9v-2z' + // Cell tower icon with detailed antenna structure + tower: 'M12 1L10.5 2.5V7H8V9H10V11H8V13H10V15H8V17H10V19H8V21H16V19H14V17H16V15H14V13H16V11H14V9H16V7H13.5V2.5L12 1M12 3.5L12.5 4V7H11.5V4L12 3.5M7 9H5V21H7V9M19 9H17V21H19V9M4 11H2V21H4V11M22 11H20V21H22V11M3 13H1V21H3V13M23 13H21V21H23V13Z', + // Smartphone icon with detailed screen and body + device: 'M17 1H7C5.89 1 5 1.89 5 3V21C5 22.1 5.9 23 7 23H17C18.1 23 19 22.1 19 21V3C19 1.89 18.1 1 17 1M17 19H7V5H17V19M12 21C11.45 21 11 20.55 11 20C11 19.45 11.45 19 12 19C12.55 19 13 19.45 13 20C13 20.55 12.55 21 12 21Z' }; // Create marker icon with SVG