mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
fix: Wire up VDL2 agent mode, fix dashboard layout gap and stats strip sizing
- Add VDL2 to syncModeUI setter map and allModes array in agents.js so agent state sync works for VDL2 - Fix dashboard bottom gap by using flex layout on body instead of hardcoded calc(100dvh - 160px) height - Match source stat font-size to other stats (14px) for consistent strip sizing - Add left-sidebars wrapper, VDL2 agent mode support, mutual sidebar collapse, and ACARS/VDL2 modeNames in index.html Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
10
setup.sh
10
setup.sh
@@ -226,7 +226,7 @@ check_tools() {
|
||||
check_optional "hackrf_sweep" "HackRF spectrum analyzer" hackrf_sweep
|
||||
check_required "dump1090" "ADS-B decoder" dump1090
|
||||
check_required "acarsdec" "ACARS decoder" acarsdec
|
||||
check_optional "dumpvdl2" "VDL2 decoder" dumpvdl2
|
||||
check_required "dumpvdl2" "VDL2 decoder" dumpvdl2
|
||||
check_required "AIS-catcher" "AIS vessel decoder" AIS-catcher aiscatcher
|
||||
check_optional "satdump" "Weather satellite decoder (NOAA/Meteor)" satdump
|
||||
echo
|
||||
@@ -949,9 +949,9 @@ install_macos_packages() {
|
||||
ok "acarsdec already installed"
|
||||
fi
|
||||
|
||||
progress "Installing dumpvdl2 (optional)"
|
||||
progress "Installing dumpvdl2"
|
||||
if ! cmd_exists dumpvdl2; then
|
||||
install_dumpvdl2_from_source_macos || warn "dumpvdl2 not available. VDL2 decoding will not be available."
|
||||
install_dumpvdl2_from_source_macos || fail "dumpvdl2 installation failed. VDL2 decoding requires dumpvdl2."
|
||||
else
|
||||
ok "dumpvdl2 already installed"
|
||||
fi
|
||||
@@ -1472,9 +1472,9 @@ install_debian_packages() {
|
||||
fi
|
||||
cmd_exists acarsdec || install_acarsdec_from_source_debian
|
||||
|
||||
progress "Installing dumpvdl2 (optional)"
|
||||
progress "Installing dumpvdl2"
|
||||
if ! cmd_exists dumpvdl2; then
|
||||
install_dumpvdl2_from_source_debian || warn "dumpvdl2 not available. VDL2 decoding will not be available."
|
||||
install_dumpvdl2_from_source_debian || fail "dumpvdl2 installation failed. VDL2 decoding requires dumpvdl2."
|
||||
else
|
||||
ok "dumpvdl2 already installed"
|
||||
fi
|
||||
|
||||
@@ -31,8 +31,11 @@ body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--bg-dark);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
height: 100dvh;
|
||||
height: 100vh; /* Fallback */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Animated radar sweep background */
|
||||
@@ -227,16 +230,14 @@ body {
|
||||
}
|
||||
|
||||
/* Main dashboard grid - Mobile first */
|
||||
/* Header ~52px + Nav 44px + Stats strip ~55px = ~151px, using 160px for safety */
|
||||
.dashboard {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
height: calc(100dvh - 160px);
|
||||
height: calc(100vh - 160px); /* Fallback */
|
||||
min-height: 400px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Tablet: Two-column layout */
|
||||
@@ -249,13 +250,29 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop: Full layout with ACARS */
|
||||
/* Desktop: Full layout with ACARS/VDL2 + map + sidebar */
|
||||
@media (min-width: 1024px) {
|
||||
.dashboard {
|
||||
grid-template-columns: auto 1fr 300px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Left sidebars wrapper (ACARS + VDL2) */
|
||||
.left-sidebars {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.left-sidebars {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* ACARS sidebar (left of map) - Collapsible */
|
||||
.acars-sidebar {
|
||||
display: none;
|
||||
@@ -267,12 +284,10 @@ body {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Show ACARS sidebar on desktop */
|
||||
@media (min-width: 1024px) {
|
||||
.acars-sidebar {
|
||||
display: flex;
|
||||
max-height: calc(100dvh - 160px);
|
||||
}
|
||||
/* Show ACARS sidebar inside wrapper */
|
||||
.left-sidebars .acars-sidebar {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.acars-collapse-btn {
|
||||
@@ -430,11 +445,10 @@ body {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.vdl2-sidebar {
|
||||
display: flex;
|
||||
max-height: calc(100dvh - 160px);
|
||||
}
|
||||
/* Show VDL2 sidebar inside wrapper */
|
||||
.left-sidebars .vdl2-sidebar {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vdl2-collapse-btn {
|
||||
@@ -652,6 +666,8 @@ body {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@@ -1453,7 +1469,7 @@ body {
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
height: auto !important;
|
||||
min-height: calc(100dvh - 160px);
|
||||
min-height: 400px;
|
||||
overflow-y: auto !important;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@@ -1654,7 +1670,7 @@ body {
|
||||
}
|
||||
|
||||
.strip-stat.source-stat .strip-value {
|
||||
font-size: 11px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.strip-stat.session-stat {
|
||||
|
||||
@@ -423,7 +423,7 @@ async function syncAgentModeStates(agentId) {
|
||||
});
|
||||
|
||||
// Also check modes that might need to be marked as stopped
|
||||
const allModes = ['sensor', 'pager', 'adsb', 'wifi', 'bluetooth', 'ais', 'dsc', 'acars', 'aprs', 'rtlamr', 'tscm', 'satellite', 'listening_post'];
|
||||
const allModes = ['sensor', 'pager', 'adsb', 'wifi', 'bluetooth', 'ais', 'dsc', 'acars', 'vdl2', 'aprs', 'rtlamr', 'tscm', 'satellite', 'listening_post'];
|
||||
allModes.forEach(mode => {
|
||||
if (!agentRunningModes.includes(mode)) {
|
||||
syncModeUI(mode, false, agentId);
|
||||
@@ -704,6 +704,7 @@ function syncModeUI(mode, isRunning, agentId = null) {
|
||||
'wifi': 'setWiFiRunning',
|
||||
'bluetooth': 'setBluetoothRunning',
|
||||
'acars': 'setAcarsRunning',
|
||||
'vdl2': 'setVdl2Running',
|
||||
'listening_post': 'setListeningPostRunning'
|
||||
};
|
||||
|
||||
@@ -865,12 +866,12 @@ function connectAgentStream(mode, onMessage) {
|
||||
}
|
||||
|
||||
let streamUrl;
|
||||
if (currentAgent === 'local') {
|
||||
streamUrl = `/${mode}/stream`;
|
||||
} else {
|
||||
// For remote agents, proxy SSE through controller
|
||||
streamUrl = `/controller/agents/${currentAgent}/${mode}/stream`;
|
||||
}
|
||||
if (currentAgent === 'local') {
|
||||
streamUrl = `/${mode}/stream`;
|
||||
} else {
|
||||
// For remote agents, proxy SSE through controller
|
||||
streamUrl = `/controller/agents/${currentAgent}/${mode}/stream`;
|
||||
}
|
||||
|
||||
agentEventSource = new EventSource(streamUrl);
|
||||
|
||||
@@ -878,7 +879,7 @@ function connectAgentStream(mode, onMessage) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
onMessage(data);
|
||||
onMessage(data);
|
||||
} catch (e) {
|
||||
console.error('Error parsing SSE message:', e);
|
||||
}
|
||||
|
||||
@@ -139,6 +139,8 @@
|
||||
</div>
|
||||
|
||||
<main class="dashboard">
|
||||
<!-- Left Sidebars (ACARS + VDL2) -->
|
||||
<div class="left-sidebars">
|
||||
<!-- ACARS Panel (left of map) - Collapsible -->
|
||||
<div class="acars-sidebar" id="acarsSidebar">
|
||||
<div class="acars-sidebar-content" id="acarsSidebarContent">
|
||||
@@ -236,6 +238,7 @@
|
||||
<span class="vdl2-collapse-label">VDL2</span>
|
||||
</button>
|
||||
</div>
|
||||
</div><!-- /left-sidebars -->
|
||||
|
||||
<!-- Main Display (Map or Radar Scope) -->
|
||||
<div class="main-display">
|
||||
@@ -3780,7 +3783,7 @@ sudo make install</code>
|
||||
let acarsCurrentAgent = null;
|
||||
let acarsPollTimer = null;
|
||||
let acarsMessageCount = 0;
|
||||
let acarsSidebarCollapsed = localStorage.getItem('acarsSidebarCollapsed') === 'true';
|
||||
let acarsSidebarCollapsed = localStorage.getItem('acarsSidebarCollapsed') !== 'false';
|
||||
let acarsFrequencies = {
|
||||
'na': ['129.125', '130.025', '130.450', '131.550'],
|
||||
'eu': ['131.525', '131.725', '131.550'],
|
||||
@@ -3792,6 +3795,14 @@ sudo make install</code>
|
||||
acarsSidebarCollapsed = !acarsSidebarCollapsed;
|
||||
sidebar.classList.toggle('collapsed', acarsSidebarCollapsed);
|
||||
localStorage.setItem('acarsSidebarCollapsed', acarsSidebarCollapsed);
|
||||
// Collapse VDL2 when expanding ACARS
|
||||
if (!acarsSidebarCollapsed && !vdl2SidebarCollapsed) {
|
||||
const vdl2 = document.getElementById('vdl2Sidebar');
|
||||
vdl2SidebarCollapsed = true;
|
||||
vdl2.classList.add('collapsed');
|
||||
localStorage.setItem('vdl2SidebarCollapsed', vdl2SidebarCollapsed);
|
||||
}
|
||||
setTimeout(() => { if (typeof radarMap !== 'undefined' && radarMap) radarMap.invalidateSize(); }, 350);
|
||||
}
|
||||
|
||||
// Initialize ACARS sidebar state and frequency checkboxes
|
||||
@@ -4099,6 +4110,8 @@ sudo make install</code>
|
||||
// ============================================
|
||||
let vdl2EventSource = null;
|
||||
let isVdl2Running = false;
|
||||
let vdl2CurrentAgent = null;
|
||||
let vdl2PollTimer = null;
|
||||
let vdl2MessageCount = 0;
|
||||
let vdl2SidebarCollapsed = localStorage.getItem('vdl2SidebarCollapsed') !== 'false';
|
||||
let vdl2Frequencies = {
|
||||
@@ -4118,6 +4131,14 @@ sudo make install</code>
|
||||
vdl2SidebarCollapsed = !vdl2SidebarCollapsed;
|
||||
sidebar.classList.toggle('collapsed', vdl2SidebarCollapsed);
|
||||
localStorage.setItem('vdl2SidebarCollapsed', vdl2SidebarCollapsed);
|
||||
// Collapse ACARS when expanding VDL2
|
||||
if (!vdl2SidebarCollapsed && !acarsSidebarCollapsed) {
|
||||
const acars = document.getElementById('acarsSidebar');
|
||||
acarsSidebarCollapsed = true;
|
||||
acars.classList.add('collapsed');
|
||||
localStorage.setItem('acarsSidebarCollapsed', acarsSidebarCollapsed);
|
||||
}
|
||||
setTimeout(() => { if (typeof radarMap !== 'undefined' && radarMap) radarMap.invalidateSize(); }, 350);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -4172,7 +4193,12 @@ sudo make install</code>
|
||||
const sdr_type = vdl2Select.selectedOptions[0]?.dataset.sdrType || 'rtlsdr';
|
||||
const frequencies = getVdl2RegionFreqs();
|
||||
|
||||
if (isTracking && device === '0') {
|
||||
// Check if using agent mode
|
||||
const isAgentMode = typeof adsbCurrentAgent !== 'undefined' && adsbCurrentAgent !== 'local';
|
||||
vdl2CurrentAgent = isAgentMode ? adsbCurrentAgent : null;
|
||||
|
||||
// Warn if using same device as ADS-B (only for local mode)
|
||||
if (!isAgentMode && isTracking && device === '0') {
|
||||
const useAnyway = confirm(
|
||||
'Warning: ADS-B tracking may be using SDR device 0.\n\n' +
|
||||
'VDL2 uses VHF frequencies (~137 MHz) while ADS-B uses 1090 MHz.\n' +
|
||||
@@ -4182,32 +4208,46 @@ sudo make install</code>
|
||||
if (!useAnyway) return;
|
||||
}
|
||||
|
||||
fetch('/vdl2/start', {
|
||||
// Determine endpoint based on agent mode
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${adsbCurrentAgent}/vdl2/start`
|
||||
: '/vdl2/start';
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ device, frequencies, gain: '40', sdr_type })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'started') {
|
||||
// Handle controller proxy response format
|
||||
const vdl2Result = isAgentMode && data.result ? data.result : data;
|
||||
|
||||
if (vdl2Result.status === 'started' || vdl2Result.status === 'success') {
|
||||
isVdl2Running = true;
|
||||
vdl2MessageCount = 0;
|
||||
document.getElementById('vdl2ToggleBtn').innerHTML = '■ STOP VDL2';
|
||||
document.getElementById('vdl2ToggleBtn').classList.add('active');
|
||||
document.getElementById('vdl2PanelIndicator').classList.add('active');
|
||||
startVdl2Stream();
|
||||
startVdl2Stream(isAgentMode);
|
||||
} else {
|
||||
alert('VDL2 Error: ' + (data.message || 'Failed to start'));
|
||||
alert('VDL2 Error: ' + (vdl2Result.message || vdl2Result.error || 'Failed to start'));
|
||||
}
|
||||
})
|
||||
.catch(err => alert('VDL2 Error: ' + err));
|
||||
}
|
||||
|
||||
function stopVdl2() {
|
||||
fetch('/vdl2/stop', { method: 'POST' })
|
||||
const isAgentMode = vdl2CurrentAgent !== null;
|
||||
const endpoint = isAgentMode
|
||||
? `/controller/agents/${vdl2CurrentAgent}/vdl2/stop`
|
||||
: '/vdl2/stop';
|
||||
|
||||
fetch(endpoint, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(() => {
|
||||
isVdl2Running = false;
|
||||
vdl2CurrentAgent = null;
|
||||
document.getElementById('vdl2ToggleBtn').innerHTML = '▶ START VDL2';
|
||||
document.getElementById('vdl2ToggleBtn').classList.remove('active');
|
||||
document.getElementById('vdl2PanelIndicator').classList.remove('active');
|
||||
@@ -4215,27 +4255,121 @@ sudo make install</code>
|
||||
vdl2EventSource.close();
|
||||
vdl2EventSource = null;
|
||||
}
|
||||
// Clear polling timer
|
||||
if (vdl2PollTimer) {
|
||||
clearInterval(vdl2PollTimer);
|
||||
vdl2PollTimer = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startVdl2Stream() {
|
||||
// Sync VDL2 UI state (called by syncModeUI in agents.js)
|
||||
function setVdl2Running(running, agentId = null) {
|
||||
isVdl2Running = running;
|
||||
const btn = document.getElementById('vdl2ToggleBtn');
|
||||
const indicator = document.getElementById('vdl2PanelIndicator');
|
||||
|
||||
if (running) {
|
||||
vdl2CurrentAgent = agentId;
|
||||
btn.innerHTML = '■ STOP VDL2';
|
||||
btn.classList.add('active');
|
||||
if (indicator) indicator.classList.add('active');
|
||||
if (!vdl2EventSource && !vdl2PollTimer) {
|
||||
startVdl2Stream(agentId !== null);
|
||||
}
|
||||
} else {
|
||||
btn.innerHTML = '▶ START VDL2';
|
||||
btn.classList.remove('active');
|
||||
if (indicator) indicator.classList.remove('active');
|
||||
}
|
||||
}
|
||||
// Expose to global scope for syncModeUI
|
||||
window.setVdl2Running = setVdl2Running;
|
||||
|
||||
function startVdl2Stream(isAgentMode = false) {
|
||||
if (vdl2EventSource) vdl2EventSource.close();
|
||||
|
||||
vdl2EventSource = new EventSource('/vdl2/stream');
|
||||
// Use different stream endpoint for agent mode
|
||||
const streamUrl = isAgentMode ? '/controller/stream/all' : '/vdl2/stream';
|
||||
vdl2EventSource = new EventSource(streamUrl);
|
||||
|
||||
vdl2EventSource.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'vdl2') {
|
||||
vdl2MessageCount++;
|
||||
if (typeof stats !== 'undefined') stats.vdl2Messages = (stats.vdl2Messages || 0) + 1;
|
||||
document.getElementById('vdl2Count').textContent = vdl2MessageCount;
|
||||
document.getElementById('stripVdl2').textContent = vdl2MessageCount;
|
||||
addVdl2Message(data);
|
||||
|
||||
if (isAgentMode) {
|
||||
// Handle multi-agent stream format
|
||||
if (data.scan_type === 'vdl2' && data.payload) {
|
||||
const payload = data.payload;
|
||||
if (payload.type === 'vdl2') {
|
||||
vdl2MessageCount++;
|
||||
if (typeof stats !== 'undefined') stats.vdl2Messages = (stats.vdl2Messages || 0) + 1;
|
||||
document.getElementById('vdl2Count').textContent = vdl2MessageCount;
|
||||
document.getElementById('stripVdl2').textContent = vdl2MessageCount;
|
||||
payload.agent_name = data.agent_name;
|
||||
addVdl2Message(payload);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Local stream format
|
||||
if (data.type === 'vdl2') {
|
||||
vdl2MessageCount++;
|
||||
if (typeof stats !== 'undefined') stats.vdl2Messages = (stats.vdl2Messages || 0) + 1;
|
||||
document.getElementById('vdl2Count').textContent = vdl2MessageCount;
|
||||
document.getElementById('stripVdl2').textContent = vdl2MessageCount;
|
||||
addVdl2Message(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
vdl2EventSource.onerror = function() {
|
||||
console.error('VDL2 stream error');
|
||||
};
|
||||
|
||||
// Start polling fallback for agent mode
|
||||
if (isAgentMode) {
|
||||
startVdl2Polling();
|
||||
}
|
||||
}
|
||||
|
||||
// Track last VDL2 message count for polling
|
||||
let lastVdl2MessageCount = 0;
|
||||
|
||||
function startVdl2Polling() {
|
||||
if (vdl2PollTimer) return;
|
||||
lastVdl2MessageCount = 0;
|
||||
|
||||
const pollInterval = 2000;
|
||||
vdl2PollTimer = setInterval(async () => {
|
||||
if (!isVdl2Running || !vdl2CurrentAgent) {
|
||||
clearInterval(vdl2PollTimer);
|
||||
vdl2PollTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/controller/agents/${vdl2CurrentAgent}/vdl2/data`);
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
const result = data.result || data;
|
||||
const messages = result.data || [];
|
||||
|
||||
if (messages.length > lastVdl2MessageCount) {
|
||||
const newMessages = messages.slice(lastVdl2MessageCount);
|
||||
newMessages.forEach(msg => {
|
||||
vdl2MessageCount++;
|
||||
if (typeof stats !== 'undefined') stats.vdl2Messages = (stats.vdl2Messages || 0) + 1;
|
||||
document.getElementById('vdl2Count').textContent = vdl2MessageCount;
|
||||
document.getElementById('stripVdl2').textContent = vdl2MessageCount;
|
||||
msg.agent_name = result.agent_name || 'Remote Agent';
|
||||
addVdl2Message(msg);
|
||||
});
|
||||
lastVdl2MessageCount = messages.length;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('VDL2 polling error:', err);
|
||||
}
|
||||
}, pollInterval);
|
||||
}
|
||||
|
||||
function addVdl2Message(data) {
|
||||
|
||||
@@ -3700,7 +3700,8 @@
|
||||
'pager': 'pager', 'sensor': '433',
|
||||
'satellite': 'satellite', 'wifi': 'wifi', 'bluetooth': 'bluetooth', 'bt_locate': 'bt locate',
|
||||
'listening': 'listening', 'aprs': 'aprs', 'tscm': 'tscm', 'meshtastic': 'meshtastic',
|
||||
'dmr': 'dmr', 'websdr': 'websdr', 'sstv_general': 'hf sstv'
|
||||
'dmr': 'dmr', 'websdr': 'websdr', 'sstv_general': 'hf sstv',
|
||||
'acars': 'acars', 'vdl2': 'vdl2'
|
||||
};
|
||||
document.querySelectorAll('.mode-nav-btn').forEach(btn => {
|
||||
const label = btn.querySelector('.nav-label');
|
||||
|
||||
Reference in New Issue
Block a user