Restore lost features and unify button styling

- Restore APRS dynamic device selection and status bar
- Add ACARS status indicator with listening/receiving states
- Fix acars.py: use -o 4 for JSON, correct command order, add macOS pty fix
- Unify all start buttons (green) and stop buttons (red) across app
- Update help documentation with all modes (APRS, ACARS, Listening Post, TSCM)
- Add TSCM Alpha badge to sidebar

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-01-14 12:20:52 +00:00
parent f36e528086
commit de13d5ea74
6 changed files with 382 additions and 70 deletions
+43 -17
View File
@@ -2,7 +2,11 @@
from __future__ import annotations
import io
import json
import os
import platform
import pty
import queue
import shutil
import subprocess
@@ -45,20 +49,25 @@ def find_acarsdec():
return shutil.which('acarsdec')
def stream_acars_output(process: subprocess.Popen) -> None:
def stream_acars_output(process: subprocess.Popen, is_text_mode: bool = False) -> None:
"""Stream acarsdec JSON output to queue."""
global acars_message_count, acars_last_message_time
try:
app_module.acars_queue.put({'type': 'status', 'status': 'started'})
for line in iter(process.stdout.readline, b''):
line = line.decode('utf-8', errors='replace').strip()
# Use appropriate sentinel based on mode (text mode for pty on macOS)
sentinel = '' if is_text_mode else b''
for line in iter(process.stdout.readline, sentinel):
if is_text_mode:
line = line.strip()
else:
line = line.decode('utf-8', errors='replace').strip()
if not line:
continue
try:
# acarsdec -j outputs JSON, one message per line
# acarsdec -o 4 outputs JSON, one message per line
data = json.loads(line)
# Add our metadata
@@ -167,33 +176,50 @@ def start_acars() -> Response:
acars_last_message_time = None
# Build acarsdec command
# acarsdec -j -r <device> -g <gain> -p <ppm> <freq1> <freq2> ...
# acarsdec -o 4 -g <gain> -p <ppm> -r <device> <freq1> <freq2> ...
# Note: -o 4 is JSON stdout, gain/ppm must come BEFORE -r
cmd = [
acarsdec_path,
'-j', # JSON output
'-r', str(device), # RTL-SDR device index
'-o', '4', # JSON output to stdout
]
# Add gain if not auto
# Add gain if not auto (must be before -r)
if gain and str(gain) != '0':
cmd.extend(['-g', str(gain)])
# Add PPM correction if specified
# Add PPM correction if specified (must be before -r)
if ppm and str(ppm) != '0':
cmd.extend(['-p', str(ppm)])
# Add frequencies
# Add device and frequencies (-r takes device, remaining args are frequencies)
cmd.extend(['-r', str(device)])
cmd.extend(frequencies)
logger.info(f"Starting ACARS decoder: {' '.join(cmd)}")
try:
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True
)
is_text_mode = False
# On macOS, use pty to avoid stdout buffering issues
if platform.system() == 'Darwin':
master_fd, slave_fd = pty.openpty()
process = subprocess.Popen(
cmd,
stdout=slave_fd,
stderr=subprocess.PIPE,
start_new_session=True
)
os.close(slave_fd)
# Wrap master_fd as a text file for line-buffered reading
process.stdout = io.open(master_fd, 'r', buffering=1)
is_text_mode = True
else:
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True
)
# Wait briefly to check if process started
time.sleep(PROCESS_START_WAIT)
@@ -214,7 +240,7 @@ def start_acars() -> Response:
# Start output streaming thread
thread = threading.Thread(
target=stream_acars_output,
args=(process,),
args=(process, is_text_mode),
daemon=True
)
thread.start()
+34 -19
View File
@@ -280,9 +280,9 @@ body {
}
.acars-sidebar .acars-btn {
background: var(--bg-card);
border: 1px solid var(--accent-cyan);
color: var(--accent-cyan);
background: var(--accent-green);
border: none;
color: #fff;
padding: 6px 10px;
font-size: 10px;
font-weight: 600;
@@ -290,10 +290,21 @@ body {
transition: all 0.2s;
text-transform: uppercase;
letter-spacing: 1px;
border-radius: 4px;
}
.acars-sidebar .acars-btn:hover {
background: rgba(74, 158, 255, 0.2);
background: #1db954;
box-shadow: 0 0 10px rgba(34, 197, 94, 0.3);
}
.acars-sidebar .acars-btn.active {
background: var(--accent-red);
}
.acars-sidebar .acars-btn.active:hover {
background: #dc2626;
box-shadow: 0 0 10px rgba(239, 68, 68, 0.3);
}
.acars-message-item {
@@ -691,9 +702,9 @@ body {
/* Start/stop button */
.start-btn {
padding: 8px 20px;
border: 1px solid var(--accent-cyan);
background: rgba(74, 158, 255, 0.1);
color: var(--accent-cyan);
border: none;
background: var(--accent-green);
color: #fff;
font-family: 'Orbitron', monospace;
font-size: 11px;
font-weight: 600;
@@ -706,19 +717,18 @@ body {
}
.start-btn:hover {
background: var(--accent-cyan);
color: var(--bg-dark);
box-shadow: 0 0 20px rgba(74, 158, 255, 0.3);
background: #1db954;
box-shadow: 0 0 20px rgba(34, 197, 94, 0.3);
}
.start-btn.active {
background: var(--accent-red);
border-color: var(--accent-red);
color: #fff;
}
.start-btn.active:hover {
box-shadow: 0 0 20px rgba(255, 68, 68, 0.3);
background: #dc2626;
box-shadow: 0 0 20px rgba(239, 68, 68, 0.3);
}
/* GPS button */
@@ -841,9 +851,9 @@ body {
.airband-btn {
padding: 6px 12px;
background: rgba(74, 158, 255, 0.1);
border: 1px solid var(--accent-cyan);
color: var(--accent-cyan);
background: var(--accent-green);
border: none;
color: #fff;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
@@ -858,13 +868,18 @@ body {
}
.airband-btn:hover {
background: rgba(74, 158, 255, 0.2);
background: #1db954;
box-shadow: 0 0 10px rgba(34, 197, 94, 0.3);
}
.airband-btn.active {
background: rgba(34, 197, 94, 0.2);
border-color: var(--accent-green);
color: var(--accent-green);
background: var(--accent-red);
color: #fff;
}
.airband-btn.active:hover {
background: #dc2626;
box-shadow: 0 0 10px rgba(239, 68, 68, 0.3);
}
.airband-btn:disabled {
+5 -5
View File
@@ -4343,14 +4343,14 @@ body::before {
}
.radio-action-btn.scan {
background: var(--accent-cyan);
border-color: var(--accent-cyan);
color: var(--bg-primary);
background: var(--accent-green);
border-color: var(--accent-green);
color: #fff;
}
.radio-action-btn.scan:hover {
background: #5aa8ff;
box-shadow: 0 0 20px var(--accent-cyan-dim);
background: #1db954;
box-shadow: 0 0 20px rgba(34, 197, 94, 0.4);
}
.radio-action-btn.scan.active {
+4 -3
View File
@@ -589,13 +589,14 @@ body {
}
.btn.primary {
background: var(--accent-cyan);
color: var(--bg-dark);
background: var(--accent-green);
color: #fff;
margin-left: auto;
}
.btn.primary:hover {
box-shadow: 0 0 25px rgba(0, 212, 255, 0.5);
background: #1db954;
box-shadow: 0 0 25px rgba(34, 197, 94, 0.5);
}
/* Leaflet dark theme overrides */
+2 -2
View File
@@ -2332,7 +2332,7 @@ sudo make install</code>
isAcarsRunning = true;
acarsMessageCount = 0;
document.getElementById('acarsToggleBtn').textContent = '■ STOP ACARS';
document.getElementById('acarsToggleBtn').style.background = 'var(--accent-red)';
document.getElementById('acarsToggleBtn').classList.add('active');
document.getElementById('acarsPanelIndicator').classList.add('active');
startAcarsStream();
} else {
@@ -2348,7 +2348,7 @@ sudo make install</code>
.then(() => {
isAcarsRunning = false;
document.getElementById('acarsToggleBtn').textContent = '▶ START ACARS';
document.getElementById('acarsToggleBtn').style.background = '';
document.getElementById('acarsToggleBtn').classList.remove('active');
document.getElementById('acarsPanelIndicator').classList.remove('active');
if (acarsEventSource) {
acarsEventSource.close();
+294 -24
View File
@@ -646,7 +646,7 @@
<button class="preset-btn" onclick="checkCaptureStatus()" style="flex: 1; font-size: 10px; padding: 4px;">
Check Status
</button>
<button class="preset-btn" onclick="stopHandshakeCapture()" style="flex: 1; font-size: 10px; padding: 4px; border-color: var(--accent-red); color: var(--accent-red);">
<button class="preset-btn" onclick="stopHandshakeCapture()" style="flex: 1; font-size: 10px; padding: 4px; background: var(--accent-red); border: none; color: #fff;">
Stop Capture
</button>
</div>
@@ -892,8 +892,7 @@
<div class="form-group">
<label>SDR Device</label>
<select id="aprsDevice">
<option value="0">Device 0</option>
<option value="1">Device 1</option>
<option value="">Loading devices...</option>
</select>
</div>
<div class="form-group">
@@ -911,6 +910,18 @@
<button class="stop-btn" id="stopAprsBtn" onclick="stopAprs()" style="display: none;">
Stop APRS
</button>
<!-- APRS Status Bar -->
<div id="aprsStatusBar" class="aprs-status-bar" style="display: none;">
<div class="aprs-status-indicator">
<span class="aprs-status-dot" id="aprsStatusDot"></span>
<span class="aprs-status-text" id="aprsStatusText">STANDBY</span>
</div>
<div class="aprs-status-stats">
<span class="aprs-stat"><span class="aprs-stat-label">FREQ:</span> <span id="aprsStatusFreq">--</span></span>
<span class="aprs-stat"><span class="aprs-stat-label">STATIONS:</span> <span id="aprsStatusStations">0</span></span>
<span class="aprs-stat"><span class="aprs-stat-label">PACKETS:</span> <span id="aprsStatusPackets">0</span></span>
</div>
</div>
</div>
<!-- SATELLITE MODE -->
@@ -1017,7 +1028,7 @@
<!-- TSCM MODE (Counter-Surveillance) -->
<div id="tscmMode" class="mode-content">
<div class="section">
<h3>TSCM Sweep</h3>
<h3 style="display: flex; align-items: center; gap: 8px;">TSCM Sweep <span style="font-size: 9px; font-weight: normal; background: var(--accent-orange); color: #000; padding: 2px 6px; border-radius: 3px; text-transform: uppercase; letter-spacing: 0.5px;">Alpha</span></h3>
<div class="form-group">
<label>Sweep Type</label>
<select id="tscmSweepType">
@@ -1060,10 +1071,10 @@
<div class="form-group">
<input type="text" id="tscmBaselineName" placeholder="Baseline name...">
</div>
<button class="preset-btn" id="tscmRecordBaselineBtn" onclick="tscmRecordBaseline()" style="width: 100%;">
<button class="run-btn" id="tscmRecordBaselineBtn" onclick="tscmRecordBaseline()" style="width: 100%; padding: 8px;">
Record New Baseline
</button>
<button class="preset-btn" id="tscmStopBaselineBtn" onclick="tscmStopBaseline()" style="width: 100%; display: none;">
<button class="stop-btn" id="tscmStopBaselineBtn" onclick="tscmStopBaseline()" style="width: 100%; padding: 8px; display: none;">
Stop Recording
</button>
<div id="tscmBaselineStatus" style="margin-top: 8px; font-size: 11px; color: var(--text-muted);"></div>
@@ -1382,9 +1393,18 @@
<option value="ap">Asia-Pac</option>
</select>
</div>
<button id="mainAcarsToggleBtn" onclick="toggleMainAcars()" style="width: 100%; padding: 5px; font-size: 9px; background: var(--bg-dark); border: 1px solid var(--accent-cyan); color: var(--accent-cyan); cursor: pointer;">
<button id="mainAcarsToggleBtn" onclick="toggleMainAcars()" style="width: 100%; padding: 5px; font-size: 9px; background: var(--accent-green); border: none; color: #fff; cursor: pointer; border-radius: 3px; font-weight: bold;">
▶ START
</button>
<!-- ACARS Status Indicator -->
<div id="acarsStatusBar" style="display: none; margin-top: 6px; padding: 5px; background: rgba(0,0,0,0.3); border-radius: 3px;">
<div style="display: flex; align-items: center; gap: 6px;">
<span class="acars-status-dot" id="acarsStatusDot" style="width: 8px; height: 8px; border-radius: 50%; background: var(--text-muted);"></span>
<span id="acarsStatusText" style="font-size: 9px; font-weight: bold; letter-spacing: 0.5px;">STANDBY</span>
<span style="flex: 1;"></span>
<span style="font-size: 8px; color: var(--text-muted);">MSGS: <span id="acarsStatusCount">0</span></span>
</div>
</div>
</div>
<div class="main-acars-messages" id="mainAcarsMessages" style="flex: 1; overflow-y: auto; font-size: 10px;">
<div style="padding: 15px; text-align: center; color: var(--text-muted);">
@@ -1664,7 +1684,7 @@
</div>
<!-- Action Buttons -->
<button class="radio-action-btn scan" id="radioScanBtn" onclick="toggleScanner()" style="padding: 12px; font-size: 13px; width: 100%; font-weight: bold;">📡 SCAN</button>
<button class="radio-action-btn" id="radioListenBtn" onclick="toggleDirectListen()" style="padding: 10px; font-size: 12px; width: 100%; background: var(--accent-purple); border-color: var(--accent-purple); color: #fff;">🎧 LISTEN</button>
<button class="radio-action-btn" id="radioListenBtn" onclick="toggleDirectListen()" style="padding: 10px; font-size: 12px; width: 100%; background: var(--accent-green); border: none; color: #fff;">🎧 LISTEN</button>
</div>
</div>
</div>
@@ -1820,6 +1840,72 @@
}
</style>
<!-- APRS Status Bar Styles -->
<style>
.aprs-status-bar {
margin-top: 12px;
padding: 10px;
background: rgba(0,0,0,0.3);
border: 1px solid var(--border-color);
border-radius: 4px;
}
.aprs-status-indicator {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.aprs-status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--text-muted);
}
.aprs-status-dot.standby { background: var(--text-muted); }
.aprs-status-dot.listening {
background: var(--accent-cyan);
animation: aprs-pulse 1.5s ease-in-out infinite;
}
.aprs-status-dot.tracking { background: var(--accent-green); }
.aprs-status-dot.error { background: var(--accent-red); }
@keyframes aprs-pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(74, 158, 255, 0.7); }
50% { opacity: 0.6; box-shadow: 0 0 8px 4px rgba(74, 158, 255, 0.3); }
}
.aprs-status-text {
font-size: 10px;
font-weight: bold;
letter-spacing: 1px;
}
.aprs-status-stats {
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 9px;
}
.aprs-stat {
color: var(--text-secondary);
}
.aprs-stat-label {
color: var(--text-muted);
}
/* ACARS Status Indicator */
.acars-status-dot.listening {
background: var(--accent-cyan) !important;
animation: acars-pulse 1.5s ease-in-out infinite;
}
.acars-status-dot.receiving {
background: var(--accent-green) !important;
}
.acars-status-dot.error {
background: var(--accent-red) !important;
}
@keyframes acars-pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(74, 158, 255, 0.7); }
50% { opacity: 0.6; box-shadow: 0 0 6px 3px rgba(74, 158, 255, 0.3); }
}
</style>
<!-- TSCM Styles -->
<style>
/* TSCM Threat Cards */
@@ -2915,6 +3001,7 @@
} else if (mode === 'aprs') {
checkAprsTools();
initAprsMap();
refreshAprsDevices();
} else if (mode === 'satellite') {
initPolarPlot();
initSatelliteList();
@@ -7940,14 +8027,18 @@
mainAcarsMessageCount = 0;
document.getElementById('mainAcarsToggleBtn').innerHTML = '■ STOP';
document.getElementById('mainAcarsToggleBtn').style.background = 'var(--accent-red)';
document.getElementById('mainAcarsToggleBtn').style.borderColor = 'var(--accent-red)';
document.getElementById('mainAcarsToggleBtn').style.color = '#fff';
// Show status bar with listening state
updateAcarsStatus('listening');
startMainAcarsStream();
} else {
alert('ACARS Error: ' + data.message);
updateAcarsStatus('error');
}
})
.catch(err => alert('ACARS Error: ' + err));
.catch(err => {
alert('ACARS Error: ' + err);
updateAcarsStatus('error');
});
}
function stopMainAcars() {
@@ -7956,9 +8047,9 @@
.then(data => {
isMainAcarsRunning = false;
document.getElementById('mainAcarsToggleBtn').innerHTML = '▶ START';
document.getElementById('mainAcarsToggleBtn').style.background = 'var(--bg-dark)';
document.getElementById('mainAcarsToggleBtn').style.borderColor = 'var(--accent-cyan)';
document.getElementById('mainAcarsToggleBtn').style.color = 'var(--accent-cyan)';
document.getElementById('mainAcarsToggleBtn').style.background = 'var(--accent-green)';
// Hide status bar
document.getElementById('acarsStatusBar').style.display = 'none';
if (mainAcarsEventSource) {
mainAcarsEventSource.close();
mainAcarsEventSource = null;
@@ -7966,6 +8057,26 @@
});
}
function updateAcarsStatus(state) {
const statusBar = document.getElementById('acarsStatusBar');
const statusDot = document.getElementById('acarsStatusDot');
const statusText = document.getElementById('acarsStatusText');
statusBar.style.display = 'block';
statusDot.className = 'acars-status-dot ' + state;
statusText.textContent = state.toUpperCase();
if (state === 'listening') {
statusText.style.color = 'var(--accent-cyan)';
} else if (state === 'receiving') {
statusText.style.color = 'var(--accent-green)';
} else if (state === 'error') {
statusText.style.color = 'var(--accent-red)';
} else {
statusText.style.color = 'var(--text-muted)';
}
}
function startMainAcarsStream() {
if (mainAcarsEventSource) mainAcarsEventSource.close();
mainAcarsEventSource = new EventSource('/acars/stream');
@@ -7974,6 +8085,12 @@
const data = JSON.parse(e.data);
if (data.type === 'acars') {
mainAcarsMessageCount++;
// Update status bar
document.getElementById('acarsStatusCount').textContent = mainAcarsMessageCount;
const dot = document.getElementById('acarsStatusDot');
if (dot && !dot.classList.contains('receiving')) {
updateAcarsStatus('receiving');
}
addMainAcarsMessage(data);
// Also add to main output if ADS-B mode is active
addAcarsToOutput(data);
@@ -7982,6 +8099,7 @@
mainAcarsEventSource.onerror = function() {
console.error('Main ACARS stream error');
updateAcarsStatus('error');
};
}
@@ -8082,6 +8200,56 @@
}, 1000);
}
function refreshAprsDevices() {
fetch('/devices')
.then(r => r.json())
.then(devices => {
const select = document.getElementById('aprsDevice');
select.innerHTML = '';
if (devices.length === 0) {
select.innerHTML = '<option value="">No SDR devices found</option>';
} else {
devices.forEach(dev => {
const opt = document.createElement('option');
opt.value = dev.index;
opt.textContent = `${dev.index}: ${dev.name || 'Unknown'}`;
select.appendChild(opt);
});
}
})
.catch(err => {
console.error('Failed to refresh APRS devices:', err);
const select = document.getElementById('aprsDevice');
select.innerHTML = '<option value="0">Error loading devices</option>';
});
}
function updateAprsStatus(state, freq) {
const statusBar = document.getElementById('aprsStatusBar');
const statusDot = document.getElementById('aprsStatusDot');
const statusText = document.getElementById('aprsStatusText');
const freqEl = document.getElementById('aprsStatusFreq');
statusBar.style.display = 'block';
statusDot.className = 'aprs-status-dot ' + state;
statusText.textContent = state.toUpperCase();
if (freq) {
freqEl.textContent = freq + ' MHz';
}
// Update color based on state
if (state === 'listening') {
statusText.style.color = 'var(--accent-cyan)';
} else if (state === 'tracking') {
statusText.style.color = 'var(--accent-green)';
} else if (state === 'error') {
statusText.style.color = 'var(--accent-red)';
} else {
statusText.style.color = 'var(--text-muted)';
}
}
function startAprs() {
const region = document.getElementById('aprsRegion').value;
const device = document.getElementById('aprsDevice').value;
@@ -8102,12 +8270,20 @@
document.getElementById('stopAprsBtn').style.display = 'block';
document.getElementById('aprsMapStatus').textContent = 'TRACKING';
document.getElementById('aprsMapStatus').style.color = 'var(--accent-green)';
// Update sidebar status bar
updateAprsStatus('listening', data.frequency);
document.getElementById('aprsStatusStations').textContent = '0';
document.getElementById('aprsStatusPackets').textContent = '0';
startAprsStream();
} else {
alert('APRS Error: ' + data.message);
updateAprsStatus('error');
}
})
.catch(err => alert('APRS Error: ' + err));
.catch(err => {
alert('APRS Error: ' + err);
updateAprsStatus('error');
});
}
function stopAprs() {
@@ -8117,6 +8293,8 @@
isAprsRunning = false;
document.getElementById('startAprsBtn').style.display = 'block';
document.getElementById('stopAprsBtn').style.display = 'none';
// Hide sidebar status bar
document.getElementById('aprsStatusBar').style.display = 'none';
document.getElementById('aprsMapStatus').textContent = 'STANDBY';
document.getElementById('aprsMapStatus').style.color = '';
if (aprsEventSource) {
@@ -8135,12 +8313,20 @@
if (data.type === 'aprs') {
aprsPacketCount++;
document.getElementById('aprsPacketCount').textContent = aprsPacketCount;
// Update sidebar status bar
document.getElementById('aprsStatusPackets').textContent = aprsPacketCount;
// Switch to tracking state on first packet
const dot = document.getElementById('aprsStatusDot');
if (dot && !dot.classList.contains('tracking')) {
updateAprsStatus('tracking');
}
processAprsPacket(data);
}
};
aprsEventSource.onerror = function() {
console.error('APRS stream error');
updateAprsStatus('error');
};
}
@@ -8188,6 +8374,7 @@
// Create new marker
aprsStationCount++;
document.getElementById('aprsStationCount').textContent = aprsStationCount;
document.getElementById('aprsStatusStations').textContent = aprsStationCount;
const icon = L.divIcon({
className: 'aprs-marker',
@@ -9790,6 +9977,7 @@
<button class="help-tab active" data-tab="icons" onclick="switchHelpTab('icons')">Icons</button>
<button class="help-tab" data-tab="modes" onclick="switchHelpTab('modes')">Modes</button>
<button class="help-tab" data-tab="wifi" onclick="switchHelpTab('wifi')">WiFi</button>
<button class="help-tab" data-tab="aircraft" onclick="switchHelpTab('aircraft')">Aircraft</button>
<button class="help-tab" data-tab="tips" onclick="switchHelpTab('tips')">Tips</button>
</div>
@@ -9810,17 +9998,21 @@
<div class="icon-item"><span class="icon">🚁</span><span class="desc">Detected drones (click for details)</span></div>
<div class="icon-item"><span class="icon">⚠️</span><span class="desc">Rogue APs (click for details)</span></div>
<div class="icon-item"><span class="icon">🔵</span><span class="desc">Bluetooth devices</span></div>
<div class="icon-item"><span class="icon">📍</span><span class="desc">BLE beacons detected</span></div>
<div class="icon-item"><span class="icon">📍</span><span class="desc">BLE beacons / APRS stations</span></div>
<div class="icon-item"><span class="icon">📨</span><span class="desc">ACARS messages received</span></div>
</div>
<h3>Mode Tab Icons</h3>
<div class="icon-grid">
<div class="icon-item"><span class="icon">📟</span><span class="desc">Pager - POCSAG/FLEX decoder</span></div>
<div class="icon-item"><span class="icon">📡</span><span class="desc">433MHz - Sensor decoder</span></div>
<div class="icon-item"><span class="icon">✈️</span><span class="desc">Aircraft - ADS-B tracker</span></div>
<div class="icon-item"><span class="icon">✈️</span><span class="desc">Aircraft - ADS-B/ACARS tracker</span></div>
<div class="icon-item"><span class="icon">📍</span><span class="desc">APRS - Amateur radio tracking</span></div>
<div class="icon-item"><span class="icon">🛰️</span><span class="desc">Satellite - Pass prediction</span></div>
<div class="icon-item"><span class="icon">📶</span><span class="desc">WiFi - Network scanner</span></div>
<div class="icon-item"><span class="icon">🔵</span><span class="desc">Bluetooth - BT/BLE scanner</span></div>
<div class="icon-item"><span class="icon">📻</span><span class="desc">Listening Post - SDR scanner</span></div>
<div class="icon-item"><span class="icon">🔍</span><span class="desc">TSCM - Counter-surveillance</span></div>
</div>
</div>
@@ -9847,8 +10039,18 @@
<li>Tracks aircraft via ADS-B using dump1090 or rtl_adsb</li>
<li>Interactive map with real OpenStreetMap tiles</li>
<li>Click aircraft markers to see callsign, altitude, speed, heading</li>
<li>Map auto-fits to show all tracked aircraft</li>
<li>ACARS messaging sidebar decodes aircraft datalink messages</li>
<li>Emergency squawk codes highlighted in red</li>
<li>Open full dashboard for radar view and airband audio</li>
</ul>
<h3>📍 APRS Mode</h3>
<ul class="tip-list">
<li>Decodes APRS (Automatic Packet Reporting System) on VHF</li>
<li>Tracks amateur radio operators transmitting position data</li>
<li>Regional frequencies: 144.390 MHz (N. America), 144.800 MHz (Europe)</li>
<li>Uses Direwolf or multimon-ng for packet decoding</li>
<li>Interactive map shows station positions in real-time</li>
</ul>
<h3>🛰️ Satellite Mode</h3>
@@ -9876,6 +10078,25 @@
<li>Manufacturer lookup from MAC address OUI</li>
<li>Radar visualization shows device proximity</li>
</ul>
<h3>📻 Listening Post 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>
<li>AM/FM/USB/LSB demodulation modes</li>
<li>Bookmark frequencies for quick recall</li>
<li>Quick tune presets for emergency and marine channels</li>
</ul>
<h3>🔍 TSCM Mode</h3>
<ul class="tip-list">
<li>Technical Surveillance Countermeasures sweep</li>
<li>Scans for unknown RF transmitters, WiFi devices, Bluetooth</li>
<li>Baseline comparison to detect new/anomalous devices</li>
<li>Threat classification: Critical, High, Medium, Low</li>
<li>Useful for security audits and bug sweeps</li>
<li><em style="color: var(--text-muted);">Note: This feature is in early development</em></li>
</ul>
</div>
<!-- WiFi Section -->
@@ -9926,6 +10147,50 @@
</ul>
</div>
<!-- Aircraft Section -->
<div id="help-aircraft" class="help-section">
<h3>ADS-B Tracking</h3>
<ul class="tip-list">
<li>Receives ADS-B broadcasts on 1090 MHz from aircraft transponders</li>
<li>Displays position, altitude, speed, heading, and squawk code</li>
<li>Interactive map and radar scope views available</li>
<li>Click aircraft to see detailed telemetry and photos</li>
<li>Emergency squawk codes (7500, 7600, 7700) highlighted</li>
</ul>
<h3>ACARS Messaging</h3>
<ul class="tip-list">
<li>ACARS (Aircraft Communications Addressing and Reporting System)</li>
<li>Decodes VHF datalink messages between aircraft and ground</li>
<li>Requires a <strong>second SDR</strong> (separate from ADS-B)</li>
<li>Regional frequencies vary - select N. America, Europe, or Asia-Pacific</li>
<li>Messages include flight info, weather, OOOI events, and free text</li>
</ul>
<h3>ACARS Frequencies</h3>
<ul class="tip-list">
<li><strong>N. America:</strong> 130.025, 130.450, 131.550 MHz</li>
<li><strong>Europe:</strong> 131.525, 131.725, 131.550 MHz</li>
<li><strong>Asia-Pacific:</strong> 131.550, 131.450 MHz</li>
</ul>
<h3>Airband Audio</h3>
<ul class="tip-list">
<li>Listen to aviation voice communications (AM mode)</li>
<li>Requires RTL-SDR or compatible SDR hardware</li>
<li>Common frequencies: 118-137 MHz (civil aviation band)</li>
<li>121.5 MHz is the international emergency/guard frequency</li>
</ul>
<h3>Full Dashboard</h3>
<ul class="tip-list">
<li>Click "Open Full Dashboard" for expanded ADS-B view</li>
<li>Toggle between map and radar scope displays</li>
<li>Watchlist feature for tracking specific aircraft</li>
<li>Aircraft photos fetched from JetPhotos when available</li>
</ul>
</div>
<!-- Tips Section -->
<div id="help-tips" class="help-section">
<h3>General Tips</h3>
@@ -9946,12 +10211,17 @@
<h3>Requirements</h3>
<ul class="tip-list">
<li><strong>Pager/433MHz:</strong> RTL-SDR dongle, rtl_fm, multimon-ng, rtl_433</li>
<li><strong>Aircraft:</strong> RTL-SDR dongle, dump1090 or rtl_adsb</li>
<li><strong>Satellite:</strong> Internet connection for Celestrak (optional)</li>
<li><strong>WiFi:</strong> Monitor-mode capable adapter, aircrack-ng suite</li>
<li><strong>Bluetooth:</strong> Bluetooth adapter, hcitool/bluetoothctl</li>
<li>Run as root/sudo for full functionality</li>
<li><strong>Pager:</strong> RTL-SDR, rtl_fm, multimon-ng</li>
<li><strong>433MHz Sensors:</strong> RTL-SDR, rtl_433</li>
<li><strong>Aircraft (ADS-B):</strong> RTL-SDR, dump1090 or rtl_adsb</li>
<li><strong>Aircraft (ACARS):</strong> Second RTL-SDR, acarsdec</li>
<li><strong>APRS:</strong> RTL-SDR, direwolf or multimon-ng</li>
<li><strong>Satellite:</strong> Internet for Celestrak (optional), skyfield</li>
<li><strong>WiFi:</strong> Monitor-mode adapter, aircrack-ng suite</li>
<li><strong>Bluetooth:</strong> Bluetooth adapter, bluez (hcitool/bluetoothctl)</li>
<li><strong>Listening Post:</strong> RTL-SDR or SoapySDR-compatible hardware</li>
<li><strong>TSCM:</strong> WiFi adapter, Bluetooth adapter, RTL-SDR (all optional)</li>
<li>Run as root/sudo for full hardware access</li>
</ul>
<h3>Legal Notice</h3>