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:
Smittix
2026-02-16 20:27:25 +00:00
parent 5605ae0359
commit 5a0589dd69
5 changed files with 201 additions and 49 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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 = '&#9632; 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 = '&#9654; 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 = '&#9632; STOP VDL2';
btn.classList.add('active');
if (indicator) indicator.classList.add('active');
if (!vdl2EventSource && !vdl2PollTimer) {
startVdl2Stream(agentId !== null);
}
} else {
btn.innerHTML = '&#9654; 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) {

View File

@@ -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');