feat: Consolidate VDL2 into ADS-B dashboard, fix blue bar, add CSV export

Remove VDL2 as a standalone mode since it's already integrated into the
ADS-B dashboard sidebar. Remove the blue border-top on the controls bar.
Add CSV export button to VDL2 panel for downloading collected messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-16 22:12:03 +00:00
parent 533e92c711
commit ffb98425f1
4 changed files with 58 additions and 16 deletions

View File

@@ -1128,7 +1128,7 @@ body {
gap: 8px;
padding: 8px 15px;
background: var(--bg-panel);
border-top: 1px solid rgba(74, 158, 255, 0.3);
border-top: none;
font-size: 11px;
overflow-x: auto;
overflow-y: hidden;

View File

@@ -198,6 +198,7 @@
<span>VDL2 MESSAGES</span>
<div style="display: flex; align-items: center; gap: 8px;">
<span id="vdl2Count" style="font-size: 10px; color: var(--accent-cyan);">0</span>
<button onclick="exportVdl2Csv()" title="Export VDL2 to CSV" style="background: none; border: 1px solid var(--border-color); color: var(--text-muted); cursor: pointer; font-size: 9px; padding: 2px 6px; border-radius: 3px; font-family: var(--font-mono);">CSV</button>
<div class="panel-indicator" id="vdl2PanelIndicator"></div>
</div>
</div>
@@ -4113,6 +4114,7 @@ sudo make install</code>
let vdl2CurrentAgent = null;
let vdl2PollTimer = null;
let vdl2MessageCount = 0;
let vdl2CollectedData = [];
let vdl2SidebarCollapsed = localStorage.getItem('vdl2SidebarCollapsed') !== 'false';
let vdl2Frequencies = {
'na': ['136975000', '136100000', '136650000', '136700000', '136800000'],
@@ -4226,6 +4228,7 @@ sudo make install</code>
if (vdl2Result.status === 'started' || vdl2Result.status === 'success') {
isVdl2Running = true;
vdl2MessageCount = 0;
vdl2CollectedData = [];
document.getElementById('vdl2ToggleBtn').innerHTML = '&#9632; STOP VDL2';
document.getElementById('vdl2ToggleBtn').classList.add('active');
document.getElementById('vdl2PanelIndicator').classList.add('active');
@@ -4550,6 +4553,27 @@ sudo make install</code>
const msgText = acars.msg_text || '';
const time = new Date().toLocaleTimeString();
const freq = inner.freq ? (inner.freq / 1000000).toFixed(3) : '';
// Store for CSV export
vdl2CollectedData.push({
timestamp: new Date().toISOString(),
frequency: freq ? freq + ' MHz' : '',
source: avlc.src?.addr || '',
source_type: avlc.src?.type || '',
destination: avlc.dst?.addr || '',
destination_type: avlc.dst?.type || '',
frame_type: avlc.frame_type || '',
flight: flight,
registration: acars.reg || '',
label: acars.label || '',
sublabel: acars.sublabel || '',
block_id: acars.blk_id || '',
msg_number: acars.msg_num || '',
message: msgText.replace(/\n/g, ' '),
signal_level: inner.sig_level != null ? inner.sig_level.toFixed(1) : '',
noise_level: inner.noise_level != null ? inner.noise_level.toFixed(1) : '',
agent: data.agent_name || ''
});
const label = flight || src || (avlc.frame_type || 'VDL2');
msg.innerHTML = `
@@ -4571,6 +4595,28 @@ sudo make install</code>
}
}
function exportVdl2Csv() {
if (vdl2CollectedData.length === 0) {
alert('No VDL2 messages to export.');
return;
}
const headers = Object.keys(vdl2CollectedData[0]);
const csvRows = [headers.join(',')];
for (const row of vdl2CollectedData) {
csvRows.push(headers.map(h => {
const val = String(row[h] || '');
return '"' + val.replace(/"/g, '""') + '"';
}).join(','));
}
const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `vdl2_${new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-')}.csv`;
a.click();
URL.revokeObjectURL(url);
}
// Populate VDL2 device selector
document.addEventListener('DOMContentLoaded', () => {
fetch('/devices')

View File

@@ -51,7 +51,6 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/acars.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/vdl2.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/aprs.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modes/tscm.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/components/signal-cards.css') }}">
@@ -572,7 +571,6 @@
{% include 'partials/modes/acars.html' %}
{% include 'partials/modes/vdl2.html' %}
<button class="preset-btn" onclick="killAll()"
style="width: 100%; margin-top: 10px; border-color: #ff3366; color: #ff3366;">
@@ -3164,7 +3162,7 @@
'pager', 'sensor', 'rtlamr', 'aprs', 'listening',
'spystations', 'meshtastic', 'wifi', 'bluetooth', 'bt_locate',
'tscm', 'satellite', 'sstv', 'weathersat', 'sstv_general', 'gps', 'websdr', 'subghz',
'acars', 'vdl2'
'acars'
]);
function getModeFromQuery() {
@@ -3623,7 +3621,7 @@
'wifi': 'wireless', 'bluetooth': 'wireless', 'bt_locate': 'wireless',
'tscm': 'security',
'rtlamr': 'sdr', 'ais': 'sdr', 'spystations': 'sdr',
'meshtastic': 'sdr', 'acars': 'sdr', 'vdl2': 'sdr',
'meshtastic': 'sdr', 'acars': 'sdr',
'satellite': 'space', 'sstv': 'space', 'weathersat': 'space', 'sstv_general': 'space', 'gps': 'space',
'subghz': 'sdr'
};
@@ -3701,7 +3699,7 @@
'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',
'acars': 'acars', 'vdl2': 'vdl2'
'acars': 'acars'
};
document.querySelectorAll('.mode-nav-btn').forEach(btn => {
const label = btn.querySelector('.nav-label');
@@ -3730,7 +3728,7 @@
document.getElementById('websdrMode')?.classList.toggle('active', mode === 'websdr');
document.getElementById('subghzMode')?.classList.toggle('active', mode === 'subghz');
document.getElementById('acarsMode')?.classList.toggle('active', mode === 'acars');
document.getElementById('vdl2Mode')?.classList.toggle('active', mode === 'vdl2');
const pagerStats = document.getElementById('pagerStats');
const sensorStats = document.getElementById('sensorStats');
const satelliteStats = document.getElementById('satelliteStats');
@@ -3772,8 +3770,7 @@
'dmr': 'DIGITAL VOICE',
'websdr': 'WEBSDR',
'subghz': 'SUBGHZ',
'acars': 'ACARS',
'vdl2': 'VDL2'
'acars': 'ACARS'
};
const activeModeIndicator = document.getElementById('activeModeIndicator');
if (activeModeIndicator) activeModeIndicator.innerHTML = '<span class="pulse-dot"></span>' + (modeNames[mode] || mode.toUpperCase());
@@ -3848,8 +3845,7 @@
'dmr': 'Digital Voice Decoder',
'websdr': 'HF/Shortwave WebSDR',
'subghz': 'SubGHz Transceiver',
'acars': 'ACARS Aircraft Messaging',
'vdl2': 'VDL2 Aircraft Datalink'
'acars': 'ACARS Aircraft Messaging'
};
const outputTitle = document.getElementById('outputTitle');
if (outputTitle) outputTitle.textContent = titles[mode] || 'Signal Monitor';
@@ -3867,7 +3863,7 @@
const reconBtn = document.getElementById('reconBtn');
const intelBtn = document.querySelector('[onclick="exportDeviceDB()"]');
const reconPanel = document.getElementById('reconPanel');
if (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'gps' || mode === 'listening' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'dmr' || mode === 'websdr' || mode === 'subghz' || mode === 'acars' || mode === 'vdl2') {
if (mode === 'satellite' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'gps' || mode === 'listening' || mode === 'aprs' || mode === 'tscm' || mode === 'spystations' || mode === 'meshtastic' || mode === 'dmr' || mode === 'websdr' || mode === 'subghz' || mode === 'acars') {
if (reconPanel) reconPanel.style.display = 'none';
if (reconBtn) reconBtn.style.display = 'none';
if (intelBtn) intelBtn.style.display = 'none';
@@ -3882,12 +3878,12 @@
// Show agent selector for modes that support remote agents
const agentSection = document.getElementById('agentSection');
const agentModes = ['pager', 'sensor', 'rtlamr', 'listening', 'aprs', 'wifi', 'bluetooth', 'aircraft', 'tscm', 'ais', 'acars', 'vdl2', 'dsc'];
const agentModes = ['pager', 'sensor', 'rtlamr', 'listening', 'aprs', 'wifi', 'bluetooth', 'aircraft', 'tscm', 'ais', 'acars', 'dsc'];
if (agentSection) agentSection.style.display = agentModes.includes(mode) ? 'block' : 'none';
// Show RTL-SDR device section for modes that use it
const rtlDeviceSection = document.getElementById('rtlDeviceSection');
if (rtlDeviceSection) rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'listening' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'dmr' || mode === 'acars' || mode === 'vdl2') ? 'block' : 'none';
if (rtlDeviceSection) rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'listening' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'dmr' || mode === 'acars') ? 'block' : 'none';
// Show waterfall panel if running in listening mode
const waterfallPanel = document.getElementById('waterfallPanel');

View File

@@ -68,7 +68,7 @@
{{ mode_item('adsb', 'Aircraft', '<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>', '/adsb/dashboard') }}
{{ mode_item('ais', 'Vessels', '<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>', '/ais/dashboard') }}
{{ mode_item('acars', 'ACARS', '<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"/><line x1="4" y1="4" x2="8" y2="8"/><line x1="20" y1="4" x2="16" y2="8"/></svg>') }}
{{ mode_item('vdl2', 'VDL2', '<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"/><path d="M2 2l4 4M22 2l-4 4M12 12v0"/></svg>') }}
{{ mode_item('aprs', 'APRS', '<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>') }}
{{ 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('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>') }}
@@ -181,7 +181,7 @@
{{ 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') }}
{{ mobile_item('ais', 'Vessels', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>', '/ais/dashboard') }}
{{ mobile_item('acars', 'ACARS', '<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>') }}
{{ mobile_item('vdl2', 'VDL2', '<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>') }}
{{ mobile_item('aprs', 'APRS', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>') }}
{{ mobile_item('wifi', 'WiFi', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><circle cx="12" cy="20" r="1" fill="currentColor"/></svg>') }}
{{ mobile_item('bluetooth', 'BT', '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6.5 6.5 17.5 17.5 12 22 12 2 17.5 6.5 6.5 17.5"/></svg>') }}