mirror of
https://github.com/smittix/intercept.git
synced 2026-06-10 15:03:31 -07:00
Fixes regarding for PR #124, also added vector images for towers and phones
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user