mirror of
https://github.com/smittix/intercept.git
synced 2026-05-03 10:57:58 -07:00
- Add --no-cache-dir and --timeout 120 to all pip calls to prevent hanging on corrupt/stale pip HTTP cache (cachecontrol .pyc issue) - Replace silent python -c import verification with pip show to avoid import-time side effects hanging the installer - Switch optional packages to --only-binary :all: to skip source compilation on Python versions without pre-built wheels (prevents gevent/numpy hangs) - Warn early when Python 3.13+ is detected that some packages may be skipped - Add ground track caching with 30-minute TTL to satellite route - Add live satellite position tracker background thread via SSE fanout - Add satellite_predict, satellite_telemetry, and satnogs utilities Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
16390 lines
846 KiB
HTML
16390 lines
846 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en" class="{% if offline_settings.tile_provider in ['cartodb_dark', 'cartodb_dark_cyan'] %}map-cyber-enabled{% endif %}">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>iNTERCEPT // See the Invisible</title>
|
||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||
<link rel="manifest" href="/static/manifest.json">
|
||
<meta name="theme-color" content="#0b1118">
|
||
<meta name="mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
|
||
<!-- Preconnect hints for CDN domains -->
|
||
{% if offline_settings.assets_source != 'local' %}
|
||
<link rel="preconnect" href="https://unpkg.com" crossorigin>
|
||
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
|
||
{% endif %}
|
||
{% if offline_settings.fonts_source != 'local' %}
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
{% endif %}
|
||
<link rel="preconnect" href="https://cartodb-basemaps-a.global.ssl.fastly.net" crossorigin>
|
||
<!-- Disclaimer gate - must accept before seeing welcome page -->
|
||
<script>
|
||
// Check BEFORE page renders - if disclaimer not accepted, hide welcome page
|
||
if (localStorage.getItem('disclaimerAccepted') !== 'true') {
|
||
document.write('<style id="disclaimer-gate">.welcome-overlay{display:none !important}</style>');
|
||
window._showDisclaimerOnLoad = true;
|
||
}
|
||
// If navigating with a mode param (e.g. /?mode=waterfall), hide welcome immediately
|
||
// to prevent flash of welcome screen before JS applies the mode
|
||
else if (new URLSearchParams(window.location.search).get('mode')) {
|
||
document.write('<style id="mode-gate">.welcome-overlay{display:none !important}</style>');
|
||
}
|
||
</script>
|
||
<script>
|
||
window.INTERCEPT_SHARED_OBSERVER_LOCATION = {{ shared_observer_location | tojson }};
|
||
window.INTERCEPT_DEFAULT_LAT = {{ default_latitude | tojson }};
|
||
window.INTERCEPT_DEFAULT_LON = {{ default_longitude | tojson }};
|
||
</script>
|
||
<!-- Fonts - Conditional CDN/Local loading -->
|
||
{% if offline_settings.fonts_source == 'local' %}
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
|
||
{% else %}
|
||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
{% endif %}
|
||
<!-- Leaflet CSS -->
|
||
{% if offline_settings.assets_source == 'local' %}
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/leaflet/leaflet.css') }}">
|
||
{% else %}
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="" />
|
||
{% endif %}
|
||
<!-- Core CSS -->
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/variables.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/layout.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-cards.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-timeline.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/activity-timeline.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/device-cards.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/proximity-viz.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ version }}&r=maptheme17">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/function-strip.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/toast.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/ux-platform.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-waveform.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/core/components.css') }}">
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/waterfall.css') }}?v={{ version }}&r=wfdeck19">
|
||
<!-- Deferred scripts - Leaflet, Chart.js, observer-location -->
|
||
<script defer src="{{ url_for('static', filename='js/core/observer-location.js') }}"></script>
|
||
{% if offline_settings.assets_source == 'local' %}
|
||
<script defer src="{{ url_for('static', filename='vendor/leaflet/leaflet.js') }}"></script>
|
||
<script defer src="{{ url_for('static', filename='vendor/leaflet-heat/leaflet-heat.js') }}"></script>
|
||
{% else %}
|
||
<script defer src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||
<script defer src="https://cdn.jsdelivr.net/npm/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
|
||
{% endif %}
|
||
{% if offline_settings.assets_source == 'local' %}
|
||
<script defer src="{{ url_for('static', filename='vendor/chartjs/chart.umd.min.js') }}"></script>
|
||
{% else %}
|
||
<script defer src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
||
{% endif %}
|
||
<script defer src="{{ url_for('static', filename='vendor/chartjs/chartjs-adapter-date-fns.bundle.min.js') }}"></script>
|
||
<script>
|
||
window.INTERCEPT_MODE_STYLE_MAP = {
|
||
aprs: "{{ url_for('static', filename='css/modes/aprs.css') }}",
|
||
tscm: "{{ url_for('static', filename='css/modes/tscm.css') }}",
|
||
spystations: "{{ url_for('static', filename='css/modes/spy-stations.css') }}",
|
||
meshtastic: "{{ url_for('static', filename='css/modes/meshtastic.css') }}",
|
||
sstv: "{{ url_for('static', filename='css/modes/sstv.css') }}",
|
||
weathersat: "{{ url_for('static', filename='css/modes/weather-satellite.css') }}",
|
||
sstv_general: "{{ url_for('static', filename='css/modes/sstv-general.css') }}",
|
||
gps: "{{ url_for('static', filename='css/modes/gps.css') }}",
|
||
subghz: "{{ url_for('static', filename='css/modes/subghz.css') }}?v={{ version }}&r=subghz_layout9",
|
||
bt_locate: "{{ url_for('static', filename='css/modes/bt_locate.css') }}?v={{ version }}&r=btlocate4",
|
||
wifi_locate: "{{ url_for('static', filename='css/modes/wifi_locate.css') }}?v={{ version }}&r=wflocate1",
|
||
spaceweather: "{{ url_for('static', filename='css/modes/space-weather.css') }}",
|
||
wefax: "{{ url_for('static', filename='css/modes/wefax.css') }}",
|
||
morse: "{{ url_for('static', filename='css/modes/morse.css') }}",
|
||
radiosonde: "{{ url_for('static', filename='css/modes/radiosonde.css') }}",
|
||
meteor: "{{ url_for('static', filename='css/modes/meteor.css') }}",
|
||
system: "{{ url_for('static', filename='css/modes/system.css') }}",
|
||
ook: "{{ url_for('static', filename='css/modes/ook.css') }}"
|
||
};
|
||
window.INTERCEPT_MODE_STYLE_LOADED = {};
|
||
window.INTERCEPT_MODE_STYLE_PROMISES = {};
|
||
window.ensureModeStyles = function(mode) {
|
||
const href = window.INTERCEPT_MODE_STYLE_MAP ? window.INTERCEPT_MODE_STYLE_MAP[mode] : null;
|
||
if (!href) return Promise.resolve();
|
||
if (window.INTERCEPT_MODE_STYLE_LOADED[href] === 'loaded') {
|
||
return Promise.resolve();
|
||
}
|
||
if (window.INTERCEPT_MODE_STYLE_PROMISES[href]) {
|
||
return window.INTERCEPT_MODE_STYLE_PROMISES[href];
|
||
}
|
||
const absHref = new URL(href, window.location.href).href;
|
||
const existing = Array.from(document.querySelectorAll('link[data-mode-style]'))
|
||
.find((link) => link.href === absHref);
|
||
if (existing && existing.sheet) {
|
||
window.INTERCEPT_MODE_STYLE_LOADED[href] = 'loaded';
|
||
return Promise.resolve();
|
||
}
|
||
window.INTERCEPT_MODE_STYLE_LOADED[href] = 'loading';
|
||
const link = existing || document.createElement('link');
|
||
if (!existing) {
|
||
link.rel = 'stylesheet';
|
||
link.href = href;
|
||
link.dataset.modeStyle = mode;
|
||
}
|
||
const promise = new Promise((resolve, reject) => {
|
||
const onLoad = () => {
|
||
window.INTERCEPT_MODE_STYLE_LOADED[href] = 'loaded';
|
||
delete window.INTERCEPT_MODE_STYLE_PROMISES[href];
|
||
resolve();
|
||
};
|
||
const onError = () => {
|
||
delete window.INTERCEPT_MODE_STYLE_LOADED[href];
|
||
delete window.INTERCEPT_MODE_STYLE_PROMISES[href];
|
||
try {
|
||
link.remove();
|
||
} catch (_) {}
|
||
reject(new Error(`failed to load mode stylesheet: ${mode}`));
|
||
};
|
||
link.addEventListener('load', onLoad, { once: true });
|
||
link.addEventListener('error', onError, { once: true });
|
||
if (existing) {
|
||
// Existing links may have finished loading before listeners attached.
|
||
if (existing.sheet) onLoad();
|
||
} else {
|
||
document.head.appendChild(link);
|
||
}
|
||
});
|
||
window.INTERCEPT_MODE_STYLE_PROMISES[href] = promise;
|
||
return promise;
|
||
};
|
||
// Start loading a deep-linked mode stylesheet as early as possible.
|
||
(function preloadQueryModeStyles() {
|
||
const queryMode = new URLSearchParams(window.location.search).get('mode');
|
||
const mode = queryMode === 'listening' ? 'waterfall' : queryMode;
|
||
if (!mode) return;
|
||
window.ensureModeStyles(mode).catch(() => {});
|
||
})();
|
||
// Warm remaining lazy mode styles in the background to avoid first-switch FOUC.
|
||
(function warmModeStylesInBackground() {
|
||
const modeMap = window.INTERCEPT_MODE_STYLE_MAP || {};
|
||
const queryMode = new URLSearchParams(window.location.search).get('mode');
|
||
const selectedMode = queryMode === 'listening' ? 'waterfall' : queryMode;
|
||
const modes = Object.keys(modeMap).filter((mode) => mode !== selectedMode);
|
||
if (!modes.length) return;
|
||
|
||
const warm = function () {
|
||
modes.forEach(function (mode, index) {
|
||
setTimeout(function () {
|
||
window.ensureModeStyles(mode).catch(() => {});
|
||
}, index * 40);
|
||
});
|
||
};
|
||
|
||
if (typeof window.requestIdleCallback === 'function') {
|
||
window.requestIdleCallback(warm, { timeout: 2000 });
|
||
} else {
|
||
setTimeout(warm, 600);
|
||
}
|
||
})();
|
||
</script>
|
||
<script>
|
||
window.INTERCEPT_MODE_SCRIPT_MAP = {
|
||
bluetooth: "{{ url_for('static', filename='js/modes/bluetooth.js') }}?v={{ version }}&r=btlocate2",
|
||
wifi: "{{ url_for('static', filename='js/modes/wifi.js') }}",
|
||
spystations: "{{ url_for('static', filename='js/modes/spy-stations.js') }}",
|
||
meshtastic: "{{ url_for('static', filename='js/modes/meshtastic.js') }}",
|
||
sstv: "{{ url_for('static', filename='js/modes/sstv.js') }}",
|
||
weathersat: "{{ url_for('static', filename='js/modes/weather-satellite.js') }}",
|
||
sstv_general: "{{ url_for('static', filename='js/modes/sstv-general.js') }}",
|
||
gps: "{{ url_for('static', filename='js/modes/gps.js') }}",
|
||
websdr: "{{ url_for('static', filename='js/modes/websdr.js') }}",
|
||
subghz: "{{ url_for('static', filename='js/modes/subghz.js') }}?v={{ version }}&r=subghz_layout9",
|
||
bt_locate: "{{ url_for('static', filename='js/modes/bt_locate.js') }}?v={{ version }}&r=btlocate4",
|
||
wifi_locate: "{{ url_for('static', filename='js/modes/wifi_locate.js') }}?v={{ version }}&r=wflocate1",
|
||
wefax: "{{ url_for('static', filename='js/modes/wefax.js') }}",
|
||
morse: "{{ url_for('static', filename='js/modes/morse.js') }}?v={{ version }}&r=morse_iq12",
|
||
ook: "{{ url_for('static', filename='js/modes/ook.js') }}?v={{ version }}&r=ook2",
|
||
spaceweather: "{{ url_for('static', filename='js/modes/space-weather.js') }}",
|
||
system: "{{ url_for('static', filename='js/modes/system.js') }}",
|
||
meteor: "{{ url_for('static', filename='js/modes/meteor.js') }}",
|
||
waterfall: "{{ url_for('static', filename='js/modes/waterfall.js') }}?v={{ version }}&r=wfdeck21"
|
||
};
|
||
window.INTERCEPT_MODE_SCRIPT_LOADED = {};
|
||
window.INTERCEPT_MODE_SCRIPT_PROMISES = {};
|
||
window.ensureModeScript = function(mode) {
|
||
var src = window.INTERCEPT_MODE_SCRIPT_MAP ? window.INTERCEPT_MODE_SCRIPT_MAP[mode] : null;
|
||
if (!src) return Promise.resolve();
|
||
if (window.INTERCEPT_MODE_SCRIPT_LOADED[src]) return Promise.resolve();
|
||
if (window.INTERCEPT_MODE_SCRIPT_PROMISES[src]) return window.INTERCEPT_MODE_SCRIPT_PROMISES[src];
|
||
var promise = new Promise(function(resolve, reject) {
|
||
var script = document.createElement('script');
|
||
script.src = src;
|
||
script.dataset.modeScript = mode;
|
||
script.onload = function() {
|
||
window.INTERCEPT_MODE_SCRIPT_LOADED[src] = true;
|
||
delete window.INTERCEPT_MODE_SCRIPT_PROMISES[src];
|
||
resolve();
|
||
};
|
||
script.onerror = function() {
|
||
delete window.INTERCEPT_MODE_SCRIPT_PROMISES[src];
|
||
reject(new Error('failed to load mode script: ' + mode));
|
||
};
|
||
(document.body || document.head).appendChild(script);
|
||
});
|
||
window.INTERCEPT_MODE_SCRIPT_PROMISES[src] = promise;
|
||
return promise;
|
||
};
|
||
// Preload script for deep-linked mode
|
||
(function preloadQueryModeScript() {
|
||
var queryMode = new URLSearchParams(window.location.search).get('mode');
|
||
var mode = queryMode === 'listening' ? 'waterfall' : queryMode;
|
||
if (!mode) return;
|
||
window.ensureModeScript(mode).catch(function() {});
|
||
})();
|
||
</script>
|
||
</head>
|
||
|
||
<body data-mode="pager">
|
||
<!-- Welcome Page -->
|
||
<div class="welcome-overlay" id="welcomePage">
|
||
<!-- Spinning Globe Background -->
|
||
<div class="globe-background">
|
||
<svg class="globe-svg" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
|
||
<!-- Outer circle -->
|
||
<circle cx="200" cy="200" r="180" fill="none" stroke="currentColor" stroke-width="0.5"/>
|
||
<!-- Equator -->
|
||
<ellipse cx="200" cy="200" rx="180" ry="40" fill="none" stroke="currentColor" stroke-width="0.5"/>
|
||
<!-- Latitude lines -->
|
||
<ellipse cx="200" cy="140" rx="145" ry="30" fill="none" stroke="currentColor" stroke-width="0.3"/>
|
||
<ellipse cx="200" cy="260" rx="145" ry="30" fill="none" stroke="currentColor" stroke-width="0.3"/>
|
||
<ellipse cx="200" cy="90" rx="85" ry="15" fill="none" stroke="currentColor" stroke-width="0.3"/>
|
||
<ellipse cx="200" cy="310" rx="85" ry="15" fill="none" stroke="currentColor" stroke-width="0.3"/>
|
||
<!-- Prime meridian -->
|
||
<ellipse cx="200" cy="200" rx="40" ry="180" fill="none" stroke="currentColor" stroke-width="0.5" class="meridian meridian-1"/>
|
||
<!-- Additional meridians -->
|
||
<ellipse cx="200" cy="200" rx="100" ry="180" fill="none" stroke="currentColor" stroke-width="0.3" class="meridian meridian-2"/>
|
||
<ellipse cx="200" cy="200" rx="150" ry="180" fill="none" stroke="currentColor" stroke-width="0.3" class="meridian meridian-3"/>
|
||
<!-- Rotating meridian group -->
|
||
<g class="rotating-meridians">
|
||
<ellipse cx="200" cy="200" rx="70" ry="180" fill="none" stroke="currentColor" stroke-width="0.3"/>
|
||
<ellipse cx="200" cy="200" rx="130" ry="180" fill="none" stroke="currentColor" stroke-width="0.3"/>
|
||
<ellipse cx="200" cy="200" rx="170" ry="180" fill="none" stroke="currentColor" stroke-width="0.2"/>
|
||
</g>
|
||
</svg>
|
||
</div>
|
||
<div class="welcome-container">
|
||
<!-- Header Section -->
|
||
<div class="welcome-header">
|
||
<div class="welcome-logo">
|
||
<svg width="60" height="60" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M15 30 Q5 50, 15 70" stroke="#00d4ff" stroke-width="3" fill="none"
|
||
stroke-linecap="round" opacity="0.5" class="signal-wave signal-wave-1" />
|
||
<path d="M22 35 Q14 50, 22 65" stroke="#00d4ff" stroke-width="2.5" fill="none"
|
||
stroke-linecap="round" opacity="0.7" class="signal-wave signal-wave-2" />
|
||
<path d="M29 40 Q23 50, 29 60" stroke="#00d4ff" stroke-width="2" fill="none"
|
||
stroke-linecap="round" class="signal-wave signal-wave-3" />
|
||
<path d="M85 30 Q95 50, 85 70" stroke="#00d4ff" stroke-width="3" fill="none"
|
||
stroke-linecap="round" opacity="0.5" class="signal-wave signal-wave-1" />
|
||
<path d="M78 35 Q86 50, 78 65" stroke="#00d4ff" stroke-width="2.5" fill="none"
|
||
stroke-linecap="round" opacity="0.7" class="signal-wave signal-wave-2" />
|
||
<path d="M71 40 Q77 50, 71 60" stroke="#00d4ff" stroke-width="2" fill="none"
|
||
stroke-linecap="round" class="signal-wave signal-wave-3" />
|
||
<circle cx="50" cy="22" r="6" fill="#00ff88" class="logo-dot" />
|
||
<rect x="44" y="35" width="12" height="45" rx="2" fill="#00d4ff" />
|
||
<rect x="38" y="35" width="24" height="4" rx="1" fill="#00d4ff" />
|
||
<rect x="38" y="76" width="24" height="4" rx="1" fill="#00d4ff" />
|
||
</svg>
|
||
</div>
|
||
<h1 class="welcome-title"><span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT</h1>
|
||
<p class="welcome-tagline">// See the Invisible</p>
|
||
<span class="welcome-version">v{{ version }}</span>
|
||
<button type="button" class="welcome-settings-btn" onclick="showSettings()" title="Settings" aria-label="Open settings">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<circle cx="12" cy="12" r="3"/>
|
||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Main Content Grid -->
|
||
<div class="welcome-content">
|
||
<!-- Left: Changelog -->
|
||
<div class="welcome-changelog">
|
||
<h2>What's New</h2>
|
||
{% for release in changelog[:2] %}
|
||
<div class="changelog-release">
|
||
<div class="changelog-version-header">
|
||
<span class="changelog-version">v{{ release.version }}</span>
|
||
<span class="changelog-date">{{ release.date }}</span>
|
||
</div>
|
||
<ul class="changelog-list">
|
||
{% for item in release.highlights %}
|
||
<li>{{ item }}</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<!-- Right: Mode Selection -->
|
||
<div class="welcome-modes">
|
||
<h2>Select Mode</h2>
|
||
|
||
<!-- Signals -->
|
||
<div class="mode-category">
|
||
<h3 class="mode-category-title"><span class="mode-category-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h4l3-8 3 16 3-8h4"/><path d="M22 12h-1"/><path d="M1 12h1"/></svg></span> Signals</h3>
|
||
<div class="mode-grid mode-grid-compact">
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('pager')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg></span>
|
||
<span class="mode-name">Pager</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('sensor')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg></span>
|
||
<span class="mode-name">433MHz</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('rtlamr')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg></span>
|
||
<span class="mode-name">Meters</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('subghz')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h6l3-9 3 18 3-9h5"/></svg></span>
|
||
<span class="mode-name">SubGHz</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('waterfall')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h4l3-8 3 16 3-8h4"/><path d="M2 18h20" opacity="0.5"/><path d="M2 21h20" opacity="0.3"/></svg></span>
|
||
<span class="mode-name">Waterfall</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('morse')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="2" y1="12" x2="5" y2="12"/><line x1="7" y1="12" x2="13" y2="12"/><line x1="15" y1="12" x2="18" y2="12"/><line x1="20" y1="12" x2="22" y2="12"/></svg></span>
|
||
<span class="mode-name">Morse</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('ook')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h3"/><path d="M19 12h3"/><rect x="5" y="8" width="4" height="8" rx="1"/><rect x="10" y="9" width="4" height="6" rx="1"/><rect x="15" y="7" width="4" height="10" rx="1"/></svg></span>
|
||
<span class="mode-name">OOK Decoder</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tracking -->
|
||
<div class="mode-category">
|
||
<h3 class="mode-category-title"><span class="mode-category-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg></span> Tracking</h3>
|
||
<div class="mode-grid mode-grid-compact">
|
||
<a href="/adsb/dashboard" class="mode-card mode-card-sm" style="text-decoration: none;">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16v-2l-8-5V3.5a1.5 1.5 0 0 0-3 0V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z"/></svg></span>
|
||
<span class="mode-name">Aircraft</span>
|
||
</a>
|
||
<a href="/ais/dashboard" class="mode-card mode-card-sm" style="text-decoration: none;">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18l2 2h14l2-2"/><path d="M5 18v-4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v4"/><path d="M12 12V6"/></svg></span>
|
||
<span class="mode-name">Vessels</span>
|
||
</a>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('aprs')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg></span>
|
||
<span class="mode-name">APRS</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('gps')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg></span>
|
||
<span class="mode-name">GPS</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('radiosonde')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v6"/><circle cx="12" cy="12" r="4"/><path d="M12 16v6"/><path d="M4.93 4.93l4.24 4.24"/><path d="M14.83 14.83l4.24 4.24"/></svg></span>
|
||
<span class="mode-name">Radiosonde</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Space -->
|
||
<div class="mode-category">
|
||
<h3 class="mode-category-title"><span class="mode-category-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg></span> Space</h3>
|
||
<div class="mode-grid mode-grid-compact">
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('satellite')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/><path d="m16 8 3-3"/><path d="M9 21a6 6 0 0 0-6-6"/></svg></span>
|
||
<span class="mode-name">Satellite</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('sstv')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M3 9h2"/><path d="M19 9h2"/><path d="M3 15h2"/><path d="M19 15h2"/></svg></span>
|
||
<span class="mode-name">ISS SSTV</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('weathersat')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span>
|
||
<span class="mode-name">Weather Sat</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('sstv_general')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg></span>
|
||
<span class="mode-name">HF SSTV</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('wefax')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg></span>
|
||
<span class="mode-name">WeFax</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('spaceweather')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></span>
|
||
<span class="mode-name">Space Wx</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Wireless -->
|
||
<div class="mode-category">
|
||
<h3 class="mode-category-title"><span class="mode-category-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span> Wireless</h3>
|
||
<div class="mode-grid mode-grid-compact">
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('wifi')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span>
|
||
<span class="mode-name">WiFi</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('bluetooth')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg></span>
|
||
<span class="mode-name">Bluetooth</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('bt_locate')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/><path d="M9.5 8.5l3 3 2-4-2 4-3 3"/></svg></span>
|
||
<span class="mode-name">BT Locate</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('wifi_locate')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/><circle cx="12" cy="10" r="2"/><path d="M12 14v-2"/></svg></span>
|
||
<span class="mode-name">WF Locate</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('meshtastic')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg></span>
|
||
<span class="mode-name">Meshtastic</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Intel -->
|
||
<div class="mode-category">
|
||
<h3 class="mode-category-title"><span class="mode-category-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg></span> Intel</h3>
|
||
<div class="mode-grid mode-grid-compact">
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('tscm')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
|
||
<span class="mode-name">TSCM</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('spystations')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><circle cx="12" cy="12" r="2"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg></span>
|
||
<span class="mode-name">Spy Stations</span>
|
||
</button>
|
||
<button class="mode-card mode-card-sm" onclick="selectMode('websdr')">
|
||
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg></span>
|
||
<span class="mode-name">WebSDR</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer -->
|
||
<div class="welcome-footer">
|
||
<p>Signal Intelligence & Counter Surveillance Platform</p>
|
||
<a href="https://www.smittix.net" target="_blank" rel="noopener noreferrer" class="welcome-footer-credit">By Smittix</a>
|
||
</div>
|
||
</div>
|
||
<div class="welcome-scanline"></div>
|
||
</div>
|
||
|
||
<!-- Disclaimer Modal -->
|
||
<div class="disclaimer-overlay" id="disclaimerModal" style="display: none;">
|
||
<div class="disclaimer-modal">
|
||
<div class="warning-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></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>
|
||
|
||
<!-- TSCM Device Details Modal -->
|
||
<div class="tscm-modal-overlay" id="tscmDeviceModal" style="display: none;"
|
||
onclick="if(event.target === this) closeTscmDeviceModal()">
|
||
<div class="tscm-modal">
|
||
<button class="tscm-modal-close" onclick="closeTscmDeviceModal()">×</button>
|
||
<div id="tscmDeviceModalContent"></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: var(--font-mono); 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>
|
||
<div class="header-left">
|
||
<!-- Hamburger Menu Button (Mobile) -->
|
||
<button class="hamburger-btn" id="hamburgerBtn" aria-label="Toggle navigation menu">
|
||
<span></span>
|
||
<span></span>
|
||
<span></span>
|
||
</button>
|
||
<a href="https://smittix.github.io/intercept" target="_blank" rel="noopener noreferrer" class="logo">
|
||
<svg width="50" height="50" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<!-- Signal brackets - left side -->
|
||
<path d="M15 30 Q5 50, 15 70" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round"
|
||
opacity="0.5" />
|
||
<path d="M22 35 Q14 50, 22 65" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round"
|
||
opacity="0.7" />
|
||
<path d="M29 40 Q23 50, 29 60" stroke="#00d4ff" stroke-width="2" fill="none" stroke-linecap="round" />
|
||
<!-- Signal brackets - right side -->
|
||
<path d="M85 30 Q95 50, 85 70" stroke="#00d4ff" stroke-width="3" fill="none" stroke-linecap="round"
|
||
opacity="0.5" />
|
||
<path d="M78 35 Q86 50, 78 65" stroke="#00d4ff" stroke-width="2.5" fill="none" stroke-linecap="round"
|
||
opacity="0.7" />
|
||
<path d="M71 40 Q77 50, 71 60" stroke="#00d4ff" stroke-width="2" fill="none" stroke-linecap="round" />
|
||
<!-- The 'i' letter -->
|
||
<!-- dot of i -->
|
||
<circle cx="50" cy="22" r="6" fill="#00ff88" />
|
||
<!-- stem of i with styled terminals -->
|
||
<rect x="44" y="35" width="12" height="45" rx="2" fill="#00d4ff" />
|
||
<!-- top terminal bar -->
|
||
<rect x="38" y="35" width="24" height="4" rx="1" fill="#00d4ff" />
|
||
<!-- bottom terminal bar -->
|
||
<rect x="38" y="76" width="24" height="4" rx="1" fill="#00d4ff" />
|
||
</svg>
|
||
</a>
|
||
<h1><span class="brand-i"><svg viewBox="36 14 28 68" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="20" r="6" fill="#00ff88"/><rect x="44" y="33" width="12" height="45" rx="2" fill="#00d4ff"/><rect x="38" y="33" width="24" height="4" rx="1" fill="#00d4ff"/><rect x="38" y="74" width="24" height="4" rx="1" fill="#00d4ff"/></svg></span>NTERCEPT <span class="tagline">// See the Invisible</span></h1>
|
||
</div>
|
||
<div class="header-right">
|
||
<span class="active-mode-indicator" id="activeModeIndicator"><span class="pulse-dot"></span>PAGER</span>
|
||
<span class="version-badge">v{{ version }}</span>
|
||
</div>
|
||
</header>
|
||
|
||
<div id="runStateStrip" class="run-state-strip" aria-live="polite">
|
||
<div class="run-state-left">
|
||
<span class="run-state-label">Run State</span>
|
||
<div id="runStateChips"></div>
|
||
</div>
|
||
<div class="run-state-right">
|
||
<span id="runStateSummary" class="run-state-value">Loading...</span>
|
||
<button id="runStateRefreshBtn" class="run-state-btn" type="button">Refresh</button>
|
||
<button id="runStateSettingsBtn" class="run-state-btn" type="button">Tools</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Mode Navigation Bar -->
|
||
{% set is_index_page = true %}
|
||
{% set active_mode = 'pager' %}
|
||
{% include 'partials/nav.html' with context %}
|
||
|
||
<!-- Mobile Drawer Overlay -->
|
||
<div class="drawer-overlay" id="drawerOverlay"></div>
|
||
|
||
<div class="container">
|
||
<div class="main-content">
|
||
<div class="sidebar mobile-drawer" id="mainSidebar">
|
||
<button class="sidebar-collapse-btn" id="sidebarCollapseBtn" onclick="toggleMainSidebarCollapse()" title="Collapse sidebar">
|
||
<span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg></span>
|
||
Collapse Sidebar
|
||
</button>
|
||
<!-- Agent Selector -->
|
||
<div class="section" id="agentSection">
|
||
<h3>Signal Source</h3>
|
||
<div class="form-group">
|
||
<label style="font-size: 11px; color: #888; margin-bottom: 4px;">Agent</label>
|
||
<div style="display: flex; align-items: center; gap: 8px;">
|
||
<select id="agentSelect" style="flex: 1;">
|
||
<option value="local">Local (This Device)</option>
|
||
</select>
|
||
<span id="agentStatusDot" class="agent-status-dot online" title="Agent status"></span>
|
||
</div>
|
||
</div>
|
||
<div id="agentInfo" class="info-text" style="font-size: 10px; color: #666; margin-top: 4px;">
|
||
<span id="agentStatusText">Local</span>
|
||
<span id="agentLatencyText" style="margin-left: 6px; color: var(--accent-cyan);"></span>
|
||
</div>
|
||
<!-- Agent health panel (shows all agents when expanded) -->
|
||
<details style="margin-top: 8px;">
|
||
<summary style="font-size: 10px; color: #888; cursor: pointer;">All Agents Health</summary>
|
||
<div id="agentHealthPanel" style="margin-top: 6px; padding: 6px; background: rgba(0,0,0,0.2); border-radius: 4px; max-height: 120px; overflow-y: auto;">
|
||
<div style="color: var(--text-muted); font-size: 11px;">Loading...</div>
|
||
</div>
|
||
</details>
|
||
<!-- Multi-agent mode toggle -->
|
||
<div class="form-group" style="margin-top: 10px;">
|
||
<label class="inline-checkbox" style="display: flex; align-items: center; gap: 8px;">
|
||
<input type="checkbox" id="showAllAgents" onchange="toggleMultiAgentMode()">
|
||
<span style="font-size: 11px;">Show All Agents Combined</span>
|
||
</label>
|
||
</div>
|
||
<a href="/controller/manage" class="preset-btn" style="display: block; text-align: center; text-decoration: none; margin-top: 8px; font-size: 11px;">
|
||
Manage Agents
|
||
</a>
|
||
</div>
|
||
|
||
<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="sdrplay">SDRplay</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 }}{% if device.serial and device.serial != 'N/A' and device.serial != 'Unknown' %} (SN: {{ device.serial }}){% endif %}</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>
|
||
<!-- Bias-T Power Toggle - Prominent Location -->
|
||
<div
|
||
style="display: flex; align-items: center; gap: 10px; padding: 8px; margin-bottom: 8px; background: linear-gradient(90deg, rgba(255,100,0,0.2), rgba(255,100,0,0.05)); border: 1px solid var(--accent-orange); border-radius: 4px;">
|
||
<input type="checkbox" id="biasT" onchange="saveBiasTSetting()"
|
||
style="width: 18px; height: 18px; accent-color: var(--accent-orange);">
|
||
<div>
|
||
<div style="color: var(--accent-orange); font-weight: bold; font-size: 12px;">Bias-T Power
|
||
</div>
|
||
<div style="color: #888; font-size: 9px;">Powers external LNA/preamp</div>
|
||
</div>
|
||
</div>
|
||
<button class="preset-btn" onclick="refreshDevices()" style="width: 100%;">
|
||
Refresh Devices
|
||
</button>
|
||
|
||
<!-- SDR Device Status -->
|
||
<div id="sdrStatusPanel" style="margin-top: 10px; border: 1px solid var(--border-color); border-radius: 4px;">
|
||
<div id="sdrStatusList" style="max-height: 150px; overflow-y: auto;"></div>
|
||
<div style="padding: 6px 8px; background: var(--bg-tertiary); border-top: 1px solid var(--border-color); font-size: 10px; color: #666;">
|
||
Auto-refreshes every 5s
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 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>
|
||
|
||
<div id="toolStatusPager" class="info-text tool-status-section">
|
||
<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">
|
||
<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>
|
||
|
||
{% include 'partials/modes/pager.html' %}
|
||
|
||
{% include 'partials/modes/sensor.html' %}
|
||
|
||
{% include 'partials/modes/rtlamr.html' %}
|
||
|
||
{% include 'partials/modes/wifi.html' %}
|
||
|
||
{% include 'partials/modes/bluetooth.html' %}
|
||
|
||
{% include 'partials/modes/aprs.html' %}
|
||
|
||
{% include 'partials/modes/satellite.html' %}
|
||
|
||
{% include 'partials/modes/sstv.html' %}
|
||
|
||
{% include 'partials/modes/weather-satellite.html' %}
|
||
|
||
{% include 'partials/modes/sstv-general.html' %}
|
||
|
||
{% include 'partials/modes/gps.html' %}
|
||
|
||
{% include 'partials/modes/wefax.html' %}
|
||
|
||
{% include 'partials/modes/morse.html' %}
|
||
|
||
{% include 'partials/modes/ook.html' %}
|
||
|
||
{% include 'partials/modes/space-weather.html' %}
|
||
|
||
{% include 'partials/modes/tscm.html' %}
|
||
|
||
{% include 'partials/modes/ais.html' %}
|
||
|
||
{% include 'partials/modes/radiosonde.html' %}
|
||
|
||
{% include 'partials/modes/spy-stations.html' %}
|
||
|
||
{% include 'partials/modes/meshtastic.html' %}
|
||
|
||
{% include 'partials/modes/websdr.html' %}
|
||
|
||
{% include 'partials/modes/subghz.html' %}
|
||
|
||
{% include 'partials/modes/bt_locate.html' %}
|
||
{% include 'partials/modes/wifi_locate.html' %}
|
||
{% include 'partials/modes/waterfall.html' %}
|
||
{% include 'partials/modes/meteor.html' %}
|
||
{% include 'partials/modes/system.html' %}
|
||
|
||
|
||
|
||
<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">
|
||
<button class="sidebar-expand-handle" id="sidebarExpandHandle" onclick="toggleMainSidebarCollapse()" title="Expand sidebar">
|
||
<span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg></span>
|
||
</button>
|
||
<div class="output-header">
|
||
<h3 id="outputTitle">Pager Decoder</h3>
|
||
<div class="header-controls">
|
||
<div class="stats" id="pagerStats">
|
||
<div title="Total Messages"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg></span> <span id="msgCount">0</span></div>
|
||
<div title="POCSAG Messages"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg></span> <span id="pocsagCount">0</span></div>
|
||
<div title="FLEX Messages"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="5" width="16" height="14" rx="2"/><line x1="8" y1="10" x2="16" y2="10"/><line x1="8" y1="14" x2="12" y2="14"/></svg></span> <span id="flexCount">0</span></div>
|
||
</div>
|
||
<div class="stats" id="sensorStats">
|
||
<div title="Unique Sensors"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="2"/><path d="M16.24 7.76a6 6 0 0 1 0 8.49m-8.48-.01a6 6 0 0 1 0-8.49"/></svg></span> <span id="sensorCount">0</span></div>
|
||
<div title="Device Types"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg></span> <span id="deviceCount">0</span></div>
|
||
</div>
|
||
<div class="stats" id="wifiStats">
|
||
<div title="Access Points"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/></svg></span> <span id="apCount">0</span></div>
|
||
<div title="Connected Clients"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></span> <span id="clientCount">0</span></div>
|
||
<div title="Captured Handshakes" style="color: var(--accent-green);"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m11 17 2 2a1 1 0 1 0 3-3"/><path d="m14 14 2.5 2.5a1 1 0 1 0 3-3l-3.88-3.88a3 3 0 0 0-4.24 0l-.88.88a1 1 0 1 1-3-3l2.81-2.81a5.79 5.79 0 0 1 7.06-.87l.47.28a2 2 0 0 0 1.42.25L21 4"/><path d="m21 3 1 11h-2"/><path d="M3 3 2 14l6.5 6.5a1 1 0 1 0 3-3"/><path d="M3 4h8"/></svg></span> <span id="handshakeCount">0</span></div>
|
||
<div style="color: var(--accent-orange); cursor: pointer;" onclick="showDroneDetails()"
|
||
title="Click: Drone details"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0 -6 0"/><path d="M3 9a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"/><path d="M17 9a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"/><path d="M3 15a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"/><path d="M17 15a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"/><path d="M9 9l-4 -1"/><path d="M15 9l4 -1"/><path d="M9 15l-4 1"/><path d="M15 15l4 1"/></svg></span> <span id="droneCount">0</span></div>
|
||
<div style="color: var(--accent-red); cursor: pointer;" onclick="showRogueApDetails()"
|
||
title="Click: Rogue AP details"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span> <span id="rogueApCount">0</span></div>
|
||
</div>
|
||
<div class="stats" id="satelliteStats">
|
||
<div title="Upcoming Passes"><span class="icon icon--sm"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 7L9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4-6 6"/></svg></span> <span id="passCount">0</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- WiFi Layout Container -->
|
||
<div class="wifi-layout-container" id="wifiLayoutContainer">
|
||
<!-- Status Bar -->
|
||
<div class="wifi-status-bar">
|
||
<div class="wifi-status-item">
|
||
<span class="wifi-status-label">Networks:</span>
|
||
<span class="wifi-status-value" id="wifiNetworkCount">0</span>
|
||
</div>
|
||
<div class="wifi-status-item">
|
||
<span class="wifi-status-label">Clients:</span>
|
||
<span class="wifi-status-value" id="wifiClientCount">0</span>
|
||
</div>
|
||
<div class="wifi-status-item">
|
||
<span class="wifi-status-label">Hidden:</span>
|
||
<span class="wifi-status-value" id="wifiHiddenCount">0</span>
|
||
</div>
|
||
<div class="wifi-status-item" id="wifiScanStatus">
|
||
<span class="wifi-status-indicator idle"></span>
|
||
<span>Ready</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main Content: 3-column layout -->
|
||
<div class="wifi-main-content">
|
||
<!-- LEFT: Networks Table -->
|
||
<div class="wifi-networks-panel">
|
||
<div class="wifi-networks-header">
|
||
<h5>Discovered Networks</h5>
|
||
<div class="wifi-network-filters" id="wifiNetworkFilters">
|
||
<button class="wifi-filter-btn active" data-filter="all">All</button>
|
||
<button class="wifi-filter-btn" data-filter="2.4">2.4G</button>
|
||
<button class="wifi-filter-btn" data-filter="5">5G</button>
|
||
<button class="wifi-filter-btn" data-filter="open">Open</button>
|
||
<button class="wifi-filter-btn" data-filter="hidden">Hidden</button>
|
||
</div>
|
||
</div>
|
||
<div class="wifi-networks-table-wrapper">
|
||
<table class="wifi-networks-table" id="wifiNetworkTable">
|
||
<thead>
|
||
<tr>
|
||
<th class="sortable" data-sort="essid">SSID</th>
|
||
<th class="sortable" data-sort="bssid">BSSID</th>
|
||
<th class="sortable" data-sort="channel">Ch</th>
|
||
<th class="sortable" data-sort="rssi">Signal</th>
|
||
<th class="sortable" data-sort="security">Security</th>
|
||
<th class="sortable" data-sort="clients">Clients</th>
|
||
<th class="col-agent sortable" data-sort="agent">Source</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="wifiNetworkTableBody">
|
||
<tr class="wifi-network-placeholder">
|
||
<td colspan="7">
|
||
<div class="placeholder-text">Start scanning to discover networks</div>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CENTER: Proximity Radar -->
|
||
<div class="wifi-radar-panel">
|
||
<h5>Proximity Radar</h5>
|
||
<div id="wifiProximityRadar" class="wifi-radar-container"></div>
|
||
<div class="wifi-zone-summary">
|
||
<div class="wifi-zone near">
|
||
<span class="wifi-zone-count" id="wifiZoneImmediate">0</span>
|
||
<span class="wifi-zone-label">Near</span>
|
||
</div>
|
||
<div class="wifi-zone mid">
|
||
<span class="wifi-zone-count" id="wifiZoneNear">0</span>
|
||
<span class="wifi-zone-label">Mid</span>
|
||
</div>
|
||
<div class="wifi-zone far">
|
||
<span class="wifi-zone-count" id="wifiZoneFar">0</span>
|
||
<span class="wifi-zone-label">Far</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RIGHT: Channel Analysis + Security -->
|
||
<div class="wifi-analysis-panel">
|
||
<div class="wifi-channel-section">
|
||
<h5>Channel Analysis</h5>
|
||
<div class="wifi-channel-tabs" id="wifiChannelBandTabs">
|
||
<button class="channel-band-tab active" data-band="2.4">2.4 GHz</button>
|
||
<button class="channel-band-tab" data-band="5">5 GHz</button>
|
||
</div>
|
||
<div id="wifiChannelChart" class="wifi-channel-chart"></div>
|
||
</div>
|
||
<div class="wifi-security-section">
|
||
<h5>Security Overview</h5>
|
||
<div class="wifi-security-stats">
|
||
<div class="wifi-security-item wpa3">
|
||
<span class="wifi-security-dot"></span>
|
||
<span>WPA3</span>
|
||
<span class="wifi-security-count" id="wpa3Count">0</span>
|
||
</div>
|
||
<div class="wifi-security-item wpa2">
|
||
<span class="wifi-security-dot"></span>
|
||
<span>WPA2</span>
|
||
<span class="wifi-security-count" id="wpa2Count">0</span>
|
||
</div>
|
||
<div class="wifi-security-item wep">
|
||
<span class="wifi-security-dot"></span>
|
||
<span>WEP</span>
|
||
<span class="wifi-security-count" id="wepCount">0</span>
|
||
</div>
|
||
<div class="wifi-security-item open">
|
||
<span class="wifi-security-dot"></span>
|
||
<span>Open</span>
|
||
<span class="wifi-security-count" id="openCount">0</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Detail Drawer (slides up on network selection) -->
|
||
<div class="wifi-detail-drawer" id="wifiDetailDrawer">
|
||
<div class="wifi-detail-header">
|
||
<div class="wifi-detail-title">
|
||
<span class="wifi-detail-essid" id="wifiDetailEssid">Network Name</span>
|
||
<span class="wifi-detail-bssid" id="wifiDetailBssid">00:00:00:00:00:00</span>
|
||
</div>
|
||
<button class="wfl-locate-btn" onclick="(function(){ var p={bssid: document.getElementById('wifiDetailBssid')?.textContent, ssid: document.getElementById('wifiDetailEssid')?.textContent}; if(typeof WiFiLocate!=='undefined'){WiFiLocate.handoff(p);return;} if(typeof switchMode==='function'){switchMode('wifi_locate').then(function(){if(typeof WiFiLocate!=='undefined')WiFiLocate.handoff(p);});} })()" title="Locate this AP">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 21.7C17.3 17 20 13 20 10a8 8 0 1 0-16 0c0 3 2.7 7 8 11.7z"/></svg>
|
||
Locate
|
||
</button>
|
||
<button class="wifi-detail-close" onclick="WiFiMode.closeDetail()">×</button>
|
||
</div>
|
||
<div class="wifi-detail-content" id="wifiDetailContent">
|
||
<div class="wifi-detail-grid">
|
||
<div class="wifi-detail-stat">
|
||
<span class="label">Signal</span>
|
||
<span class="value" id="wifiDetailRssi">--</span>
|
||
</div>
|
||
<div class="wifi-detail-stat">
|
||
<span class="label">Channel</span>
|
||
<span class="value" id="wifiDetailChannel">--</span>
|
||
</div>
|
||
<div class="wifi-detail-stat">
|
||
<span class="label">Band</span>
|
||
<span class="value" id="wifiDetailBand">--</span>
|
||
</div>
|
||
<div class="wifi-detail-stat">
|
||
<span class="label">Security</span>
|
||
<span class="value" id="wifiDetailSecurity">--</span>
|
||
</div>
|
||
<div class="wifi-detail-stat">
|
||
<span class="label">Cipher</span>
|
||
<span class="value" id="wifiDetailCipher">--</span>
|
||
</div>
|
||
<div class="wifi-detail-stat">
|
||
<span class="label">Vendor</span>
|
||
<span class="value" id="wifiDetailVendor">--</span>
|
||
</div>
|
||
<div class="wifi-detail-stat">
|
||
<span class="label">Clients</span>
|
||
<span class="value" id="wifiDetailClients">--</span>
|
||
</div>
|
||
<div class="wifi-detail-stat">
|
||
<span class="label">First Seen</span>
|
||
<span class="value" id="wifiDetailFirstSeen">--</span>
|
||
</div>
|
||
</div>
|
||
<div class="wifi-detail-clients" id="wifiDetailClientList" style="display: none;">
|
||
<h6>Connected Clients <span class="wifi-client-count-badge" id="wifiClientCountBadge"></span></h6>
|
||
<div class="wifi-client-list"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bluetooth Layout Container (visualizations left, device cards right) -->
|
||
<div class="bt-layout-container" id="btLayoutContainer">
|
||
<!-- Left: Bluetooth Visualizations -->
|
||
<div class="bt-visuals-column" id="btVisuals">
|
||
<!-- Device Detail Panel (always visible) -->
|
||
<div class="bt-detail-panel" id="btDetailPanel">
|
||
<div class="bt-detail-header">
|
||
<h5>Device Details</h5>
|
||
</div>
|
||
<div class="bt-detail-body">
|
||
<!-- Placeholder shown when no device selected -->
|
||
<div class="bt-detail-placeholder" id="btDetailPlaceholder">
|
||
<span>Select a device to view details</span>
|
||
</div>
|
||
<!-- Content shown when device is selected -->
|
||
<div class="bt-detail-content" id="btDetailContent" style="display: none;">
|
||
<div class="bt-detail-top-row">
|
||
<div class="bt-detail-identity">
|
||
<div class="bt-detail-name" id="btDetailName">Device Name</div>
|
||
<div class="bt-detail-address">
|
||
<span id="btDetailAddress">00:00:00:00:00:00</span>
|
||
<span class="bt-mac-cluster-badge" id="btDetailMacCluster" style="display:none;"></span>
|
||
</div>
|
||
</div>
|
||
<div class="bt-detail-rssi-display">
|
||
<span class="bt-detail-rssi-value" id="btDetailRssi">--</span>
|
||
<span class="bt-detail-rssi-unit">dBm</span>
|
||
</div>
|
||
</div>
|
||
<div class="bt-detail-badges" id="btDetailBadges"></div>
|
||
<div class="bt-detail-tracker-analysis" id="btDetailTrackerAnalysis" style="display: none;"></div>
|
||
<div class="bt-detail-grid">
|
||
<div class="bt-detail-stat">
|
||
<span class="bt-detail-stat-label">Manufacturer</span>
|
||
<span class="bt-detail-stat-value" id="btDetailMfr">--</span>
|
||
</div>
|
||
<div class="bt-detail-stat">
|
||
<span class="bt-detail-stat-label">Type</span>
|
||
<span class="bt-detail-stat-value" id="btDetailAddrType">--</span>
|
||
</div>
|
||
<div class="bt-detail-stat">
|
||
<span class="bt-detail-stat-label">Seen</span>
|
||
<span class="bt-detail-stat-value" id="btDetailSeen">--</span>
|
||
</div>
|
||
<div class="bt-detail-stat">
|
||
<span class="bt-detail-stat-label">Range</span>
|
||
<span class="bt-detail-stat-value" id="btDetailRange">--</span>
|
||
</div>
|
||
<div class="bt-detail-stat">
|
||
<span class="bt-detail-stat-label">Min/Max</span>
|
||
<span class="bt-detail-stat-value" id="btDetailRssiRange">--</span>
|
||
</div>
|
||
<div class="bt-detail-stat">
|
||
<span class="bt-detail-stat-label">First Seen</span>
|
||
<span class="bt-detail-stat-value" id="btDetailFirstSeen">--</span>
|
||
</div>
|
||
<div class="bt-detail-stat">
|
||
<span class="bt-detail-stat-label">Last Seen</span>
|
||
<span class="bt-detail-stat-value" id="btDetailLastSeen">--</span>
|
||
</div>
|
||
<div class="bt-detail-stat">
|
||
<span class="bt-detail-stat-label">Mfr ID</span>
|
||
<span class="bt-detail-stat-value" id="btDetailMfrId">--</span>
|
||
</div>
|
||
<div class="bt-detail-stat">
|
||
<span class="bt-detail-stat-label">TX Power</span>
|
||
<span class="bt-detail-stat-value" id="btDetailTxPower">--</span>
|
||
</div>
|
||
<div class="bt-detail-stat">
|
||
<span class="bt-detail-stat-label">Seen Rate</span>
|
||
<span class="bt-detail-stat-value" id="btDetailSeenRate">--</span>
|
||
</div>
|
||
<div class="bt-detail-stat">
|
||
<span class="bt-detail-stat-label">Stability</span>
|
||
<span class="bt-detail-stat-value" id="btDetailStability">--</span>
|
||
</div>
|
||
<div class="bt-detail-stat">
|
||
<span class="bt-detail-stat-label">Distance</span>
|
||
<span class="bt-detail-stat-value" id="btDetailDistance">--</span>
|
||
</div>
|
||
</div>
|
||
<!-- Service Data Inspector (collapsible) -->
|
||
<div class="bt-detail-service-inspector" id="btDetailServiceInspector" style="display:none;">
|
||
<div class="bt-inspector-toggle" onclick="BluetoothMode.toggleServiceInspector()">
|
||
<span class="bt-inspector-arrow" id="btInspectorArrow">▸</span> Raw Data
|
||
</div>
|
||
<div class="bt-inspector-content" id="btInspectorContent" style="display:none;">
|
||
</div>
|
||
</div>
|
||
<div class="bt-detail-bottom-row">
|
||
<div class="bt-detail-irk" id="btDetailIrk" style="display: none;">
|
||
<span class="bt-irk-badge">IRK</span>
|
||
<span class="bt-detail-irk-value" id="btDetailIrkValue" style="font-size:10px;color:var(--text-dim);font-family:var(--font-mono);margin-left:6px;word-break:break-all;"></span>
|
||
</div>
|
||
<div class="bt-detail-services" id="btDetailServices" style="display: none;">
|
||
<span class="bt-detail-services-list" id="btDetailServicesList"></span>
|
||
</div>
|
||
<button class="bt-detail-btn" id="btDetailWatchBtn" onclick="BluetoothMode.toggleWatchlist()">Watchlist</button>
|
||
<button class="bt-detail-btn" id="btDetailCopyBtn" onclick="BluetoothMode.copyAddress()">Copy</button>
|
||
<button class="bt-detail-btn bt-locate-btn" id="btDetailLocateBtn" onclick="BluetoothMode.locateDevice()">Locate</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Main area: Side panels + Radar -->
|
||
<div class="bt-main-area">
|
||
<!-- Left side panels -->
|
||
<div class="bt-side-panels">
|
||
<div class="wifi-visual-panel bt-side-panel bt-tracker-panel">
|
||
<h5>Tracker Detection</h5>
|
||
<div id="btTrackerList" class="bt-tracker-list">
|
||
<div class="app-collection-state is-empty">Monitoring for AirTags, Tiles...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Proximity Radar -->
|
||
<div class="wifi-visual-panel bt-radar-panel">
|
||
<h5>Proximity Radar</h5>
|
||
<div id="btProximityRadar" style="display: flex; justify-content: center; padding: 8px 0;"></div>
|
||
<div id="btRadarControls" style="display: flex; gap: 6px; justify-content: center; margin-top: 8px; flex-wrap: wrap;">
|
||
<button data-filter="newOnly" class="bt-radar-filter-btn">New Only</button>
|
||
<button data-filter="strongest" class="bt-radar-filter-btn">Strongest</button>
|
||
<button data-filter="unapproved" class="bt-radar-filter-btn">Unapproved</button>
|
||
<button id="btRadarPauseBtn">Pause</button>
|
||
</div>
|
||
<div id="btZoneSummary" class="bt-zone-summary">
|
||
<div class="bt-zone-card immediate">
|
||
<span id="btZoneImmediate" class="bt-zone-value">0</span>
|
||
<div class="bt-zone-label">Immediate</div>
|
||
</div>
|
||
<div class="bt-zone-card near">
|
||
<span id="btZoneNear" class="bt-zone-value">0</span>
|
||
<div class="bt-zone-label">Near</div>
|
||
</div>
|
||
<div class="bt-zone-card far">
|
||
<span id="btZoneFar" class="bt-zone-value">0</span>
|
||
<div class="bt-zone-label">Far</div>
|
||
</div>
|
||
</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="bt-list-summary" id="btListSummary">
|
||
<div class="bt-summary-item">
|
||
<span class="bt-summary-label">Total</span>
|
||
<span class="bt-summary-value" id="btSummaryTotal">0</span>
|
||
</div>
|
||
<div class="bt-summary-item">
|
||
<span class="bt-summary-label">New</span>
|
||
<span class="bt-summary-value" id="btSummaryNew">0</span>
|
||
</div>
|
||
<div class="bt-summary-item">
|
||
<span class="bt-summary-label">Trackers</span>
|
||
<span class="bt-summary-value" id="btSummaryTrackers">0</span>
|
||
</div>
|
||
<div class="bt-summary-item">
|
||
<span class="bt-summary-label">Strongest</span>
|
||
<span class="bt-summary-value" id="btSummaryStrongest">--</span>
|
||
</div>
|
||
</div>
|
||
<div class="bt-list-signal-strip">
|
||
<div class="bt-list-signal-title">Signal Distribution</div>
|
||
<div class="bt-signal-dist bt-signal-dist-compact" id="btSignalDist">
|
||
<div class="signal-range"><span>Strong</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</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</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>
|
||
<div class="bt-device-toolbar">
|
||
<input type="search" id="btDeviceSearch" class="bt-device-search" placeholder="Filter by name, MAC, manufacturer...">
|
||
</div>
|
||
<div class="bt-device-filters" id="btDeviceFilters">
|
||
<button class="bt-filter-btn active" data-filter="all">All</button>
|
||
<button class="bt-filter-btn" data-filter="new">New</button>
|
||
<button class="bt-filter-btn" data-filter="named">Named</button>
|
||
<button class="bt-filter-btn" data-filter="strong">Strong</button>
|
||
<button class="bt-filter-btn" data-filter="trackers">Trackers</button>
|
||
</div>
|
||
<div class="wifi-device-list-content" id="btDeviceListContent">
|
||
<div class="app-collection-state is-empty">Start scanning to discover Bluetooth devices</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bluetooth Device Detail Modal -->
|
||
<div id="btDeviceModal" class="bt-modal-overlay" style="display: none;">
|
||
<div class="bt-modal">
|
||
<div class="bt-modal-header">
|
||
<h4 id="btModalTitle">Device Details</h4>
|
||
<button class="bt-modal-close" onclick="BluetoothMode.closeModal()">×</button>
|
||
</div>
|
||
<div class="bt-modal-body" id="btModalBody">
|
||
<!-- Populated by JavaScript -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- APRS Visualizations -->
|
||
<div id="aprsVisuals" style="display: none; flex-direction: column; gap: 10px; flex: 1; padding: 10px; overflow: hidden; min-height: 0;">
|
||
<!-- APRS Function Bar -->
|
||
<div class="aprs-strip">
|
||
<div class="aprs-strip-inner">
|
||
<!-- Stats -->
|
||
<div class="strip-stat">
|
||
<span class="strip-value" id="aprsStripFreq">--</span>
|
||
<span class="strip-label">MHz</span>
|
||
</div>
|
||
<div class="strip-stat">
|
||
<span class="strip-value" id="aprsStripStations">0</span>
|
||
<span class="strip-label">STATIONS</span>
|
||
</div>
|
||
<div class="strip-stat">
|
||
<span class="strip-value" id="aprsStripPackets">0</span>
|
||
<span class="strip-label">PACKETS</span>
|
||
</div>
|
||
<div class="strip-stat signal-stat" id="aprsStripSignalStat">
|
||
<span class="strip-value" id="aprsStripSignal">--</span>
|
||
<span class="strip-label">SIGNAL</span>
|
||
</div>
|
||
<div class="strip-divider"></div>
|
||
<!-- Controls -->
|
||
<div class="strip-control">
|
||
<select id="aprsStripRegion" class="strip-select">
|
||
<option value="north_america">N. America (144.390)</option>
|
||
<option value="europe">Europe (144.800)</option>
|
||
<option value="uk">UK (144.800)</option>
|
||
<option value="australia">Australia (145.175)</option>
|
||
<option value="new_zealand">New Zealand (144.575)</option>
|
||
<option value="argentina">Argentina (144.930)</option>
|
||
<option value="brazil">Brazil (145.570)</option>
|
||
<option value="japan">Japan (144.640)</option>
|
||
<option value="china">China (144.640)</option>
|
||
<option value="iss">ISS (145.825)</option>
|
||
<option value="sonate2">SONATE-2 (145.825)</option>
|
||
<option value="custom">Custom Frequency</option>
|
||
</select>
|
||
</div>
|
||
<div class="strip-control" id="aprsStripCustomFreqControl" style="display: none;">
|
||
<span class="strip-input-label">FREQ (MHz)</span>
|
||
<input type="number" id="aprsStripCustomFreq" class="strip-input" placeholder="144.390" step="0.001" min="144" max="146">
|
||
</div>
|
||
<div class="strip-control">
|
||
<span class="strip-input-label">GAIN</span>
|
||
<input type="number" id="aprsStripGain" class="strip-input" value="40" min="0" max="50">
|
||
</div>
|
||
<div class="strip-divider"></div>
|
||
<!-- Tool Status Indicators -->
|
||
<div class="strip-tools">
|
||
<span class="strip-tool" id="aprsStripDirewolf" title="direwolf">DW</span>
|
||
<span class="strip-tool" id="aprsStripMultimon" title="multimon-ng">MM</span>
|
||
</div>
|
||
<span id="aprsGpsIndicator" class="gps-indicator" style="display: none;"
|
||
title="GPS connected via gpsd"><span class="gps-dot"></span> GPS</span>
|
||
<div class="strip-divider"></div>
|
||
<!-- Actions -->
|
||
<button type="button" class="strip-btn primary" id="aprsStripStartBtn"
|
||
onclick="startAprs()">
|
||
▶ START
|
||
</button>
|
||
<button type="button" class="strip-btn stop" id="aprsStripStopBtn" onclick="stopAprs()"
|
||
style="display: none;">
|
||
◼ STOP
|
||
</button>
|
||
<!-- Status -->
|
||
<div class="strip-status">
|
||
<div class="status-dot inactive" id="aprsStripDot"></div>
|
||
<span id="aprsStripStatus">STANDBY</span>
|
||
</div>
|
||
<div class="strip-time" id="aprsStripTime">--:--:-- UTC</div>
|
||
</div>
|
||
</div>
|
||
<!-- Top row: Map and Station List side by side -->
|
||
<div style="display: flex; gap: 10px; flex: 1; min-height: 0;">
|
||
<!-- Map Panel (larger) -->
|
||
<div class="wifi-visual-panel"
|
||
style="flex: 2; display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden;">
|
||
<h5
|
||
style="color: var(--accent-cyan); text-shadow: 0 0 10px var(--accent-cyan); padding: 0 10px; margin-bottom: 8px;">
|
||
APRS STATION MAP</h5>
|
||
<div class="aircraft-map-container" style="flex: 1; display: flex; flex-direction: column;">
|
||
<div class="map-header">
|
||
<span id="aprsMapTime">--:--:--</span>
|
||
<span id="aprsMapStatus">STANDBY</span>
|
||
</div>
|
||
<div id="aprsMap" style="flex: 1; min-height: 350px;"></div>
|
||
<div class="map-footer">
|
||
<span>STATIONS: <span id="aprsStationCount">0</span></span>
|
||
<span>PACKETS: <span id="aprsPacketCount">0</span></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Station List Panel -->
|
||
<div class="wifi-visual-panel"
|
||
style="flex: 1; min-width: 300px; max-width: 400px; display: flex; flex-direction: column; min-height: 0; overflow: hidden;">
|
||
<h5
|
||
style="color: var(--accent-green); text-shadow: 0 0 10px var(--accent-green); margin-bottom: 8px; flex-shrink: 0;">
|
||
STATION LIST</h5>
|
||
<div id="aprsFilterBarContainer" style="flex-shrink: 0;"></div>
|
||
<div id="aprsStationList" class="signal-cards-container" style="flex: 1; overflow-y: auto; font-size: 11px; gap: 8px; min-height: 0;">
|
||
<div class="signal-cards-placeholder" style="padding: 20px; text-align: center; color: var(--text-muted);">
|
||
No stations received yet
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bottom row: Packet Log -->
|
||
<div class="wifi-visual-panel" style="display: flex; flex-direction: column; max-height: 200px; flex-shrink: 0;">
|
||
<h5
|
||
style="color: var(--accent-orange); text-shadow: 0 0 10px var(--accent-orange); margin-bottom: 8px;">
|
||
PACKET LOG</h5>
|
||
<div id="aprsPacketLog"
|
||
style="flex: 1; overflow-y: auto; font-family: var(--font-mono); font-size: 10px; background: rgba(0,0,0,0.3); padding: 8px; border-radius: 4px;">
|
||
<div style="color: var(--text-muted);">Waiting for packets...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- GPS Receiver Dashboard -->
|
||
<div id="gpsVisuals" class="gps-visuals-container" style="display: none;">
|
||
<div class="gps-visuals-top">
|
||
<!-- Sky View Globe -->
|
||
<div class="gps-skyview-panel">
|
||
<h4>Satellite Globe View</h4>
|
||
<div class="gps-skyview-canvas-wrap" id="gpsSkyViewWrap">
|
||
<div id="gpsSkyGlobe" class="gps-sky-globe" aria-label="GPS satellite globe"></div>
|
||
<canvas id="gpsSkyCanvas" width="400" height="400" aria-label="GPS satellite sky fallback globe"></canvas>
|
||
<div class="gps-sky-overlay" id="gpsSkyOverlay" aria-hidden="true"></div>
|
||
</div>
|
||
<div class="gps-sky-hint">Drag to orbit globe | Scroll to zoom | Hover satellites for details</div>
|
||
<div class="gps-legend">
|
||
<div class="gps-legend-item"><span class="gps-legend-dot" style="background:#ffffff;"></span> Observer</div>
|
||
<div class="gps-legend-item"><span class="gps-legend-dot" style="background:#00d4ff;"></span> GPS</div>
|
||
<div class="gps-legend-item"><span class="gps-legend-dot" style="background:#00ff88;"></span> GLONASS</div>
|
||
<div class="gps-legend-item"><span class="gps-legend-dot" style="background:#ff8800;"></span> Galileo</div>
|
||
<div class="gps-legend-item"><span class="gps-legend-dot" style="background:#ff4466;"></span> BeiDou</div>
|
||
<div class="gps-legend-item"><span class="gps-legend-dot" style="background:#ffdd00;"></span> SBAS</div>
|
||
<div class="gps-legend-item"><span class="gps-legend-dot" style="background:#cc66ff;"></span> QZSS</div>
|
||
<div class="gps-legend-item"><span class="gps-legend-dot" style="background:#00d4ff;"></span> Used (bright)</div>
|
||
<div class="gps-legend-item"><span class="gps-legend-dot" style="background:rgba(0,212,255,0.45);"></span> Unused (dim)</div>
|
||
</div>
|
||
</div>
|
||
<!-- Position Info -->
|
||
<div class="gps-position-panel">
|
||
<h4>Position</h4>
|
||
<div class="gps-pos-big">
|
||
<div id="gpsVisPosLat">---</div>
|
||
<div id="gpsVisPosLon">---</div>
|
||
</div>
|
||
<div style="margin-top: 4px;">
|
||
<span class="gps-fix-badge no-fix" id="gpsVisFixBadge">NO FIX</span>
|
||
</div>
|
||
<div style="margin-top: 12px;">
|
||
<div class="gps-pos-row">
|
||
<span class="gps-pos-label">Altitude</span>
|
||
<span class="gps-pos-value" id="gpsVisPosAlt">---</span>
|
||
</div>
|
||
<div class="gps-pos-row">
|
||
<span class="gps-pos-label">Speed</span>
|
||
<span class="gps-pos-value" id="gpsVisPosSpeed">---</span>
|
||
</div>
|
||
<div class="gps-pos-row">
|
||
<span class="gps-pos-label">Heading</span>
|
||
<span class="gps-pos-value" id="gpsVisPosHeading">---</span>
|
||
</div>
|
||
<div class="gps-pos-row">
|
||
<span class="gps-pos-label">Climb</span>
|
||
<span class="gps-pos-value" id="gpsVisPosClimb">---</span>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: auto; padding-top: 12px; border-top: 1px solid var(--border-color);">
|
||
<div class="gps-pos-label">GPS TIME</div>
|
||
<div class="gps-pos-value" id="gpsVisTime" style="font-size: 14px; color: var(--accent-cyan);">---</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Signal Strength Bars -->
|
||
<div class="gps-signal-panel">
|
||
<h4>Signal Strength (SNR dB-Hz)</h4>
|
||
<div class="gps-signal-bars" id="gpsSignalBars"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Satellite Dashboard (Embedded) -->
|
||
<div id="satelliteVisuals" class="satellite-dashboard-embed" style="display: none;">
|
||
<iframe id="satelliteDashboardFrame" src="/satellite/dashboard?embedded=true" frameborder="0"
|
||
style="width: 100%; height: 100%; min-height: 700px; border: none; border-radius: 8px;"
|
||
allowfullscreen>
|
||
</iframe>
|
||
</div>
|
||
|
||
<!-- TSCM Dashboard -->
|
||
<div id="tscmVisuals" class="tscm-dashboard" style="display: none; padding: 16px;">
|
||
<!-- Legal Disclaimer Banner -->
|
||
<div class="tscm-legal-banner"
|
||
style="margin-bottom: 12px; padding: 8px 12px; background: rgba(74, 158, 255, 0.1); border: 1px solid rgba(74, 158, 255, 0.3); border-radius: 4px; font-size: 10px; color: #ffffff;">
|
||
<strong>TSCM Screening Tool:</strong> This system identifies wireless and RF anomalies.
|
||
Findings are indicators, NOT confirmed surveillance devices.
|
||
No content is intercepted or decoded. Professional verification required.
|
||
</div>
|
||
|
||
<!-- Active Meeting Banner (hidden by default) -->
|
||
<div id="tscmMeetingBanner" class="tscm-meeting-banner" style="display: none;">
|
||
<div class="meeting-indicator">
|
||
<span class="meeting-pulse"></span>
|
||
<span class="meeting-text">MEETING WINDOW ACTIVE</span>
|
||
</div>
|
||
<div class="meeting-info">
|
||
<span id="tscmMeetingBannerName"></span>
|
||
<span id="tscmMeetingBannerTime"></span>
|
||
</div>
|
||
<div class="meeting-actions">
|
||
<button class="preset-btn" onclick="tscmShowMeetingSummary()" style="font-size: 10px; padding: 6px 8px;">
|
||
Summary
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Capabilities Summary Bar -->
|
||
<div id="tscmCapabilitiesBar" class="tscm-capabilities-bar" style="display: none;">
|
||
<div class="cap-item" id="capWifi" title="WiFi Capability">
|
||
<span class="cap-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/></svg></span>
|
||
<span class="cap-status" id="capWifiStatus">--</span>
|
||
</div>
|
||
<div class="cap-item" id="capBt" title="Bluetooth Capability">
|
||
<span class="cap-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg></span>
|
||
<span class="cap-status" id="capBtStatus">--</span>
|
||
</div>
|
||
<div class="cap-item" id="capRf" title="RF/SDR Capability">
|
||
<span class="cap-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2 12c0-3 2-6 5-6s4 3 5 6c1 3 2 6 5 6s5-3 5-6"/></svg></span>
|
||
<span class="cap-status" id="capRfStatus">--</span>
|
||
</div>
|
||
<div class="cap-item" id="capRoot" title="Privilege Level">
|
||
<span class="cap-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></span>
|
||
<span class="cap-status" id="capRootStatus">--</span>
|
||
</div>
|
||
<div class="cap-limitations" id="capLimitations" onclick="tscmShowCapabilities()"
|
||
style="cursor: pointer;">
|
||
<span class="cap-warn icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>
|
||
<span id="capLimitationCount">0</span> limitations
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Baseline Health Indicator -->
|
||
<div id="tscmBaselineHealth" class="tscm-baseline-health" style="display: none;">
|
||
<span class="health-label">Baseline:</span>
|
||
<span class="health-name" id="baselineHealthName">--</span>
|
||
<span class="health-badge" id="baselineHealthBadge">--</span>
|
||
<span class="health-age" id="baselineHealthAge"></span>
|
||
</div>
|
||
|
||
<!-- Filters -->
|
||
<div class="tscm-filter-bar">
|
||
<div class="tscm-filter-group">
|
||
<label for="tscmFilterProtocol">Protocol</label>
|
||
<select id="tscmFilterProtocol">
|
||
<option value="all">All</option>
|
||
<option value="wifi">WiFi</option>
|
||
<option value="bluetooth">Bluetooth</option>
|
||
<option value="rf">RF</option>
|
||
</select>
|
||
</div>
|
||
<div class="tscm-filter-group">
|
||
<label for="tscmFilterRisk">Risk</label>
|
||
<select id="tscmFilterRisk">
|
||
<option value="all">All</option>
|
||
<option value="high_interest">High Interest</option>
|
||
<option value="review">Needs Review</option>
|
||
<option value="informational">Informational</option>
|
||
</select>
|
||
</div>
|
||
<div class="tscm-filter-group">
|
||
<label for="tscmFilterStatus">Status</label>
|
||
<select id="tscmFilterStatus">
|
||
<option value="all">All</option>
|
||
<option value="new">New</option>
|
||
<option value="baseline">Baseline</option>
|
||
</select>
|
||
</div>
|
||
<div class="tscm-filter-group">
|
||
<label for="tscmFilterKnown">Known</label>
|
||
<select id="tscmFilterKnown">
|
||
<option value="all">All</option>
|
||
<option value="known">Known</option>
|
||
<option value="unknown">Unknown</option>
|
||
</select>
|
||
</div>
|
||
<div class="tscm-filter-status" id="tscmFilterStatusText">Filters: none</div>
|
||
</div>
|
||
|
||
<!-- Risk Summary Banner (new scoring model) - clickable cards -->
|
||
<div class="tscm-threat-banner">
|
||
<div class="threat-card critical clickable" id="tscmHighInterestCard"
|
||
onclick="showDevicesByCategory('high_interest')"
|
||
title="Click to view high interest devices">
|
||
<span class="count" id="tscmHighInterestCount">0</span>
|
||
<span class="label">High Interest</span>
|
||
</div>
|
||
<div class="threat-card high clickable" id="tscmNeedsReviewCard"
|
||
onclick="showDevicesByCategory('review')" title="Click to view devices needing review">
|
||
<span class="count" id="tscmNeedsReviewCount">0</span>
|
||
<span class="label">Needs Review</span>
|
||
</div>
|
||
<div class="threat-card low clickable" id="tscmInformationalCard"
|
||
onclick="showDevicesByCategory('informational')"
|
||
title="Click to view informational devices">
|
||
<span class="count" id="tscmInformationalCount">0</span>
|
||
<span class="label">Informational</span>
|
||
</div>
|
||
<div class="threat-card medium clickable" id="tscmCorrelationsCard"
|
||
onclick="showDevicesByCategory('correlations')" title="Click to view correlations">
|
||
<span class="count" id="tscmCorrelationsCount">0</span>
|
||
<span class="label">Correlations</span>
|
||
</div>
|
||
<div class="threat-card medium clickable" id="tscmIdentityCard"
|
||
onclick="showDevicesByCategory('identity')" title="Click to view identity clusters">
|
||
<span class="count" id="tscmIdentityCount">0</span>
|
||
<span class="label">Identity Clusters</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sweep Summary (shown after sweep completes) -->
|
||
<div id="tscmSweepSummary" style="display: none; margin-bottom: 16px;"></div>
|
||
|
||
<!-- Signal Activity Timeline -->
|
||
<div id="tscmTimelineContainer" style="margin-bottom: 16px;"></div>
|
||
|
||
<!-- Cross-Protocol Correlations (shown when correlations found) -->
|
||
<div id="tscmCorrelationsContainer" style="display: none;"></div>
|
||
|
||
<!-- Main Content Grid -->
|
||
<div class="tscm-main-grid">
|
||
<!-- WiFi Panel -->
|
||
<div class="tscm-panel" id="tscmWifiPanel">
|
||
<div class="tscm-panel-header">
|
||
WiFi Networks
|
||
<span class="badge" id="tscmWifiCount">0</span>
|
||
</div>
|
||
<div class="tscm-panel-content" id="tscmWifiList">
|
||
<div class="tscm-empty">Start a sweep to scan for WiFi networks</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- WiFi Clients Panel -->
|
||
<div class="tscm-panel" id="tscmWifiClientPanel">
|
||
<div class="tscm-panel-header">
|
||
WiFi Clients
|
||
<span class="badge" id="tscmWifiClientCount">0</span>
|
||
</div>
|
||
<div class="tscm-panel-content" id="tscmWifiClientList">
|
||
<div class="tscm-empty">Start a sweep to scan for WiFi clients</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bluetooth Panel -->
|
||
<div class="tscm-panel" id="tscmBtPanel">
|
||
<div class="tscm-panel-header">
|
||
Bluetooth Devices
|
||
<span class="badge" id="tscmBtCount">0</span>
|
||
</div>
|
||
<div class="tscm-panel-content" id="tscmBtList">
|
||
<div class="tscm-empty">Start a sweep to scan for Bluetooth devices</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- RF Signals Panel -->
|
||
<div class="tscm-panel" id="tscmRfPanel">
|
||
<div class="tscm-panel-header">
|
||
RF Signals
|
||
<span class="badge" id="tscmRfCount">0</span>
|
||
</div>
|
||
<div class="tscm-panel-content" id="tscmRfList">
|
||
<div class="tscm-empty">Enable RF scanning with an SDR device</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Threats Panel -->
|
||
<div class="tscm-panel" id="tscmThreatPanel">
|
||
<div class="tscm-panel-header">
|
||
Detected Threats
|
||
<span class="badge" id="tscmThreatCount">0</span>
|
||
</div>
|
||
<div class="tscm-panel-content" id="tscmThreatList">
|
||
<div class="tscm-empty">
|
||
<div class="tscm-empty-primary">No anomalies detected</div>
|
||
<div class="tscm-empty-secondary">Start a sweep to scan for signals of interest</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Device Timelines Overview -->
|
||
<div class="tscm-panel" id="tscmDeviceTimelinesPanel" style="margin-top: 12px;">
|
||
<div class="tscm-panel-header" style="display: flex; justify-content: space-between; align-items: center;">
|
||
Device Timelines
|
||
<button class="preset-btn" onclick="loadDeviceTimelines()" style="font-size: 9px; padding: 3px 8px;">Refresh</button>
|
||
</div>
|
||
<div class="tscm-panel-content" id="tscmDeviceTimelinesList">
|
||
<div class="tscm-empty">Run a sweep to see device timelines</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SubGHz Transceiver Dashboard -->
|
||
<div id="subghzVisuals" class="subghz-visuals-container" style="display: none;">
|
||
|
||
<!-- Stats Strip -->
|
||
<div class="subghz-stats-strip">
|
||
<div class="subghz-strip-group">
|
||
<span class="subghz-strip-device-badge" id="subghzStripDevice">
|
||
<span class="subghz-strip-device-dot" id="subghzStripDeviceDot"></span>
|
||
HackRF
|
||
</span>
|
||
<div class="subghz-strip-status">
|
||
<span class="subghz-strip-dot" id="subghzStripDot"></span>
|
||
<span class="subghz-strip-status-text" id="subghzStripStatus">Idle</span>
|
||
</div>
|
||
</div>
|
||
<div class="subghz-strip-divider"></div>
|
||
<div class="subghz-strip-group">
|
||
<div class="subghz-strip-stat">
|
||
<span class="subghz-strip-value accent-cyan" id="subghzStripFreq">--</span>
|
||
<span class="subghz-strip-label">MHZ</span>
|
||
</div>
|
||
<div class="subghz-strip-stat">
|
||
<span class="subghz-strip-value" id="subghzStripMode">--</span>
|
||
<span class="subghz-strip-label">MODE</span>
|
||
</div>
|
||
</div>
|
||
<div class="subghz-strip-divider"></div>
|
||
<div class="subghz-strip-group">
|
||
<div class="subghz-strip-stat">
|
||
<span class="subghz-strip-value accent-green" id="subghzStripSignals">0</span>
|
||
<span class="subghz-strip-label">SIGNALS</span>
|
||
</div>
|
||
<div class="subghz-strip-stat">
|
||
<span class="subghz-strip-value accent-orange" id="subghzStripCaptures">0</span>
|
||
<span class="subghz-strip-label">CAPTURES</span>
|
||
</div>
|
||
</div>
|
||
<div class="subghz-strip-divider"></div>
|
||
<div class="subghz-strip-group">
|
||
<span class="subghz-strip-timer" id="subghzStripTimer"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Signal Console (collapsible) -->
|
||
<div class="subghz-signal-console" id="subghzConsole" style="display: none;">
|
||
<div class="subghz-console-header" onclick="SubGhz.toggleConsole()">
|
||
<div class="subghz-phase-strip">
|
||
<span class="subghz-phase-step" id="subghzPhaseTuning">TUNING</span>
|
||
<span class="subghz-phase-arrow">▸</span>
|
||
<span class="subghz-phase-step" id="subghzPhaseListening">LISTENING</span>
|
||
<span class="subghz-phase-arrow">▸</span>
|
||
<span class="subghz-phase-step" id="subghzPhaseDecoding">DECODING</span>
|
||
</div>
|
||
<div class="subghz-burst-indicator" id="subghzBurstIndicator" title="Live burst detector">
|
||
<span class="subghz-burst-dot"></span>
|
||
<span class="subghz-burst-text" id="subghzBurstText">NO BURST</span>
|
||
</div>
|
||
<button class="subghz-console-toggle" id="subghzConsoleToggleBtn">▼</button>
|
||
</div>
|
||
<div class="subghz-console-body" id="subghzConsoleBody">
|
||
<div class="subghz-console-log" id="subghzConsoleLog"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Action Hub (idle state — 2x2 Flipper-style cards) -->
|
||
<div class="subghz-action-hub" id="subghzActionHub">
|
||
<div class="subghz-hub-header">
|
||
<div class="subghz-hub-header-title">HackRF One</div>
|
||
<div class="subghz-hub-header-sub">SubGHz Transceiver — 1 MHz - 6 GHz</div>
|
||
</div>
|
||
<div class="subghz-hub-grid">
|
||
<div class="subghz-hub-card subghz-hub-card--green" onclick="SubGhz.hubAction('rx')">
|
||
<div class="subghz-hub-icon">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="32" height="32"><circle cx="12" cy="12" r="3"/><path d="M12 1v4m0 14v4M1 12h4m14 0h4"/><path d="M5.6 5.6l2.85 2.85m7.1 7.1l2.85 2.85M5.6 18.4l2.85-2.85m7.1-7.1l2.85-2.85"/></svg>
|
||
</div>
|
||
<div class="subghz-hub-title">Read RAW</div>
|
||
<div class="subghz-hub-desc">Capture raw IQ via hackrf_transfer</div>
|
||
</div>
|
||
<div class="subghz-hub-card subghz-hub-card--red" onclick="SubGhz.hubAction('txselect')">
|
||
<div class="subghz-hub-icon">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="32" height="32"><path d="M5 19h14"/><path d="M12 5v11"/><path d="M8 9l4-4 4 4"/></svg>
|
||
</div>
|
||
<div class="subghz-hub-title">Transmit</div>
|
||
<div class="subghz-hub-desc">Replay a saved capture</div>
|
||
</div>
|
||
<div class="subghz-hub-card subghz-hub-card--orange" onclick="SubGhz.hubAction('sweep')">
|
||
<div class="subghz-hub-icon">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="32" height="32"><path d="M3 20h18"/><path d="M3 17l3-7 3 4 3-9 3 6 3-3 3 9"/></svg>
|
||
</div>
|
||
<div class="subghz-hub-title">Freq Analyzer</div>
|
||
<div class="subghz-hub-desc">Wideband sweep via hackrf_sweep</div>
|
||
</div>
|
||
<div class="subghz-hub-card subghz-hub-card--purple" onclick="SubGhz.hubAction('saved')">
|
||
<div class="subghz-hub-icon">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="32" height="32"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
|
||
</div>
|
||
<div class="subghz-hub-title">Saved</div>
|
||
<div class="subghz-hub-desc">Signal library & replay</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Operation Panels (one visible at a time, replaces hub) -->
|
||
|
||
<!-- RX (Raw Capture) Panel -->
|
||
<div class="subghz-op-panel" id="subghzPanelRx" style="display: none;">
|
||
<div class="subghz-op-panel-header">
|
||
<button class="subghz-op-back-btn" onclick="SubGhz.backToHub()" title="Back to hub">◀ Back</button>
|
||
<span class="subghz-op-panel-title">Read RAW — Signal Capture</span>
|
||
<div class="subghz-op-panel-actions">
|
||
<button class="subghz-btn start" id="subghzRxStartBtnPanel" onclick="SubGhz.startRx()">Start</button>
|
||
<button class="subghz-btn stop" id="subghzRxStopBtnPanel" onclick="SubGhz.stopRx()" disabled>Stop</button>
|
||
</div>
|
||
</div>
|
||
<div class="subghz-rx-display">
|
||
<div class="subghz-rx-recording" id="subghzRxRecording" style="display: none;">
|
||
<span class="subghz-rx-rec-dot"></span>
|
||
<span>RECORDING</span>
|
||
</div>
|
||
<div class="subghz-rx-info-grid">
|
||
<div class="subghz-rx-info-item">
|
||
<span class="subghz-rx-info-label">FREQUENCY</span>
|
||
<span class="subghz-rx-info-value accent-cyan" id="subghzRxFreq">--</span>
|
||
</div>
|
||
<div class="subghz-rx-info-item">
|
||
<span class="subghz-rx-info-label">LNA GAIN</span>
|
||
<span class="subghz-rx-info-value" id="subghzRxLna">--</span>
|
||
</div>
|
||
<div class="subghz-rx-info-item">
|
||
<span class="subghz-rx-info-label">VGA GAIN</span>
|
||
<span class="subghz-rx-info-value" id="subghzRxVga">--</span>
|
||
</div>
|
||
<div class="subghz-rx-info-item">
|
||
<span class="subghz-rx-info-label">SAMPLE RATE</span>
|
||
<span class="subghz-rx-info-value" id="subghzRxSampleRate">--</span>
|
||
</div>
|
||
<div class="subghz-rx-info-item">
|
||
<span class="subghz-rx-info-label">FILE SIZE</span>
|
||
<span class="subghz-rx-info-value" id="subghzRxFileSize">0 KB</span>
|
||
</div>
|
||
<div class="subghz-rx-info-item">
|
||
<span class="subghz-rx-info-label">DATA RATE</span>
|
||
<span class="subghz-rx-info-value" id="subghzRxRate">0 KB/s</span>
|
||
</div>
|
||
</div>
|
||
<div class="subghz-rx-level-wrapper">
|
||
<span class="subghz-rx-level-label">SIGNAL</span>
|
||
<span class="subghz-rx-burst-pill" id="subghzRxBurstPill">IDLE</span>
|
||
<div class="subghz-rx-level-bar">
|
||
<div class="subghz-rx-level-fill" id="subghzRxLevel" style="width: 0%;"></div>
|
||
</div>
|
||
</div>
|
||
<div class="subghz-rx-hint" id="subghzRxHint">
|
||
<span class="subghz-rx-hint-label">ANALYSIS</span>
|
||
<span class="subghz-rx-hint-text" id="subghzRxHintText">No modulation hint yet</span>
|
||
<span class="subghz-rx-hint-confidence" id="subghzRxHintConfidence">--</span>
|
||
</div>
|
||
<div class="subghz-rx-scope-wrap">
|
||
<span class="subghz-rx-scope-label">WAVEFORM</span>
|
||
<div class="subghz-rx-scope">
|
||
<canvas id="subghzRxScope"></canvas>
|
||
</div>
|
||
</div>
|
||
<div class="subghz-rx-scope-wrap">
|
||
<div class="subghz-rx-waterfall-header">
|
||
<span class="subghz-rx-scope-label">WATERFALL</span>
|
||
<div class="subghz-rx-waterfall-controls">
|
||
<div class="subghz-wf-control">
|
||
<span>FLOOR</span>
|
||
<input type="range" id="subghzWfFloor" min="0" max="200" value="20" oninput="SubGhz.setWaterfallFloor(this.value)">
|
||
<span class="subghz-wf-value" id="subghzWfFloorVal">20</span>
|
||
</div>
|
||
<div class="subghz-wf-control">
|
||
<span>RANGE</span>
|
||
<input type="range" id="subghzWfRange" min="16" max="255" value="180" oninput="SubGhz.setWaterfallRange(this.value)">
|
||
<span class="subghz-wf-value" id="subghzWfRangeVal">180</span>
|
||
</div>
|
||
<button class="subghz-wf-pause-btn" id="subghzWfPauseBtn" onclick="SubGhz.toggleWaterfall()">PAUSE</button>
|
||
</div>
|
||
</div>
|
||
<div class="subghz-rx-waterfall">
|
||
<canvas id="subghzRxWaterfall"></canvas>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sweep Panel -->
|
||
<div class="subghz-op-panel" id="subghzPanelSweep" style="display: none;">
|
||
<div class="subghz-op-panel-header">
|
||
<button class="subghz-op-back-btn" onclick="SubGhz.backToHub()" title="Back to hub">◀ Back</button>
|
||
<span class="subghz-op-panel-title">Frequency Analyzer</span>
|
||
<div class="subghz-op-panel-actions">
|
||
<button class="subghz-btn start" id="subghzSweepStartBtnPanel" onclick="SubGhz.startSweep()">Start</button>
|
||
<button class="subghz-btn stop" id="subghzSweepStopBtnPanel" onclick="SubGhz.stopSweep()" disabled>Stop</button>
|
||
</div>
|
||
</div>
|
||
<div class="subghz-sweep-layout">
|
||
<div class="subghz-sweep-chart-wrapper" id="subghzSweepChartWrapper">
|
||
<canvas id="subghzSweepCanvas"></canvas>
|
||
</div>
|
||
<div class="subghz-sweep-peaks-sidebar" id="subghzSweepPeaksSidebar">
|
||
<div class="subghz-sweep-peaks-title">PEAKS</div>
|
||
<div class="subghz-peak-list" id="subghzSweepPeakList"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TX Panel -->
|
||
<div class="subghz-op-panel" id="subghzPanelTx" style="display: none;">
|
||
<div class="subghz-op-panel-header">
|
||
<button class="subghz-op-back-btn" onclick="SubGhz.backToHub()" title="Back to hub">◀ Back</button>
|
||
<span class="subghz-op-panel-title">Transmit</span>
|
||
</div>
|
||
<div class="subghz-tx-display" id="subghzTxDisplay">
|
||
<div class="subghz-tx-pulse-ring">
|
||
<div class="subghz-tx-pulse-dot"></div>
|
||
</div>
|
||
<div class="subghz-tx-label" id="subghzTxStateLabel">READY</div>
|
||
<div class="subghz-tx-info-grid">
|
||
<div class="subghz-tx-info-item">
|
||
<span class="subghz-tx-info-label">FREQUENCY</span>
|
||
<span class="subghz-tx-info-value accent-red" id="subghzTxFreqDisplay">--</span>
|
||
</div>
|
||
<div class="subghz-tx-info-item">
|
||
<span class="subghz-tx-info-label">TX GAIN</span>
|
||
<span class="subghz-tx-info-value" id="subghzTxGainDisplay">--</span>
|
||
</div>
|
||
<div class="subghz-tx-info-item">
|
||
<span class="subghz-tx-info-label">ELAPSED</span>
|
||
<span class="subghz-tx-info-value" id="subghzTxElapsed">0s</span>
|
||
</div>
|
||
</div>
|
||
<div class="subghz-btn-row" style="max-width: 420px; margin: 16px auto 0;">
|
||
<button class="subghz-btn" id="subghzTxChooseCaptureBtn" onclick="SubGhz.showPanel('saved')">Choose Capture</button>
|
||
<button class="subghz-btn stop" id="subghzTxStopBtn" onclick="SubGhz.stopTx()">Stop Transmission</button>
|
||
<button class="subghz-btn start" id="subghzTxReplayLastBtn" onclick="SubGhz.replayLastTx()" style="display: none;">Replay Last</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Saved Panel -->
|
||
<div class="subghz-op-panel" id="subghzPanelSaved" style="display: none;">
|
||
<div class="subghz-op-panel-header">
|
||
<button class="subghz-op-back-btn" onclick="SubGhz.backToHub()" title="Back to hub">◀ Back</button>
|
||
<span class="subghz-op-panel-title">Saved Captures</span>
|
||
<div class="subghz-op-panel-actions subghz-saved-actions">
|
||
<span class="subghz-saved-selection-count" id="subghzSavedSelectionCount" style="display: none;">0 selected</span>
|
||
<button class="subghz-btn" id="subghzSavedSelectBtn" onclick="SubGhz.toggleCaptureSelectMode()">Select</button>
|
||
<button class="subghz-btn" id="subghzSavedSelectAllBtn" onclick="SubGhz.selectAllCaptures()" style="display: none;">Select All</button>
|
||
<button class="subghz-btn stop" id="subghzSavedDeleteSelectedBtn" onclick="SubGhz.deleteSelectedCaptures()" style="display: none;" disabled>Delete Selected</button>
|
||
</div>
|
||
</div>
|
||
<div class="subghz-captures-list subghz-captures-list-main" id="subghzCapturesList" style="flex: 1; min-height: 0; max-height: none; overflow-y: auto;">
|
||
<div class="subghz-empty" id="subghzCapturesEmpty">No captures yet</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- TX Confirmation Modal -->
|
||
<div id="subghzTxModalOverlay" class="subghz-tx-modal-overlay">
|
||
<div class="subghz-tx-modal">
|
||
<h3>Confirm Transmission</h3>
|
||
<p>You are about to transmit a radio signal on:</p>
|
||
<p class="tx-freq" id="subghzTxModalFreq">--- MHz</p>
|
||
<p class="tx-duration">Capture duration: <span id="subghzTxModalDuration">--</span></p>
|
||
<div class="subghz-tx-segment-box">
|
||
<label class="subghz-tx-segment-toggle">
|
||
<input type="checkbox" id="subghzTxSegmentEnabled" onchange="SubGhz.syncTxSegmentSelection()">
|
||
Transmit selected segment only
|
||
</label>
|
||
<div class="subghz-tx-segment-grid">
|
||
<label>Start (s)</label>
|
||
<input type="number" id="subghzTxSegmentStart" min="0" step="0.01" value="0" disabled oninput="SubGhz.syncTxSegmentSelection('start')">
|
||
<label>End (s)</label>
|
||
<input type="number" id="subghzTxSegmentEnd" min="0" step="0.01" value="0" disabled oninput="SubGhz.syncTxSegmentSelection('end')">
|
||
</div>
|
||
<p class="subghz-tx-segment-summary" id="subghzTxSegmentSummary">Full capture</p>
|
||
</div>
|
||
<div class="subghz-tx-burst-assist" id="subghzTxBurstAssist" style="display: none;">
|
||
<div class="subghz-tx-burst-title">Detected Bursts</div>
|
||
<div class="subghz-tx-burst-timeline" id="subghzTxBurstTimeline"></div>
|
||
<div class="subghz-tx-burst-range" id="subghzTxBurstRange">Drag on timeline to select TX segment</div>
|
||
<div class="subghz-tx-burst-list" id="subghzTxBurstList"></div>
|
||
</div>
|
||
<p>Ensure you have proper authorization to transmit on this frequency.</p>
|
||
<div class="subghz-tx-modal-actions">
|
||
<button class="subghz-tx-cancel-btn" onclick="SubGhz.cancelTx()">Cancel</button>
|
||
<button class="subghz-tx-trim-btn" id="subghzTxTrimBtn" onclick="SubGhz.trimCaptureSelection()">Trim + Save</button>
|
||
<button class="subghz-tx-confirm-btn" onclick="SubGhz.confirmTx()">Transmit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- BT Locate SAR Dashboard -->
|
||
<div id="btLocateVisuals" class="btl-visuals-container" style="display: none; flex-direction: column; gap: 8px; flex: 1; min-height: 0; overflow: hidden; padding: 8px;">
|
||
<!-- Proximity HUD -->
|
||
<div class="btl-hud" id="btLocateHud" style="display: none;">
|
||
<div class="btl-hud-top">
|
||
<div class="btl-hud-band" id="btLocateBand">---</div>
|
||
<div class="btl-hud-metrics">
|
||
<div class="btl-hud-metric btl-hud-metric-lg">
|
||
<span class="btl-hud-value" id="btLocateDistance">--</span>
|
||
<span class="btl-hud-unit">m</span>
|
||
<span class="btl-hud-label">Est. Distance</span>
|
||
</div>
|
||
<div class="btl-hud-metric">
|
||
<span class="btl-hud-value" id="btLocateRssi">--</span>
|
||
<span class="btl-hud-unit">dBm</span>
|
||
<span class="btl-hud-label">RSSI</span>
|
||
</div>
|
||
<div class="btl-hud-metric">
|
||
<span class="btl-hud-value" id="btLocateRssiEma">--</span>
|
||
<span class="btl-hud-unit">dBm</span>
|
||
<span class="btl-hud-label">RSSI avg</span>
|
||
</div>
|
||
<div class="btl-hud-separator"></div>
|
||
<div class="btl-hud-metric">
|
||
<span class="btl-hud-value" id="btLocateDetectionCount">0</span>
|
||
<span class="btl-hud-unit"> </span>
|
||
<span class="btl-hud-label">Detections</span>
|
||
</div>
|
||
<div class="btl-hud-metric">
|
||
<span class="btl-hud-value" id="btLocateGpsCount">0</span>
|
||
<span class="btl-hud-unit"> </span>
|
||
<span class="btl-hud-label">GPS pts</span>
|
||
</div>
|
||
<div class="btl-hud-metric">
|
||
<span class="btl-hud-value" id="btLocateSessionTime">0:00</span>
|
||
<span class="btl-hud-unit"> </span>
|
||
<span class="btl-hud-label">Duration</span>
|
||
</div>
|
||
</div>
|
||
<div class="btl-hud-controls">
|
||
<label class="btl-hud-audio-toggle">
|
||
<input type="checkbox" id="btLocateAudioEnable" onchange="BtLocate.toggleAudio()">
|
||
<span>Audio</span>
|
||
</label>
|
||
<div class="btl-hud-export-row">
|
||
<select id="btLocateExportFormat" class="btl-hud-export-format">
|
||
<option value="csv">CSV</option>
|
||
<option value="gpx">GPX</option>
|
||
<option value="kml">KML</option>
|
||
</select>
|
||
<button class="btl-hud-clear-btn" onclick="BtLocate.exportTrail()">Export</button>
|
||
</div>
|
||
<button class="btl-hud-clear-btn" onclick="BtLocate.clearTrail()">Clear Trail</button>
|
||
</div>
|
||
</div>
|
||
<div class="btl-hud-bottom">
|
||
<div class="btl-hud-info">
|
||
<span class="btl-hud-info-item" id="btLocateTargetInfo">--</span>
|
||
<span class="btl-hud-info-sep">·</span>
|
||
<span class="btl-hud-info-item" id="btLocateEnvInfo">--</span>
|
||
<span class="btl-hud-info-sep">·</span>
|
||
<span class="btl-hud-info-item" id="btLocateGpsStatus">GPS: --</span>
|
||
<span class="btl-hud-info-sep">·</span>
|
||
<span class="btl-hud-info-item" id="btLocateLastSeen">Last: --</span>
|
||
<span class="btl-hud-info-sep">·</span>
|
||
<span class="btl-hud-info-item" id="btLocateConfidenceInfo">Confidence: --</span>
|
||
<span class="btl-hud-info-sep">·</span>
|
||
<span class="btl-hud-info-item" id="btLocateBestSignal">Best: --</span>
|
||
</div>
|
||
<div id="btLocateDiag" class="btl-hud-diag"></div>
|
||
</div>
|
||
</div>
|
||
<div class="btl-map-container" style="flex: 1; min-height: 250px; position: relative; overflow: hidden;">
|
||
<div id="btLocateMap" style="position: absolute; inset: 0;"></div>
|
||
<div id="btLocateCrosshairOverlay" class="btl-crosshair-overlay" aria-hidden="true">
|
||
<div class="btl-crosshair-line btl-crosshair-vertical"></div>
|
||
<div class="btl-crosshair-line btl-crosshair-horizontal"></div>
|
||
</div>
|
||
<div class="btl-map-overlay-controls">
|
||
<label class="btl-map-overlay-toggle">
|
||
<input type="checkbox" id="btLocateHeatmapEnable" onchange="BtLocate.toggleHeatmap()">
|
||
<span>Heatmap</span>
|
||
</label>
|
||
<label class="btl-map-overlay-toggle">
|
||
<input type="checkbox" id="btLocateMovementEnable" onchange="BtLocate.toggleMovement()">
|
||
<span>Movement</span>
|
||
</label>
|
||
<label class="btl-map-overlay-toggle">
|
||
<input type="checkbox" id="btLocateFollowEnable" onchange="BtLocate.toggleFollow()">
|
||
<span>Auto follow</span>
|
||
</label>
|
||
<label class="btl-map-overlay-toggle">
|
||
<input type="checkbox" id="btLocateSmoothEnable" onchange="BtLocate.toggleSmoothing()">
|
||
<span>Smooth path</span>
|
||
</label>
|
||
</div>
|
||
<div class="btl-map-heat-legend" id="btLocateHeatLegend">
|
||
<span class="btl-map-heat-label">Signal Heat</span>
|
||
<div class="btl-map-heat-bar"></div>
|
||
<div class="btl-map-heat-scale">
|
||
<span>Weak</span>
|
||
<span>Strong</span>
|
||
</div>
|
||
</div>
|
||
<div class="btl-map-track-stats" id="btLocateTrackStats">Track: 0 m | 0 pts</div>
|
||
</div>
|
||
<div class="btl-rssi-chart-container">
|
||
<span class="btl-chart-label">RSSI History</span>
|
||
<canvas id="btLocateRssiChart"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- WiFi Locate Dashboard -->
|
||
<div id="wflVisuals" class="wfl-visuals-container" style="display: none;">
|
||
<div class="wfl-hud" id="wflHud" style="display: none;">
|
||
<div class="wfl-hud-header">
|
||
<div class="wfl-hud-target">
|
||
<span class="wfl-target-ssid" id="wflTargetSsid">--</span>
|
||
<span class="wfl-target-bssid" id="wflTargetBssid">--</span>
|
||
</div>
|
||
<label class="wfl-hud-audio-toggle">
|
||
<input type="checkbox" id="wflAudioEnable" onchange="WiFiLocate.toggleAudio()"> Audio
|
||
</label>
|
||
<button class="wfl-hud-stop-btn" onclick="WiFiLocate.stop()">Stop Tracking</button>
|
||
</div>
|
||
<div class="wfl-rssi-display" id="wflRssiValue">--</div>
|
||
<div class="wfl-distance" id="wflDistance">--</div>
|
||
<div class="wfl-bar-container" id="wflBarContainer"></div>
|
||
<div class="wfl-rssi-chart-container">
|
||
<span class="wfl-chart-label">RSSI History</span>
|
||
<canvas id="wflRssiChart"></canvas>
|
||
</div>
|
||
<div class="wfl-stats">
|
||
<div class="wfl-stat"><span class="wfl-stat-value" id="wflStatCurrent">--</span><span class="wfl-stat-label">Current</span></div>
|
||
<div class="wfl-stat"><span class="wfl-stat-value" id="wflStatMin">--</span><span class="wfl-stat-label">Min</span></div>
|
||
<div class="wfl-stat"><span class="wfl-stat-value" id="wflStatMax">--</span><span class="wfl-stat-label">Max</span></div>
|
||
<div class="wfl-stat"><span class="wfl-stat-value" id="wflStatAvg">--</span><span class="wfl-stat-label">Avg</span></div>
|
||
</div>
|
||
<div class="wfl-signal-lost" id="wflSignalLost" style="display: none;">SIGNAL LOST</div>
|
||
</div>
|
||
<div class="wfl-waiting" id="wflWaiting">
|
||
<p>Enter a target BSSID and click Start Locate</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- WebSDR Dashboard -->
|
||
<div id="websdrVisuals" style="display: none; padding: 12px; flex-direction: column; gap: 12px; flex: 1; min-height: 0; overflow: hidden;">
|
||
<!-- Audio Control Bar (hidden until connected) -->
|
||
<div id="kiwiAudioControls" class="radio-module-box" style="display: none; padding: 8px 12px; flex-shrink: 0;">
|
||
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
||
<!-- Live indicator -->
|
||
<div style="display: flex; align-items: center; gap: 5px;">
|
||
<div id="kiwiLiveIndicator" style="width: 8px; height: 8px; border-radius: 50%; background: var(--accent-green); animation: pulse 1.5s infinite;"></div>
|
||
<span style="font-size: 10px; color: var(--text-muted); text-transform: uppercase;">LIVE</span>
|
||
</div>
|
||
<!-- Receiver name -->
|
||
<span id="kiwiBarReceiverName" style="font-size: 11px; color: var(--accent-cyan); max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"></span>
|
||
<!-- Frequency input -->
|
||
<div style="display: flex; align-items: center; gap: 4px;">
|
||
<input type="number" id="kiwiBarFrequency" step="1" style="width: 80px; font-size: 12px; font-family: var(--font-mono); padding: 2px 6px; background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary);">
|
||
<span style="font-size: 10px; color: var(--text-muted);">kHz</span>
|
||
</div>
|
||
<!-- Mode selector -->
|
||
<select id="kiwiBarMode" style="font-size: 11px; padding: 2px 6px; background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: 4px; color: var(--text-primary);">
|
||
<option value="am">AM</option>
|
||
<option value="usb">USB</option>
|
||
<option value="lsb">LSB</option>
|
||
<option value="cw">CW</option>
|
||
</select>
|
||
<!-- Tune button -->
|
||
<button class="preset-btn" onclick="tuneFromBar()" style="font-size: 10px; padding: 3px 10px;">Tune</button>
|
||
<!-- Volume -->
|
||
<div style="display: flex; align-items: center; gap: 4px;">
|
||
<span style="font-size: 10px; color: var(--text-muted);">VOL</span>
|
||
<input type="range" id="kiwiBarVolume" min="0" max="100" value="80" style="width: 60px;" oninput="setKiwiVolume(this.value)">
|
||
</div>
|
||
<!-- S-meter mini -->
|
||
<div style="display: flex; align-items: center; gap: 4px;">
|
||
<div style="width: 50px; height: 6px; background: rgba(0,0,0,0.5); border-radius: 3px; overflow: hidden;">
|
||
<div id="kiwiBarSmeter" style="height: 100%; width: 0%; background: linear-gradient(to right, var(--accent-green), var(--accent-orange)); transition: width 0.2s; border-radius: 3px;"></div>
|
||
</div>
|
||
<span id="kiwiBarSmeterValue" style="font-size: 9px; color: var(--text-muted); font-family: var(--font-mono); min-width: 20px;">S0</span>
|
||
</div>
|
||
<!-- Disconnect -->
|
||
<button class="stop-btn" onclick="disconnectFromReceiver()" style="font-size: 10px; padding: 3px 10px; margin-left: auto;">Disconnect</button>
|
||
</div>
|
||
</div>
|
||
<!-- Map and receiver list side by side -->
|
||
<div style="display: grid; grid-template-columns: 3fr 1fr; gap: 12px; flex: 1; min-height: 0; overflow: hidden;">
|
||
<!-- Map -->
|
||
<div class="radio-module-box" style="padding: 0; overflow: hidden; position: relative; min-height: 0;">
|
||
<div id="websdrMap" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0;"></div>
|
||
</div>
|
||
<!-- Receiver List -->
|
||
<div style="display: flex; flex-direction: column; gap: 12px; min-width: 0; min-height: 0; overflow: hidden;">
|
||
<div class="radio-module-box" style="padding: 10px; flex: 1; overflow-y: auto; min-height: 0;">
|
||
<div class="module-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 10px;">
|
||
<span>RECEIVERS</span>
|
||
<span id="websdrReceiverCount" style="color: var(--accent-cyan);">0 found</span>
|
||
</div>
|
||
<div id="websdrReceiverList" style="font-size: 11px;">
|
||
<div style="color: var(--text-muted); text-align: center; padding: 20px;">Click "Find Receivers" to search</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Spy Stations Dashboard -->
|
||
<div id="spyStationsVisuals" class="spy-stations-container" style="display: none;">
|
||
<div class="spy-stations-header">
|
||
<div class="spy-stations-title">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 20px; height: 20px;">
|
||
<path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/>
|
||
<path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/>
|
||
<circle cx="12" cy="12" r="2"/>
|
||
<path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/>
|
||
<path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/>
|
||
</svg>
|
||
Number Stations & Diplomatic Networks
|
||
</div>
|
||
<div class="spy-stations-count">
|
||
<span id="spyStationsVisibleCount">0</span> stations
|
||
</div>
|
||
</div>
|
||
<div class="spy-stations-grid" id="spyStationsGrid">
|
||
<!-- Station cards populated by JavaScript -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Meshtastic Messages Dashboard -->
|
||
<div id="meshtasticVisuals" class="mesh-visuals-container" style="display: none;">
|
||
<!-- Compact Status Strip -->
|
||
<div class="mesh-stats-strip">
|
||
<div class="mesh-strip-group">
|
||
<button class="mesh-strip-sidebar-toggle" onclick="Meshtastic.toggleSidebar()" title="Toggle sidebar">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/>
|
||
</svg>
|
||
</button>
|
||
<div class="mesh-strip-status">
|
||
<span class="mesh-strip-dot disconnected" id="meshStripDot"></span>
|
||
<span class="mesh-strip-status-text" id="meshStripStatus">Disconnected</span>
|
||
</div>
|
||
<select id="meshStripConnType" class="mesh-strip-select" title="Connection Type" onchange="Meshtastic.onConnectionTypeChange()" style="width: 70px;">
|
||
<option value="serial">Serial</option>
|
||
<option value="tcp">TCP</option>
|
||
</select>
|
||
<select id="meshStripDevice" class="mesh-strip-select" title="Device">
|
||
<option value="">Auto-detect</option>
|
||
</select>
|
||
<input type="text" id="meshStripHostname" class="mesh-strip-input" placeholder="IP address" title="Hostname/IP for TCP" style="display: none; width: 120px;">
|
||
<button class="mesh-strip-btn connect" id="meshStripConnectBtn" onclick="Meshtastic.start()">Connect</button>
|
||
<button class="mesh-strip-btn disconnect" id="meshStripDisconnectBtn" onclick="Meshtastic.stop()" style="display: none;">Disconnect</button>
|
||
</div>
|
||
<div class="mesh-strip-divider"></div>
|
||
<div class="mesh-strip-group">
|
||
<div class="mesh-strip-stat">
|
||
<span class="mesh-strip-value" id="meshStripNodeName">--</span>
|
||
<span class="mesh-strip-label">NODE</span>
|
||
</div>
|
||
<div class="mesh-strip-stat">
|
||
<span class="mesh-strip-value mesh-strip-id" id="meshStripNodeId">--</span>
|
||
<span class="mesh-strip-label">ID</span>
|
||
</div>
|
||
<div class="mesh-strip-stat">
|
||
<span class="mesh-strip-value" id="meshStripModel">--</span>
|
||
<span class="mesh-strip-label">MODEL</span>
|
||
</div>
|
||
</div>
|
||
<div class="mesh-strip-divider"></div>
|
||
<div class="mesh-strip-group">
|
||
<div class="mesh-strip-stat">
|
||
<span class="mesh-strip-value accent-cyan" id="meshStripMsgCount">0</span>
|
||
<span class="mesh-strip-label">MSGS</span>
|
||
</div>
|
||
<div class="mesh-strip-stat">
|
||
<span class="mesh-strip-value accent-green" id="meshStripNodeCount">0</span>
|
||
<span class="mesh-strip-label">NODES</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main Content Row (Messages + Map side by side) -->
|
||
<div class="mesh-main-row">
|
||
<!-- Messages Section -->
|
||
<div class="mesh-messages-section">
|
||
<div class="mesh-messages-header">
|
||
<div class="mesh-messages-title">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 18px; height: 18px;">
|
||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||
</svg>
|
||
Messages
|
||
</div>
|
||
<div class="mesh-messages-filter">
|
||
<select id="meshVisualsFilter" onchange="Meshtastic.applyFilter()">
|
||
<option value="">All Channels</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="mesh-messages-list" id="meshMessagesGrid">
|
||
<div class="mesh-messages-empty">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="12" cy="12" r="10"/>
|
||
<circle cx="12" cy="12" r="3"/>
|
||
<path d="M12 2v4m0 12v4M2 12h4m12 0h4"/>
|
||
</svg>
|
||
<p>Connect to a Meshtastic device to see messages</p>
|
||
</div>
|
||
</div>
|
||
<!-- Message Compose Box -->
|
||
<div class="mesh-compose" id="meshCompose" style="display: none;">
|
||
<div class="mesh-compose-header">
|
||
<select id="meshComposeChannel" class="mesh-compose-channel" title="Channel to send on">
|
||
<option value="0">CH 0</option>
|
||
</select>
|
||
<input type="text" id="meshComposeTo" placeholder="^all (broadcast)" class="mesh-compose-to" title="Destination node ID or ^all for broadcast">
|
||
</div>
|
||
<div class="mesh-compose-body">
|
||
<input type="text" id="meshComposeText" placeholder="Type a message..." maxlength="237" class="mesh-compose-input" oninput="Meshtastic.updateCharCount()" onkeydown="Meshtastic.handleComposeKeydown(event)">
|
||
<button onclick="Meshtastic.sendMessage()" class="mesh-compose-send" title="Send message">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 18px; height: 18px;">
|
||
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div class="mesh-compose-hint">
|
||
<span id="meshComposeCount">0</span>/237
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Node Map -->
|
||
<div class="mesh-map-section">
|
||
<div class="mesh-map-header">
|
||
<div class="mesh-map-title">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 18px; height: 18px;">
|
||
<circle cx="12" cy="12" r="10"/>
|
||
<circle cx="12" cy="12" r="3"/>
|
||
<path d="M12 2v4m0 12v4M2 12h4m12 0h4"/>
|
||
</svg>
|
||
Node Map
|
||
</div>
|
||
<div class="mesh-map-stats">
|
||
<span>NODES: <span id="meshMapNodeCount">0</span></span>
|
||
<span>WITH GPS: <span id="meshMapGpsCount">0</span></span>
|
||
</div>
|
||
</div>
|
||
<div id="meshMap" class="mesh-map"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SSTV Decoder Dashboard -->
|
||
<div id="sstvVisuals" class="sstv-visuals-container" style="display: none;">
|
||
<!-- Status Strip -->
|
||
<div class="sstv-stats-strip">
|
||
<div class="sstv-strip-group">
|
||
<div class="sstv-strip-status">
|
||
<span class="sstv-strip-dot idle" id="sstvStripDot"></span>
|
||
<span class="sstv-strip-status-text" id="sstvStripStatus">Idle</span>
|
||
</div>
|
||
<button class="sstv-strip-btn start" id="sstvStartBtn" onclick="SSTV.start()">Start</button>
|
||
<button class="sstv-strip-btn stop" id="sstvStopBtn" onclick="SSTV.stop()" style="display: none;">Stop</button>
|
||
</div>
|
||
<div class="sstv-strip-divider"></div>
|
||
<div class="sstv-strip-group">
|
||
<div class="sstv-strip-stat">
|
||
<span class="sstv-strip-value accent-cyan">145.800</span>
|
||
<span class="sstv-strip-label">MHZ</span>
|
||
</div>
|
||
<div class="sstv-strip-stat">
|
||
<span class="sstv-strip-value" id="sstvStripImageCount">0</span>
|
||
<span class="sstv-strip-label">IMAGES</span>
|
||
</div>
|
||
</div>
|
||
<div class="sstv-strip-divider"></div>
|
||
<!-- Location Controls -->
|
||
<div class="sstv-strip-group">
|
||
<div class="sstv-strip-location">
|
||
<span class="sstv-strip-label" style="margin-right: 6px;">LOC</span>
|
||
<input type="number" id="sstvObsLat" class="sstv-loc-input" step="0.0001" placeholder="Lat" title="Latitude">
|
||
<input type="number" id="sstvObsLon" class="sstv-loc-input" step="0.0001" placeholder="Lon" title="Longitude">
|
||
<button class="sstv-strip-btn gps" onclick="SSTV.useGPS(this)" title="Use GPS location">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 12px; height: 12px;">
|
||
<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/>
|
||
<line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/>
|
||
<line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/>
|
||
</svg>
|
||
GPS
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="sstv-strip-divider"></div>
|
||
<!-- TLE Update -->
|
||
<div class="sstv-strip-group">
|
||
<button class="sstv-strip-btn update-tle" onclick="SSTV.updateTLE(this)" title="Update satellite orbital data from CelesTrak">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 12px; height: 12px;">
|
||
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||
<path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/>
|
||
<path d="M16 21h5v-5"/>
|
||
</svg>
|
||
Update TLE
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ISS Tracking Map + Countdown Row -->
|
||
<div class="sstv-top-row">
|
||
<!-- ISS Map -->
|
||
<div class="sstv-map-row">
|
||
<div class="sstv-map-container">
|
||
<div id="sstvIssMap" class="sstv-iss-map"></div>
|
||
<div class="sstv-map-overlay">
|
||
<div class="sstv-map-info">
|
||
<span class="sstv-map-label">ISS</span>
|
||
<span class="sstv-map-coords"><span id="sstvIssLat">--.-</span>°, <span id="sstvIssLon">--.-</span>°</span>
|
||
<span class="sstv-map-alt">Alt: <span id="sstvIssAlt">---</span> km</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Next Pass Countdown Panel -->
|
||
<div class="sstv-countdown-panel">
|
||
<div class="sstv-countdown-header">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 16px; height: 16px;">
|
||
<circle cx="12" cy="12" r="10"/>
|
||
<polyline points="12 6 12 12 16 14"/>
|
||
</svg>
|
||
<span>Next Pass</span>
|
||
</div>
|
||
<div class="sstv-countdown-body">
|
||
<div class="sstv-countdown-timer" id="sstvCountdownTimer">
|
||
<span class="sstv-countdown-value" id="sstvCountdownValue">--:--:--</span>
|
||
<span class="sstv-countdown-label" id="sstvCountdownLabel">Set location</span>
|
||
</div>
|
||
<div class="sstv-countdown-details">
|
||
<div class="sstv-countdown-detail">
|
||
<span class="sstv-detail-label">Start</span>
|
||
<span class="sstv-detail-value" id="sstvPassStart">--:--</span>
|
||
</div>
|
||
<div class="sstv-countdown-detail">
|
||
<span class="sstv-detail-label">Max El</span>
|
||
<span class="sstv-detail-value" id="sstvPassMaxEl">--°</span>
|
||
</div>
|
||
<div class="sstv-countdown-detail">
|
||
<span class="sstv-detail-label">Duration</span>
|
||
<span class="sstv-detail-value" id="sstvPassDuration">-- min</span>
|
||
</div>
|
||
<div class="sstv-countdown-detail">
|
||
<span class="sstv-detail-label">Direction</span>
|
||
<span class="sstv-detail-value" id="sstvPassDirection">--</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="sstv-countdown-status" id="sstvCountdownStatus">
|
||
<span class="sstv-status-dot"></span>
|
||
<span>Waiting for pass data...</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Signal Scope -->
|
||
<div id="sstvScopePanel" style="display: none; margin-bottom: 12px;">
|
||
<div style="background: #0a0a0a; border: 1px solid #1e1a2e; border-radius: 6px; padding: 8px 10px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
|
||
<span>Audio Waveform</span>
|
||
<div style="display: flex; gap: 14px;">
|
||
<span>RMS: <span id="sstvScopeRmsLabel" style="color: #c080ff; font-variant-numeric: tabular-nums;">0</span></span>
|
||
<span>PEAK: <span id="sstvScopePeakLabel" style="color: #f44; font-variant-numeric: tabular-nums;">0</span></span>
|
||
<span id="sstvScopeToneLabel" style="color: #444;">QUIET</span>
|
||
<span id="sstvScopeStatusLabel" style="color: #444;">IDLE</span>
|
||
</div>
|
||
</div>
|
||
<canvas id="sstvScopeCanvas" style="width: 100%; height: 80px; display: block; border-radius: 3px; background: #050510;"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main Row (Live + Gallery) -->
|
||
<div class="sstv-main-row">
|
||
<!-- Live Decode Section -->
|
||
<div class="sstv-live-section">
|
||
<div class="sstv-live-header">
|
||
<div class="sstv-live-title">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 18px; height: 18px;">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||
<circle cx="12" cy="12" r="3"/>
|
||
</svg>
|
||
Live Decode
|
||
</div>
|
||
</div>
|
||
<div class="sstv-live-content" id="sstvLiveContent">
|
||
<div class="sstv-idle-state">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||
<circle cx="12" cy="12" r="3"/>
|
||
<path d="M3 9h2M19 9h2M3 15h2M19 15h2"/>
|
||
</svg>
|
||
<h4>ISS SSTV Decoder</h4>
|
||
<p>Click Start to listen for SSTV transmissions on 145.800 MHz</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Gallery Section -->
|
||
<div class="sstv-gallery-section">
|
||
<div class="sstv-gallery-header">
|
||
<div class="sstv-gallery-title">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 18px; height: 18px;">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||
<polyline points="21 15 16 10 5 21"/>
|
||
</svg>
|
||
Decoded Images
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:4px;">
|
||
<span class="sstv-gallery-count" id="sstvImageCount">0</span>
|
||
<button class="sstv-gallery-clear-btn" onclick="SSTV.deleteAllImages()" title="Delete all images">Clear All</button>
|
||
</div>
|
||
</div>
|
||
<div class="sstv-gallery-grid" id="sstvGallery">
|
||
<div class="sstv-gallery-empty">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||
<polyline points="21 15 16 10 5 21"/>
|
||
</svg>
|
||
<p>No images decoded yet</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Weather Satellite visuals (pass predictions + image gallery) -->
|
||
<div id="weatherSatVisuals" class="wxsat-visuals-container" style="display: none;">
|
||
<!-- Stats strip -->
|
||
<div class="wxsat-stats-strip">
|
||
<div class="wxsat-strip-group">
|
||
<div class="wxsat-strip-status">
|
||
<span class="wxsat-strip-dot" id="wxsatStripDot"></span>
|
||
<span class="wxsat-strip-status-text" id="wxsatStripStatus">Idle</span>
|
||
</div>
|
||
<button class="wxsat-strip-btn start" id="wxsatStartBtn" onclick="WeatherSat.start()">Start</button>
|
||
<button class="wxsat-strip-btn stop" id="wxsatStopBtn" onclick="WeatherSat.stop()" style="display: none;">Stop</button>
|
||
</div>
|
||
<div class="wxsat-strip-divider"></div>
|
||
<div class="wxsat-strip-group">
|
||
<div class="wxsat-strip-stat">
|
||
<span class="wxsat-strip-value accent-cyan" id="wxsatStripFreq">--</span>
|
||
<span class="wxsat-strip-label">MHZ</span>
|
||
</div>
|
||
<div class="wxsat-strip-stat">
|
||
<span class="wxsat-strip-value" id="wxsatStripMode">--</span>
|
||
<span class="wxsat-strip-label">MODE</span>
|
||
</div>
|
||
<div class="wxsat-strip-stat">
|
||
<span class="wxsat-strip-value" id="wxsatStripImageCount">0</span>
|
||
<span class="wxsat-strip-label">IMAGES</span>
|
||
</div>
|
||
</div>
|
||
<div class="wxsat-strip-divider"></div>
|
||
<div class="wxsat-strip-group">
|
||
<div class="wxsat-strip-location">
|
||
<span class="wxsat-strip-label" style="margin-right: 6px;">LOC</span>
|
||
<input type="number" id="wxsatObsLat" class="wxsat-loc-input" step="0.0001" placeholder="Lat" title="Latitude">
|
||
<input type="number" id="wxsatObsLon" class="wxsat-loc-input" step="0.0001" placeholder="Lon" title="Longitude">
|
||
<button class="wxsat-strip-btn gps" onclick="WeatherSat.useGPS(this)" title="Use GPS location">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="wxsat-strip-divider"></div>
|
||
<div class="wxsat-strip-group">
|
||
<label class="wxsat-schedule-toggle" title="Auto-capture passes">
|
||
<input type="checkbox" id="wxsatAutoSchedule" onchange="WeatherSat.toggleScheduler(this)">
|
||
<span class="wxsat-toggle-label">AUTO</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Countdown + Timeline -->
|
||
<div class="wxsat-countdown-bar">
|
||
<div class="wxsat-countdown-next">
|
||
<div class="wxsat-countdown-boxes" id="wxsatCountdownBoxes">
|
||
<div class="wxsat-countdown-box"><span class="wxsat-cd-value" id="wxsatCdDays">--</span><span class="wxsat-cd-unit">DAYS</span></div>
|
||
<div class="wxsat-countdown-box"><span class="wxsat-cd-value" id="wxsatCdHours">--</span><span class="wxsat-cd-unit">HRS</span></div>
|
||
<div class="wxsat-countdown-box"><span class="wxsat-cd-value" id="wxsatCdMins">--</span><span class="wxsat-cd-unit">MIN</span></div>
|
||
<div class="wxsat-countdown-box"><span class="wxsat-cd-value" id="wxsatCdSecs">--</span><span class="wxsat-cd-unit">SEC</span></div>
|
||
</div>
|
||
<div class="wxsat-countdown-info" id="wxsatCountdownInfo">
|
||
<span class="wxsat-countdown-sat" id="wxsatCountdownSat">--</span>
|
||
<span class="wxsat-countdown-detail" id="wxsatCountdownDetail">No passes predicted</span>
|
||
</div>
|
||
</div>
|
||
<div class="wxsat-timeline" id="wxsatTimeline">
|
||
<div class="wxsat-timeline-track" id="wxsatTimelineTrack"></div>
|
||
<div class="wxsat-timeline-cursor" id="wxsatTimelineCursor"></div>
|
||
<div class="wxsat-timeline-labels">
|
||
<span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span><span>24:00</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Capture progress -->
|
||
<div class="wxsat-capture-status" id="wxsatCaptureStatus">
|
||
<div class="wxsat-capture-info">
|
||
<span class="wxsat-capture-message" id="wxsatCaptureMsg">--</span>
|
||
<span class="wxsat-capture-elapsed" id="wxsatCaptureElapsed">0:00</span>
|
||
</div>
|
||
<div class="wxsat-progress-bar">
|
||
<div class="progress" id="wxsatProgressFill" style="width: 0%"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Decoder Console -->
|
||
<div class="wxsat-signal-console" id="wxsatSignalConsole">
|
||
<div class="wxsat-console-header">
|
||
<div class="wxsat-console-title-group">
|
||
<span class="wxsat-console-title">DECODER CONSOLE</span>
|
||
<div class="wxsat-phase-indicator" id="wxsatPhaseIndicator">
|
||
<span class="wxsat-phase-step" data-phase="tuning">TUNING</span>
|
||
<span class="wxsat-phase-arrow">▸</span>
|
||
<span class="wxsat-phase-step" data-phase="listening">LISTENING</span>
|
||
<span class="wxsat-phase-arrow">▸</span>
|
||
<span class="wxsat-phase-step" data-phase="signal_detected">SIGNAL</span>
|
||
<span class="wxsat-phase-arrow">▸</span>
|
||
<span class="wxsat-phase-step" data-phase="decoding">DECODING</span>
|
||
<span class="wxsat-phase-arrow">▸</span>
|
||
<span class="wxsat-phase-step" data-phase="complete">COMPLETE</span>
|
||
</div>
|
||
</div>
|
||
<button class="wxsat-strip-btn" id="wxsatConsoleToggle"
|
||
onclick="WeatherSat.toggleConsole()" title="Toggle console">▼</button>
|
||
</div>
|
||
<div class="wxsat-console-body" id="wxsatConsoleBody">
|
||
<div class="wxsat-console-log" id="wxsatConsoleLog">
|
||
<div class="wxsat-console-entry wxsat-log-info">Waiting for capture...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main content: 3-column layout -->
|
||
<div class="wxsat-content">
|
||
<!-- Left: Pass predictions -->
|
||
<div class="wxsat-passes-panel">
|
||
<div class="wxsat-passes-header">
|
||
<span class="wxsat-passes-title">Upcoming Passes</span>
|
||
<span class="wxsat-passes-count" id="wxsatPassesCount">0</span>
|
||
</div>
|
||
<div class="wxsat-passes-list" id="wxsatPassesList">
|
||
<div class="wxsat-gallery-empty">
|
||
<p>Set location to see pass predictions</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Center: Polar plot + Ground track map -->
|
||
<div class="wxsat-center-panel">
|
||
<div class="wxsat-polar-container">
|
||
<div class="wxsat-panel-header">
|
||
<span class="wxsat-panel-title">Polar Plot</span>
|
||
<span class="wxsat-panel-subtitle" id="wxsatPolarSat">--</span>
|
||
</div>
|
||
<canvas id="wxsatPolarCanvas" width="300" height="300"></canvas>
|
||
</div>
|
||
<div class="wxsat-map-container">
|
||
<div class="wxsat-panel-header">
|
||
<span class="wxsat-panel-title">Global Projection</span>
|
||
<span class="wxsat-panel-subtitle" id="wxsatMapInfo">--</span>
|
||
</div>
|
||
<div id="wxsatGroundMap" class="wxsat-ground-map"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right: Image gallery -->
|
||
<div class="wxsat-gallery-panel">
|
||
<div class="wxsat-gallery-header">
|
||
<span class="wxsat-gallery-title">Decoded Images</span>
|
||
<span class="wxsat-gallery-count" id="wxsatImageCount">0</span>
|
||
<button class="wxsat-gallery-clear-btn" onclick="WeatherSat.deleteAllImages()" title="Delete all images">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div class="wxsat-gallery-grid" id="wxsatGallery">
|
||
<div class="wxsat-gallery-empty">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<circle cx="12" cy="12" r="10"/>
|
||
<path d="M2 12h20"/>
|
||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||
</svg>
|
||
<p>No images decoded yet</p>
|
||
<p style="margin-top: 4px; font-size: 11px;">Select a satellite pass and start capturing</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SSTV General Decoder Dashboard -->
|
||
<div id="sstvGeneralVisuals" class="sstv-general-visuals-container" style="display: none;">
|
||
<!-- Status Strip -->
|
||
<div class="sstv-general-stats-strip">
|
||
<div class="sstv-general-strip-group">
|
||
<div class="sstv-general-strip-status">
|
||
<span class="sstv-general-strip-dot idle" id="sstvGeneralStripDot"></span>
|
||
<span class="sstv-general-strip-status-text" id="sstvGeneralStripStatus">Idle</span>
|
||
</div>
|
||
<button class="sstv-general-strip-btn start" id="sstvGeneralStartBtn" onclick="SSTVGeneral.start()">Start</button>
|
||
<button class="sstv-general-strip-btn stop" id="sstvGeneralStopBtn" onclick="SSTVGeneral.stop()" style="display: none;">Stop</button>
|
||
</div>
|
||
<div class="sstv-general-strip-divider"></div>
|
||
<div class="sstv-general-strip-group">
|
||
<div class="sstv-general-strip-stat">
|
||
<span class="sstv-general-strip-value accent-cyan" id="sstvGeneralStripFreq">14.230</span>
|
||
<span class="sstv-general-strip-label">MHZ</span>
|
||
</div>
|
||
<div class="sstv-general-strip-stat">
|
||
<span class="sstv-general-strip-value" id="sstvGeneralStripMod">FM</span>
|
||
<span class="sstv-general-strip-label">MOD</span>
|
||
</div>
|
||
<div class="sstv-general-strip-stat">
|
||
<span class="sstv-general-strip-value" id="sstvGeneralStripImageCount">0</span>
|
||
<span class="sstv-general-strip-label">IMAGES</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Signal Scope -->
|
||
<div id="sstvGeneralScopePanel" style="display: none; margin-bottom: 12px;">
|
||
<div style="background: #0a0a0a; border: 1px solid #1e1a2e; border-radius: 6px; padding: 8px 10px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
|
||
<span>Audio Waveform</span>
|
||
<div style="display: flex; gap: 14px;">
|
||
<span>RMS: <span id="sstvGeneralScopeRmsLabel" style="color: #c080ff; font-variant-numeric: tabular-nums;">0</span></span>
|
||
<span>PEAK: <span id="sstvGeneralScopePeakLabel" style="color: #f44; font-variant-numeric: tabular-nums;">0</span></span>
|
||
<span id="sstvGeneralScopeToneLabel" style="color: #444;">QUIET</span>
|
||
<span id="sstvGeneralScopeStatusLabel" style="color: #444;">IDLE</span>
|
||
</div>
|
||
</div>
|
||
<canvas id="sstvGeneralScopeCanvas" style="width: 100%; height: 80px; display: block; border-radius: 3px; background: #050510;"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main Row (Live + Gallery) -->
|
||
<div class="sstv-general-main-row">
|
||
<!-- Live Decode Section -->
|
||
<div class="sstv-general-live-section">
|
||
<div class="sstv-general-live-header">
|
||
<div class="sstv-general-live-title">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 18px; height: 18px;">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||
<circle cx="12" cy="12" r="3"/>
|
||
</svg>
|
||
Live Decode
|
||
</div>
|
||
</div>
|
||
<div class="sstv-general-live-content" id="sstvGeneralLiveContent">
|
||
<div class="sstv-general-idle-state">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||
<circle cx="12" cy="12" r="3"/>
|
||
<path d="M3 9h2M19 9h2M3 15h2M19 15h2"/>
|
||
</svg>
|
||
<h4>SSTV Decoder</h4>
|
||
<p>Select a frequency and click Start to listen for SSTV transmissions</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Gallery Section -->
|
||
<div class="sstv-general-gallery-section">
|
||
<div class="sstv-general-gallery-header">
|
||
<div class="sstv-general-gallery-title">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width: 18px; height: 18px;">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||
<polyline points="21 15 16 10 5 21"/>
|
||
</svg>
|
||
Decoded Images
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:4px;">
|
||
<span class="sstv-general-gallery-count" id="sstvGeneralImageCount">0</span>
|
||
<button class="sstv-general-gallery-clear-btn" onclick="SSTVGeneral.deleteAllImages()" title="Delete all images">Clear All</button>
|
||
</div>
|
||
</div>
|
||
<div class="sstv-general-gallery-grid" id="sstvGeneralGallery">
|
||
<div class="sstv-general-gallery-empty">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||
<polyline points="21 15 16 10 5 21"/>
|
||
</svg>
|
||
<p>No images decoded yet</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- WeFax Decoder Dashboard -->
|
||
<div id="wefaxVisuals" class="wefax-visuals-container" style="display: none;">
|
||
<!-- Stats Strip -->
|
||
<div class="wefax-stats-strip">
|
||
<div class="wefax-strip-group">
|
||
<div class="wefax-strip-status">
|
||
<span class="wefax-strip-dot idle" id="wefaxStripDot"></span>
|
||
<span class="wefax-strip-status-text" id="wefaxStripStatus">Idle</span>
|
||
</div>
|
||
<button class="wefax-strip-btn start" id="wefaxStartBtn" onclick="WeFax.start()">Start</button>
|
||
<button class="wefax-strip-btn stop" id="wefaxStopBtn" onclick="WeFax.stop()" style="display: none;">Stop</button>
|
||
<label class="wefax-schedule-toggle" title="Auto-capture broadcasts">
|
||
<input type="checkbox" id="wefaxStripAutoSchedule"
|
||
onchange="WeFax.toggleScheduler(this)">
|
||
<span>Auto</span>
|
||
</label>
|
||
</div>
|
||
<div class="wefax-strip-divider"></div>
|
||
<div class="wefax-strip-group">
|
||
<div class="wefax-strip-stat">
|
||
<span class="wefax-strip-value accent-amber" id="wefaxStripFreq">---</span>
|
||
<span class="wefax-strip-label">KHZ</span>
|
||
</div>
|
||
<div class="wefax-strip-stat">
|
||
<span class="wefax-strip-value" id="wefaxStripLines">0</span>
|
||
<span class="wefax-strip-label">LINES</span>
|
||
</div>
|
||
<div class="wefax-strip-stat">
|
||
<span class="wefax-strip-value" id="wefaxStripImageCount">0</span>
|
||
<span class="wefax-strip-label">IMAGES</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Countdown + Timeline -->
|
||
<div class="wefax-countdown-bar" id="wefaxCountdownBar" style="display: none;">
|
||
<div class="wefax-countdown-next">
|
||
<div class="wefax-countdown-boxes" id="wefaxCountdownBoxes">
|
||
<div class="wefax-countdown-box"><span class="wefax-cd-value" id="wefaxCdHours">--</span><span class="wefax-cd-unit">HRS</span></div>
|
||
<div class="wefax-countdown-box"><span class="wefax-cd-value" id="wefaxCdMins">--</span><span class="wefax-cd-unit">MIN</span></div>
|
||
<div class="wefax-countdown-box"><span class="wefax-cd-value" id="wefaxCdSecs">--</span><span class="wefax-cd-unit">SEC</span></div>
|
||
</div>
|
||
<div class="wefax-countdown-info" id="wefaxCountdownInfo">
|
||
<span class="wefax-countdown-content" id="wefaxCountdownContent">--</span>
|
||
<span class="wefax-countdown-detail" id="wefaxCountdownDetail">Select a station</span>
|
||
</div>
|
||
</div>
|
||
<div class="wefax-timeline" id="wefaxTimeline">
|
||
<div class="wefax-timeline-track" id="wefaxTimelineTrack"></div>
|
||
<div class="wefax-timeline-cursor" id="wefaxTimelineCursor"></div>
|
||
<div class="wefax-timeline-labels">
|
||
<span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span><span>24:00</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Audio Waveform Scope -->
|
||
<div id="wefaxScopePanel" style="display: none;">
|
||
<div style="background: #0a0a0a; border: 1px solid #2e2a1a; border-radius: 6px; padding: 8px 10px; font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
|
||
<span>Audio Waveform</span>
|
||
<div style="display: flex; gap: 14px;">
|
||
<span>RMS: <span id="wefaxScopeRmsLabel" style="color: #ffaa00; font-variant-numeric: tabular-nums;">0</span></span>
|
||
<span>PEAK: <span id="wefaxScopePeakLabel" style="color: #f44; font-variant-numeric: tabular-nums;">0</span></span>
|
||
<span id="wefaxScopeStatusLabel" style="color: #444;">IDLE</span>
|
||
</div>
|
||
</div>
|
||
<canvas id="wefaxScopeCanvas" style="width: 100%; height: 80px; display: block; border-radius: 3px; background: #050510;"></canvas>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Schedule Timeline -->
|
||
<div class="wefax-schedule-panel">
|
||
<div class="wefax-schedule-header">
|
||
<span class="wefax-schedule-title">Broadcast Schedule</span>
|
||
<span id="wefaxStatusText" style="font-family: var(--font-mono); font-size: 10px; color: var(--text-dim);"></span>
|
||
</div>
|
||
<div id="wefaxScheduleTimeline">
|
||
<div class="wefax-schedule-empty">Select a station to see broadcast schedule</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Main Content: Live Preview + Gallery -->
|
||
<div class="wefax-main-row">
|
||
<div class="wefax-live-section">
|
||
<div class="wefax-live-header">
|
||
<div class="wefax-live-title">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="vertical-align: -2px; margin-right: 4px; color: #ffaa00;">
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||
<polyline points="14 2 14 8 20 8"/>
|
||
</svg>
|
||
Live Decode
|
||
</div>
|
||
</div>
|
||
<div class="wefax-live-content" id="wefaxLiveContent">
|
||
<div class="wefax-idle-state" id="wefaxIdleState">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||
<polyline points="14 2 14 8 20 8"/>
|
||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||
</svg>
|
||
<h4>WeFax Decoder</h4>
|
||
<p>Select a station and click Start to decode weather fax transmissions</p>
|
||
</div>
|
||
<img id="wefaxLivePreview" class="wefax-live-preview" style="display: none;" alt="WeFax decode in progress">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wefax-gallery-section">
|
||
<div class="wefax-gallery-header">
|
||
<div class="wefax-gallery-title">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14" style="vertical-align: -2px; margin-right: 4px; color: #ffaa00;">
|
||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||
<polyline points="21 15 16 10 5 21"/>
|
||
</svg>
|
||
Decoded Images
|
||
</div>
|
||
<div class="wefax-gallery-controls">
|
||
<span class="wefax-gallery-count" id="wefaxImageCount">0</span>
|
||
<button class="wefax-gallery-clear-btn" onclick="WeFax.deleteAllImages()" title="Delete all images">Clear All</button>
|
||
</div>
|
||
</div>
|
||
<div class="wefax-gallery-grid" id="wefaxGallery">
|
||
<div class="wefax-gallery-empty">No images decoded yet</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Space Weather Dashboard -->
|
||
<div id="spaceWeatherVisuals" class="sw-visuals-container" style="display: none;">
|
||
<!-- Header metrics strip -->
|
||
<div class="sw-header-strip">
|
||
<div class="sw-header-stat">
|
||
<span class="sw-header-value accent-cyan" id="swStripSfi">--</span>
|
||
<span class="sw-header-label">SFI</span>
|
||
</div>
|
||
<div class="sw-header-stat">
|
||
<span class="sw-header-value" id="swStripKp">--</span>
|
||
<span class="sw-header-label">Kp</span>
|
||
</div>
|
||
<div class="sw-header-stat">
|
||
<span class="sw-header-value" id="swStripA">--</span>
|
||
<span class="sw-header-label">A-Index</span>
|
||
</div>
|
||
<div class="sw-header-stat">
|
||
<span class="sw-header-value" id="swStripSsn">--</span>
|
||
<span class="sw-header-label">SSN</span>
|
||
</div>
|
||
<div class="sw-header-stat">
|
||
<span class="sw-header-value" id="swStripWind">--</span>
|
||
<span class="sw-header-label">Wind</span>
|
||
</div>
|
||
<div class="sw-header-stat">
|
||
<span class="sw-header-value" id="swStripBz">--</span>
|
||
<span class="sw-header-label">Bz</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- NOAA G/S/R Scales -->
|
||
<div class="sw-scales-row">
|
||
<div class="sw-scale-card" id="swScaleG">
|
||
<div class |