mirror of
https://github.com/smittix/intercept.git
synced 2026-06-15 17:11:56 -07:00
82830c86ac
- Fix scannerStatus -> scannerStatusText element reference - Hide global signal meter (individual panels show signal) - Expand Bluetooth device classification patterns - Add more audio, phone, wearable, computer patterns - Add manufacturer-based device type inference Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
10174 lines
504 KiB
HTML
10174 lines
504 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>INTERCEPT // Signal Intelligence</title>
|
||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||
<!-- Leaflet.js for aircraft map -->
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin=""/>
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||
<!-- Leaflet MarkerCluster for aircraft clustering -->
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css"/>
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css"/>
|
||
<script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"></script>
|
||
<!-- Chart.js for signal strength graphs -->
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
|
||
</head>
|
||
<body>
|
||
<!-- Disclaimer Modal -->
|
||
<div class="disclaimer-overlay" id="disclaimerModal">
|
||
<div class="disclaimer-modal">
|
||
<div class="warning-icon">⚠️</div>
|
||
<h2>DISCLAIMER</h2>
|
||
<p>
|
||
<strong>INTERCEPT</strong> is a signal intelligence tool designed for <strong>educational purposes only</strong>.
|
||
</p>
|
||
<p>By using this software, you acknowledge and agree that:</p>
|
||
<ul>
|
||
<li>This tool is intended for use by <strong>cyber security professionals</strong> and researchers only</li>
|
||
<li>You will only use this software in a <strong>controlled environment</strong> with proper authorization</li>
|
||
<li>Intercepting communications without consent may be <strong>illegal</strong> in your jurisdiction</li>
|
||
<li>You are solely responsible for ensuring compliance with all applicable laws and regulations</li>
|
||
<li>The developers assume no liability for misuse of this software</li>
|
||
</ul>
|
||
<p style="color: var(--accent-red); font-weight: bold;">
|
||
Only proceed if you understand and accept these terms.
|
||
</p>
|
||
<div style="display: flex; gap: 15px; justify-content: center; margin-top: 20px;">
|
||
<button class="accept-btn" onclick="acceptDisclaimer()">I UNDERSTAND & ACCEPT</button>
|
||
<button class="accept-btn" onclick="declineDisclaimer()" style="background: transparent; border: 1px solid var(--accent-red); color: var(--accent-red);">DECLINE</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Rejection Page -->
|
||
<div class="disclaimer-overlay disclaimer-hidden" id="rejectionPage">
|
||
<div class="disclaimer-modal" style="max-width: 600px;">
|
||
<pre style="color: var(--accent-red); font-size: 9px; line-height: 1.1; margin-bottom: 20px; text-align: center;">
|
||
█████╗ ██████╗ ██████╗███████╗███████╗███████╗
|
||
██╔══██╗██╔════╝██╔════╝██╔════╝██╔════╝██╔════╝
|
||
███████║██║ ██║ █████╗ ███████╗███████╗
|
||
██╔══██║██║ ██║ ██╔══╝ ╚════██║╚════██║
|
||
██║ ██║╚██████╗╚██████╗███████╗███████║███████║
|
||
╚═╝ ╚═╝ ╚═════╝ ╚═════╝╚══════╝╚══════╝╚══════╝
|
||
██████╗ ███████╗███╗ ██╗██╗███████╗██████╗
|
||
██╔══██╗██╔════╝████╗ ██║██║██╔════╝██╔══██╗
|
||
██║ ██║█████╗ ██╔██╗ ██║██║█████╗ ██║ ██║
|
||
██║ ██║██╔══╝ ██║╚██╗██║██║██╔══╝ ██║ ██║
|
||
██████╔╝███████╗██║ ╚████║██║███████╗██████╔╝
|
||
╚═════╝ ╚══════╝╚═╝ ╚═══╝╚═╝╚══════╝╚═════╝</pre>
|
||
<div style="margin: 25px 0; padding: 15px; background: #0a0a0a; border-left: 3px solid var(--accent-red);">
|
||
<p style="font-family: 'JetBrains Mono', monospace; font-size: 11px; color: #888; text-align: left; margin: 0;">
|
||
<span style="color: var(--accent-red);">root@intercepted:</span><span style="color: var(--accent-cyan);">~#</span> sudo access --grant-permission<br>
|
||
<span style="color: #666;">[sudo] password for user: ********</span><br>
|
||
<span style="color: var(--accent-red);">Error:</span> User is not in the sudoers file.<br>
|
||
<span style="color: var(--accent-orange);">This incident will be reported.</span>
|
||
</p>
|
||
</div>
|
||
<p style="color: #666; font-size: 11px; text-align: center;">
|
||
"In a world of locked doors, the man with the key is king.<br>
|
||
And you, my friend, just threw away the key."
|
||
</p>
|
||
<button class="accept-btn" onclick="location.reload()" style="margin-top: 20px; background: transparent; border: 1px solid var(--accent-cyan); color: var(--accent-cyan);">
|
||
TRY AGAIN
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<header>
|
||
<!-- UTC Clock -->
|
||
<div class="header-clock">
|
||
<span class="utc-label">UTC</span>
|
||
<span class="utc-time" id="headerUtcTime">--:--:--</span>
|
||
</div>
|
||
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle Light/Dark Theme">
|
||
<span class="icon-moon">🌙</span>
|
||
<span class="icon-sun">☀️</span>
|
||
</button>
|
||
<button class="help-btn" onclick="showDependencies()" title="Check Tool Dependencies" id="depsBtn" style="margin-right: 5px;">🔧</button>
|
||
<button class="help-btn" onclick="showHelp()" title="Help & Documentation">?</button>
|
||
<div class="logo">
|
||
<svg width="50" height="50" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<!-- Outer hexagon -->
|
||
<path d="M50 5 L90 27.5 L90 72.5 L50 95 L10 72.5 L10 27.5 Z" stroke="#00d4ff" stroke-width="2" fill="none"/>
|
||
<!-- Inner signal waves -->
|
||
<path d="M30 50 Q40 35, 50 50 Q60 65, 70 50" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||
<path d="M35 50 Q42 40, 50 50 Q58 60, 65 50" stroke="#00ff88" stroke-width="2" fill="none" stroke-linecap="round"/>
|
||
<path d="M40 50 Q45 45, 50 50 Q55 55, 60 50" stroke="#ffffff" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
||
<!-- Center dot -->
|
||
<circle cx="50" cy="50" r="3" fill="#00d4ff"/>
|
||
<!-- Corner accents -->
|
||
<path d="M50 12 L55 17 L50 17 Z" fill="#00d4ff"/>
|
||
<path d="M50 88 L45 83 L50 83 Z" fill="#00d4ff"/>
|
||
</svg>
|
||
</div>
|
||
<h1>INTERCEPT <span class="version-badge">v{{ version }}</span></h1>
|
||
<p>Signal Intelligence // by smittix <span class="active-mode-indicator" id="activeModeIndicator"><span class="pulse-dot"></span>PAGER</span></p>
|
||
|
||
<!-- Header Stats (mode-specific) -->
|
||
<div class="header-stats">
|
||
<!-- Pager Stats -->
|
||
<div class="header-stats-group active" id="headerPagerStats">
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">📨</span>
|
||
<div>
|
||
<span class="badge-value" id="headerMsgCount">0</span>
|
||
<span class="badge-label">messages</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">📟</span>
|
||
<div>
|
||
<span class="badge-value" id="headerPocsagCount">0</span>
|
||
<span class="badge-label">POCSAG</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">📠</span>
|
||
<div>
|
||
<span class="badge-value" id="headerFlexCount">0</span>
|
||
<span class="badge-label">FLEX</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 433MHz Sensor Stats -->
|
||
<div class="header-stats-group" id="headerSensorStats">
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">🌡️</span>
|
||
<div>
|
||
<span class="badge-value" id="headerSensorCount">0</span>
|
||
<span class="badge-label">sensors</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">📊</span>
|
||
<div>
|
||
<span class="badge-value" id="headerDeviceTypeCount">0</span>
|
||
<span class="badge-label">types</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- WiFi Stats -->
|
||
<div class="header-stats-group" id="headerWifiStats">
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">📡</span>
|
||
<div>
|
||
<span class="badge-value" id="headerApCount">0</span>
|
||
<span class="badge-label">networks</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">👤</span>
|
||
<div>
|
||
<span class="badge-value" id="headerClientCount">0</span>
|
||
<span class="badge-label">clients</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">🤝</span>
|
||
<div>
|
||
<span class="badge-value highlight" id="headerHandshakeCount">0</span>
|
||
<span class="badge-label">handshakes</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">🚁</span>
|
||
<div>
|
||
<span class="badge-value warning" id="headerDroneCount">0</span>
|
||
<span class="badge-label">drones</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bluetooth Stats -->
|
||
<div class="header-stats-group" id="headerBtStats">
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">🔵</span>
|
||
<div>
|
||
<span class="badge-value" id="headerBtDeviceCount">0</span>
|
||
<span class="badge-label">devices</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">📍</span>
|
||
<div>
|
||
<span class="badge-value" id="headerBtBeaconCount">0</span>
|
||
<span class="badge-label">beacons</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Aircraft Stats -->
|
||
<div class="header-stats-group" id="headerAircraftStats">
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">✈️</span>
|
||
<div>
|
||
<span class="badge-value" id="headerAircraftCount">0</span>
|
||
<span class="badge-label">aircraft</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">📨</span>
|
||
<div>
|
||
<span class="badge-value" id="headerAdsbMsgCount">0</span>
|
||
<span class="badge-label">messages</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">🔢</span>
|
||
<div>
|
||
<span class="badge-value" id="headerIcaoCount">0</span>
|
||
<span class="badge-label">ICAO</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Satellite Stats -->
|
||
<div class="header-stats-group" id="headerSatelliteStats">
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">🛰️</span>
|
||
<div>
|
||
<span class="badge-value" id="headerPassCount">0</span>
|
||
<span class="badge-label">passes</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-badge">
|
||
<span class="badge-icon">📡</span>
|
||
<div>
|
||
<span class="badge-value" id="headerBurstCount">0</span>
|
||
<span class="badge-label">bursts</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Mode Navigation Bar -->
|
||
<nav class="mode-nav">
|
||
<div class="mode-nav-group">
|
||
<span class="mode-nav-label">SDR / RF</span>
|
||
<button class="mode-nav-btn active" onclick="switchMode('pager')"><span class="nav-icon">📟</span><span class="nav-label">Pager</span></button>
|
||
<button class="mode-nav-btn" onclick="switchMode('sensor')"><span class="nav-icon">📡</span><span class="nav-label">433MHz</span></button>
|
||
<button class="mode-nav-btn" onclick="switchMode('aircraft')"><span class="nav-icon">✈️</span><span class="nav-label">Aircraft</span></button>
|
||
<button class="mode-nav-btn" onclick="switchMode('satellite')"><span class="nav-icon">🛰️</span><span class="nav-label">Satellite</span></button>
|
||
<button class="mode-nav-btn" onclick="switchMode('listening')"><span class="nav-icon">📻</span><span class="nav-label">Listening Post</span></button>
|
||
</div>
|
||
<div class="mode-nav-divider"></div>
|
||
<div class="mode-nav-group">
|
||
<span class="mode-nav-label">Wireless</span>
|
||
<button class="mode-nav-btn" onclick="switchMode('wifi')"><span class="nav-icon">📶</span><span class="nav-label">WiFi</span></button>
|
||
<button class="mode-nav-btn" onclick="switchMode('bluetooth')"><span class="nav-icon">🔵</span><span class="nav-label">Bluetooth</span></button>
|
||
</div>
|
||
<div class="mode-nav-actions">
|
||
<a href="/adsb/dashboard" target="_blank" class="nav-action-btn" id="adsbDashboardBtn" style="display: none;">
|
||
<span class="nav-icon">🖥️</span><span class="nav-label">Full Dashboard</span>
|
||
</a>
|
||
<a href="/satellite/dashboard" target="_blank" class="nav-action-btn" id="satelliteDashboardBtn" style="display: none;">
|
||
<span class="nav-icon">🖥️</span><span class="nav-label">Full Dashboard</span>
|
||
</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="container">
|
||
<div class="main-content">
|
||
<div class="sidebar">
|
||
<div class="section" id="rtlDeviceSection">
|
||
<h3>SDR Device</h3>
|
||
<div class="form-group">
|
||
<label style="font-size: 11px; color: #888; margin-bottom: 4px;">Hardware Type</label>
|
||
<select id="sdrTypeSelect" onchange="onSDRTypeChanged()">
|
||
<option value="rtlsdr">RTL-SDR</option>
|
||
<option value="limesdr">LimeSDR</option>
|
||
<option value="hackrf">HackRF</option>
|
||
<option value="airspy">Airspy</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label style="font-size: 11px; color: #888; margin-bottom: 4px;">Device</label>
|
||
<select id="deviceSelect">
|
||
{% if devices %}
|
||
{% for device in devices %}
|
||
<option value="{{ device.index }}" data-sdr-type="{{ device.sdr_type | default('rtlsdr') }}">{{ device.index }}: {{ device.name }}</option>
|
||
{% endfor %}
|
||
{% else %}
|
||
<option value="0">No devices found</option>
|
||
{% endif %}
|
||
</select>
|
||
</div>
|
||
<div id="deviceCapabilities" class="info-text" style="font-size: 11px; margin-bottom: 8px; padding: 6px; background: #0a0a1a; border-radius: 4px;">
|
||
<div style="display: grid; grid-template-columns: auto 1fr; gap: 2px 8px;">
|
||
<span style="color: #888;">Freq:</span><span id="capFreqRange">24-1766 MHz</span>
|
||
<span style="color: #888;">Gain:</span><span id="capGainRange">0-50 dB</span>
|
||
</div>
|
||
</div>
|
||
<button class="preset-btn" onclick="refreshDevices()" style="width: 100%;">
|
||
Refresh Devices
|
||
</button>
|
||
|
||
<!-- Remote SDR (rtl_tcp) -->
|
||
<div class="form-group" style="margin-top: 10px;">
|
||
<label class="inline-checkbox">
|
||
<input type="checkbox" id="useRemoteSDR" onchange="toggleRemoteSDR()">
|
||
Use Remote SDR (rtl_tcp)
|
||
</label>
|
||
</div>
|
||
<div id="remoteSDRConfig" style="display: none; margin-bottom: 10px;">
|
||
<div class="form-group">
|
||
<label style="font-size: 11px; color: #888;">Host</label>
|
||
<input type="text" id="rtlTcpHost" placeholder="192.168.1.100" style="width: 100%;">
|
||
</div>
|
||
<div class="form-group">
|
||
<label style="font-size: 11px; color: #888;">Port</label>
|
||
<input type="number" id="rtlTcpPort" value="1234" min="1" max="65535" style="width: 100%;">
|
||
</div>
|
||
<div class="info-text" style="font-size: 10px; color: #666; margin-top: 4px;">
|
||
Connect to rtl_tcp server on remote machine.<br>
|
||
Start server with: <code style="color: #00d4ff;">rtl_tcp -a 0.0.0.0</code>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Global SDR Power Settings -->
|
||
<div class="section" style="padding: 8px; margin-bottom: 10px; background: rgba(0, 212, 255, 0.05); border-radius: 4px;">
|
||
<div class="checkbox-group" style="margin: 0;">
|
||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||
<input type="checkbox" id="biasT" onchange="saveBiasTSetting()">
|
||
<span style="color: var(--accent-cyan);">Bias-T Power</span>
|
||
<span style="font-size: 10px; color: #666;">(LNA/Preamp)</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="toolStatusPager" class="info-text tool-status-section" style="display: grid; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;">
|
||
<span>rtl_fm:</span><span class="tool-status {{ 'ok' if tools.rtl_fm else 'missing' }}">{{ 'OK' if tools.rtl_fm else 'Missing' }}</span>
|
||
<span>multimon-ng:</span><span class="tool-status {{ 'ok' if tools.multimon else 'missing' }}">{{ 'OK' if tools.multimon else 'Missing' }}</span>
|
||
</div>
|
||
<div id="toolStatusSensor" class="info-text tool-status-section" style="display: none; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;">
|
||
<span>rtl_433:</span><span class="tool-status {{ 'ok' if tools.rtl_433 else 'missing' }}">{{ 'OK' if tools.rtl_433 else 'Missing' }}</span>
|
||
</div>
|
||
<div id="toolStatusAircraft" class="info-text tool-status-section" style="display: none; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;">
|
||
<span>dump1090:</span><span id="dump1090StatusSidebar" class="tool-status">Checking...</span>
|
||
<span>rtl_adsb:</span><span id="rtlAdsbStatusSidebar" class="tool-status">Checking...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PAGER MODE -->
|
||
<div id="pagerMode" class="mode-content active">
|
||
<div class="section">
|
||
<h3>Frequency</h3>
|
||
<div class="form-group">
|
||
<label>Frequency (MHz)</label>
|
||
<input type="text" id="frequency" value="153.350" placeholder="e.g., 153.350">
|
||
</div>
|
||
<div class="preset-buttons" id="presetButtons">
|
||
<!-- Populated by JavaScript -->
|
||
</div>
|
||
<div style="margin-top: 8px; display: flex; gap: 5px;">
|
||
<input type="text" id="newPresetFreq" placeholder="New freq (MHz)" style="flex: 1; padding: 6px; background: #0f3460; border: 1px solid #1a1a2e; color: #fff; border-radius: 4px; font-size: 12px;">
|
||
<button class="preset-btn" onclick="addPreset()" style="background: #2ecc71;">Add</button>
|
||
</div>
|
||
<div style="margin-top: 5px;">
|
||
<button class="preset-btn" onclick="resetPresets()" style="font-size: 11px;">Reset to Defaults</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Protocols</h3>
|
||
<div class="checkbox-group">
|
||
<label><input type="checkbox" id="proto_pocsag512" checked> POCSAG-512</label>
|
||
<label><input type="checkbox" id="proto_pocsag1200" checked> POCSAG-1200</label>
|
||
<label><input type="checkbox" id="proto_pocsag2400" checked> POCSAG-2400</label>
|
||
<label><input type="checkbox" id="proto_flex" checked> FLEX</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Settings</h3>
|
||
<div class="form-group">
|
||
<label>Gain (dB, 0 = auto)</label>
|
||
<input type="text" id="gain" value="0" placeholder="0-49 or 0 for auto">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Squelch Level</label>
|
||
<input type="text" id="squelch" value="0" placeholder="0 = off">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>PPM Correction</label>
|
||
<input type="text" id="ppm" value="0" placeholder="Frequency correction">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Logging</h3>
|
||
<div class="checkbox-group" style="margin-bottom: 15px;">
|
||
<label>
|
||
<input type="checkbox" id="loggingEnabled" onchange="toggleLogging()">
|
||
Enable Logging
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Log file path</label>
|
||
<input type="text" id="logFilePath" value="pager_messages.log" placeholder="pager_messages.log">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Message Filters</h3>
|
||
<div class="checkbox-group" style="margin-bottom: 10px;">
|
||
<label>
|
||
<input type="checkbox" id="filterToneOnly" checked onchange="savePagerFilters()">
|
||
Hide "Tone Only" messages
|
||
</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Hide messages containing (comma-separated)</label>
|
||
<input type="text" id="filterKeywords" placeholder="e.g. test, spam, alert" onchange="savePagerFilters()">
|
||
</div>
|
||
<div class="info-text" style="font-size: 10px; color: #666; margin-top: 5px;">
|
||
Messages matching these keywords will be hidden from display but still logged.
|
||
</div>
|
||
</div>
|
||
|
||
<button class="run-btn" id="startBtn" onclick="startDecoding()">
|
||
Start Decoding
|
||
</button>
|
||
<button class="stop-btn" id="stopBtn" onclick="stopDecoding()" style="display: none;">
|
||
Stop Decoding
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 433MHz SENSOR MODE -->
|
||
<div id="sensorMode" class="mode-content">
|
||
<div class="section">
|
||
<h3>Frequency</h3>
|
||
<div class="form-group">
|
||
<label>Frequency (MHz)</label>
|
||
<input type="text" id="sensorFrequency" value="433.92" placeholder="e.g., 433.92">
|
||
</div>
|
||
<div class="preset-buttons">
|
||
<button class="preset-btn" onclick="setSensorFreq('433.92')">433.92</button>
|
||
<button class="preset-btn" onclick="setSensorFreq('315.00')">315.00</button>
|
||
<button class="preset-btn" onclick="setSensorFreq('868.00')">868.00</button>
|
||
<button class="preset-btn" onclick="setSensorFreq('915.00')">915.00</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Settings</h3>
|
||
<div class="form-group">
|
||
<label>Gain (dB, 0 = auto)</label>
|
||
<input type="text" id="sensorGain" value="0" placeholder="0-49 or 0 for auto">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>PPM Correction</label>
|
||
<input type="text" id="sensorPpm" value="0" placeholder="Frequency correction">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Protocols</h3>
|
||
<div class="info-text" style="margin-bottom: 10px;">
|
||
rtl_433 auto-detects 200+ device protocols including weather stations, TPMS, doorbells, and more.
|
||
</div>
|
||
<div class="checkbox-group">
|
||
<label>
|
||
<input type="checkbox" id="sensorLogging" onchange="toggleSensorLogging()">
|
||
Enable Logging
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="run-btn" id="startSensorBtn" onclick="startSensorDecoding()">
|
||
Start Listening
|
||
</button>
|
||
<button class="stop-btn" id="stopSensorBtn" onclick="stopSensorDecoding()" style="display: none;">
|
||
Stop Listening
|
||
</button>
|
||
</div>
|
||
|
||
<!-- WiFi MODE -->
|
||
<div id="wifiMode" class="mode-content">
|
||
<div class="section">
|
||
<h3>WiFi Adapter</h3>
|
||
<div class="form-group">
|
||
<label>Select Device</label>
|
||
<select id="wifiInterfaceSelect" style="font-size: 12px;">
|
||
<option value="">Detecting interfaces...</option>
|
||
</select>
|
||
</div>
|
||
<button class="preset-btn" onclick="refreshWifiInterfaces()" style="width: 100%;">
|
||
🔄 Refresh Devices
|
||
</button>
|
||
<div class="info-text" style="margin-top: 8px; display: grid; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;" id="wifiToolStatus">
|
||
<span>airmon-ng:</span><span class="tool-status missing">Checking...</span>
|
||
<span>airodump-ng:</span><span class="tool-status missing">Checking...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Monitor Mode</h3>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button class="preset-btn" id="monitorStartBtn" onclick="enableMonitorMode()" style="flex: 1; background: var(--accent-green); color: #000;">
|
||
Enable Monitor
|
||
</button>
|
||
<button class="preset-btn" id="monitorStopBtn" onclick="disableMonitorMode()" style="flex: 1; display: none;">
|
||
Disable Monitor
|
||
</button>
|
||
</div>
|
||
<div class="checkbox-group" style="margin-top: 8px;">
|
||
<label style="font-size: 10px;">
|
||
<input type="checkbox" id="killProcesses">
|
||
Kill interfering processes (may drop other connections)
|
||
</label>
|
||
</div>
|
||
<div id="monitorStatus" class="info-text" style="margin-top: 8px;">
|
||
Monitor mode: <span style="color: var(--accent-red);">Inactive</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Scan Settings</h3>
|
||
<div class="form-group">
|
||
<label>Band</label>
|
||
<select id="wifiBand">
|
||
<option value="abg">All (2.4 + 5 GHz)</option>
|
||
<option value="bg">2.4 GHz only</option>
|
||
<option value="a">5 GHz only</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Channel (empty = hop)</label>
|
||
<input type="text" id="wifiChannel" placeholder="e.g., 6 or 36">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Proximity Alerts</h3>
|
||
<div class="info-text" style="margin-bottom: 8px;">
|
||
Alert when specific MAC addresses appear
|
||
</div>
|
||
<div class="form-group">
|
||
<input type="text" id="watchMacInput" placeholder="AA:BB:CC:DD:EE:FF">
|
||
</div>
|
||
<button class="preset-btn" onclick="addWatchMac()" style="width: 100%; margin-bottom: 8px;">
|
||
Add to Watch List
|
||
</button>
|
||
<div id="watchList" style="max-height: 80px; overflow-y: auto; font-size: 10px; color: var(--text-dim);"></div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Attack Options</h3>
|
||
<div class="info-text" style="color: var(--accent-red); margin-bottom: 10px;">
|
||
⚠ Only use on authorized networks
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Target BSSID</label>
|
||
<input type="text" id="targetBssid" placeholder="AA:BB:CC:DD:EE:FF">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Target Client (optional)</label>
|
||
<input type="text" id="targetClient" placeholder="FF:FF:FF:FF:FF:FF (broadcast)">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Deauth Count</label>
|
||
<input type="text" id="deauthCount" value="5" placeholder="5">
|
||
</div>
|
||
<button class="preset-btn" onclick="sendDeauth()" style="width: 100%; border-color: var(--accent-red); color: var(--accent-red);">
|
||
Send Deauth
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Handshake Capture Status Panel -->
|
||
<div class="section" id="captureStatusPanel" style="display: none; border: 1px solid var(--accent-orange); border-radius: 4px; padding: 10px; background: rgba(255, 165, 0, 0.1);">
|
||
<h3 style="color: var(--accent-orange); margin: 0 0 8px 0;">🎯 Handshake Capture</h3>
|
||
<div style="font-size: 11px;">
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: var(--text-dim);">Target:</span>
|
||
<span id="captureTargetBssid" style="font-family: monospace;">--</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: var(--text-dim);">Channel:</span>
|
||
<span id="captureTargetChannel">--</span>
|
||
</div>
|
||
<div style="margin-bottom: 4px;">
|
||
<span style="color: var(--text-dim);">File:</span>
|
||
<span id="captureFilePath" style="font-size: 9px; word-break: break-all;">--</span>
|
||
</div>
|
||
<div style="margin-bottom: 8px;">
|
||
<span style="color: var(--text-dim);">Status:</span>
|
||
<span id="captureStatus" style="font-weight: bold;">--</span>
|
||
</div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button class="preset-btn" onclick="checkCaptureStatus()" style="flex: 1; font-size: 10px; padding: 4px;">
|
||
Check Status
|
||
</button>
|
||
<button class="preset-btn" onclick="stopHandshakeCapture()" style="flex: 1; font-size: 10px; padding: 4px; border-color: var(--accent-red); color: var(--accent-red);">
|
||
Stop Capture
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Beacon Flood Alert Panel -->
|
||
<div id="beaconFloodAlert" class="beacon-flood-alert" style="display: none;">
|
||
<h4 style="color: #ff4444; margin: 0 0 8px 0;">⚠️ BEACON FLOOD DETECTED</h4>
|
||
<div style="font-size: 11px;">
|
||
<div id="beaconFloodDetails">Multiple beacon frames detected from same channel</div>
|
||
<div style="margin-top: 8px;">
|
||
<span style="color: var(--text-dim);">Networks/sec:</span>
|
||
<span id="beaconFloodRate" style="font-weight: bold; color: #ff4444;">--</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="run-btn" id="startWifiBtn" onclick="startWifiScan()">
|
||
Start Scanning
|
||
</button>
|
||
<button class="stop-btn" id="stopWifiBtn" onclick="stopWifiScan()" style="display: none;">
|
||
Stop Scanning
|
||
</button>
|
||
</div>
|
||
|
||
<!-- BLUETOOTH MODE -->
|
||
<div id="bluetoothMode" class="mode-content">
|
||
<div class="section">
|
||
<h3>Bluetooth Interface</h3>
|
||
<div class="form-group">
|
||
<select id="btInterfaceSelect">
|
||
<option value="">Detecting interfaces...</option>
|
||
</select>
|
||
</div>
|
||
<button class="preset-btn" onclick="refreshBtInterfaces()" style="width: 100%;">
|
||
Refresh Interfaces
|
||
</button>
|
||
<div class="info-text" style="margin-top: 8px; display: grid; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;" id="btToolStatus">
|
||
<span>hcitool:</span><span class="tool-status missing">Checking...</span>
|
||
<span>bluetoothctl:</span><span class="tool-status missing">Checking...</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Scan Mode</h3>
|
||
<div class="checkbox-group" style="margin-bottom: 10px;">
|
||
<label><input type="radio" name="btScanMode" value="bluetoothctl" checked> bluetoothctl (Recommended)</label>
|
||
<label><input type="radio" name="btScanMode" value="hcitool"> hcitool (Legacy)</label>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Scan Duration (sec)</label>
|
||
<input type="text" id="btScanDuration" value="30" placeholder="30">
|
||
</div>
|
||
<div class="checkbox-group">
|
||
<label>
|
||
<input type="checkbox" id="btScanBLE" checked>
|
||
Scan BLE Devices
|
||
</label>
|
||
<label>
|
||
<input type="checkbox" id="btScanClassic" checked>
|
||
Scan Classic BT
|
||
</label>
|
||
<label>
|
||
<input type="checkbox" id="btDetectBeacons" checked>
|
||
Detect Trackers (AirTag/Tile)
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Device Actions</h3>
|
||
<div class="form-group">
|
||
<label>Target MAC</label>
|
||
<input type="text" id="btTargetMac" placeholder="AA:BB:CC:DD:EE:FF">
|
||
</div>
|
||
<button class="preset-btn" onclick="btEnumServices()" style="width: 100%;">
|
||
Enumerate Services
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Tracker Following Alert -->
|
||
<div id="trackerFollowingAlert" class="tracker-following-alert" style="display: none;">
|
||
<!-- Populated by JavaScript -->
|
||
</div>
|
||
|
||
<button class="run-btn" id="startBtBtn" onclick="startBtScan()">
|
||
Start Scanning
|
||
</button>
|
||
<button class="stop-btn" id="stopBtBtn" onclick="stopBtScan()" style="display: none;">
|
||
Stop Scanning
|
||
</button>
|
||
<button class="preset-btn" onclick="resetBtAdapter()" style="margin-top: 5px; width: 100%;">
|
||
Reset Adapter
|
||
</button>
|
||
</div>
|
||
|
||
<!-- AIRCRAFT MODE (ADS-B) -->
|
||
<div id="aircraftMode" class="mode-content">
|
||
<div class="section">
|
||
<h3>ADS-B Receiver</h3>
|
||
<div class="form-group">
|
||
<label>Frequency</label>
|
||
<input type="text" id="adsbFrequency" value="1090" readonly style="opacity: 0.7;">
|
||
<div class="info-text">Fixed at 1090 MHz</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Gain (dB)</label>
|
||
<input type="text" id="adsbGain" value="40" placeholder="40">
|
||
</div>
|
||
<div class="checkbox-group">
|
||
<label>
|
||
<input type="checkbox" id="adsbEnableMap" checked onchange="toggleAircraftRadar()">
|
||
Show Radar Display
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Display Settings</h3>
|
||
<div class="form-group">
|
||
<label>Range (nm)</label>
|
||
<select id="adsbRange">
|
||
<option value="50">50 nm</option>
|
||
<option value="100" selected>100 nm</option>
|
||
<option value="200">200 nm</option>
|
||
<option value="500">500 nm</option>
|
||
</select>
|
||
</div>
|
||
<div class="checkbox-group">
|
||
<label>
|
||
<input type="checkbox" id="adsbShowLabels" checked>
|
||
Show Callsigns
|
||
</label>
|
||
<label>
|
||
<input type="checkbox" id="adsbShowAltitude" checked>
|
||
Show Altitude
|
||
</label>
|
||
<label>
|
||
<input type="checkbox" id="adsbShowTrails">
|
||
Show Flight Trails
|
||
</label>
|
||
<label>
|
||
<input type="checkbox" id="adsbEnableClustering" onchange="toggleAircraftClustering()">
|
||
Cluster Markers
|
||
</label>
|
||
<label>
|
||
<input type="checkbox" id="adsbShowRangeRings" checked onchange="drawRangeRings()">
|
||
Show Range Rings
|
||
</label>
|
||
</div>
|
||
<div class="form-group" style="margin-top: 10px;">
|
||
<label style="display: flex; align-items: center; gap: 8px;">
|
||
Observer Location
|
||
<span id="adsbGpsIndicator" class="gps-indicator" style="display: none;" title="GPS connected via gpsd">
|
||
<span class="gps-dot"></span> GPS
|
||
</span>
|
||
</label>
|
||
<div style="display: flex; gap: 5px;">
|
||
<input type="text" id="adsbObsLat" value="51.5074" placeholder="Latitude" style="flex: 1;" onchange="updateObserverLocation()">
|
||
<input type="text" id="adsbObsLon" value="-0.1278" placeholder="Longitude" style="flex: 1;" onchange="updateObserverLocation()">
|
||
</div>
|
||
<button class="preset-btn" id="adsbGeolocateBtn" onclick="getAdsbGeolocation()" style="width: 100%; margin-top: 5px;">
|
||
📍 Use Browser Location
|
||
</button>
|
||
</div>
|
||
<div class="form-group" style="margin-top: 10px;">
|
||
<label>Aircraft Filter</label>
|
||
<select id="adsbAircraftFilter" onchange="applyAircraftFilter()">
|
||
<option value="all">All Aircraft</option>
|
||
<option value="military">Military Only</option>
|
||
<option value="civil">Civil Only</option>
|
||
<option value="emergency">Emergency Only</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" style="margin-top: 10px;">
|
||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||
<input type="checkbox" id="adsbAlertToggle" checked onchange="adsbAlertsEnabled = this.checked">
|
||
🔔 Audio Alerts (Military/Emergency)
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Reception Statistics</h3>
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 11px;">
|
||
<div style="background: rgba(0,212,255,0.1); padding: 8px; border-radius: 4px; text-align: center;">
|
||
<div style="color: var(--text-secondary); font-size: 9px; text-transform: uppercase;">Max Range</div>
|
||
<div id="adsbMaxRange" style="color: var(--accent-cyan); font-size: 14px; font-weight: bold;">0.0 nm</div>
|
||
</div>
|
||
<div style="background: rgba(0,212,255,0.1); padding: 8px; border-radius: 4px; text-align: center;">
|
||
<div style="color: var(--text-secondary); font-size: 9px; text-transform: uppercase;">Total Seen</div>
|
||
<div id="adsbTotalSeen" style="color: var(--accent-cyan); font-size: 14px; font-weight: bold;">0</div>
|
||
</div>
|
||
<div style="background: rgba(0,212,255,0.1); padding: 8px; border-radius: 4px; text-align: center;">
|
||
<div style="color: var(--text-secondary); font-size: 9px; text-transform: uppercase;">Msg Rate</div>
|
||
<div id="adsbMsgRate" style="color: var(--accent-cyan); font-size: 14px; font-weight: bold;">0.0/s</div>
|
||
</div>
|
||
<div style="background: rgba(0,212,255,0.1); padding: 8px; border-radius: 4px; text-align: center;">
|
||
<div style="color: var(--text-secondary); font-size: 9px; text-transform: uppercase;">Busiest Hour</div>
|
||
<div id="adsbBusiestHour" style="color: var(--accent-cyan); font-size: 14px; font-weight: bold;">--</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-text" style="margin-top: 8px; display: grid; grid-template-columns: auto auto; gap: 4px 8px; align-items: center;" id="adsbToolStatus">
|
||
<span>dump1090:</span><span class="tool-status" id="dump1090Status">Checking...</span>
|
||
<span>rtl_adsb:</span><span class="tool-status" id="rtlAdsbStatus">Checking...</span>
|
||
</div>
|
||
|
||
<button class="run-btn" id="startAdsbBtn" onclick="startAdsbScan()">
|
||
Start Tracking
|
||
</button>
|
||
<button class="stop-btn" id="stopAdsbBtn" onclick="stopAdsbScan()" style="display: none;">
|
||
Stop Tracking
|
||
</button>
|
||
</div>
|
||
|
||
<!-- SATELLITE MODE -->
|
||
<div id="satelliteMode" class="mode-content">
|
||
<div class="satellite-tabs">
|
||
<button class="satellite-tab active">🛰️ Pass Predictor</button>
|
||
</div>
|
||
|
||
<!-- Pass Predictor Sub-tab -->
|
||
<div id="predictorTab" class="satellite-content active">
|
||
<div class="section">
|
||
<h3 style="display: flex; align-items: center; gap: 8px;">
|
||
Observer Location
|
||
<span id="satGpsIndicator" class="gps-indicator" style="display: none;" title="GPS connected via gpsd">
|
||
<span class="gps-dot"></span> GPS
|
||
</span>
|
||
</h3>
|
||
<div class="form-group">
|
||
<label>Latitude</label>
|
||
<input type="text" id="obsLat" value="51.5074" placeholder="51.5074">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Longitude</label>
|
||
<input type="text" id="obsLon" value="-0.1278" placeholder="-0.1278">
|
||
</div>
|
||
<button class="preset-btn" onclick="getLocation()" style="width: 100%;">
|
||
📍 Use Browser Location
|
||
</button>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Satellites to Track</h3>
|
||
<div id="satelliteList" class="satellite-list">
|
||
<!-- Dynamically populated -->
|
||
</div>
|
||
<div style="margin-top: 10px; display: flex; gap: 5px;">
|
||
<button class="preset-btn" onclick="showAddSatelliteModal()" style="flex: 1;">
|
||
➕ Add Satellite
|
||
</button>
|
||
<button class="preset-btn" onclick="fetchCelestrak()" style="flex: 1;">
|
||
🌐 Celestrak
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="section">
|
||
<h3>Prediction Settings</h3>
|
||
<div class="form-group">
|
||
<label>Time Range</label>
|
||
<select id="predictionHours">
|
||
<option value="12">12 hours</option>
|
||
<option value="24" selected>24 hours</option>
|
||
<option value="48">48 hours</option>
|
||
<option value="72">72 hours</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Min Elevation</label>
|
||
<select id="minElevation">
|
||
<option value="0">0° (All passes)</option>
|
||
<option value="10" selected>10° (Good)</option>
|
||
<option value="30">30° (Best)</option>
|
||
<option value="45">45° (Overhead)</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="run-btn" onclick="calculatePasses()">
|
||
Calculate Passes
|
||
</button>
|
||
<button class="preset-btn" onclick="updateTLE()" style="width: 100%; margin-top: 5px;">
|
||
🔄 Update TLE Data
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- LISTENING POST MODE -->
|
||
<div id="listeningPostMode" class="mode-content">
|
||
<div class="section">
|
||
<h3>Frequency Scanner</h3>
|
||
<div id="scannerToolsWarning" style="display: none; background: rgba(255, 100, 100, 0.1); border: 1px solid var(--accent-red); border-radius: 4px; padding: 10px; margin-bottom: 10px;">
|
||
<p style="color: var(--accent-red); margin: 0; font-size: 0.85em;">
|
||
<strong>Missing Dependencies:</strong><br>
|
||
<span id="scannerToolsWarningText"></span>
|
||
</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>SDR Device</label>
|
||
<select id="scannerDeviceSelect">
|
||
<option value="0">Device 0 (Default)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Band Preset</label>
|
||
<select id="scannerPreset" onchange="applyScannerPreset()">
|
||
<option value="custom">Custom Range</option>
|
||
<option value="fm">FM Broadcast (88-108 MHz)</option>
|
||
<option value="air" selected>Airband (118-137 MHz)</option>
|
||
<option value="marine">Marine VHF (156-163 MHz)</option>
|
||
<option value="amateur2m">Amateur 2m (144-148 MHz)</option>
|
||
<option value="pager">Pager Band (152-160 MHz)</option>
|
||
<option value="amateur70cm">Amateur 70cm (420-450 MHz)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Frequency Range (MHz)</label>
|
||
<div style="display: flex; gap: 5px; align-items: center;">
|
||
<input type="number" id="scannerStartFreq" value="118" step="0.1" style="flex: 1;" placeholder="Start">
|
||
<span>-</span>
|
||
<input type="number" id="scannerEndFreq" value="137" step="0.1" style="flex: 1;" placeholder="End">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Step Size (kHz)</label>
|
||
<select id="scannerStep">
|
||
<option value="12.5">12.5 kHz (Narrow)</option>
|
||
<option value="25" selected>25 kHz (Standard)</option>
|
||
<option value="50">50 kHz</option>
|
||
<option value="100">100 kHz</option>
|
||
<option value="200">200 kHz (FM Broadcast)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Modulation</label>
|
||
<select id="scannerModulation">
|
||
<option value="am" selected>AM (Airband)</option>
|
||
<option value="fm">Narrow FM</option>
|
||
<option value="wfm">Wide FM (Broadcast)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Squelch Level</label>
|
||
<input type="range" id="scannerSquelch" min="0" max="100" value="30" oninput="document.getElementById('scannerSquelchValue').textContent = this.value">
|
||
<span id="scannerSquelchValue">30</span>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Dwell Time (seconds)</label>
|
||
<select id="scannerDwell">
|
||
<option value="2">2s (Quick)</option>
|
||
<option value="5">5s (Short)</option>
|
||
<option value="10" selected>10s (Normal)</option>
|
||
<option value="30">30s (Extended)</option>
|
||
</select>
|
||
</div>
|
||
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
|
||
<button class="run-btn" id="scannerStartBtn" onclick="toggleScanner()" style="flex: 2;">
|
||
Start Scanner
|
||
</button>
|
||
<button class="preset-btn" id="scannerPauseBtn" onclick="pauseScanner()" style="flex: 1;" disabled>
|
||
⏸ Pause
|
||
</button>
|
||
</div>
|
||
<!-- Prominent frequency display -->
|
||
<div id="scannerFreqDisplay" style="background: rgba(0,0,0,0.4); border-radius: 8px; padding: 15px; margin: 10px 0; text-align: center;">
|
||
<div style="font-size: 10px; color: var(--text-muted); text-transform: uppercase; margin-bottom: 5px;">
|
||
<span id="scannerModeLabel">STOPPED</span>
|
||
</div>
|
||
<div id="scannerCurrentFreq" style="font-size: 28px; font-weight: bold; font-family: 'JetBrains Mono', monospace; color: var(--text-muted);">
|
||
---.--- MHz
|
||
</div>
|
||
<div id="scannerModLabel" style="font-size: 11px; color: var(--text-muted); margin-top: 5px;">
|
||
--
|
||
</div>
|
||
<!-- Progress bar for scan range -->
|
||
<div id="scannerProgress" style="display: none; margin-top: 10px;">
|
||
<div style="display: flex; justify-content: space-between; font-size: 9px; color: var(--text-muted);">
|
||
<span id="scannerRangeStart">88.0</span>
|
||
<span id="scannerRangeEnd">108.0</span>
|
||
</div>
|
||
<div style="background: rgba(255,255,255,0.1); height: 4px; border-radius: 2px; margin-top: 2px;">
|
||
<div id="scannerProgressBar" style="background: var(--accent-cyan); height: 100%; border-radius: 2px; width: 0%; transition: width 0.2s;"></div>
|
||
</div>
|
||
</div>
|
||
<!-- Signal level meter -->
|
||
<div id="scannerLevelMeter" style="display: none; margin-top: 10px;">
|
||
<div style="display: flex; justify-content: space-between; font-size: 9px; color: var(--text-muted);">
|
||
<span>Signal Level</span>
|
||
<span id="scannerLevelValue">0</span>
|
||
</div>
|
||
<div style="background: rgba(255,255,255,0.1); height: 8px; border-radius: 4px; margin-top: 2px; overflow: hidden;">
|
||
<div id="scannerLevelBar" style="background: var(--accent-green); height: 100%; border-radius: 4px; width: 0%; transition: width 0.1s, background 0.1s;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Signal detected panel -->
|
||
<div id="scannerSignalPanel" style="display: none; background: rgba(0, 255, 100, 0.1); border: 1px solid var(--accent-green); border-radius: 6px; padding: 10px; margin: 10px 0;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<div>
|
||
<div style="font-size: 11px; color: var(--accent-green); font-weight: bold;">SIGNAL DETECTED</div>
|
||
<div style="font-size: 10px; color: var(--text-secondary);">Audio streaming...</div>
|
||
</div>
|
||
<button class="preset-btn" id="scannerSkipBtn" onclick="skipSignal()" style="background: var(--accent-orange); border-color: var(--accent-orange); color: #000;">
|
||
Skip >>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 10px; font-size: 11px; color: var(--text-muted);">
|
||
<span>Signals: <strong id="scannerSignalCount" style="color: var(--accent-green);">0</strong></span>
|
||
<span>|</span>
|
||
<span id="scannerStatusText">Ready</span>
|
||
</div>
|
||
</div>
|
||
<div class="section">
|
||
<h3>Manual Audio Receiver</h3>
|
||
<div id="audioToolsWarning" style="display: none; background: rgba(255, 100, 100, 0.1); border: 1px solid var(--accent-red); border-radius: 4px; padding: 10px; margin-bottom: 10px;">
|
||
<p style="color: var(--accent-red); margin: 0; font-size: 0.85em;">
|
||
<strong>Missing Dependencies:</strong><br>
|
||
<span id="audioToolsWarningText"></span>
|
||
</p>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>SDR Device</label>
|
||
<select id="audioDeviceSelect">
|
||
<option value="0">Device 0 (Default)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Frequency Preset</label>
|
||
<select id="audioPreset" onchange="applyAudioPreset()">
|
||
<option value="custom">Custom Frequency</option>
|
||
<option value="fm">FM Radio (87.5-108 MHz)</option>
|
||
<option value="airband">Airband (118-137 MHz)</option>
|
||
<option value="marine">Marine VHF (156-163 MHz)</option>
|
||
<option value="amateur2m">Amateur 2m (144-148 MHz)</option>
|
||
<option value="amateur70cm">Amateur 70cm (420-450 MHz)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Tune Frequency (MHz)</label>
|
||
<div style="display: flex; gap: 5px; align-items: center;">
|
||
<input type="number" id="audioFrequency" value="98.1" step="0.1" style="flex: 1;">
|
||
<button class="preset-btn" onclick="audioFreqDown()" style="padding: 5px 10px;">-</button>
|
||
<button class="preset-btn" onclick="audioFreqUp()" style="padding: 5px 10px;">+</button>
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Modulation</label>
|
||
<select id="audioModulation">
|
||
<option value="wfm">Wide FM (Broadcast)</option>
|
||
<option value="fm">Narrow FM</option>
|
||
<option value="am">AM</option>
|
||
<option value="usb">USB (Upper Sideband)</option>
|
||
<option value="lsb">LSB (Lower Sideband)</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Squelch</label>
|
||
<input type="range" id="audioSquelch" min="0" max="100" value="0" oninput="document.getElementById('audioSquelchValue').textContent = this.value">
|
||
<span id="audioSquelchValue">0</span>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Gain (dB)</label>
|
||
<input type="range" id="audioGain" min="0" max="50" value="40" oninput="document.getElementById('audioGainValue').textContent = this.value">
|
||
<span id="audioGainValue">40</span>
|
||
</div>
|
||
<div style="display: flex; gap: 10px;">
|
||
<button class="run-btn" id="audioStartBtn" onclick="toggleAudio()" style="flex: 1;">
|
||
▶ Play Audio
|
||
</button>
|
||
</div>
|
||
<div class="status-line" style="margin-top: 10px;">
|
||
<span>Status:</span>
|
||
<span id="audioStatus" style="color: var(--text-muted);">STOPPED</span>
|
||
</div>
|
||
<div class="status-line">
|
||
<span>Tuned:</span>
|
||
<span id="audioTunedFreq">-- MHz</span>
|
||
</div>
|
||
<div class="status-line">
|
||
<span>Device:</span>
|
||
<span id="audioDeviceStatus">--</span>
|
||
</div>
|
||
<!-- Audio Visualizer -->
|
||
<div class="audio-visualizer" id="audioVisualizerContainer" style="display: none; margin-top: 12px;">
|
||
<div class="signal-meter">
|
||
<label>Signal</label>
|
||
<div class="meter-bar">
|
||
<div class="meter-fill" id="audioSignalMeter"></div>
|
||
<div class="meter-peak" id="audioSignalPeak"></div>
|
||
</div>
|
||
<span class="meter-value" id="audioSignalValue">-∞ dB</span>
|
||
</div>
|
||
<canvas id="audioSpectrumCanvas" width="280" height="60"></canvas>
|
||
</div>
|
||
<!-- Hidden audio element for browser playback -->
|
||
<audio id="audioPlayer" style="display: none;" crossorigin="anonymous"></audio>
|
||
<div class="form-group" style="margin-top: 10px;">
|
||
<label>Volume</label>
|
||
<input type="range" id="audioVolume" min="0" max="100" value="80"
|
||
oninput="updateAudioVolume()" style="width: 100%;">
|
||
</div>
|
||
<p style="font-size: 11px; color: var(--text-muted); margin-top: 8px;">
|
||
Audio streams to your <strong>browser</strong>.<br>
|
||
Requires rtl_fm and ffmpeg installed on server.
|
||
</p>
|
||
</div>
|
||
<div class="section">
|
||
<h3>Activity Log</h3>
|
||
<div id="scannerLogContainer" style="max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.3); border-radius: 4px; padding: 8px; font-family: 'JetBrains Mono', monospace; font-size: 11px;">
|
||
<div id="scannerLog" style="color: var(--text-secondary);">
|
||
<div style="color: var(--text-muted);">Scanner activity will appear here...</div>
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; gap: 10px; margin-top: 10px;">
|
||
<button class="preset-btn" onclick="clearScannerLog()" style="flex: 1;">Clear Log</button>
|
||
<button class="preset-btn" onclick="exportScannerLog()" style="flex: 1;">Export Log</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button class="preset-btn" onclick="killAll()" style="width: 100%; margin-top: 10px; border-color: #ff3366; color: #ff3366;">
|
||
Kill All Processes
|
||
</button>
|
||
</div>
|
||
|
||
<div class="output-panel">
|
||
<div class="output-header">
|
||
<h3 id="outputTitle">Pager Decoder</h3>
|
||
<div class="header-controls">
|
||
<div id="signalMeter" class="signal-meter" title="Signal Activity">
|
||
<div class="signal-bar"></div>
|
||
<div class="signal-bar"></div>
|
||
<div class="signal-bar"></div>
|
||
<div class="signal-bar"></div>
|
||
<div class="signal-bar"></div>
|
||
</div>
|
||
<div class="stats" id="pagerStats">
|
||
<div title="Total Messages">📨 <span id="msgCount">0</span></div>
|
||
<div title="POCSAG Messages">📟 <span id="pocsagCount">0</span></div>
|
||
<div title="FLEX Messages">📠 <span id="flexCount">0</span></div>
|
||
</div>
|
||
<div class="stats" id="sensorStats" style="display: none;">
|
||
<div title="Unique Sensors">🌡️ <span id="sensorCount">0</span></div>
|
||
<div title="Device Types">📊 <span id="deviceCount">0</span></div>
|
||
</div>
|
||
<div class="stats" id="wifiStats" style="display: none;">
|
||
<div title="Access Points">📡 <span id="apCount">0</span></div>
|
||
<div title="Connected Clients">👤 <span id="clientCount">0</span></div>
|
||
<div title="Captured Handshakes" style="color: var(--accent-green);">🤝 <span id="handshakeCount">0</span></div>
|
||
<div style="color: var(--accent-orange); cursor: pointer;" onclick="showDroneDetails()" title="Click: Drone details">🚁 <span id="droneCount">0</span></div>
|
||
<div style="color: var(--accent-red); cursor: pointer;" onclick="showRogueApDetails()" title="Click: Rogue AP details">⚠️ <span id="rogueApCount">0</span></div>
|
||
</div>
|
||
<div class="stats" id="btStats" style="display: none;">
|
||
<div title="Bluetooth Devices">🔵 <span id="btDeviceCount">0</span></div>
|
||
<div title="BLE Beacons">📍 <span id="btBeaconCount">0</span></div>
|
||
</div>
|
||
<div class="stats" id="aircraftStats" style="display: none;">
|
||
<div title="Aircraft Tracked">✈️ <span id="aircraftCount">0</span></div>
|
||
<div title="Messages Received">📨 <span id="adsbMsgCount">0</span></div>
|
||
<div title="Unique ICAO Codes">🔢 <span id="icaoCount">0</span></div>
|
||
</div>
|
||
<div class="stats" id="satelliteStats" style="display: none;">
|
||
<div title="Upcoming Passes">🛰️ <span id="passCount">0</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- WiFi Layout Container (visualizations left, device cards right) -->
|
||
<div class="wifi-layout-container" id="wifiLayoutContainer" style="display: none;">
|
||
<!-- Left: WiFi Visualizations -->
|
||
<div class="wifi-visuals" id="wifiVisuals">
|
||
<!-- Selected WiFi Device Info - at top for visibility -->
|
||
<div class="wifi-visual-panel" style="grid-column: span 2;">
|
||
<h5>📋 Selected Device</h5>
|
||
<div id="wifiSelectedDevice" style="font-size: 11px; min-height: 100px;">
|
||
<div style="color: var(--text-dim); padding: 20px; text-align: center;">Click a network or client to view details</div>
|
||
</div>
|
||
</div>
|
||
<!-- Row 1: Network Radar + Security Overview -->
|
||
<div class="wifi-visual-panel">
|
||
<h5>Network Radar</h5>
|
||
<div class="radar-container">
|
||
<canvas id="radarCanvas" width="150" height="150"></canvas>
|
||
</div>
|
||
</div>
|
||
<div class="wifi-visual-panel">
|
||
<h5>Security Overview</h5>
|
||
<div class="security-container">
|
||
<div class="security-donut">
|
||
<canvas id="securityCanvas" width="80" height="80"></canvas>
|
||
</div>
|
||
<div class="security-legend">
|
||
<div class="security-legend-item"><div class="security-legend-dot wpa3"></div>WPA3: <span id="wpa3Count">0</span></div>
|
||
<div class="security-legend-item"><div class="security-legend-dot wpa2"></div>WPA2: <span id="wpa2Count">0</span></div>
|
||
<div class="security-legend-item"><div class="security-legend-dot wep"></div>WEP: <span id="wepCount">0</span></div>
|
||
<div class="security-legend-item"><div class="security-legend-dot open"></div>Open: <span id="openCount">0</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Row 2: Channel Utilization (2.4 GHz + 5 GHz side by side) -->
|
||
<div class="wifi-visual-panel">
|
||
<h5>Channel Utilization (2.4 GHz)</h5>
|
||
<div class="channel-graph" id="channelGraph">
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">1</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">2</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">3</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">4</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">5</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">6</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">7</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">8</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">9</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">10</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">11</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">12</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">13</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="wifi-visual-panel">
|
||
<h5>Channel Utilization (5 GHz)</h5>
|
||
<div class="channel-graph" id="channelGraph5g" style="font-size: 7px;">
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">36</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">40</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">44</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">48</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">52</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">56</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">60</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">64</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">100</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">149</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">153</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">157</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">161</span></div>
|
||
<div class="channel-bar-wrapper"><div class="channel-bar" style="height: 2px;"></div><span class="channel-label">165</span></div>
|
||
</div>
|
||
</div>
|
||
<!-- Row 3: Channel Recommendation -->
|
||
<div class="wifi-visual-panel channel-recommendation" id="channelRecommendation">
|
||
<h4>💡 Channel Recommendation</h4>
|
||
<div class="rec-text">
|
||
<strong>2.4 GHz:</strong> Use channel <span class="rec-channel" id="rec24Channel">--</span>
|
||
<span id="rec24Reason" style="font-size: 10px; color: var(--text-dim);"></span>
|
||
</div>
|
||
<div class="rec-text" style="margin-top: 5px;">
|
||
<strong>5 GHz:</strong> Use channel <span class="rec-channel" id="rec5Channel">--</span>
|
||
<span id="rec5Reason" style="font-size: 10px; color: var(--text-dim);"></span>
|
||
</div>
|
||
</div>
|
||
<!-- Device Correlation -->
|
||
<div class="wifi-visual-panel" id="correlationPanel">
|
||
<h5>🔗 Device Correlation</h5>
|
||
<div id="correlationList" style="font-size: 11px; max-height: 100px; overflow-y: auto;">
|
||
<div style="color: var(--text-dim);">Analyzing WiFi/BT device patterns...</div>
|
||
</div>
|
||
</div>
|
||
<!-- Hidden SSID Revealer -->
|
||
<div class="wifi-visual-panel" id="hiddenSsidPanel">
|
||
<h5>👁️ Hidden SSIDs Revealed</h5>
|
||
<div id="hiddenSsidList" style="font-size: 11px; max-height: 100px; overflow-y: auto;">
|
||
<div style="color: var(--text-dim);">Monitoring probe requests...</div>
|
||
</div>
|
||
</div>
|
||
<!-- Client Probe Analysis -->
|
||
<div class="wifi-visual-panel" id="probeAnalysisPanel" style="grid-column: span 2;">
|
||
<h5>📡 Client Probe Analysis</h5>
|
||
<div style="display: flex; gap: 10px; margin-bottom: 8px; font-size: 10px;">
|
||
<span>Clients: <strong id="probeClientCount">0</strong></span>
|
||
<span>Unique SSIDs: <strong id="probeSSIDCount">0</strong></span>
|
||
<span>Privacy Leaks: <strong id="probePrivacyCount" style="color: var(--accent-orange);">0</strong></span>
|
||
</div>
|
||
<div id="probeAnalysisList" style="font-size: 11px; max-height: 200px; overflow-y: auto;">
|
||
<div style="color: var(--text-dim);">Waiting for client probe requests...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Right: WiFi Device Cards -->
|
||
<div class="wifi-device-list" id="wifiDeviceList">
|
||
<div class="wifi-device-list-header">
|
||
<h5>📡 Discovered Networks</h5>
|
||
<span class="device-count">(<span id="wifiDeviceListCount">0</span>)</span>
|
||
</div>
|
||
<div class="wifi-device-list-content" id="wifiDeviceListContent">
|
||
<div style="color: var(--text-dim); text-align: center; padding: 30px;">
|
||
Start scanning to discover WiFi networks
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bluetooth Layout Container (visualizations left, device cards right) -->
|
||
<div class="bt-layout-container" id="btLayoutContainer" style="display: none;">
|
||
<!-- Left: Bluetooth Visualizations -->
|
||
<div class="wifi-visuals" id="btVisuals">
|
||
<!-- Selected Bluetooth Device Info - at top for visibility -->
|
||
<div class="wifi-visual-panel" style="grid-column: span 2;">
|
||
<h5>📋 Selected Device</h5>
|
||
<div id="btSelectedDevice" style="font-size: 11px; min-height: 100px;">
|
||
<div style="color: var(--text-dim); padding: 20px; text-align: center;">Click a device to view details</div>
|
||
</div>
|
||
</div>
|
||
<!-- Row 1: Bluetooth Radar + Device Types -->
|
||
<div class="wifi-visual-panel">
|
||
<h5>Proximity Radar</h5>
|
||
<div class="radar-container">
|
||
<canvas id="btRadarCanvas" width="150" height="150"></canvas>
|
||
</div>
|
||
</div>
|
||
<div class="wifi-visual-panel">
|
||
<h5>Device Types</h5>
|
||
<div class="bt-type-overview" id="btTypeOverview">
|
||
<div class="bt-type-item"><span class="bt-type-icon">📱</span> Phones: <strong id="btPhoneCount">0</strong></div>
|
||
<div class="bt-type-item"><span class="bt-type-icon">💻</span> Computers: <strong id="btComputerCount">0</strong></div>
|
||
<div class="bt-type-item"><span class="bt-type-icon">🎧</span> Audio: <strong id="btAudioCount">0</strong></div>
|
||
<div class="bt-type-item"><span class="bt-type-icon">⌚</span> Wearables: <strong id="btWearableCount">0</strong></div>
|
||
<div class="bt-type-item"><span class="bt-type-icon">🔵</span> Other: <strong id="btOtherCount">0</strong></div>
|
||
</div>
|
||
</div>
|
||
<!-- Row 2: Tracker Detection + Signal Analysis -->
|
||
<div class="wifi-visual-panel">
|
||
<h5>📍 Tracker Detection</h5>
|
||
<div id="btTrackerList" style="max-height: 120px; overflow-y: auto; font-size: 11px;">
|
||
<div style="color: var(--text-dim); padding: 10px; text-align: center;">Monitoring for AirTags, Tiles...</div>
|
||
</div>
|
||
</div>
|
||
<div class="wifi-visual-panel">
|
||
<h5>📶 Signal Distribution</h5>
|
||
<div class="bt-signal-dist" id="btSignalDist">
|
||
<div class="signal-range"><span>Strong (-50+)</span><div class="signal-bar-bg"><div class="signal-bar strong" id="btSignalStrong" style="width: 0%;"></div></div><span id="btSignalStrongCount">0</span></div>
|
||
<div class="signal-range"><span>Medium (-70)</span><div class="signal-bar-bg"><div class="signal-bar medium" id="btSignalMedium" style="width: 0%;"></div></div><span id="btSignalMediumCount">0</span></div>
|
||
<div class="signal-range"><span>Weak (-90)</span><div class="signal-bar-bg"><div class="signal-bar weak" id="btSignalWeak" style="width: 0%;"></div></div><span id="btSignalWeakCount">0</span></div>
|
||
</div>
|
||
</div>
|
||
<!-- Row 3: FindMy Detection -->
|
||
<div class="wifi-visual-panel" style="grid-column: span 2;">
|
||
<h5>🍎 Apple FindMy Network</h5>
|
||
<div id="btFindMyList" style="max-height: 100px; overflow-y: auto; font-size: 11px;">
|
||
<div style="color: var(--text-dim); padding: 10px; text-align: center;">Scanning for FindMy-compatible devices...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Right: Bluetooth Device Cards -->
|
||
<div class="wifi-device-list bt-device-list" id="btDeviceListPanel">
|
||
<div class="wifi-device-list-header">
|
||
<h5>🔵 Bluetooth Devices</h5>
|
||
<span class="device-count">(<span id="btDeviceListCount">0</span>)</span>
|
||
</div>
|
||
<div class="wifi-device-list-content" id="btDeviceListContent">
|
||
<div style="color: var(--text-dim); text-align: center; padding: 30px;">
|
||
Start scanning to discover Bluetooth devices
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Aircraft Visualizations - Leaflet Map -->
|
||
<div class="wifi-visuals" id="aircraftVisuals" style="display: none;">
|
||
<!-- Map Panel -->
|
||
<div class="wifi-visual-panel" style="grid-column: span 2;">
|
||
<h5 style="color: var(--accent-cyan); text-shadow: 0 0 10px var(--accent-cyan);">ADS-B AIRCRAFT TRACKING</h5>
|
||
<div class="aircraft-map-container">
|
||
<div class="map-header">
|
||
<span id="radarTime">--:--:--</span>
|
||
<span id="radarStatus">TRACKING</span>
|
||
</div>
|
||
<div id="aircraftMap"></div>
|
||
<div class="map-footer">
|
||
<span>AIRCRAFT: <span id="aircraftCount">0</span></span>
|
||
<span>CENTER: <span id="mapCenter">--</span></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Aircraft List & Details Panel -->
|
||
<div class="wifi-visual-panel" style="grid-column: span 1; display: flex; flex-direction: column; gap: 10px;">
|
||
<!-- Selected Aircraft -->
|
||
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); padding: 10px;">
|
||
<h5 style="color: var(--accent-orange); margin-bottom: 10px;">SELECTED TARGET</h5>
|
||
<div id="selectedAircraftInfo" style="font-size: 11px;">
|
||
<div style="color: var(--text-muted); text-align: center; padding: 20px;">
|
||
Click an aircraft to view details
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Aircraft List -->
|
||
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); padding: 10px; flex: 1; overflow-y: auto;">
|
||
<h5 style="color: var(--accent-cyan); margin-bottom: 10px;">TRACKED AIRCRAFT</h5>
|
||
<div id="aircraftListPanel" style="font-size: 11px; max-height: 300px; overflow-y: auto;">
|
||
<div style="color: var(--text-muted); text-align: center; padding: 20px;">
|
||
No aircraft detected
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Open Full Dashboard Button -->
|
||
<a href="/adsb/dashboard" target="_blank" class="preset-btn" style="display: block; text-align: center; text-decoration: none; background: var(--accent-cyan); color: #000; padding: 8px;">
|
||
Open Full Dashboard
|
||
</a>
|
||
</div>
|
||
<!-- Aircraft Image Panel -->
|
||
<div class="wifi-visual-panel" style="grid-column: span 1; display: flex; flex-direction: column;">
|
||
<h5 style="color: var(--accent-green); margin-bottom: 10px;">AIRCRAFT IMAGE</h5>
|
||
<div id="mainAircraftPhotoContainer" style="flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center;">
|
||
<div id="mainAircraftPhotoPlaceholder" style="color: var(--text-muted); text-align: center; padding: 40px 20px;">
|
||
<div style="font-size: 48px; opacity: 0.3; margin-bottom: 10px;">✈️</div>
|
||
<div>Select an aircraft to view photo</div>
|
||
</div>
|
||
<div id="mainAircraftPhotoWrapper" style="display: none; width: 100%;">
|
||
<a id="mainAircraftPhotoLink" href="#" target="_blank" rel="noopener">
|
||
<img id="mainAircraftPhoto" src="" alt="Aircraft photo" style="width: 100%; border-radius: 6px; border: 1px solid var(--border-color);">
|
||
</a>
|
||
<div id="mainAircraftPhotoCredit" style="font-size: 9px; color: var(--text-dim); margin-top: 4px; text-align: right;"></div>
|
||
<div id="mainAircraftPhotoInfo" style="margin-top: 8px; padding: 8px; background: rgba(0,0,0,0.3); border-radius: 4px; font-size: 10px;">
|
||
<div style="color: var(--text-dim);">REGISTRATION</div>
|
||
<div id="mainAircraftPhotoReg" style="color: var(--accent-cyan); font-size: 14px; font-weight: bold;">--</div>
|
||
</div>
|
||
</div>
|
||
<div id="mainAircraftPhotoLoading" style="display: none; color: var(--text-muted); text-align: center; padding: 20px;">
|
||
<div style="font-size: 24px; animation: pulse 1s infinite;">📷</div>
|
||
<div style="margin-top: 8px;">Loading photo...</div>
|
||
</div>
|
||
<div id="mainAircraftPhotoNoPhoto" style="display: none; color: var(--text-muted); text-align: center; padding: 20px;">
|
||
<div style="font-size: 32px; opacity: 0.5; margin-bottom: 8px;">📷</div>
|
||
<div>No photo available</div>
|
||
<div style="font-size: 10px; margin-top: 4px; color: var(--text-dim);">Registration not found in database</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Listening Post Visualizations -->
|
||
<div class="wifi-visuals" id="listeningPostVisuals" style="display: none;">
|
||
<!-- Large frequency display panel -->
|
||
<div class="wifi-visual-panel" style="grid-column: span 4;">
|
||
<div style="display: flex; gap: 20px; align-items: stretch;">
|
||
<!-- Big frequency display -->
|
||
<div style="flex: 2; background: rgba(0,0,0,0.4); border-radius: 8px; padding: 20px; text-align: center;">
|
||
<div style="font-size: 12px; color: var(--text-muted); text-transform: uppercase; margin-bottom: 10px;">
|
||
<span id="mainScannerModeLabel">SCANNER STOPPED</span>
|
||
</div>
|
||
<div id="mainScannerFreq" style="font-size: 56px; font-weight: bold; font-family: 'JetBrains Mono', monospace; color: var(--accent-cyan); line-height: 1;">
|
||
---.---
|
||
</div>
|
||
<div style="font-size: 24px; color: var(--text-muted);">MHz</div>
|
||
<div id="mainScannerMod" style="font-size: 14px; color: var(--text-secondary); margin-top: 10px;">
|
||
--
|
||
</div>
|
||
<!-- Scanning animation -->
|
||
<div id="mainScannerAnimation" style="display: none; margin-top: 15px;">
|
||
<div style="display: flex; justify-content: center; gap: 4px;">
|
||
<div class="scan-dot" style="width: 8px; height: 8px; background: var(--accent-cyan); border-radius: 50%; animation: pulse 1s infinite;"></div>
|
||
<div class="scan-dot" style="width: 8px; height: 8px; background: var(--accent-cyan); border-radius: 50%; animation: pulse 1s infinite 0.2s;"></div>
|
||
<div class="scan-dot" style="width: 8px; height: 8px; background: var(--accent-cyan); border-radius: 50%; animation: pulse 1s infinite 0.4s;"></div>
|
||
</div>
|
||
<div style="font-size: 11px; color: var(--text-muted); margin-top: 5px;">Scanning...</div>
|
||
</div>
|
||
<!-- Progress bar -->
|
||
<div id="mainScannerProgress" style="display: none; margin-top: 15px;">
|
||
<div style="display: flex; justify-content: space-between; font-size: 10px; color: var(--text-muted);">
|
||
<span id="mainRangeStart">--</span>
|
||
<span id="mainRangeEnd">--</span>
|
||
</div>
|
||
<div style="background: rgba(255,255,255,0.1); height: 6px; border-radius: 3px; margin-top: 4px;">
|
||
<div id="mainProgressBar" style="background: var(--accent-cyan); height: 100%; border-radius: 3px; width: 0%; transition: width 0.3s;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Status panel -->
|
||
<div style="flex: 1; display: flex; flex-direction: column; gap: 10px;">
|
||
<!-- Signal found alert -->
|
||
<div id="mainSignalAlert" style="display: none; background: rgba(0, 255, 100, 0.15); border: 2px solid var(--accent-green); border-radius: 8px; padding: 15px; text-align: center;">
|
||
<div style="font-size: 16px; color: var(--accent-green); font-weight: bold;">SIGNAL FOUND!</div>
|
||
<div style="font-size: 12px; color: var(--text-secondary); margin: 5px 0;">Audio streaming to browser</div>
|
||
<button class="run-btn" onclick="skipSignal()" style="margin-top: 10px; background: var(--accent-orange); border-color: var(--accent-orange);">
|
||
Skip & Continue >>
|
||
</button>
|
||
</div>
|
||
<!-- Stats -->
|
||
<div style="background: rgba(0,0,0,0.3); border-radius: 6px; padding: 12px; flex: 1;">
|
||
<div style="font-size: 10px; color: var(--text-muted); text-transform: uppercase;">Statistics</div>
|
||
<div style="margin-top: 10px; font-size: 12px;">
|
||
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
|
||
<span style="color: var(--text-secondary);">Signals Found</span>
|
||
<span id="mainSignalCount" style="color: var(--accent-green); font-weight: bold;">0</span>
|
||
</div>
|
||
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
|
||
<span style="color: var(--text-secondary);">Frequencies Scanned</span>
|
||
<span id="mainFreqsScanned" style="color: var(--text-primary);">0</span>
|
||
</div>
|
||
<div style="display: flex; justify-content: space-between;">
|
||
<span style="color: var(--text-secondary);">Scan Cycles</span>
|
||
<span id="mainScanCycles" style="color: var(--text-primary);">0</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Audio player for scanner -->
|
||
<div style="background: rgba(0,0,0,0.3); border-radius: 6px; padding: 12px;">
|
||
<div style="font-size: 10px; color: var(--text-muted); text-transform: uppercase; margin-bottom: 8px;">Scanner Audio</div>
|
||
<audio id="scannerAudioPlayer" style="width: 100%; height: 30px;" controls></audio>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Activity log -->
|
||
<div class="wifi-visual-panel" style="grid-column: span 2;">
|
||
<div class="signal-graph-header">
|
||
<h4>Activity Log</h4>
|
||
<button class="preset-btn" onclick="clearScannerLog()" style="padding: 2px 8px; font-size: 10px;">Clear</button>
|
||
</div>
|
||
<div id="scannerActivityLog" style="height: 200px; overflow-y: auto; background: rgba(0,0,0,0.2); border-radius: 4px; padding: 10px; font-family: 'JetBrains Mono', monospace; font-size: 11px;">
|
||
<div class="scanner-log-entry" style="color: var(--text-muted);">
|
||
Waiting for scanner to start...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Signal hits table -->
|
||
<div class="wifi-visual-panel" style="grid-column: span 2;">
|
||
<div class="signal-graph-header">
|
||
<h4>Signal Hits</h4>
|
||
<span class="signal-graph-device" id="scannerHitCount">0 signals found</span>
|
||
</div>
|
||
<div id="scannerHitsList" style="max-height: 200px; overflow-y: auto;">
|
||
<table style="width: 100%; font-size: 11px; border-collapse: collapse;">
|
||
<thead>
|
||
<tr style="color: var(--text-muted); border-bottom: 1px solid var(--border-color);">
|
||
<th style="text-align: left; padding: 5px;">Time</th>
|
||
<th style="text-align: left; padding: 5px;">Frequency</th>
|
||
<th style="text-align: center; padding: 5px;">Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="scannerHitsBody">
|
||
<tr style="color: var(--text-muted);">
|
||
<td colspan="3" style="padding: 20px; text-align: center;">No signals detected yet</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<style>
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 0.3; transform: scale(0.8); }
|
||
50% { opacity: 1; transform: scale(1); }
|
||
}
|
||
</style>
|
||
|
||
<!-- Satellite Visualizations -->
|
||
<div id="satelliteVisuals" style="display: none;">
|
||
<div class="pass-predictor">
|
||
<!-- Cell 1: Polar Plot (Top Left) -->
|
||
<div class="polar-plot-container">
|
||
<div class="polar-plot-header">
|
||
<span class="polar-plot-title">Sky View</span>
|
||
<button class="popout-btn" onclick="popoutSatellite()">⛶ Pop Out</button>
|
||
</div>
|
||
<div class="polar-plot">
|
||
<canvas id="polarPlotCanvas"></canvas>
|
||
</div>
|
||
<div style="text-align: center; margin-top: 10px; font-size: 10px; color: var(--text-secondary);">
|
||
<span style="color: var(--accent-cyan);">N</span> = North |
|
||
Center = Overhead (90°) |
|
||
Edge = Horizon (0°)
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cell 2: Ground Track Map (Top Right) -->
|
||
<div class="ground-track-cell">
|
||
<div class="ground-track-header">
|
||
<span class="ground-track-title">🌍 Ground Track</span>
|
||
<label style="font-size: 11px; display: flex; align-items: center; gap: 5px;">
|
||
<input type="checkbox" id="showGroundTrack" checked onchange="toggleGroundTrack()">
|
||
Show Track
|
||
</label>
|
||
</div>
|
||
<div id="groundTrackMap"></div>
|
||
<div style="text-align: center; margin-top: 8px; font-size: 10px; color: var(--text-secondary);">
|
||
<span style="color: #666;">---</span> Past |
|
||
<span style="color: #ffff00;">●</span> Current |
|
||
<span style="color: #00ff00;">―</span> Future |
|
||
<span style="color: #ff6600;">◉</span> Observer
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cell 3: Countdown (Bottom Left) -->
|
||
<div class="countdown-cell">
|
||
<div class="pass-list-header">
|
||
<span>Next Pass Countdown</span>
|
||
</div>
|
||
<div id="satelliteCountdown" class="satellite-countdown">
|
||
<div class="countdown-satellite-name" id="countdownSatName">--</div>
|
||
<div class="countdown-grid">
|
||
<div class="countdown-block">
|
||
<div class="countdown-label">Next Pass In</div>
|
||
<div class="countdown-value" id="countdownToPass">--:--:--</div>
|
||
<div class="countdown-sublabel" id="countdownPassTime">--</div>
|
||
</div>
|
||
<div class="countdown-block">
|
||
<div class="countdown-label">Visibility</div>
|
||
<div class="countdown-value" id="countdownVisibility">--:--</div>
|
||
<div class="countdown-sublabel" id="countdownVisLabel">Duration</div>
|
||
</div>
|
||
<div class="countdown-block">
|
||
<div class="countdown-label">Max Elevation</div>
|
||
<div class="countdown-value" id="countdownMaxEl">--°</div>
|
||
<div class="countdown-sublabel" id="countdownDirection">--</div>
|
||
</div>
|
||
</div>
|
||
<div class="countdown-status" id="countdownStatus">Calculate passes to see countdown</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cell 4: Pass List (Bottom Right) -->
|
||
<div class="pass-list-cell">
|
||
<div class="pass-list-header">
|
||
<span>Upcoming Passes</span>
|
||
<span id="passListCount">0 passes</span>
|
||
</div>
|
||
<div id="passList">
|
||
<div style="color: #666; text-align: center; padding: 30px; font-size: 11px;">
|
||
Click "Calculate Passes" to predict satellite passes for your location.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Satellite Popout Container -->
|
||
<div id="satellitePopout" class="popout-container">
|
||
<div class="popout-header">
|
||
<span class="popout-title">🛰️ Satellite Pass Predictor</span>
|
||
<button class="popout-close" onclick="closeSatellitePopout()">✕ Close</button>
|
||
</div>
|
||
<div class="popout-body">
|
||
<div class="pass-predictor" style="height: 100%;">
|
||
<div class="polar-plot-container" style="height: 100%;">
|
||
<div class="polar-plot-header">
|
||
<span class="polar-plot-title">Sky View - Full Screen</span>
|
||
</div>
|
||
<div style="height: calc(100% - 80px);">
|
||
<canvas id="polarPlotCanvasPopout" style="width: 100%; height: 100%;"></canvas>
|
||
</div>
|
||
<div style="text-align: center; margin-top: 10px; font-size: 12px; color: var(--text-secondary);">
|
||
<span style="color: var(--accent-cyan);">N</span> = North |
|
||
Center = Overhead (90°) |
|
||
Edge = Horizon (0°)
|
||
</div>
|
||
</div>
|
||
<div class="pass-list-container" style="height: 100%; max-height: none;">
|
||
<div class="pass-list-header">
|
||
<span>Upcoming Passes</span>
|
||
</div>
|
||
<!-- Countdown Block for Popout -->
|
||
<div id="satelliteCountdownPopout" class="satellite-countdown" style="display: none;">
|
||
<div class="countdown-satellite-name" id="countdownSatNamePopout">--</div>
|
||
<div class="countdown-grid">
|
||
<div class="countdown-block">
|
||
<div class="countdown-label">Next Pass In</div>
|
||
<div class="countdown-value" id="countdownToPassPopout">--:--:--</div>
|
||
<div class="countdown-sublabel" id="countdownPassTimePopout">--</div>
|
||
</div>
|
||
<div class="countdown-block">
|
||
<div class="countdown-label">Visibility</div>
|
||
<div class="countdown-value" id="countdownVisibilityPopout">--:--</div>
|
||
<div class="countdown-sublabel" id="countdownVisLabelPopout">Duration</div>
|
||
</div>
|
||
<div class="countdown-block">
|
||
<div class="countdown-label">Max Elevation</div>
|
||
<div class="countdown-value" id="countdownMaxElPopout">--°</div>
|
||
<div class="countdown-sublabel" id="countdownDirectionPopout">--</div>
|
||
</div>
|
||
</div>
|
||
<div class="countdown-status" id="countdownStatusPopout">Waiting for pass data...</div>
|
||
</div>
|
||
<div id="passListPopout" style="height: calc(100% - 40px); overflow-y: auto;">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Satellite Add Modal -->
|
||
<div id="satModal" class="sat-modal">
|
||
<div class="sat-modal-content">
|
||
<div class="sat-modal-header">
|
||
<h3>🛰️ Add Satellites</h3>
|
||
<button class="sat-modal-close" onclick="closeSatModal()">×</button>
|
||
</div>
|
||
<div class="sat-modal-tabs">
|
||
<button class="sat-modal-tab active" onclick="switchSatModalTab('tle')">Paste TLE</button>
|
||
<button class="sat-modal-tab" onclick="switchSatModalTab('celestrak')">Celestrak</button>
|
||
</div>
|
||
<div id="tleSection" class="sat-modal-section active">
|
||
<p style="font-size: 11px; color: var(--text-secondary); margin-bottom: 10px;">
|
||
Paste TLE data (3 lines per satellite: name, line 1, line 2)
|
||
</p>
|
||
<textarea id="tleInput" class="tle-textarea" placeholder="SATELLITE NAME 1 NNNNN... 2 NNNNN..."></textarea>
|
||
<button class="run-btn" onclick="addFromTLE()" style="margin-top: 10px;">
|
||
Add Satellites from TLE
|
||
</button>
|
||
</div>
|
||
<div id="celestrakSection" class="sat-modal-section">
|
||
<p style="font-size: 11px; color: var(--text-secondary); margin-bottom: 10px;">
|
||
Select a category to fetch satellites from Celestrak
|
||
</p>
|
||
<div class="celestrak-categories">
|
||
<button class="celestrak-cat" onclick="fetchCelestrakCategory('stations')">🚀 Space Stations</button>
|
||
<button class="celestrak-cat" onclick="fetchCelestrakCategory('visual')">👁️ Brightest</button>
|
||
<button class="celestrak-cat" onclick="fetchCelestrakCategory('weather')">🌤️ Weather</button>
|
||
<button class="celestrak-cat" onclick="fetchCelestrakCategory('noaa')">📡 NOAA</button>
|
||
<button class="celestrak-cat" onclick="fetchCelestrakCategory('amateur')">📻 Amateur Radio</button>
|
||
<button class="celestrak-cat" onclick="fetchCelestrakCategory('starlink')">⭐ Starlink</button>
|
||
<button class="celestrak-cat" onclick="fetchCelestrakCategory('gps-ops')">🛰️ GPS</button>
|
||
<button class="celestrak-cat" onclick="fetchCelestrakCategory('iridium')">📱 Iridium</button>
|
||
</div>
|
||
<div id="celestrakStatus" style="margin-top: 10px; font-size: 11px; color: var(--text-secondary);"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Device Intelligence Dashboard (above waterfall for prominence) -->
|
||
<div class="recon-panel" id="reconPanel">
|
||
<div class="recon-header" onclick="toggleReconCollapse()" style="cursor: pointer;">
|
||
<h4><span id="reconCollapseIcon">▼</span> Device Intelligence</h4>
|
||
<div class="recon-stats">
|
||
<div>TRACKED: <span id="trackedCount">0</span></div>
|
||
<div>NEW: <span id="newDeviceCount">0</span></div>
|
||
<div>ANOMALIES: <span id="anomalyCount">0</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="recon-content" id="reconContent">
|
||
<div style="color: #444; text-align: center; padding: 20px; font-size: 11px;">
|
||
Device intelligence data will appear here as signals are intercepted.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="waterfall-container">
|
||
<canvas id="waterfallCanvas" width="800" height="60"></canvas>
|
||
</div>
|
||
|
||
<div class="output-content" id="output">
|
||
<div class="placeholder" style="color: #888; text-align: center; padding: 50px;">
|
||
Configure settings and click "Start Decoding" to begin.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="status-bar">
|
||
<div class="status-indicator">
|
||
<div class="status-dot" id="statusDot"></div>
|
||
<span id="statusText">Idle</span>
|
||
</div>
|
||
<div class="status-controls">
|
||
<div class="control-group">
|
||
<span class="control-group-label">Mode</span>
|
||
<button id="reconBtn" class="recon-toggle" onclick="toggleRecon()">RECON</button>
|
||
<button id="muteBtn" class="control-btn" onclick="toggleMute()">🔊</button>
|
||
<button id="autoScrollBtn" class="control-btn active" onclick="toggleAutoScroll()">⬇ AUTO</button>
|
||
</div>
|
||
<div class="control-group">
|
||
<span class="control-group-label">Export</span>
|
||
<button class="control-btn" onclick="exportCSV()">CSV</button>
|
||
<button class="control-btn" onclick="exportJSON()">JSON</button>
|
||
<button class="control-btn" onclick="exportDeviceDB()" title="Export Device Intelligence">INTEL</button>
|
||
</div>
|
||
<button class="clear-btn" onclick="clearMessages()">Clear</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Disclaimer handling
|
||
function checkDisclaimer() {
|
||
const accepted = localStorage.getItem('disclaimerAccepted');
|
||
if (accepted === 'true') {
|
||
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
||
}
|
||
}
|
||
|
||
function acceptDisclaimer() {
|
||
localStorage.setItem('disclaimerAccepted', 'true');
|
||
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
||
}
|
||
|
||
function declineDisclaimer() {
|
||
document.getElementById('disclaimerModal').classList.add('disclaimer-hidden');
|
||
document.getElementById('rejectionPage').classList.remove('disclaimer-hidden');
|
||
}
|
||
|
||
// Check disclaimer on load
|
||
checkDisclaimer();
|
||
|
||
let eventSource = null;
|
||
let isRunning = false;
|
||
let isSensorRunning = false;
|
||
let isAdsbRunning = false;
|
||
let isWifiRunning = false;
|
||
let isBtRunning = false;
|
||
let currentMode = 'pager';
|
||
let msgCount = 0;
|
||
let pocsagCount = 0;
|
||
let flexCount = 0;
|
||
let sensorCount = 0;
|
||
let filteredCount = 0; // Count of filtered messages
|
||
let deviceList = {{ devices | tojson | safe }};
|
||
|
||
// Pager message filter settings
|
||
let pagerFilters = {
|
||
hideToneOnly: true,
|
||
keywords: []
|
||
};
|
||
|
||
// Aircraft (ADS-B) state
|
||
let adsbAircraft = {};
|
||
let adsbMsgCount = 0;
|
||
let adsbEventSource = null;
|
||
let aircraftTrails = {}; // ICAO -> array of positions
|
||
let activeSquawkAlerts = {}; // Active emergency squawk alerts
|
||
let alertedAircraft = {}; // Track aircraft that have already triggered alerts
|
||
let adsbAlertsEnabled = true; // Toggle for audio alerts
|
||
let selectedMainAircraft = null; // Selected aircraft in main tab
|
||
|
||
// UTC Clock Update
|
||
function updateHeaderClock() {
|
||
const now = new Date();
|
||
const utc = now.toISOString().substring(11, 19);
|
||
document.getElementById('headerUtcTime').textContent = utc;
|
||
}
|
||
// Update clock every second
|
||
setInterval(updateHeaderClock, 1000);
|
||
updateHeaderClock(); // Initial call
|
||
|
||
// Pager message filter functions
|
||
function loadPagerFilters() {
|
||
const saved = localStorage.getItem('pagerFilters');
|
||
if (saved) {
|
||
try {
|
||
pagerFilters = JSON.parse(saved);
|
||
} catch (e) {
|
||
console.warn('Failed to load pager filters:', e);
|
||
}
|
||
}
|
||
// Update UI
|
||
document.getElementById('filterToneOnly').checked = pagerFilters.hideToneOnly;
|
||
document.getElementById('filterKeywords').value = pagerFilters.keywords.join(', ');
|
||
}
|
||
|
||
function savePagerFilters() {
|
||
pagerFilters.hideToneOnly = document.getElementById('filterToneOnly').checked;
|
||
const keywordsInput = document.getElementById('filterKeywords').value;
|
||
pagerFilters.keywords = keywordsInput
|
||
.split(',')
|
||
.map(k => k.trim().toLowerCase())
|
||
.filter(k => k.length > 0);
|
||
localStorage.setItem('pagerFilters', JSON.stringify(pagerFilters));
|
||
}
|
||
|
||
function shouldFilterMessage(msg) {
|
||
// Check for Tone Only filter
|
||
if (pagerFilters.hideToneOnly) {
|
||
if (msg.message === '[Tone Only]' || msg.msg_type === 'Tone') {
|
||
return true;
|
||
}
|
||
}
|
||
// Check keyword filters
|
||
if (pagerFilters.keywords.length > 0) {
|
||
const msgLower = (msg.message || '').toLowerCase();
|
||
for (const keyword of pagerFilters.keywords) {
|
||
if (msgLower.includes(keyword)) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// Sync header stats with output panel stats
|
||
function syncHeaderStats() {
|
||
// Pager stats
|
||
document.getElementById('headerMsgCount').textContent = msgCount;
|
||
document.getElementById('headerPocsagCount').textContent = pocsagCount;
|
||
document.getElementById('headerFlexCount').textContent = flexCount;
|
||
|
||
// Sensor stats
|
||
document.getElementById('headerSensorCount').textContent = document.getElementById('sensorCount')?.textContent || '0';
|
||
document.getElementById('headerDeviceTypeCount').textContent = document.getElementById('deviceCount')?.textContent || '0';
|
||
|
||
// WiFi stats
|
||
document.getElementById('headerApCount').textContent = document.getElementById('apCount')?.textContent || '0';
|
||
document.getElementById('headerClientCount').textContent = document.getElementById('clientCount')?.textContent || '0';
|
||
document.getElementById('headerHandshakeCount').textContent = document.getElementById('handshakeCount')?.textContent || '0';
|
||
document.getElementById('headerDroneCount').textContent = document.getElementById('droneCount')?.textContent || '0';
|
||
|
||
// Bluetooth stats
|
||
document.getElementById('headerBtDeviceCount').textContent = document.getElementById('btDeviceCount')?.textContent || '0';
|
||
document.getElementById('headerBtBeaconCount').textContent = document.getElementById('btBeaconCount')?.textContent || '0';
|
||
|
||
// Aircraft stats
|
||
document.getElementById('headerAircraftCount').textContent = document.getElementById('aircraftCount')?.textContent || '0';
|
||
document.getElementById('headerAdsbMsgCount').textContent = document.getElementById('adsbMsgCount')?.textContent || '0';
|
||
document.getElementById('headerIcaoCount').textContent = document.getElementById('icaoCount')?.textContent || '0';
|
||
|
||
// Satellite stats
|
||
document.getElementById('headerPassCount').textContent = document.getElementById('passCount')?.textContent || '0';
|
||
}
|
||
// Sync stats periodically
|
||
setInterval(syncHeaderStats, 500);
|
||
|
||
// ADS-B Statistics tracking
|
||
let adsbStats = {
|
||
totalAircraftSeen: new Set(), // Unique ICAO codes seen
|
||
maxRange: 0, // Max distance in nm
|
||
maxRangeAircraft: null, // Aircraft that achieved max range
|
||
hourlyCount: {}, // Hour -> count of aircraft
|
||
messagesPerSecond: 0, // Current msg/sec rate
|
||
messageTimestamps: [], // Recent message timestamps for rate calc
|
||
sessionStart: null // When tracking started
|
||
};
|
||
|
||
// Observer location for distance calculations (load from localStorage or default to London)
|
||
let observerLocation = (function() {
|
||
const saved = localStorage.getItem('observerLocation');
|
||
if (saved) {
|
||
try {
|
||
const parsed = JSON.parse(saved);
|
||
if (parsed.lat && parsed.lon) return parsed;
|
||
} catch (e) {}
|
||
}
|
||
return { lat: 51.5074, lon: -0.1278 };
|
||
})();
|
||
let rangeRingsLayer = null;
|
||
let observerMarkerAdsb = null;
|
||
|
||
// GPS Dongle state
|
||
let gpsConnected = false;
|
||
let gpsEventSource = null;
|
||
let gpsLastPosition = null;
|
||
|
||
// Audio alert system using Web Audio API (uses shared audioContext declared later)
|
||
function getAdsbAudioContext() {
|
||
if (!window.adsbAudioCtx) {
|
||
window.adsbAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||
}
|
||
return window.adsbAudioCtx;
|
||
}
|
||
|
||
function playAlertSound(type) {
|
||
if (!adsbAlertsEnabled) return;
|
||
try {
|
||
const ctx = getAdsbAudioContext();
|
||
const oscillator = ctx.createOscillator();
|
||
const gainNode = ctx.createGain();
|
||
|
||
oscillator.connect(gainNode);
|
||
gainNode.connect(ctx.destination);
|
||
|
||
if (type === 'emergency') {
|
||
// Urgent two-tone alert for emergencies
|
||
oscillator.frequency.setValueAtTime(880, ctx.currentTime);
|
||
oscillator.frequency.setValueAtTime(660, ctx.currentTime + 0.15);
|
||
oscillator.frequency.setValueAtTime(880, ctx.currentTime + 0.3);
|
||
gainNode.gain.setValueAtTime(0.3, ctx.currentTime);
|
||
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5);
|
||
oscillator.start(ctx.currentTime);
|
||
oscillator.stop(ctx.currentTime + 0.5);
|
||
} else if (type === 'military') {
|
||
// Single tone for military
|
||
oscillator.frequency.setValueAtTime(523, ctx.currentTime);
|
||
gainNode.gain.setValueAtTime(0.2, ctx.currentTime);
|
||
gainNode.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3);
|
||
oscillator.start(ctx.currentTime);
|
||
oscillator.stop(ctx.currentTime + 0.3);
|
||
}
|
||
} catch (e) {
|
||
console.warn('Audio alert failed:', e);
|
||
}
|
||
}
|
||
|
||
function checkAndAlertAircraft(icao, aircraft) {
|
||
// Skip if already alerted for this aircraft
|
||
if (alertedAircraft[icao]) return;
|
||
|
||
const militaryInfo = isMilitaryAircraft(icao, aircraft.callsign);
|
||
const squawkInfo = checkSquawkCode(aircraft);
|
||
|
||
if (squawkInfo) {
|
||
alertedAircraft[icao] = 'emergency';
|
||
playAlertSound('emergency');
|
||
showAlertBanner(`⚠️ EMERGENCY: ${squawkInfo.name} - ${aircraft.callsign || icao}`, squawkInfo.color);
|
||
} else if (militaryInfo.military) {
|
||
alertedAircraft[icao] = 'military';
|
||
playAlertSound('military');
|
||
showAlertBanner(`🎖️ MILITARY: ${aircraft.callsign || icao}${militaryInfo.country ? ' (' + militaryInfo.country + ')' : ''}`, '#556b2f');
|
||
}
|
||
}
|
||
|
||
function showAlertBanner(message, color) {
|
||
const banner = document.createElement('div');
|
||
banner.style.cssText = `
|
||
position: fixed;
|
||
top: 80px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: ${color};
|
||
color: white;
|
||
padding: 12px 24px;
|
||
border-radius: 8px;
|
||
font-weight: bold;
|
||
font-size: 14px;
|
||
z-index: 10000;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||
animation: slideDown 0.3s ease-out;
|
||
`;
|
||
banner.textContent = message;
|
||
document.body.appendChild(banner);
|
||
|
||
// Auto-remove after 5 seconds
|
||
setTimeout(() => {
|
||
banner.style.animation = 'fadeOut 0.3s ease-out forwards';
|
||
setTimeout(() => banner.remove(), 300);
|
||
}, 5000);
|
||
}
|
||
|
||
// Emergency squawk codes
|
||
const SQUAWK_CODES = {
|
||
'7500': { type: 'hijack', name: 'HIJACK', color: '#ff0000', description: 'Aircraft being hijacked' },
|
||
'7600': { type: 'radio', name: 'RADIO FAILURE', color: '#ff6600', description: 'Radio communications failure' },
|
||
'7700': { type: 'mayday', name: 'EMERGENCY', color: '#ff0000', description: 'General emergency' }
|
||
};
|
||
|
||
// Military ICAO hex ranges (specific military-only sub-ranges)
|
||
const MILITARY_RANGES = [
|
||
{ start: 0xADF7C0, end: 0xADFFFF, country: 'US' }, // US Military
|
||
{ start: 0xAE0000, end: 0xAEFFFF, country: 'US' }, // US Military
|
||
{ start: 0x3F4000, end: 0x3F7FFF, country: 'FR' }, // France Military (Armee de l'Air)
|
||
{ start: 0x43C000, end: 0x43CFFF, country: 'UK' }, // UK Military (RAF)
|
||
{ start: 0x3D0000, end: 0x3DFFFF, country: 'DE' }, // Germany Military (Luftwaffe)
|
||
{ start: 0x501C00, end: 0x501FFF, country: 'NATO' }, // NATO
|
||
];
|
||
|
||
// Military callsign prefixes
|
||
const MILITARY_PREFIXES = [
|
||
'REACH', 'JAKE', 'DOOM', 'IRON', 'HAWK', 'VIPER', 'COBRA', 'THUNDER',
|
||
'SHADOW', 'NIGHT', 'STEEL', 'GRIM', 'REAPER', 'BLADE', 'STRIKE',
|
||
'RCH', 'CNV', 'MCH', 'EVAC', 'TOPCAT', 'ASCOT', 'RRR', 'HRK',
|
||
'NAVY', 'ARMY', 'USAF', 'RAF', 'RCAF', 'RAAF', 'IAF', 'PAF'
|
||
];
|
||
|
||
function isMilitaryAircraft(icao, callsign) {
|
||
// Check ICAO hex range
|
||
const icaoInt = parseInt(icao, 16);
|
||
for (const range of MILITARY_RANGES) {
|
||
if (icaoInt >= range.start && icaoInt <= range.end) {
|
||
return { military: true, country: range.country };
|
||
}
|
||
}
|
||
|
||
// Check callsign prefix
|
||
if (callsign) {
|
||
const upper = callsign.toUpperCase();
|
||
for (const prefix of MILITARY_PREFIXES) {
|
||
if (upper.startsWith(prefix)) {
|
||
return { military: true, type: 'callsign' };
|
||
}
|
||
}
|
||
}
|
||
|
||
return { military: false };
|
||
}
|
||
|
||
function checkSquawkCode(aircraft) {
|
||
if (!aircraft.squawk) return null;
|
||
|
||
const squawkInfo = SQUAWK_CODES[aircraft.squawk];
|
||
if (squawkInfo) {
|
||
// Show alert if not already shown
|
||
if (!activeSquawkAlerts[aircraft.icao]) {
|
||
activeSquawkAlerts[aircraft.icao] = true;
|
||
showSquawkAlert(aircraft, squawkInfo);
|
||
}
|
||
return squawkInfo;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function showSquawkAlert(aircraft, squawkInfo) {
|
||
// Create banner alert
|
||
const banner = document.createElement('div');
|
||
banner.className = 'squawk-alert-banner';
|
||
banner.id = 'squawkBanner_' + aircraft.icao;
|
||
banner.innerHTML = `
|
||
⚠️ ${squawkInfo.name} - ${aircraft.callsign || aircraft.icao} (${aircraft.squawk})
|
||
<br><small>${squawkInfo.description}</small>
|
||
<button onclick="this.parentElement.remove()" style="margin-left: 20px; background: transparent; border: 1px solid white; color: white; padding: 2px 10px; cursor: pointer;">✕</button>
|
||
`;
|
||
document.body.appendChild(banner);
|
||
|
||
// Auto-remove after 30 seconds
|
||
setTimeout(() => {
|
||
const el = document.getElementById('squawkBanner_' + aircraft.icao);
|
||
if (el) el.remove();
|
||
}, 30000);
|
||
|
||
// Audio alert
|
||
if (!muted) {
|
||
for (let i = 0; i < 5; i++) {
|
||
setTimeout(() => playAlertSound(), i * 200);
|
||
}
|
||
}
|
||
|
||
showNotification(`⚠️ ${squawkInfo.name}`, `${aircraft.callsign || aircraft.icao} - Squawk ${aircraft.squawk}`);
|
||
}
|
||
|
||
function updateAircraftTrail(icao, lat, lon) {
|
||
if (!aircraftTrails[icao]) {
|
||
aircraftTrails[icao] = [];
|
||
}
|
||
|
||
const trail = aircraftTrails[icao];
|
||
const lastPos = trail[trail.length - 1];
|
||
|
||
// Only add if position changed significantly
|
||
if (!lastPos || Math.abs(lastPos.lat - lat) > 0.001 || Math.abs(lastPos.lon - lon) > 0.001) {
|
||
trail.push({ lat, lon, time: Date.now() });
|
||
|
||
// Keep only last 100 positions (about 10 minutes at 1 update/6 seconds)
|
||
if (trail.length > 100) {
|
||
trail.shift();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Satellite state
|
||
let satellitePasses = [];
|
||
let selectedPass = null;
|
||
let selectedPassIndex = 0;
|
||
let countdownInterval = null;
|
||
|
||
// Start satellite countdown timer
|
||
function startCountdownTimer() {
|
||
if (countdownInterval) clearInterval(countdownInterval);
|
||
countdownInterval = setInterval(updateSatelliteCountdown, 1000);
|
||
}
|
||
|
||
// Update satellite countdown display
|
||
function updateSatelliteCountdown() {
|
||
// Update both main and popout countdowns
|
||
updateCountdownDisplay('');
|
||
updateCountdownDisplay('Popout');
|
||
}
|
||
|
||
// Helper to update countdown elements by suffix
|
||
function updateCountdownDisplay(suffix) {
|
||
const container = document.getElementById('satelliteCountdown' + suffix);
|
||
if (!container) return;
|
||
|
||
// Use the globally selected pass
|
||
if (!selectedPass || satellitePasses.length === 0) {
|
||
container.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
const now = new Date();
|
||
const startTime = parsePassTime(selectedPass.startTime);
|
||
const endTime = new Date(startTime.getTime() + selectedPass.duration * 60000);
|
||
|
||
container.style.display = 'block';
|
||
document.getElementById('countdownSatName' + suffix).textContent = selectedPass.satellite;
|
||
|
||
if (now >= startTime && now <= endTime) {
|
||
// Currently visible
|
||
const remaining = Math.max(0, Math.floor((endTime - now) / 1000));
|
||
const mins = Math.floor(remaining / 60);
|
||
const secs = remaining % 60;
|
||
|
||
document.getElementById('countdownToPass' + suffix).textContent = 'VISIBLE';
|
||
document.getElementById('countdownToPass' + suffix).classList.add('active');
|
||
document.getElementById('countdownPassTime' + suffix).textContent = 'Now overhead';
|
||
|
||
document.getElementById('countdownVisibility' + suffix).textContent = `${mins}:${secs.toString().padStart(2, '0')}`;
|
||
document.getElementById('countdownVisLabel' + suffix).textContent = 'Remaining';
|
||
|
||
document.getElementById('countdownMaxEl' + suffix).textContent = selectedPass.maxEl + '°';
|
||
document.getElementById('countdownDirection' + suffix).textContent = selectedPass.direction || 'Pass';
|
||
|
||
document.getElementById('countdownStatus' + suffix).textContent = '🟢 SATELLITE CURRENTLY VISIBLE';
|
||
document.getElementById('countdownStatus' + suffix).className = 'countdown-status visible';
|
||
|
||
} else if (startTime > now) {
|
||
// Upcoming pass
|
||
const secsToPass = Math.max(0, Math.floor((startTime - now) / 1000));
|
||
const hours = Math.floor(secsToPass / 3600);
|
||
const mins = Math.floor((secsToPass % 3600) / 60);
|
||
const secs = secsToPass % 60;
|
||
|
||
let countdownStr;
|
||
if (hours > 0) {
|
||
countdownStr = `${hours}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||
} else {
|
||
countdownStr = `${mins}:${secs.toString().padStart(2, '0')}`;
|
||
}
|
||
|
||
document.getElementById('countdownToPass' + suffix).textContent = countdownStr;
|
||
document.getElementById('countdownToPass' + suffix).classList.remove('active');
|
||
document.getElementById('countdownPassTime' + suffix).textContent = selectedPass.startTime;
|
||
|
||
document.getElementById('countdownVisibility' + suffix).textContent = selectedPass.duration + 'm';
|
||
document.getElementById('countdownVisLabel' + suffix).textContent = 'Duration';
|
||
|
||
document.getElementById('countdownMaxEl' + suffix).textContent = selectedPass.maxEl + '°';
|
||
document.getElementById('countdownDirection' + suffix).textContent = selectedPass.direction || 'Pass';
|
||
|
||
if (secsToPass < 300) {
|
||
document.getElementById('countdownStatus' + suffix).textContent = '🟡 PASS STARTING SOON';
|
||
document.getElementById('countdownStatus' + suffix).className = 'countdown-status upcoming';
|
||
} else {
|
||
document.getElementById('countdownStatus' + suffix).textContent = 'Selected pass';
|
||
document.getElementById('countdownStatus' + suffix).className = 'countdown-status';
|
||
}
|
||
|
||
} else {
|
||
// Pass already happened
|
||
document.getElementById('countdownToPass' + suffix).textContent = 'PASSED';
|
||
document.getElementById('countdownToPass' + suffix).classList.remove('active');
|
||
document.getElementById('countdownPassTime' + suffix).textContent = selectedPass.startTime;
|
||
|
||
document.getElementById('countdownVisibility' + suffix).textContent = selectedPass.duration + 'm';
|
||
document.getElementById('countdownVisLabel' + suffix).textContent = 'Duration';
|
||
|
||
document.getElementById('countdownMaxEl' + suffix).textContent = selectedPass.maxEl + '°';
|
||
document.getElementById('countdownDirection' + suffix).textContent = selectedPass.direction || 'Pass';
|
||
|
||
document.getElementById('countdownStatus' + suffix).textContent = 'Pass has ended';
|
||
document.getElementById('countdownStatus' + suffix).className = 'countdown-status';
|
||
}
|
||
}
|
||
|
||
// Parse pass time string to Date object
|
||
function parsePassTime(timeStr) {
|
||
// Expected format: "2025-12-21 14:32 UTC"
|
||
// Remove "UTC" suffix and parse as ISO-like format
|
||
const cleanTime = timeStr.replace(' UTC', '').replace(' ', 'T') + ':00Z';
|
||
const parsed = new Date(cleanTime);
|
||
|
||
// Fallback if that doesn't work
|
||
if (isNaN(parsed.getTime())) {
|
||
// Try parsing as-is
|
||
return new Date(timeStr.replace(' UTC', ''));
|
||
}
|
||
return parsed;
|
||
}
|
||
|
||
// Make sections collapsible
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
document.querySelectorAll('.section h3').forEach(h3 => {
|
||
h3.addEventListener('click', function() {
|
||
this.parentElement.classList.toggle('collapsed');
|
||
});
|
||
});
|
||
|
||
// Collapse all sections by default (except SDR Device which is first)
|
||
document.querySelectorAll('.section').forEach((section, index) => {
|
||
// Keep first section expanded, collapse rest
|
||
if (index > 0) {
|
||
section.classList.add('collapsed');
|
||
}
|
||
});
|
||
|
||
// Load bias-T setting from localStorage
|
||
loadBiasTSetting();
|
||
|
||
// Initialize observer location input fields from saved location
|
||
const adsbLatInput = document.getElementById('adsbObsLat');
|
||
const adsbLonInput = document.getElementById('adsbObsLon');
|
||
const obsLatInput = document.getElementById('obsLat');
|
||
const obsLonInput = document.getElementById('obsLon');
|
||
if (adsbLatInput) adsbLatInput.value = observerLocation.lat.toFixed(4);
|
||
if (adsbLonInput) adsbLonInput.value = observerLocation.lon.toFixed(4);
|
||
if (obsLatInput) obsLatInput.value = observerLocation.lat.toFixed(4);
|
||
if (obsLonInput) obsLonInput.value = observerLocation.lon.toFixed(4);
|
||
|
||
// Auto-connect to gpsd if available
|
||
autoConnectGps();
|
||
|
||
// Load pager message filters
|
||
loadPagerFilters();
|
||
});
|
||
|
||
// Toggle section collapse
|
||
function toggleSection(el) {
|
||
el.closest('.section').classList.toggle('collapsed');
|
||
}
|
||
|
||
// Mode switching
|
||
function switchMode(mode) {
|
||
// Stop any running scans when switching modes
|
||
if (isRunning) stopDecoding();
|
||
if (isSensorRunning) stopSensorDecoding();
|
||
if (isWifiRunning) stopWifiScan();
|
||
if (isBtRunning) stopBtScan();
|
||
if (isAdsbRunning) stopAdsbScan();
|
||
|
||
currentMode = mode;
|
||
// Remove active from all nav buttons, then add to the correct one
|
||
document.querySelectorAll('.mode-nav-btn').forEach(btn => btn.classList.remove('active'));
|
||
const modeMap = {
|
||
'pager': 'pager', 'sensor': '433', 'aircraft': 'aircraft',
|
||
'satellite': 'satellite', 'wifi': 'wifi', 'bluetooth': 'bluetooth',
|
||
'listening': 'listening'
|
||
};
|
||
document.querySelectorAll('.mode-nav-btn').forEach(btn => {
|
||
const label = btn.querySelector('.nav-label');
|
||
if (label && label.textContent.toLowerCase().includes(modeMap[mode])) {
|
||
btn.classList.add('active');
|
||
}
|
||
});
|
||
document.getElementById('pagerMode').classList.toggle('active', mode === 'pager');
|
||
document.getElementById('sensorMode').classList.toggle('active', mode === 'sensor');
|
||
document.getElementById('aircraftMode').classList.toggle('active', mode === 'aircraft');
|
||
document.getElementById('satelliteMode').classList.toggle('active', mode === 'satellite');
|
||
document.getElementById('wifiMode').classList.toggle('active', mode === 'wifi');
|
||
document.getElementById('bluetoothMode').classList.toggle('active', mode === 'bluetooth');
|
||
document.getElementById('listeningPostMode').classList.toggle('active', mode === 'listening');
|
||
document.getElementById('pagerStats').style.display = mode === 'pager' ? 'flex' : 'none';
|
||
document.getElementById('sensorStats').style.display = mode === 'sensor' ? 'flex' : 'none';
|
||
document.getElementById('aircraftStats').style.display = mode === 'aircraft' ? 'flex' : 'none';
|
||
document.getElementById('satelliteStats').style.display = mode === 'satellite' ? 'flex' : 'none';
|
||
document.getElementById('wifiStats').style.display = mode === 'wifi' ? 'flex' : 'none';
|
||
document.getElementById('btStats').style.display = mode === 'bluetooth' ? 'flex' : 'none';
|
||
|
||
// Hide signal meter - individual panels show signal strength where needed
|
||
document.getElementById('signalMeter').style.display = 'none';
|
||
|
||
// Update header stats groups
|
||
document.getElementById('headerPagerStats').classList.toggle('active', mode === 'pager');
|
||
document.getElementById('headerSensorStats').classList.toggle('active', mode === 'sensor');
|
||
document.getElementById('headerAircraftStats').classList.toggle('active', mode === 'aircraft');
|
||
document.getElementById('headerSatelliteStats').classList.toggle('active', mode === 'satellite');
|
||
document.getElementById('headerWifiStats').classList.toggle('active', mode === 'wifi');
|
||
document.getElementById('headerBtStats').classList.toggle('active', mode === 'bluetooth');
|
||
|
||
// Show/hide dashboard buttons in nav bar
|
||
document.getElementById('adsbDashboardBtn').style.display = mode === 'aircraft' ? 'inline-flex' : 'none';
|
||
document.getElementById('satelliteDashboardBtn').style.display = mode === 'satellite' ? 'inline-flex' : 'none';
|
||
|
||
// Update active mode indicator
|
||
const modeNames = {
|
||
'pager': 'PAGER',
|
||
'sensor': '433MHZ',
|
||
'aircraft': 'AIRCRAFT',
|
||
'satellite': 'SATELLITE',
|
||
'wifi': 'WIFI',
|
||
'bluetooth': 'BLUETOOTH',
|
||
'listening': 'LISTENING POST'
|
||
};
|
||
document.getElementById('activeModeIndicator').innerHTML = '<span class="pulse-dot"></span>' + modeNames[mode];
|
||
document.getElementById('wifiLayoutContainer').style.display = mode === 'wifi' ? 'flex' : 'none';
|
||
document.getElementById('btLayoutContainer').style.display = mode === 'bluetooth' ? 'flex' : 'none';
|
||
// Respect the "Show Radar Display" checkbox for aircraft mode
|
||
const showRadar = document.getElementById('adsbEnableMap').checked;
|
||
document.getElementById('aircraftVisuals').style.display = (mode === 'aircraft' && showRadar) ? 'grid' : 'none';
|
||
document.getElementById('satelliteVisuals').style.display = mode === 'satellite' ? 'block' : 'none';
|
||
document.getElementById('listeningPostVisuals').style.display = mode === 'listening' ? 'grid' : 'none';
|
||
|
||
// Update output panel title based on mode
|
||
const titles = {
|
||
'pager': 'Pager Decoder',
|
||
'sensor': '433MHz Sensor Monitor',
|
||
'aircraft': 'ADS-B Aircraft Tracker',
|
||
'satellite': 'Satellite Monitor',
|
||
'wifi': 'WiFi Scanner',
|
||
'bluetooth': 'Bluetooth Scanner',
|
||
'listening': 'Listening Post'
|
||
};
|
||
document.getElementById('outputTitle').textContent = titles[mode] || 'Signal Monitor';
|
||
|
||
// Show/hide Device Intelligence for modes that use it (not for satellite/aircraft)
|
||
const reconBtn = document.getElementById('reconBtn');
|
||
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
|
||
if (mode === 'satellite' || mode === 'aircraft' || mode === 'listening') {
|
||
document.getElementById('reconPanel').style.display = 'none';
|
||
if (reconBtn) reconBtn.style.display = 'none';
|
||
if (intelBtn) intelBtn.style.display = 'none';
|
||
} else {
|
||
if (reconBtn) reconBtn.style.display = 'inline-block';
|
||
if (intelBtn) intelBtn.style.display = 'inline-block';
|
||
// Restore panel visibility based on reconEnabled state
|
||
if (reconEnabled) {
|
||
document.getElementById('reconPanel').style.display = 'block';
|
||
}
|
||
}
|
||
|
||
// Show RTL-SDR device section for modes that use it
|
||
document.getElementById('rtlDeviceSection').style.display = (mode === 'pager' || mode === 'sensor' || mode === 'aircraft') ? 'block' : 'none';
|
||
|
||
// Toggle mode-specific tool status displays
|
||
document.getElementById('toolStatusPager').style.display = (mode === 'pager') ? 'grid' : 'none';
|
||
document.getElementById('toolStatusSensor').style.display = (mode === 'sensor') ? 'grid' : 'none';
|
||
document.getElementById('toolStatusAircraft').style.display = (mode === 'aircraft') ? 'grid' : 'none';
|
||
|
||
// Hide waterfall and output console for modes with their own visualizations
|
||
document.querySelector('.waterfall-container').style.display = (mode === 'satellite' || mode === 'listening' || mode === 'aircraft' || mode === 'wifi') ? 'none' : 'block';
|
||
document.getElementById('output').style.display = (mode === 'satellite' || mode === 'aircraft' || mode === 'wifi') ? 'none' : 'block';
|
||
document.querySelector('.status-bar').style.display = (mode === 'satellite') ? 'none' : 'flex';
|
||
|
||
// Load interfaces and initialize visualizations when switching modes
|
||
if (mode === 'wifi') {
|
||
refreshWifiInterfaces();
|
||
initRadar();
|
||
initWatchList();
|
||
} else if (mode === 'bluetooth') {
|
||
refreshBtInterfaces();
|
||
initBtRadar();
|
||
} else if (mode === 'aircraft') {
|
||
checkAdsbTools();
|
||
initAircraftRadar();
|
||
} else if (mode === 'satellite') {
|
||
initPolarPlot();
|
||
initSatelliteList();
|
||
}
|
||
}
|
||
|
||
// Track unique sensor devices
|
||
let uniqueDevices = new Set();
|
||
|
||
// Sensor frequency
|
||
function setSensorFreq(freq) {
|
||
document.getElementById('sensorFrequency').value = freq;
|
||
if (isSensorRunning) {
|
||
fetch('/stop_sensor', {method: 'POST'})
|
||
.then(() => setTimeout(() => startSensorDecoding(), 500));
|
||
}
|
||
}
|
||
|
||
// Start sensor decoding
|
||
function startSensorDecoding() {
|
||
const freq = document.getElementById('sensorFrequency').value;
|
||
const gain = document.getElementById('sensorGain').value;
|
||
const ppm = document.getElementById('sensorPpm').value;
|
||
const device = getSelectedDevice();
|
||
|
||
// Check if device is available
|
||
if (!checkDeviceAvailability('sensor')) {
|
||
return;
|
||
}
|
||
|
||
// Check for remote SDR
|
||
const remoteConfig = getRemoteSDRConfig();
|
||
if (remoteConfig === false) return; // Validation failed
|
||
|
||
const config = {
|
||
frequency: freq,
|
||
gain: gain,
|
||
ppm: ppm,
|
||
device: device,
|
||
sdr_type: getSelectedSDRType(),
|
||
bias_t: getBiasTEnabled()
|
||
};
|
||
|
||
// Add rtl_tcp params if using remote SDR
|
||
if (remoteConfig) {
|
||
config.rtl_tcp_host = remoteConfig.host;
|
||
config.rtl_tcp_port = remoteConfig.port;
|
||
}
|
||
|
||
fetch('/start_sensor', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(config)
|
||
}).then(r => r.json())
|
||
.then(data => {
|
||
if (data.status === 'started') {
|
||
reserveDevice(parseInt(device), 'sensor');
|
||
setSensorRunning(true);
|
||
startSensorStream();
|
||
} else {
|
||
alert('Error: ' + data.message);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Stop sensor decoding
|
||
function stopSensorDecoding() {
|
||
fetch('/stop_sensor', {method: 'POST'})
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
releaseDevice('sensor');
|
||
setSensorRunning(false);
|
||
if (eventSource) {
|
||
eventSource.close();
|
||
eventSource = null;
|
||
}
|
||
});
|
||
}
|
||
|
||
function setSensorRunning(running) {
|
||
isSensorRunning = running;
|
||
document.getElementById('statusDot').classList.toggle('running', running);
|
||
document.getElementById('statusText').textContent = running ? 'Listening...' : 'Idle';
|
||
document.getElementById('startSensorBtn').style.display = running ? 'none' : 'block';
|
||
document.getElementById('stopSensorBtn').style.display = running ? 'block' : 'none';
|
||
}
|
||
|
||
function startSensorStream() {
|
||
if (eventSource) {
|
||
eventSource.close();
|
||
}
|
||
|
||
eventSource = new EventSource('/stream_sensor');
|
||
|
||
eventSource.onopen = function() {
|
||
showInfo('Sensor stream connected...');
|
||
};
|
||
|
||
eventSource.onmessage = function(e) {
|
||
const data = JSON.parse(e.data);
|
||
if (data.type === 'sensor') {
|
||
addSensorReading(data);
|
||
} else if (data.type === 'status') {
|
||
if (data.text === 'stopped') {
|
||
setSensorRunning(false);
|
||
}
|
||
} else if (data.type === 'info' || data.type === 'raw') {
|
||
showInfo(data.text);
|
||
}
|
||
};
|
||
|
||
eventSource.onerror = function(e) {
|
||
console.error('Sensor stream error');
|
||
};
|
||
}
|
||
|
||
function addSensorReading(data) {
|
||
const output = document.getElementById('output');
|
||
const placeholder = output.querySelector('.placeholder');
|
||
if (placeholder) placeholder.remove();
|
||
|
||
// Store for export
|
||
allMessages.push(data);
|
||
playAlert();
|
||
pulseSignal();
|
||
addWaterfallPoint(Date.now(), 0.8);
|
||
|
||
sensorCount++;
|
||
document.getElementById('sensorCount').textContent = sensorCount;
|
||
|
||
// Track unique devices by model + id
|
||
const deviceKey = (data.model || 'Unknown') + '_' + (data.id || data.channel || '0');
|
||
if (!uniqueDevices.has(deviceKey)) {
|
||
uniqueDevices.add(deviceKey);
|
||
document.getElementById('deviceCount').textContent = uniqueDevices.size;
|
||
}
|
||
|
||
const card = document.createElement('div');
|
||
card.className = 'sensor-card';
|
||
|
||
let dataItems = '';
|
||
const skipKeys = ['type', 'time', 'model', 'raw'];
|
||
for (const [key, value] of Object.entries(data)) {
|
||
if (!skipKeys.includes(key) && value !== null && value !== undefined) {
|
||
const label = key.replace(/_/g, ' ');
|
||
let displayValue = value;
|
||
if (key === 'temperature_C') displayValue = value + ' °C';
|
||
else if (key === 'temperature_F') displayValue = value + ' °F';
|
||
else if (key === 'humidity') displayValue = value + ' %';
|
||
else if (key === 'pressure_hPa') displayValue = value + ' hPa';
|
||
else if (key === 'wind_avg_km_h') displayValue = value + ' km/h';
|
||
else if (key === 'rain_mm') displayValue = value + ' mm';
|
||
else if (key === 'battery_ok') displayValue = value ? 'OK' : 'Low';
|
||
|
||
dataItems += '<div class="data-item"><div class="data-label">' + label + '</div><div class="data-value">' + displayValue + '</div></div>';
|
||
}
|
||
}
|
||
|
||
const relTime = data.time ? getRelativeTime(data.time.split(' ')[1] || data.time) : 'now';
|
||
|
||
card.innerHTML =
|
||
'<div class="header" style="display: flex; justify-content: space-between; margin-bottom: 8px;">' +
|
||
'<span class="device-name">' + (data.model || 'Unknown Device') + '</span>' +
|
||
'<span class="msg-time" data-timestamp="' + (data.time || '') + '" style="color: #444; font-size: 10px;">' + relTime + '</span>' +
|
||
'</div>' +
|
||
'<div class="sensor-data">' + dataItems + '</div>';
|
||
|
||
output.insertBefore(card, output.firstChild);
|
||
|
||
if (autoScroll) output.scrollTop = 0;
|
||
while (output.children.length > 100) {
|
||
output.removeChild(output.lastChild);
|
||
}
|
||
}
|
||
|
||
function toggleSensorLogging() {
|
||
const enabled = document.getElementById('sensorLogging').checked;
|
||
fetch('/logging', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({enabled: enabled, log_file: 'sensor_data.log'})
|
||
});
|
||
}
|
||
|
||
// Audio alert settings
|
||
let audioMuted = localStorage.getItem('audioMuted') === 'true';
|
||
let audioContext = null;
|
||
|
||
function initAudio() {
|
||
if (!audioContext) {
|
||
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||
}
|
||
}
|
||
|
||
function playAlert() {
|
||
if (audioMuted || !audioContext) return;
|
||
const oscillator = audioContext.createOscillator();
|
||
const gainNode = audioContext.createGain();
|
||
oscillator.connect(gainNode);
|
||
gainNode.connect(audioContext.destination);
|
||
oscillator.frequency.value = 880;
|
||
oscillator.type = 'sine';
|
||
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
|
||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
|
||
oscillator.start(audioContext.currentTime);
|
||
oscillator.stop(audioContext.currentTime + 0.2);
|
||
}
|
||
|
||
function toggleMute() {
|
||
audioMuted = !audioMuted;
|
||
localStorage.setItem('audioMuted', audioMuted);
|
||
updateMuteButton();
|
||
}
|
||
|
||
function updateMuteButton() {
|
||
const btn = document.getElementById('muteBtn');
|
||
if (btn) {
|
||
btn.innerHTML = audioMuted ? '🔇 UNMUTE' : '🔊 MUTE';
|
||
btn.classList.toggle('muted', audioMuted);
|
||
}
|
||
}
|
||
|
||
// Message storage for export
|
||
let allMessages = [];
|
||
|
||
function exportCSV() {
|
||
if (allMessages.length === 0) {
|
||
alert('No messages to export');
|
||
return;
|
||
}
|
||
const headers = ['Timestamp', 'Protocol', 'Address', 'Function', 'Type', 'Message'];
|
||
const csv = [headers.join(',')];
|
||
allMessages.forEach(msg => {
|
||
const row = [
|
||
msg.timestamp || '',
|
||
msg.protocol || '',
|
||
msg.address || '',
|
||
msg.function || '',
|
||
msg.msg_type || '',
|
||
'"' + (msg.message || '').replace(/"/g, '""') + '"'
|
||
];
|
||
csv.push(row.join(','));
|
||
});
|
||
downloadFile(csv.join('\n'), 'intercept_messages.csv', 'text/csv');
|
||
}
|
||
|
||
function exportJSON() {
|
||
if (allMessages.length === 0) {
|
||
alert('No messages to export');
|
||
return;
|
||
}
|
||
downloadFile(JSON.stringify(allMessages, null, 2), 'intercept_messages.json', 'application/json');
|
||
}
|
||
|
||
function downloadFile(content, filename, type) {
|
||
const blob = new Blob([content], { type });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = filename;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
// Auto-scroll setting
|
||
let autoScroll = localStorage.getItem('autoScroll') !== 'false';
|
||
|
||
function toggleAutoScroll() {
|
||
autoScroll = !autoScroll;
|
||
localStorage.setItem('autoScroll', autoScroll);
|
||
updateAutoScrollButton();
|
||
}
|
||
|
||
function updateAutoScrollButton() {
|
||
const btn = document.getElementById('autoScrollBtn');
|
||
if (btn) {
|
||
btn.innerHTML = autoScroll ? '⬇ AUTO-SCROLL ON' : '⬇ AUTO-SCROLL OFF';
|
||
btn.classList.toggle('active', autoScroll);
|
||
}
|
||
}
|
||
|
||
// Signal activity meter
|
||
let signalActivity = 0;
|
||
let lastMessageTime = 0;
|
||
|
||
function updateSignalMeter() {
|
||
const now = Date.now();
|
||
const timeSinceLastMsg = now - lastMessageTime;
|
||
|
||
// Decay signal activity over time
|
||
if (timeSinceLastMsg > 1000) {
|
||
signalActivity = Math.max(0, signalActivity - 0.05);
|
||
}
|
||
|
||
const meter = document.getElementById('signalMeter');
|
||
const bars = meter?.querySelectorAll('.signal-bar');
|
||
if (bars) {
|
||
const activeBars = Math.ceil(signalActivity * bars.length);
|
||
bars.forEach((bar, i) => {
|
||
bar.classList.toggle('active', i < activeBars);
|
||
});
|
||
}
|
||
}
|
||
|
||
function pulseSignal() {
|
||
signalActivity = Math.min(1, signalActivity + 0.4);
|
||
lastMessageTime = Date.now();
|
||
|
||
// Flash waterfall canvas
|
||
const canvas = document.getElementById('waterfallCanvas');
|
||
if (canvas) {
|
||
canvas.classList.add('active');
|
||
setTimeout(() => canvas.classList.remove('active'), 500);
|
||
}
|
||
}
|
||
|
||
// Waterfall display
|
||
const waterfallData = [];
|
||
const maxWaterfallRows = 50;
|
||
|
||
function addWaterfallPoint(timestamp, intensity) {
|
||
waterfallData.push({ time: timestamp, intensity });
|
||
if (waterfallData.length > maxWaterfallRows * 100) {
|
||
waterfallData.shift();
|
||
}
|
||
renderWaterfall();
|
||
}
|
||
|
||
function renderWaterfall() {
|
||
const canvas = document.getElementById('waterfallCanvas');
|
||
if (!canvas) return;
|
||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||
const width = canvas.width;
|
||
const height = canvas.height;
|
||
|
||
// Shift existing image down
|
||
const imageData = ctx.getImageData(0, 0, width, height - 2);
|
||
ctx.putImageData(imageData, 0, 2);
|
||
|
||
// Draw new row at top
|
||
ctx.fillStyle = '#000';
|
||
ctx.fillRect(0, 0, width, 2);
|
||
|
||
// Add activity markers
|
||
const now = Date.now();
|
||
const recentData = waterfallData.filter(d => now - d.time < 100);
|
||
recentData.forEach(d => {
|
||
const x = Math.random() * width;
|
||
const hue = 180 + (d.intensity * 60); // cyan to green
|
||
ctx.fillStyle = `hsla(${hue}, 100%, 50%, ${d.intensity})`;
|
||
ctx.fillRect(x - 2, 0, 4, 2);
|
||
});
|
||
}
|
||
|
||
// Relative timestamps
|
||
function getRelativeTime(timestamp) {
|
||
if (!timestamp) return '';
|
||
const now = new Date();
|
||
const parts = timestamp.split(':');
|
||
const msgTime = new Date();
|
||
msgTime.setHours(parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2]));
|
||
|
||
const diff = Math.floor((now - msgTime) / 1000);
|
||
if (diff < 5) return 'just now';
|
||
if (diff < 60) return diff + 's ago';
|
||
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
||
return timestamp;
|
||
}
|
||
|
||
function updateRelativeTimes() {
|
||
document.querySelectorAll('.msg-time').forEach(el => {
|
||
const ts = el.dataset.timestamp;
|
||
if (ts) el.textContent = getRelativeTime(ts);
|
||
});
|
||
}
|
||
|
||
// Update timers
|
||
setInterval(updateSignalMeter, 100);
|
||
setInterval(updateRelativeTimes, 10000);
|
||
|
||
// Default presets (UK frequencies)
|
||
const defaultPresets = ['153.350', '153.025'];
|
||
|
||
// Load presets from localStorage or use defaults
|
||
function loadPresets() {
|
||
const saved = localStorage.getItem('pagerPresets');
|
||
return saved ? JSON.parse(saved) : [...defaultPresets];
|
||
}
|
||
|
||
function savePresets(presets) {
|
||
localStorage.setItem('pagerPresets', JSON.m�� |