Fixes regarding for PR #124, also added vector images for towers and phones

This commit is contained in:
Marc
2026-02-08 03:23:23 -06:00
parent 297f971bd5
commit 44b1a74838
5 changed files with 122 additions and 839 deletions
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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)
+115 -119
View File
@@ -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)
-715
View File
@@ -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;
}
+5 -3
View File
@@ -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