mirror of
https://github.com/smittix/intercept.git
synced 2026-07-07 09:08:12 -07:00
Merge upstream/main and resolve adsb_dashboard.html conflict
Take upstream's crosshair animation system and updated selectAircraft(icao, source) signature. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+158
-30
@@ -258,6 +258,10 @@
|
||||
<div class="display-container">
|
||||
<div id="radarMap">
|
||||
</div>
|
||||
<div id="mapCrosshairOverlay" class="map-crosshair-overlay" aria-hidden="true">
|
||||
<div class="map-crosshair-line map-crosshair-vertical"></div>
|
||||
<div class="map-crosshair-line map-crosshair-horizontal"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -429,6 +433,17 @@
|
||||
let alertsEnabled = true;
|
||||
let detectionSoundEnabled = localStorage.getItem('adsb_detectionSound') !== 'false'; // Default on
|
||||
let soundedAircraft = {}; // Track aircraft we've played detection sound for
|
||||
const MAP_CROSSHAIR_DURATION_MS = 1500;
|
||||
const PANEL_SELECTION_BASE_ZOOM = 10;
|
||||
const PANEL_SELECTION_MAX_ZOOM = 12;
|
||||
const PANEL_SELECTION_ZOOM_INCREMENT = 1.4;
|
||||
const PANEL_SELECTION_STAGE1_DURATION_SEC = 1.05;
|
||||
const PANEL_SELECTION_STAGE2_DURATION_SEC = 1.15;
|
||||
const PANEL_SELECTION_STAGE_GAP_MS = 180;
|
||||
let mapCrosshairResetTimer = null;
|
||||
let panelSelectionFallbackTimer = null;
|
||||
let panelSelectionStageTimer = null;
|
||||
let mapCrosshairRequestId = 0;
|
||||
// Watchlist - persisted to localStorage
|
||||
let watchlist = JSON.parse(localStorage.getItem('adsb_watchlist') || '[]');
|
||||
|
||||
@@ -2620,7 +2635,7 @@ sudo make install</code>
|
||||
} else {
|
||||
markers[icao] = L.marker([ac.lat, ac.lon], { icon: createMarkerIcon(rotation, color, iconType, isSelected) })
|
||||
.addTo(radarMap)
|
||||
.on('click', () => selectAircraft(icao));
|
||||
.on('click', () => selectAircraft(icao, 'map'));
|
||||
markers[icao].bindTooltip(`${callsign}<br>${alt}`, {
|
||||
permanent: false, direction: 'top', className: 'aircraft-tooltip'
|
||||
});
|
||||
@@ -2724,7 +2739,7 @@ sudo make install</code>
|
||||
const div = document.createElement('div');
|
||||
div.className = `aircraft-item ${selectedIcao === ac.icao ? 'selected' : ''} ${isOnWatchlist(ac) ? 'watched' : ''}`;
|
||||
div.setAttribute('data-icao', ac.icao);
|
||||
div.onclick = () => selectAircraft(ac.icao);
|
||||
div.onclick = () => selectAircraft(ac.icao, 'panel');
|
||||
div.innerHTML = buildAircraftItemHTML(ac);
|
||||
fragment.appendChild(div);
|
||||
});
|
||||
@@ -2797,34 +2812,139 @@ sudo make install</code>
|
||||
`;
|
||||
}
|
||||
|
||||
function selectAircraft(icao) {
|
||||
// Toggle: clicking the same aircraft deselects it
|
||||
if (selectedIcao === icao) {
|
||||
const prev = selectedIcao;
|
||||
selectedIcao = null;
|
||||
// Reset previous marker icon
|
||||
if (prev && markers[prev] && aircraft[prev]) {
|
||||
const ac = aircraft[prev];
|
||||
const militaryInfo = isMilitaryAircraft(prev, ac.callsign);
|
||||
const rotation = Math.round((ac.heading || 0) / 5) * 5;
|
||||
const color = militaryInfo.military ? '#556b2f' : getAltitudeColor(ac.altitude);
|
||||
const iconType = getAircraftIconType(ac.type_code, militaryInfo.military);
|
||||
markers[prev].setIcon(createMarkerIcon(rotation, color, iconType, false));
|
||||
if (markerState[prev]) markerState[prev].isSelected = false;
|
||||
}
|
||||
renderAircraftList();
|
||||
showAircraftDetails(null);
|
||||
updateFlightLookupBtn();
|
||||
highlightSidebarMessages(null);
|
||||
if (acarsMessageTimer) {
|
||||
clearInterval(acarsMessageTimer);
|
||||
acarsMessageTimer = null;
|
||||
}
|
||||
function triggerMapCrosshairAnimation(lat, lon, durationMs = MAP_CROSSHAIR_DURATION_MS, lockToMapCenter = false) {
|
||||
if (!radarMap) return;
|
||||
const overlay = document.getElementById('mapCrosshairOverlay');
|
||||
if (!overlay) return;
|
||||
|
||||
const size = radarMap.getSize();
|
||||
let targetX;
|
||||
let targetY;
|
||||
|
||||
if (lockToMapCenter) {
|
||||
targetX = size.x / 2;
|
||||
targetY = size.y / 2;
|
||||
} else {
|
||||
const point = radarMap.latLngToContainerPoint([lat, lon]);
|
||||
targetX = Math.max(0, Math.min(size.x, point.x));
|
||||
targetY = Math.max(0, Math.min(size.y, point.y));
|
||||
}
|
||||
|
||||
const startX = size.x + 8;
|
||||
const startY = size.y + 8;
|
||||
|
||||
overlay.style.setProperty('--crosshair-x-start', `${startX}px`);
|
||||
overlay.style.setProperty('--crosshair-y-start', `${startY}px`);
|
||||
overlay.style.setProperty('--crosshair-x-end', `${targetX}px`);
|
||||
overlay.style.setProperty('--crosshair-y-end', `${targetY}px`);
|
||||
overlay.style.setProperty('--crosshair-duration', `${durationMs}ms`);
|
||||
overlay.classList.remove('active');
|
||||
void overlay.offsetWidth;
|
||||
overlay.classList.add('active');
|
||||
|
||||
if (mapCrosshairResetTimer) {
|
||||
clearTimeout(mapCrosshairResetTimer);
|
||||
}
|
||||
mapCrosshairResetTimer = setTimeout(() => {
|
||||
overlay.classList.remove('active');
|
||||
mapCrosshairResetTimer = null;
|
||||
}, durationMs + 100);
|
||||
}
|
||||
|
||||
function getPanelSelectionFinalZoom() {
|
||||
if (!radarMap) return PANEL_SELECTION_BASE_ZOOM;
|
||||
const currentZoom = radarMap.getZoom();
|
||||
const maxZoom = typeof radarMap.getMaxZoom === 'function' ? radarMap.getMaxZoom() : PANEL_SELECTION_MAX_ZOOM;
|
||||
return Math.min(
|
||||
PANEL_SELECTION_MAX_ZOOM,
|
||||
maxZoom,
|
||||
Math.max(PANEL_SELECTION_BASE_ZOOM, currentZoom + PANEL_SELECTION_ZOOM_INCREMENT)
|
||||
);
|
||||
}
|
||||
|
||||
function getPanelSelectionIntermediateZoom(finalZoom) {
|
||||
if (!radarMap) return finalZoom;
|
||||
const currentZoom = radarMap.getZoom();
|
||||
if (finalZoom - currentZoom < 0.8) {
|
||||
return finalZoom;
|
||||
}
|
||||
const midpointZoom = currentZoom + ((finalZoom - currentZoom) * 0.55);
|
||||
return Math.min(finalZoom - 0.45, midpointZoom);
|
||||
}
|
||||
|
||||
function runPanelSelectionAnimation(lat, lon, requestId) {
|
||||
if (!radarMap) return;
|
||||
|
||||
const finalZoom = getPanelSelectionFinalZoom();
|
||||
const intermediateZoom = getPanelSelectionIntermediateZoom(finalZoom);
|
||||
const sequenceDurationMs = Math.round(
|
||||
((PANEL_SELECTION_STAGE1_DURATION_SEC + PANEL_SELECTION_STAGE2_DURATION_SEC) * 1000) +
|
||||
PANEL_SELECTION_STAGE_GAP_MS + 260
|
||||
);
|
||||
const startSecondStage = () => {
|
||||
if (requestId !== mapCrosshairRequestId) return;
|
||||
radarMap.flyTo([lat, lon], finalZoom, {
|
||||
animate: true,
|
||||
duration: PANEL_SELECTION_STAGE2_DURATION_SEC,
|
||||
easeLinearity: 0.2
|
||||
});
|
||||
};
|
||||
|
||||
triggerMapCrosshairAnimation(
|
||||
lat,
|
||||
lon,
|
||||
Math.max(MAP_CROSSHAIR_DURATION_MS, sequenceDurationMs),
|
||||
true
|
||||
);
|
||||
|
||||
if (intermediateZoom >= finalZoom - 0.1) {
|
||||
radarMap.flyTo([lat, lon], finalZoom, {
|
||||
animate: true,
|
||||
duration: PANEL_SELECTION_STAGE2_DURATION_SEC,
|
||||
easeLinearity: 0.2
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let stage1Handled = false;
|
||||
const finishStage1 = () => {
|
||||
if (stage1Handled || requestId !== mapCrosshairRequestId) return;
|
||||
stage1Handled = true;
|
||||
if (panelSelectionFallbackTimer) {
|
||||
clearTimeout(panelSelectionFallbackTimer);
|
||||
panelSelectionFallbackTimer = null;
|
||||
}
|
||||
panelSelectionStageTimer = setTimeout(() => {
|
||||
panelSelectionStageTimer = null;
|
||||
startSecondStage();
|
||||
}, PANEL_SELECTION_STAGE_GAP_MS);
|
||||
};
|
||||
|
||||
radarMap.once('moveend', finishStage1);
|
||||
panelSelectionFallbackTimer = setTimeout(
|
||||
finishStage1,
|
||||
Math.round(PANEL_SELECTION_STAGE1_DURATION_SEC * 1000) + 160
|
||||
);
|
||||
|
||||
radarMap.flyTo([lat, lon], intermediateZoom, {
|
||||
animate: true,
|
||||
duration: PANEL_SELECTION_STAGE1_DURATION_SEC,
|
||||
easeLinearity: 0.2
|
||||
});
|
||||
}
|
||||
|
||||
function selectAircraft(icao, source = 'map') {
|
||||
const prevSelected = selectedIcao;
|
||||
selectedIcao = icao;
|
||||
mapCrosshairRequestId += 1;
|
||||
if (panelSelectionFallbackTimer) {
|
||||
clearTimeout(panelSelectionFallbackTimer);
|
||||
panelSelectionFallbackTimer = null;
|
||||
}
|
||||
if (panelSelectionStageTimer) {
|
||||
clearTimeout(panelSelectionStageTimer);
|
||||
panelSelectionStageTimer = null;
|
||||
}
|
||||
|
||||
// Update marker icons for both previous and new selection
|
||||
[prevSelected, icao].forEach(targetIcao => {
|
||||
@@ -2850,7 +2970,15 @@ sudo make install</code>
|
||||
|
||||
const ac = aircraft[icao];
|
||||
if (ac && ac.lat !== undefined && ac.lat !== null && ac.lon !== undefined && ac.lon !== null) {
|
||||
radarMap.setView([ac.lat, ac.lon], 10);
|
||||
const targetLat = ac.lat;
|
||||
const targetLon = ac.lon;
|
||||
|
||||
if (source === 'panel' && radarMap) {
|
||||
runPanelSelectionAnimation(targetLat, targetLon, mapCrosshairRequestId);
|
||||
return;
|
||||
}
|
||||
|
||||
radarMap.setView([targetLat, targetLon], 10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3264,7 +3392,7 @@ sudo make install</code>
|
||||
|
||||
function initAirband() {
|
||||
// Check if audio tools are available
|
||||
fetch('/listening/tools')
|
||||
fetch('/receiver/tools')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const missingTools = [];
|
||||
@@ -3414,7 +3542,7 @@ sudo make install</code>
|
||||
|
||||
try {
|
||||
// Start audio on backend
|
||||
const response = await fetch('/listening/audio/start', {
|
||||
const response = await fetch('/receiver/audio/start', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -3451,7 +3579,7 @@ sudo make install</code>
|
||||
audioPlayer.load();
|
||||
|
||||
// Connect to stream
|
||||
const streamUrl = `/listening/audio/stream?t=${Date.now()}`;
|
||||
const streamUrl = `/receiver/audio/stream?t=${Date.now()}`;
|
||||
console.log('[AIRBAND] Connecting to stream:', streamUrl);
|
||||
audioPlayer.src = streamUrl;
|
||||
|
||||
@@ -3495,7 +3623,7 @@ sudo make install</code>
|
||||
audioPlayer.pause();
|
||||
audioPlayer.src = '';
|
||||
|
||||
fetch('/listening/audio/stop', { method: 'POST' })
|
||||
fetch('/receiver/audio/stop', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(() => {
|
||||
isAirbandPlaying = false;
|
||||
|
||||
+908
-708
File diff suppressed because it is too large
Load Diff
@@ -4,20 +4,20 @@
|
||||
#}
|
||||
|
||||
<!-- Help Modal -->
|
||||
<div id="helpModal" class="help-modal" role="dialog" aria-modal="true" aria-hidden="true" aria-labelledby="helpModalTitle" onclick="if(event.target === this) hideHelp()">
|
||||
<div class="help-content" tabindex="-1">
|
||||
<button type="button" class="help-close" onclick="hideHelp()" aria-label="Close help">×</button>
|
||||
<h2 id="helpModalTitle">iNTERCEPT Help</h2>
|
||||
|
||||
<div class="help-tabs" role="tablist" aria-label="Help sections">
|
||||
<button type="button" class="help-tab active" data-tab="icons" onclick="switchHelpTab('icons')" role="tab" aria-controls="help-icons" aria-selected="true">Icons</button>
|
||||
<button type="button" class="help-tab" data-tab="modes" onclick="switchHelpTab('modes')" role="tab" aria-controls="help-modes" aria-selected="false">Modes</button>
|
||||
<button type="button" class="help-tab" data-tab="wifi" onclick="switchHelpTab('wifi')" role="tab" aria-controls="help-wifi" aria-selected="false">WiFi</button>
|
||||
<button type="button" class="help-tab" data-tab="tips" onclick="switchHelpTab('tips')" role="tab" aria-controls="help-tips" aria-selected="false">Tips</button>
|
||||
</div>
|
||||
|
||||
<!-- Icons Section -->
|
||||
<div id="help-icons" class="help-section active" role="tabpanel">
|
||||
<div id="helpModal" class="help-modal" role="dialog" aria-modal="true" aria-hidden="true" aria-labelledby="helpModalTitle" onclick="if(event.target === this) hideHelp()">
|
||||
<div class="help-content" tabindex="-1">
|
||||
<button type="button" class="help-close" onclick="hideHelp()" aria-label="Close help">×</button>
|
||||
<h2 id="helpModalTitle">iNTERCEPT Help</h2>
|
||||
|
||||
<div class="help-tabs" role="tablist" aria-label="Help sections">
|
||||
<button type="button" class="help-tab active" data-tab="icons" onclick="switchHelpTab('icons')" role="tab" aria-controls="help-icons" aria-selected="true">Icons</button>
|
||||
<button type="button" class="help-tab" data-tab="modes" onclick="switchHelpTab('modes')" role="tab" aria-controls="help-modes" aria-selected="false">Modes</button>
|
||||
<button type="button" class="help-tab" data-tab="wifi" onclick="switchHelpTab('wifi')" role="tab" aria-controls="help-wifi" aria-selected="false">WiFi</button>
|
||||
<button type="button" class="help-tab" data-tab="tips" onclick="switchHelpTab('tips')" role="tab" aria-controls="help-tips" aria-selected="false">Tips</button>
|
||||
</div>
|
||||
|
||||
<!-- Icons Section -->
|
||||
<div id="help-icons" class="help-section active" role="tabpanel">
|
||||
<h3>Stats Bar Icons</h3>
|
||||
<div class="icon-grid">
|
||||
<div class="icon-item"><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 class="desc">POCSAG messages decoded</span></div>
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="icon-item"><span class="icon icon--sm"><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="desc">Aircraft - ADS-B tracking & history</span></div>
|
||||
<div class="icon-item"><span class="icon icon--sm"><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"/><path d="M12 6l4 3"/></svg></span><span class="desc">Vessels - AIS & VHF DSC distress</span></div>
|
||||
<div class="icon-item"><span class="icon icon--sm"><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="desc">APRS - Amateur radio tracking</span></div>
|
||||
<div class="icon-item"><span class="icon icon--sm"><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"/><path d="M3 9h18"/><path d="M9 21V9"/></svg></span><span class="desc">Listening Post - SDR scanner</span></div>
|
||||
<div class="icon-item"><span class="icon icon--sm"><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.4"/><path d="M2 21h20" opacity="0.2"/></svg></span><span class="desc">Waterfall - SDR receiver + signal ID</span></div>
|
||||
<div class="icon-item"><span class="icon icon--sm"><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="desc">Spy Stations - Number stations database</span></div>
|
||||
<div class="icon-item"><span class="icon icon--sm"><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="desc">Meshtastic - LoRa mesh networking</span></div>
|
||||
<div class="icon-item"><span class="icon icon--sm"><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="desc">WebSDR - Remote SDR receivers</span></div>
|
||||
@@ -62,7 +62,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Modes Section -->
|
||||
<div id="help-modes" class="help-section" role="tabpanel" hidden>
|
||||
<div id="help-modes" class="help-section" role="tabpanel" hidden>
|
||||
<h3>Pager Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Decodes POCSAG and FLEX pager signals using RTL-SDR</li>
|
||||
@@ -114,7 +114,7 @@
|
||||
<li>Interactive map shows station positions in real-time</li>
|
||||
</ul>
|
||||
|
||||
<h3>Listening Post Mode</h3>
|
||||
<h3>Spectrum Waterfall Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Wideband SDR scanner with spectrum visualization</li>
|
||||
<li>Tune to any frequency supported by your SDR hardware</li>
|
||||
@@ -129,7 +129,7 @@
|
||||
<li>Browse stations from priyom.org with frequencies and schedules</li>
|
||||
<li>Filter by type (number/diplomatic), country, and mode</li>
|
||||
<li>Famous stations: UVB-76 "The Buzzer", Cuban HM01, Israeli E17z</li>
|
||||
<li>Click "Tune" to listen via Listening Post mode</li>
|
||||
<li>Click "Tune" to listen via Spectrum Waterfall mode</li>
|
||||
</ul>
|
||||
|
||||
<h3>Meshtastic Mode</h3>
|
||||
@@ -166,11 +166,27 @@
|
||||
<li>View next pass predictions with elevation and duration</li>
|
||||
</ul>
|
||||
|
||||
<h3>ACARS Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Decodes Aircraft Communications Addressing and Reporting System messages via acarsdec</li>
|
||||
<li>Receives operational, weather, and position reports on 129-136 MHz</li>
|
||||
<li>Supports North America, Europe, and Asia-Pacific regional frequency presets</li>
|
||||
<li>Filter by message type, flight ID, or aircraft registration</li>
|
||||
</ul>
|
||||
|
||||
<h3>VDL2 Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Decodes VHF Data Link Mode 2 aircraft datalink messages via dumpvdl2</li>
|
||||
<li>Captures ACARS-over-AVLC frames with full signal analysis (SNR, burst length)</li>
|
||||
<li>Monitor multiple VDL2 frequencies simultaneously (136.725, 136.775, 136.975 MHz)</li>
|
||||
<li>Export captured messages to CSV or JSON for offline analysis</li>
|
||||
</ul>
|
||||
|
||||
<h3>ISS SSTV Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li>Decodes Slow Scan Television (SSTV) images from the International Space Station</li>
|
||||
<li>Automated ISS pass tracking with Doppler correction on 145.800 MHz</li>
|
||||
<li>Images decoded in real-time using slowrx</li>
|
||||
<li>Images decoded in real-time using the built-in pure Python decoder</li>
|
||||
<li>Gallery view with timestamped decoded images</li>
|
||||
</ul>
|
||||
|
||||
@@ -254,7 +270,7 @@
|
||||
</div>
|
||||
|
||||
<!-- WiFi Section -->
|
||||
<div id="help-wifi" class="help-section" role="tabpanel" hidden>
|
||||
<div id="help-wifi" class="help-section" role="tabpanel" hidden>
|
||||
<h3>Monitor Mode</h3>
|
||||
<ul class="tip-list">
|
||||
<li><strong>Enable Monitor:</strong> Puts WiFi adapter in monitor mode for passive scanning</li>
|
||||
@@ -302,7 +318,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Tips Section -->
|
||||
<div id="help-tips" class="help-section" role="tabpanel" hidden>
|
||||
<div id="help-tips" class="help-section" role="tabpanel" hidden>
|
||||
<h3>General Tips</h3>
|
||||
<ul class="tip-list">
|
||||
<li><strong>Collapsible sections:</strong> Click any section header (∇) to collapse/expand</li>
|
||||
@@ -330,15 +346,17 @@
|
||||
<li><strong>Aircraft (ACARS):</strong> Second RTL-SDR, acarsdec</li>
|
||||
<li><strong>Vessels (AIS):</strong> RTL-SDR, AIS-catcher</li>
|
||||
<li><strong>APRS:</strong> RTL-SDR, direwolf or multimon-ng</li>
|
||||
<li><strong>Listening Post:</strong> RTL-SDR or SoapySDR-compatible hardware</li>
|
||||
<li><strong>Spectrum Waterfall:</strong> RTL-SDR or SoapySDR-compatible hardware</li>
|
||||
<li><strong>Spy Stations:</strong> Internet connection (database lookup)</li>
|
||||
<li><strong>Meshtastic:</strong> Meshtastic LoRa device, <code>pip install meshtastic</code></li>
|
||||
<li><strong>WebSDR:</strong> Internet connection (remote receivers)</li>
|
||||
<li><strong>SubGHz:</strong> RTL-SDR or compatible SDR hardware</li>
|
||||
<li><strong>Satellite:</strong> Internet for Celestrak (optional), skyfield</li>
|
||||
<li><strong>ISS SSTV:</strong> RTL-SDR, slowrx</li>
|
||||
<li><strong>ACARS:</strong> RTL-SDR, acarsdec</li>
|
||||
<li><strong>VDL2:</strong> RTL-SDR, dumpvdl2</li>
|
||||
<li><strong>ISS SSTV:</strong> RTL-SDR (pure Python decoder — no external tools needed)</li>
|
||||
<li><strong>Weather Sat:</strong> RTL-SDR, SatDump</li>
|
||||
<li><strong>HF SSTV:</strong> RTL-SDR or SoapySDR-compatible hardware, slowrx</li>
|
||||
<li><strong>HF SSTV:</strong> RTL-SDR or SoapySDR-compatible hardware (pure Python decoder)</li>
|
||||
<li><strong>GPS:</strong> RTL-SDR or GPS-capable SDR</li>
|
||||
<li><strong>Space Weather:</strong> Internet connection (public APIs)</li>
|
||||
<li><strong>WiFi:</strong> Monitor-mode adapter, aircrack-ng suite</li>
|
||||
@@ -360,62 +378,62 @@
|
||||
|
||||
<script>
|
||||
// Help modal functions - defined here so all pages have them
|
||||
(function() {
|
||||
let lastHelpFocusEl = null;
|
||||
|
||||
// Only define if not already defined (index.html defines its own)
|
||||
if (typeof window.showHelp === 'undefined') {
|
||||
window.showHelp = function() {
|
||||
const modal = document.getElementById('helpModal');
|
||||
lastHelpFocusEl = document.activeElement;
|
||||
modal.classList.add('active');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
const content = modal.querySelector('.help-content');
|
||||
if (content) content.focus();
|
||||
document.body.style.overflow = 'hidden';
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window.hideHelp === 'undefined') {
|
||||
window.hideHelp = function() {
|
||||
const modal = document.getElementById('helpModal');
|
||||
modal.classList.remove('active');
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
document.body.style.overflow = '';
|
||||
if (lastHelpFocusEl && typeof lastHelpFocusEl.focus === 'function') {
|
||||
lastHelpFocusEl.focus();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window.switchHelpTab === 'undefined') {
|
||||
window.switchHelpTab = function(tab) {
|
||||
document.querySelectorAll('.help-tab').forEach(t => {
|
||||
const isActive = t.dataset.tab === tab;
|
||||
t.classList.toggle('active', isActive);
|
||||
t.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||
});
|
||||
document.querySelectorAll('.help-section').forEach(s => {
|
||||
const isActive = s.id === ('help-' + tab);
|
||||
s.classList.toggle('active', isActive);
|
||||
s.hidden = !isActive;
|
||||
});
|
||||
};
|
||||
}
|
||||
(function() {
|
||||
let lastHelpFocusEl = null;
|
||||
|
||||
// Only define if not already defined (index.html defines its own)
|
||||
if (typeof window.showHelp === 'undefined') {
|
||||
window.showHelp = function() {
|
||||
const modal = document.getElementById('helpModal');
|
||||
lastHelpFocusEl = document.activeElement;
|
||||
modal.classList.add('active');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
const content = modal.querySelector('.help-content');
|
||||
if (content) content.focus();
|
||||
document.body.style.overflow = 'hidden';
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window.hideHelp === 'undefined') {
|
||||
window.hideHelp = function() {
|
||||
const modal = document.getElementById('helpModal');
|
||||
modal.classList.remove('active');
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
document.body.style.overflow = '';
|
||||
if (lastHelpFocusEl && typeof lastHelpFocusEl.focus === 'function') {
|
||||
lastHelpFocusEl.focus();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window.switchHelpTab === 'undefined') {
|
||||
window.switchHelpTab = function(tab) {
|
||||
document.querySelectorAll('.help-tab').forEach(t => {
|
||||
const isActive = t.dataset.tab === tab;
|
||||
t.classList.toggle('active', isActive);
|
||||
t.setAttribute('aria-selected', isActive ? 'true' : 'false');
|
||||
});
|
||||
document.querySelectorAll('.help-section').forEach(s => {
|
||||
const isActive = s.id === ('help-' + tab);
|
||||
s.classList.toggle('active', isActive);
|
||||
s.hidden = !isActive;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Keyboard shortcuts for help (only add once)
|
||||
if (!window._helpKeyboardSetup) {
|
||||
window._helpKeyboardSetup = true;
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
const modal = document.getElementById('helpModal');
|
||||
if (modal && modal.classList.contains('active')) hideHelp();
|
||||
}
|
||||
// Open help with F1 or ? key (when not typing in an input)
|
||||
var helpModal = document.getElementById('helpModal');
|
||||
if (helpModal && (e.key === 'F1' || (e.key === '?' && !e.target.matches('input, textarea, select'))) && !helpModal.classList.contains('active')) {
|
||||
e.preventDefault();
|
||||
showHelp();
|
||||
if (!window._helpKeyboardSetup) {
|
||||
window._helpKeyboardSetup = true;
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
const modal = document.getElementById('helpModal');
|
||||
if (modal && modal.classList.contains('active')) hideHelp();
|
||||
}
|
||||
// Open help with F1 or ? key (when not typing in an input)
|
||||
var helpModal = document.getElementById('helpModal');
|
||||
if (helpModal && (e.key === 'F1' || (e.key === '?' && !e.target.matches('input, textarea, select'))) && !helpModal.classList.contains('active')) {
|
||||
e.preventDefault();
|
||||
showHelp();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
<!-- ANALYTICS MODE -->
|
||||
<div id="analyticsMode" class="mode-content">
|
||||
{# Analytics Dashboard Sidebar Panel #}
|
||||
|
||||
<div class="section">
|
||||
<h3 class="section-header collapsible" onclick="toggleSection(this)">
|
||||
<span>Summary</span>
|
||||
<span class="collapse-icon">▼</span>
|
||||
</h3>
|
||||
<div class="section-content">
|
||||
<div class="analytics-grid" id="analyticsSummaryCards">
|
||||
<div class="analytics-card" data-mode="adsb">
|
||||
<div class="card-count" id="analyticsCountAdsb">0</div>
|
||||
<div class="card-label">Aircraft</div>
|
||||
<div class="card-sparkline" id="analyticsSparkAdsb"></div>
|
||||
</div>
|
||||
<div class="analytics-card" data-mode="ais">
|
||||
<div class="card-count" id="analyticsCountAis">0</div>
|
||||
<div class="card-label">Vessels</div>
|
||||
<div class="card-sparkline" id="analyticsSparkAis"></div>
|
||||
</div>
|
||||
<div class="analytics-card" data-mode="wifi">
|
||||
<div class="card-count" id="analyticsCountWifi">0</div>
|
||||
<div class="card-label">WiFi</div>
|
||||
<div class="card-sparkline" id="analyticsSparkWifi"></div>
|
||||
</div>
|
||||
<div class="analytics-card" data-mode="bluetooth">
|
||||
<div class="card-count" id="analyticsCountBt">0</div>
|
||||
<div class="card-label">Bluetooth</div>
|
||||
<div class="card-sparkline" id="analyticsSparkBt"></div>
|
||||
</div>
|
||||
<div class="analytics-card" data-mode="dsc">
|
||||
<div class="card-count" id="analyticsCountDsc">0</div>
|
||||
<div class="card-label">DSC</div>
|
||||
<div class="card-sparkline" id="analyticsSparkDsc"></div>
|
||||
</div>
|
||||
<div class="analytics-card" data-mode="acars">
|
||||
<div class="card-count" id="analyticsCountAcars">0</div>
|
||||
<div class="card-label">ACARS</div>
|
||||
<div class="card-sparkline" id="analyticsSparkAcars"></div>
|
||||
</div>
|
||||
<div class="analytics-card" data-mode="vdl2">
|
||||
<div class="card-count" id="analyticsCountVdl2">0</div>
|
||||
<div class="card-label">VDL2</div>
|
||||
<div class="card-sparkline" id="analyticsSparkVdl2"></div>
|
||||
</div>
|
||||
<div class="analytics-card" data-mode="aprs">
|
||||
<div class="card-count" id="analyticsCountAprs">0</div>
|
||||
<div class="card-label">APRS</div>
|
||||
<div class="card-sparkline" id="analyticsSparkAprs"></div>
|
||||
</div>
|
||||
<div class="analytics-card" data-mode="meshtastic">
|
||||
<div class="card-count" id="analyticsCountMesh">0</div>
|
||||
<div class="card-label">Mesh</div>
|
||||
<div class="card-sparkline" id="analyticsSparkMesh"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3 class="section-header collapsible" onclick="toggleSection(this)">
|
||||
<span>Operational Insights</span>
|
||||
<span class="collapse-icon">▼</span>
|
||||
</h3>
|
||||
<div class="section-content">
|
||||
<div class="analytics-insight-grid" id="analyticsInsights">
|
||||
<div class="analytics-empty">Insights loading...</div>
|
||||
</div>
|
||||
<div class="analytics-top-changes">
|
||||
<div class="analytics-section-header">Top Changes</div>
|
||||
<div id="analyticsTopChanges">
|
||||
<div class="analytics-empty">No change signals yet</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3 class="section-header collapsible" onclick="toggleSection(this)">
|
||||
<span>Mode Health</span>
|
||||
<span class="collapse-icon">▼</span>
|
||||
</h3>
|
||||
<div class="section-content">
|
||||
<div class="analytics-health" id="analyticsHealth"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section" id="analyticsSquawkSection" style="display:none;">
|
||||
<h3 class="section-header collapsible" onclick="toggleSection(this)">
|
||||
<span>Emergency Squawks</span>
|
||||
<span class="collapse-icon">▼</span>
|
||||
</h3>
|
||||
<div class="section-content">
|
||||
<div class="squawk-emergency" id="analyticsSquawkPanel">
|
||||
<div class="squawk-title">Active Emergency Codes</div>
|
||||
<div id="analyticsSquawkList"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3 class="section-header collapsible" onclick="toggleSection(this)">
|
||||
<span>Temporal Patterns</span>
|
||||
<span class="collapse-icon">▼</span>
|
||||
</h3>
|
||||
<div class="section-content">
|
||||
<div id="analyticsPatternList">
|
||||
<div class="analytics-empty">No recurring patterns detected</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3 class="section-header collapsible" onclick="toggleSection(this)">
|
||||
<span>Recent Alerts</span>
|
||||
<span class="collapse-icon">▼</span>
|
||||
</h3>
|
||||
<div class="section-content">
|
||||
<div class="analytics-alert-feed" id="analyticsAlertFeed">
|
||||
<div class="analytics-empty">No recent alerts</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3 class="section-header collapsible" onclick="toggleSection(this)">
|
||||
<span>Correlations</span>
|
||||
<span class="collapse-icon">▼</span>
|
||||
</h3>
|
||||
<div class="section-content">
|
||||
<div id="analyticsCorrelations">
|
||||
<div class="analytics-empty">No correlations detected</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3 class="section-header collapsible" onclick="toggleSection(this)">
|
||||
<span>Geofences</span>
|
||||
<span class="collapse-icon">▼</span>
|
||||
</h3>
|
||||
<div class="section-content">
|
||||
<div id="analyticsGeofenceList"></div>
|
||||
<button class="btn btn-sm" onclick="Analytics.addGeofence()" style="margin-top:8px; font-size:10px; padding:4px 10px; background:var(--accent-cyan); color:#fff; border:none; border-radius:4px; cursor:pointer;">
|
||||
+ Add Zone
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3 class="section-header collapsible" onclick="toggleSection(this)">
|
||||
<span>Target View</span>
|
||||
<span class="collapse-icon">▼</span>
|
||||
</h3>
|
||||
<div class="section-content">
|
||||
<div class="analytics-target-toolbar">
|
||||
<input id="analyticsTargetQuery" type="text" placeholder="Search callsign, ICAO, MMSI, MAC, SSID, node..." onkeydown="if(event.key==='Enter'){Analytics.searchTarget();}">
|
||||
<button onclick="Analytics.searchTarget()">Search</button>
|
||||
</div>
|
||||
<div id="analyticsTargetSummary" class="analytics-target-summary">Search to correlate entities across modes</div>
|
||||
<div id="analyticsTargetResults">
|
||||
<div class="analytics-empty">No target selected</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3 class="section-header collapsible" onclick="toggleSection(this)">
|
||||
<span>Session Replay</span>
|
||||
<span class="collapse-icon">▼</span>
|
||||
</h3>
|
||||
<div class="section-content">
|
||||
<div class="analytics-replay-toolbar">
|
||||
<select id="analyticsReplaySelect"></select>
|
||||
<button onclick="Analytics.loadReplay()">Load</button>
|
||||
<button onclick="Analytics.playReplay()">Play</button>
|
||||
<button onclick="Analytics.pauseReplay()">Pause</button>
|
||||
<button onclick="Analytics.stepReplay()">Step</button>
|
||||
</div>
|
||||
<div id="analyticsReplayMeta" class="analytics-target-summary">No replay loaded</div>
|
||||
<div id="analyticsReplayTimeline">
|
||||
<div class="analytics-empty">Select a recording to replay key events</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3 class="section-header collapsible" onclick="toggleSection(this)">
|
||||
<span>Export Data</span>
|
||||
<span class="collapse-icon">▼</span>
|
||||
</h3>
|
||||
<div class="section-content">
|
||||
<div class="export-controls">
|
||||
<select id="exportMode">
|
||||
<option value="adsb">ADS-B</option>
|
||||
<option value="ais">AIS</option>
|
||||
<option value="wifi">WiFi</option>
|
||||
<option value="bluetooth">Bluetooth</option>
|
||||
<option value="dsc">DSC</option>
|
||||
</select>
|
||||
<select id="exportFormat">
|
||||
<option value="json">JSON</option>
|
||||
<option value="csv">CSV</option>
|
||||
</select>
|
||||
<button onclick="Analytics.exportData()">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,68 +0,0 @@
|
||||
<!-- LISTENING POST MODE -->
|
||||
<div id="listeningPostMode" class="mode-content">
|
||||
<div class="section">
|
||||
<h3>Status</h3>
|
||||
|
||||
<!-- Dependency Warning -->
|
||||
<div id="scannerToolsWarning" style="display: none; background: rgba(255, 100, 100, 0.1); border: 1px solid var(--accent-red); border-radius: 4px; padding: 10px; margin-bottom: 10px;">
|
||||
<p style="color: var(--accent-red); margin: 0; font-size: 0.85em;">
|
||||
<strong>Missing:</strong><br>
|
||||
<span id="scannerToolsWarningText"></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Quick Status -->
|
||||
<div style="background: rgba(0,0,0,0.3); border-radius: 6px; padding: 12px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||
<span style="font-size: 10px; color: var(--text-muted); text-transform: uppercase;">Status</span>
|
||||
<span id="lpQuickStatus" style="font-size: 11px; color: var(--accent-cyan);">IDLE</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||
<span style="font-size: 10px; color: var(--text-muted); text-transform: uppercase;">Frequency</span>
|
||||
<span id="lpQuickFreq" style="font-size: 14px; font-family: var(--font-mono); color: var(--text-primary);">---.--- MHz</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<span style="font-size: 10px; color: var(--text-muted); text-transform: uppercase;">Signals</span>
|
||||
<span id="lpQuickSignals" style="font-size: 14px; font-weight: bold; color: var(--accent-green);">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Bookmarks</h3>
|
||||
<div style="display: flex; gap: 4px; margin-bottom: 8px;">
|
||||
<input type="text" id="bookmarkFreqInput" placeholder="Freq (MHz)" style="flex: 1; padding: 6px; background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 4px; font-size: 11px;">
|
||||
<button class="preset-btn" onclick="addFrequencyBookmark()" style="background: var(--accent-green); color: #000; padding: 6px 10px;">+</button>
|
||||
</div>
|
||||
<div id="bookmarksList" style="max-height: 150px; overflow-y: auto; font-size: 11px;">
|
||||
<div style="color: var(--text-muted); text-align: center; padding: 10px;">No bookmarks saved</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Recent Signals</h3>
|
||||
<div id="sidebarRecentSignals" style="max-height: 150px; overflow-y: auto; font-size: 11px;">
|
||||
<div style="color: var(--text-muted); text-align: center; padding: 10px;">No signals yet</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signal Identification -->
|
||||
<div class="section">
|
||||
<h3>Signal Identification</h3>
|
||||
<div style="display: flex; gap: 4px; margin-bottom: 8px;">
|
||||
<input type="text" id="signalGuessFreqInput" placeholder="Freq (MHz)" style="flex: 1; padding: 6px; background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-primary); border-radius: 4px; font-size: 11px;">
|
||||
<button class="preset-btn" onclick="manualSignalGuess()" style="background: var(--accent-cyan); color: #000; padding: 6px 10px; font-weight: 600;">ID</button>
|
||||
</div>
|
||||
<div id="signalGuessPanel" style="display: none; background: rgba(0,0,0,0.3); border-radius: 6px; padding: 10px; font-size: 11px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
|
||||
<span id="signalGuessLabel" style="font-weight: bold; color: var(--text-primary);"></span>
|
||||
<span id="signalGuessBadge" style="padding: 2px 8px; border-radius: 3px; font-size: 9px; font-weight: bold;"></span>
|
||||
</div>
|
||||
<div id="signalGuessExplanation" style="color: var(--text-muted); font-size: 10px; margin-bottom: 6px;"></div>
|
||||
<div id="signalGuessTags" style="display: flex; flex-wrap: wrap; gap: 3px;"></div>
|
||||
<div id="signalGuessAlternatives" style="margin-top: 6px; font-size: 10px; color: var(--text-muted);"></div>
|
||||
<div id="signalGuessSendTo" style="margin-top: 8px; display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,327 @@
|
||||
<!-- WATERFALL MODE -->
|
||||
<div id="waterfallMode" class="mode-content wf-side">
|
||||
<div class="section wf-side-hero">
|
||||
<div class="wf-side-hero-title-row">
|
||||
<div class="wf-side-hero-title">Spectrum Waterfall</div>
|
||||
<div class="wf-side-chip" id="wfHeroVisualStatus">CONNECTING</div>
|
||||
</div>
|
||||
<div class="wf-side-hero-subtext">
|
||||
Click spectrum/waterfall to tune. Scroll to step-tune. Ctrl/Cmd + scroll to zoom span.
|
||||
</div>
|
||||
<div class="wf-side-hero-stats">
|
||||
<div class="wf-side-stat">
|
||||
<div class="wf-side-stat-label">Tuned</div>
|
||||
<div class="wf-side-stat-value" id="wfHeroFreq">100.0000 MHz</div>
|
||||
</div>
|
||||
<div class="wf-side-stat">
|
||||
<div class="wf-side-stat-label">Mode</div>
|
||||
<div class="wf-side-stat-value" id="wfHeroMode">WFM</div>
|
||||
</div>
|
||||
<div class="wf-side-stat">
|
||||
<div class="wf-side-stat-label">Scan</div>
|
||||
<div class="wf-side-stat-value" id="wfHeroScan">Idle</div>
|
||||
</div>
|
||||
<div class="wf-side-stat">
|
||||
<div class="wf-side-stat-label">Hits</div>
|
||||
<div class="wf-side-stat-value" id="wfHeroHits">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wf-side-hero-actions">
|
||||
<button class="run-btn" id="wfStartBtn" onclick="Waterfall.start()">Start Waterfall</button>
|
||||
<button class="stop-btn" id="wfStopBtn" onclick="Waterfall.stop()" style="display:none;">Stop Waterfall</button>
|
||||
</div>
|
||||
<div id="wfStatus" class="wf-side-status-line"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Device</h3>
|
||||
<div class="form-group">
|
||||
<label>SDR Device</label>
|
||||
<select id="wfDevice" onchange="Waterfall && Waterfall.onDeviceChange && Waterfall.onDeviceChange()">
|
||||
<option value="">Loading devices...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="wfDeviceInfo" class="wf-side-box" style="display:none;">
|
||||
<div class="wf-side-kv">
|
||||
<span class="wf-side-kv-label">Type</span>
|
||||
<span id="wfDeviceType" class="wf-side-kv-value">--</span>
|
||||
</div>
|
||||
<div class="wf-side-kv">
|
||||
<span class="wf-side-kv-label">Range</span>
|
||||
<span id="wfDeviceRange" class="wf-side-kv-value">--</span>
|
||||
</div>
|
||||
<div class="wf-side-kv">
|
||||
<span class="wf-side-kv-label">Capture SR</span>
|
||||
<span id="wfDeviceBw" class="wf-side-kv-value">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Tuning</h3>
|
||||
<div class="form-group">
|
||||
<label>Center Frequency (MHz)</label>
|
||||
<input type="number" id="wfCenterFreq" value="100.0000" step="0.001" min="0.001" max="6000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Span (MHz)</label>
|
||||
<input type="number" id="wfSpanMhz" value="2.4" step="0.1" min="0.05" max="30">
|
||||
</div>
|
||||
<div class="wf-side-grid-2">
|
||||
<button class="preset-btn" onclick="Waterfall.applyPreset('fm')">FM Broadcast</button>
|
||||
<button class="preset-btn" onclick="Waterfall.applyPreset('air')">Airband</button>
|
||||
<button class="preset-btn" onclick="Waterfall.applyPreset('marine')">Marine</button>
|
||||
<button class="preset-btn" onclick="Waterfall.applyPreset('ham2m')">2m Ham</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Quick Tune & Bookmarks</h3>
|
||||
<div class="wf-side-grid-2">
|
||||
<button class="preset-btn" onclick="Waterfall.quickTune(121.5, 'am')">121.5 Guard</button>
|
||||
<button class="preset-btn" onclick="Waterfall.quickTune(156.8, 'fm')">156.8 CH16</button>
|
||||
<button class="preset-btn" onclick="Waterfall.quickTune(145.5, 'fm')">145.5 2m</button>
|
||||
<button class="preset-btn" onclick="Waterfall.quickTune(98.1, 'wfm')">98.1 FM</button>
|
||||
<button class="preset-btn" onclick="Waterfall.quickTune(462.5625, 'fm')">462.56 FRS</button>
|
||||
<button class="preset-btn" onclick="Waterfall.quickTune(446.0, 'fm')">446.0 PMR</button>
|
||||
</div>
|
||||
<div class="wf-side-divider"></div>
|
||||
<div class="wf-bookmark-row">
|
||||
<input type="number" id="wfBookmarkFreqInput" step="0.0001" min="0.001" max="6000" placeholder="Frequency MHz">
|
||||
<select id="wfBookmarkMode">
|
||||
<option value="auto" selected>Auto</option>
|
||||
<option value="wfm">WFM</option>
|
||||
<option value="fm">NFM</option>
|
||||
<option value="am">AM</option>
|
||||
<option value="usb">USB</option>
|
||||
<option value="lsb">LSB</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="wf-side-grid-2">
|
||||
<button class="preset-btn" onclick="Waterfall.useTuneForBookmark()">Use Tuned</button>
|
||||
<button class="preset-btn" onclick="Waterfall.addBookmarkFromInput()">Save Bookmark</button>
|
||||
</div>
|
||||
<div id="wfBookmarkList" class="wf-bookmark-list">
|
||||
<div class="wf-empty">No bookmarks saved</div>
|
||||
</div>
|
||||
<div class="wf-side-inline-label">Recent Hits</div>
|
||||
<div id="wfRecentSignals" class="wf-recent-list">
|
||||
<div class="wf-empty">No recent signal hits</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Handoff</h3>
|
||||
<div class="wf-side-help">
|
||||
Send current tuned frequency to another decoder/workflow.
|
||||
</div>
|
||||
<div class="wf-side-grid-2">
|
||||
<button class="preset-btn" onclick="Waterfall.handoff('pager')">To Pager</button>
|
||||
<button class="preset-btn" onclick="Waterfall.handoff('subghz')">To SubGHz</button>
|
||||
<button class="preset-btn" onclick="Waterfall.handoff('subghz433')">433 Profile</button>
|
||||
<button class="preset-btn" onclick="Waterfall.handoff('signalid')">Signal ID</button>
|
||||
</div>
|
||||
<div id="wfHandoffStatus" class="wf-side-status-line">Ready</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Signal Identification</h3>
|
||||
<div class="wf-side-help">
|
||||
Identify current frequency using local catalog and SigID Wiki matches.
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Frequency (MHz)</label>
|
||||
<input type="number" id="wfSigIdFreq" value="100.0000" step="0.0001" min="0.001" max="6000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Mode Hint</label>
|
||||
<select id="wfSigIdMode">
|
||||
<option value="auto" selected>Auto (Current Mode)</option>
|
||||
<option value="wfm">WFM</option>
|
||||
<option value="fm">NFM</option>
|
||||
<option value="am">AM</option>
|
||||
<option value="usb">USB</option>
|
||||
<option value="lsb">LSB</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="wf-side-grid-2">
|
||||
<button class="preset-btn" onclick="Waterfall.useTuneForSignalId()">Use Tuned</button>
|
||||
<button class="preset-btn" onclick="Waterfall.identifySignal()">Identify</button>
|
||||
</div>
|
||||
<div id="wfSigIdStatus" class="wf-side-status-line">Ready</div>
|
||||
<div id="wfSigIdResult" class="wf-side-box" style="display:none;"></div>
|
||||
<div id="wfSigIdExternal" class="wf-side-box wf-side-box-muted" style="display:none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Scan</h3>
|
||||
<div class="form-group">
|
||||
<label>Range Start (MHz)</label>
|
||||
<input type="number" id="wfScanStart" value="98.8000" step="0.001" min="0.001" max="6000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Range End (MHz)</label>
|
||||
<input type="number" id="wfScanEnd" value="101.2000" step="0.001" min="0.001" max="6000">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Step (kHz)</label>
|
||||
<select id="wfScanStepKhz">
|
||||
<option value="5">5</option>
|
||||
<option value="10">10</option>
|
||||
<option value="12.5">12.5</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100" selected>100</option>
|
||||
<option value="200">200</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Dwell (ms)</label>
|
||||
<input type="number" id="wfScanDwellMs" value="450" min="60" max="10000" step="10">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Signal Threshold <span id="wfScanThresholdValue" class="wf-inline-value">170</span></label>
|
||||
<input type="range" id="wfScanThreshold" min="0" max="255" value="170">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Hold On Hit (ms)</label>
|
||||
<input type="number" id="wfScanHoldMs" value="2500" min="0" max="30000" step="100">
|
||||
</div>
|
||||
<div class="checkbox-group wf-scan-checkboxes">
|
||||
<label>
|
||||
<input type="checkbox" id="wfScanStopOnSignal" checked>
|
||||
Pause scan when signal is above threshold
|
||||
</label>
|
||||
</div>
|
||||
<div class="wf-side-grid-2 wf-side-grid-gap-top">
|
||||
<button class="preset-btn" onclick="Waterfall.setScanRangeFromView()">Use Current Span</button>
|
||||
<button class="preset-btn" id="wfScanStartBtn" onclick="Waterfall.startScan()">Start Scan</button>
|
||||
<button class="preset-btn" id="wfScanStopBtn" onclick="Waterfall.stopScan()" disabled>Stop Scan</button>
|
||||
</div>
|
||||
<div id="wfScanState" class="wf-side-status-line">Scan idle</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Scan Activity</h3>
|
||||
<div class="wf-scan-metric-grid">
|
||||
<div class="wf-scan-metric-card">
|
||||
<div class="wf-scan-metric-label">Signals</div>
|
||||
<div class="wf-scan-metric-value" id="wfScanSignalsCount">0</div>
|
||||
</div>
|
||||
<div class="wf-scan-metric-card">
|
||||
<div class="wf-scan-metric-label">Scanned</div>
|
||||
<div class="wf-scan-metric-value" id="wfScanStepsCount">0</div>
|
||||
</div>
|
||||
<div class="wf-scan-metric-card">
|
||||
<div class="wf-scan-metric-label">Cycles</div>
|
||||
<div class="wf-scan-metric-value" id="wfScanCyclesCount">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wf-side-grid-2 wf-side-grid-gap-top">
|
||||
<button class="preset-btn" onclick="Waterfall.exportScanLog()">Export Log</button>
|
||||
<button class="preset-btn" onclick="Waterfall.clearScanHistory()">Clear History</button>
|
||||
</div>
|
||||
<div class="wf-hit-table-wrap">
|
||||
<table class="wf-hit-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Frequency</th>
|
||||
<th>Level</th>
|
||||
<th>Mode</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="wfSignalHitsBody">
|
||||
<tr><td colspan="5" class="wf-empty">No signals detected</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="wfSignalHitCount" class="wf-side-inline-label">0 signals found</div>
|
||||
<div id="wfActivityLog" class="wf-activity-log">
|
||||
<div class="wf-empty">Ready</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Capture</h3>
|
||||
<div class="form-group">
|
||||
<label>Gain <span class="wf-inline-value">(dB or AUTO)</span></label>
|
||||
<input type="text" id="wfGain" value="AUTO" placeholder="AUTO or numeric">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>FFT Size</label>
|
||||
<select id="wfFftSize">
|
||||
<option value="256">256</option>
|
||||
<option value="512">512</option>
|
||||
<option value="1024" selected>1024</option>
|
||||
<option value="2048">2048</option>
|
||||
<option value="4096">4096</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Frame Rate</label>
|
||||
<select id="wfFps">
|
||||
<option value="10">10 fps</option>
|
||||
<option value="20" selected>20 fps</option>
|
||||
<option value="30">30 fps</option>
|
||||
<option value="40">40 fps</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>FFT Averaging</label>
|
||||
<select id="wfAvgCount">
|
||||
<option value="1">1 (none)</option>
|
||||
<option value="2">2</option>
|
||||
<option value="4" selected>4</option>
|
||||
<option value="8">8</option>
|
||||
<option value="16">16</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>PPM Correction</label>
|
||||
<input type="number" id="wfPpm" value="0" step="1" min="-200" max="200" placeholder="0">
|
||||
</div>
|
||||
<div class="checkbox-group wf-scan-checkboxes">
|
||||
<label>
|
||||
<input type="checkbox" id="wfBiasT">
|
||||
Bias-T (antenna power)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Display</h3>
|
||||
<div class="form-group">
|
||||
<label>Color Palette</label>
|
||||
<select id="wfPalette" onchange="Waterfall.setPalette(this.value)">
|
||||
<option value="turbo" selected>Turbo</option>
|
||||
<option value="plasma">Plasma</option>
|
||||
<option value="inferno">Inferno</option>
|
||||
<option value="viridis">Viridis</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Noise Floor (dB)</label>
|
||||
<input type="number" id="wfDbMin" value="-100" step="5" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Ceiling (dB)</label>
|
||||
<input type="number" id="wfDbMax" value="-20" step="5" disabled>
|
||||
</div>
|
||||
<div class="checkbox-group wf-scan-checkboxes">
|
||||
<label>
|
||||
<input type="checkbox" id="wfPeakHold" onchange="Waterfall.togglePeakHold(this.checked)">
|
||||
Peak Hold
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="wfBandAnnotations" checked onchange="Waterfall.toggleAnnotations(this.checked)">
|
||||
Band Labels
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="wfAutoRange" checked onchange="Waterfall.toggleAutoRange(this.checked)">
|
||||
Auto Range
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,8 +65,8 @@
|
||||
{{ mode_item('pager', 'Pager', '<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>') }}
|
||||
{{ mode_item('sensor', '433MHz', '<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>') }}
|
||||
{{ mode_item('rtlamr', 'Meters', '<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>') }}
|
||||
{{ mode_item('listening', 'Listening Post', '<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"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }}
|
||||
{{ mode_item('subghz', 'SubGHz', '<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>') }}
|
||||
{{ mode_item('waterfall', 'Waterfall', '<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.4"/><path d="M2 21h20" opacity="0.2"/></svg>') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -133,7 +133,6 @@
|
||||
|
||||
<div class="mode-nav-dropdown-menu">
|
||||
{{ mode_item('tscm', 'TSCM', '<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>') }}
|
||||
{{ mode_item('analytics', 'Analytics', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4Z"/></svg>') }}
|
||||
{{ mode_item('spystations', 'Spy Stations', '<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"/><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>') }}
|
||||
{{ mode_item('websdr', 'WebSDR', '<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>') }}
|
||||
</div>
|
||||
@@ -177,6 +176,15 @@
|
||||
<button type="button" class="nav-tool-btn" onclick="showSettings()" title="Settings" aria-label="Open settings">
|
||||
<span class="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="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></span>
|
||||
</button>
|
||||
<button type="button" class="nav-tool-btn" id="voiceMuteBtn" onclick="window.VoiceAlerts && VoiceAlerts.toggleMute()" title="Toggle voice alerts" aria-label="Toggle voice alerts">
|
||||
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/></svg></span>
|
||||
</button>
|
||||
<button type="button" class="nav-tool-btn" onclick="window.CheatSheets && CheatSheets.showForCurrentMode()" title="Mode cheat sheet (Alt+C)" aria-label="Mode cheat sheet">
|
||||
<span class="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>
|
||||
</button>
|
||||
<button type="button" class="nav-tool-btn" onclick="window.KeyboardShortcuts && KeyboardShortcuts.showHelp()" title="Keyboard shortcuts (Alt+K)" aria-label="Keyboard shortcuts">
|
||||
<span class="icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10"/></svg></span>
|
||||
</button>
|
||||
<button type="button" class="nav-tool-btn" onclick="showHelp()" title="Help & Documentation" aria-label="Open help">?</button>
|
||||
<button type="button" class="nav-tool-btn" onclick="logout(event)" title="Logout" aria-label="Logout">
|
||||
<span class="power-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg></span>
|
||||
@@ -191,7 +199,6 @@
|
||||
{{ mobile_item('pager', 'Pager', '<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>') }}
|
||||
{{ mobile_item('sensor', '433MHz', '<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>') }}
|
||||
{{ mobile_item('rtlamr', 'Meters', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>') }}
|
||||
{{ mobile_item('listening', 'Scanner', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg>') }}
|
||||
{{ mobile_item('subghz', 'SubGHz', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h6l3-9 3 18 3-9h5"/></svg>') }}
|
||||
{# Tracking #}
|
||||
{{ mobile_item('adsb', 'Aircraft', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>', '/adsb/dashboard') }}
|
||||
@@ -215,13 +222,70 @@
|
||||
{{ mobile_item('meshtastic', 'Mesh', '<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>') }}
|
||||
{# Intel #}
|
||||
{{ mobile_item('tscm', 'TSCM', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>') }}
|
||||
{{ mobile_item('analytics', 'Analytics', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4Z"/></svg>') }}
|
||||
{{ mobile_item('spystations', 'Spy', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>') }}
|
||||
{{ mobile_item('websdr', 'WebSDR', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>') }}
|
||||
{# New modes #}
|
||||
{{ mobile_item('waterfall', 'Waterfall', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h4l3-8 3 16 3-8h4"/></svg>') }}
|
||||
</nav>
|
||||
|
||||
{# JavaScript stub for pages that don't have switchMode defined #}
|
||||
<script>
|
||||
(function () {
|
||||
const NAV_PERF_KEY = 'intercept_nav_perf_v1';
|
||||
const MAX_NAV_AGE_MS = 30000;
|
||||
|
||||
function parseNavPerf(raw) {
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.InterceptNavPerf) {
|
||||
window.InterceptNavPerf = {
|
||||
markStart(meta = {}) {
|
||||
try {
|
||||
const payload = {
|
||||
startedAtEpochMs: Date.now(),
|
||||
sourcePath: window.location.pathname + window.location.search,
|
||||
sourceMode: document.body?.getAttribute('data-mode') || null,
|
||||
...meta,
|
||||
};
|
||||
sessionStorage.setItem(NAV_PERF_KEY, JSON.stringify(payload));
|
||||
} catch (_) {
|
||||
// Ignore storage errors in private/incognito mode.
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const payload = parseNavPerf(sessionStorage.getItem(NAV_PERF_KEY));
|
||||
if (!payload || !payload.targetPath) return;
|
||||
|
||||
const ageMs = Date.now() - (payload.startedAtEpochMs || 0);
|
||||
if (ageMs < 0 || ageMs > MAX_NAV_AGE_MS) {
|
||||
try { sessionStorage.removeItem(NAV_PERF_KEY); } catch (_) { }
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.location.pathname !== payload.targetPath) return;
|
||||
|
||||
console.info(
|
||||
`[Perf] Nav ${payload.sourcePath || '(unknown)'} -> ${payload.targetPath} in ${Math.round(ageMs)}ms`,
|
||||
{
|
||||
trigger: payload.trigger || 'unknown',
|
||||
sourceMode: payload.sourceMode || null,
|
||||
activeScans: payload.activeScans || null,
|
||||
}
|
||||
);
|
||||
|
||||
try { sessionStorage.removeItem(NAV_PERF_KEY); } catch (_) { }
|
||||
});
|
||||
})();
|
||||
|
||||
// Ensure navigation functions exist (for dashboard pages that don't have the full JS)
|
||||
if (typeof switchMode === 'undefined') {
|
||||
window.switchMode = function(mode) {
|
||||
|
||||
@@ -284,6 +284,93 @@
|
||||
|
||||
<!-- Alerts Section -->
|
||||
<div id="settings-alerts" class="settings-section">
|
||||
<div class="settings-group">
|
||||
<div class="settings-group-title">Voice Alerts</div>
|
||||
<p style="color: var(--text-dim); margin-bottom: 10px; font-size: 12px;">
|
||||
Configure which events trigger spoken alerts and adjust voice settings.
|
||||
</p>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="settings-label">
|
||||
<span class="settings-label-text">Pager Messages</span>
|
||||
<span class="settings-label-desc">Speak decoded pager messages</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="voiceCfgPager" checked onchange="saveVoiceAlertConfig()">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="settings-label">
|
||||
<span class="settings-label-text">TSCM Alerts</span>
|
||||
<span class="settings-label-desc">Speak counter-surveillance detections</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="voiceCfgTscm" checked onchange="saveVoiceAlertConfig()">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="settings-label">
|
||||
<span class="settings-label-text">Tracker Detection</span>
|
||||
<span class="settings-label-desc">Speak when AirTag, Tile, or SmartTag found</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="voiceCfgTracker" checked onchange="saveVoiceAlertConfig()">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="settings-label">
|
||||
<span class="settings-label-text">Emergency Squawks</span>
|
||||
<span class="settings-label-desc">Speak aircraft emergency transponder codes</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="voiceCfgSquawk" checked onchange="saveVoiceAlertConfig()">
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="settings-label">
|
||||
<span class="settings-label-text">Voice</span>
|
||||
<span class="settings-label-desc">Speech synthesis voice</span>
|
||||
</div>
|
||||
<select id="voiceCfgVoice" class="settings-select" style="width: 200px;" onchange="saveVoiceAlertConfig()">
|
||||
<option value="">Default</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="settings-label">
|
||||
<span class="settings-label-text">Rate</span>
|
||||
<span class="settings-label-desc">Speech speed (0.5 – 2.0)</span>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:8px; width:200px;">
|
||||
<input type="range" id="voiceCfgRate" min="0.5" max="2.0" step="0.1" value="1.1" style="flex:1;" oninput="document.getElementById('voiceCfgRateVal').textContent=this.value; saveVoiceAlertConfig();">
|
||||
<span id="voiceCfgRateVal" style="font-family:var(--font-mono); font-size:11px; color:var(--text-dim); min-width:28px; text-align:right;">1.1</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-row">
|
||||
<div class="settings-label">
|
||||
<span class="settings-label-text">Pitch</span>
|
||||
<span class="settings-label-desc">Voice pitch (0.5 – 2.0)</span>
|
||||
</div>
|
||||
<div style="display:flex; align-items:center; gap:8px; width:200px;">
|
||||
<input type="range" id="voiceCfgPitch" min="0.5" max="2.0" step="0.1" value="0.9" style="flex:1;" oninput="document.getElementById('voiceCfgPitchVal').textContent=this.value; saveVoiceAlertConfig();">
|
||||
<span id="voiceCfgPitchVal" style="font-family:var(--font-mono); font-size:11px; color:var(--text-dim); min-width:28px; text-align:right;">0.9</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 8px;">
|
||||
<button class="check-assets-btn" onclick="testVoiceAlert()">Test Voice</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-group">
|
||||
<div class="settings-group-title">Alert Feed <span id="alertsFeedCount" style="color: var(--text-dim); font-weight: 500;"></span></div>
|
||||
<div id="alertsFeedList" class="settings-feed">
|
||||
@@ -316,7 +403,6 @@
|
||||
<option value="acars">ACARS</option>
|
||||
<option value="vdl2">VDL2</option>
|
||||
<option value="aprs">APRS</option>
|
||||
<option value="dsc">DSC</option>
|
||||
<option value="meshtastic">Meshtastic</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -392,14 +478,12 @@
|
||||
<option value="bluetooth">Bluetooth</option>
|
||||
<option value="adsb">ADS-B</option>
|
||||
<option value="ais">AIS</option>
|
||||
<option value="dsc">DSC</option>
|
||||
<option value="acars">ACARS</option>
|
||||
<option value="aprs">APRS</option>
|
||||
<option value="rtlamr">RTLAMR</option>
|
||||
<option value="tscm">TSCM</option>
|
||||
<option value="sstv">SSTV</option>
|
||||
<option value="sstv_general">SSTV General</option>
|
||||
<option value="listening_scanner">Listening Post</option>
|
||||
<option value="waterfall">Waterfall</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user