diff --git a/static/css/adsb_dashboard.css b/static/css/adsb_dashboard.css index 409fd3c..63728bb 100644 --- a/static/css/adsb_dashboard.css +++ b/static/css/adsb_dashboard.css @@ -584,12 +584,111 @@ body { border-bottom: 1px solid var(--border-color); font-size: 10px; animation: fadeIn 0.3s ease; + cursor: pointer; + transition: background 0.2s; } .vdl2-message-item:hover { + background: rgba(74, 158, 255, 0.08); +} + +.vdl2-message-item.expanded { background: rgba(74, 158, 255, 0.05); } +.vdl2-msg-summary { + display: flex; + justify-content: space-between; + align-items: center; +} + +.vdl2-msg-summary .vdl2-expand-icon { + color: var(--text-dim); + font-size: 8px; + transition: transform 0.2s; + flex-shrink: 0; + margin-left: 6px; +} + +.vdl2-message-item.expanded .vdl2-expand-icon { + transform: rotate(90deg); +} + +.vdl2-msg-details { + display: none; + margin-top: 6px; + padding-top: 6px; + border-top: 1px solid var(--border-color); +} + +.vdl2-message-item.expanded .vdl2-msg-details { + display: block; +} + +.vdl2-detail-row { + display: flex; + justify-content: space-between; + padding: 2px 0; + font-size: 9px; + line-height: 1.4; +} + +.vdl2-detail-label { + color: var(--text-muted); + flex-shrink: 0; + margin-right: 8px; +} + +.vdl2-detail-value { + color: var(--text-primary); + text-align: right; + word-break: break-word; +} + +.vdl2-msg-body { + margin-top: 4px; + padding: 4px 6px; + background: rgba(0, 0, 0, 0.2); + border-radius: 3px; + font-family: var(--font-mono); + font-size: 9px; + color: var(--text-primary); + white-space: pre-wrap; + word-break: break-word; + line-height: 1.4; + max-height: 200px; + overflow-y: auto; +} + +.vdl2-raw-toggle { + display: inline-block; + margin-top: 4px; + font-size: 8px; + color: var(--accent-cyan); + cursor: pointer; + opacity: 0.7; +} + +.vdl2-raw-toggle:hover { + opacity: 1; +} + +.vdl2-raw-json { + display: none; + margin-top: 4px; + padding: 4px 6px; + background: rgba(0, 0, 0, 0.3); + border-radius: 3px; + font-family: var(--font-mono); + font-size: 8px; + color: var(--text-dim); + white-space: pre-wrap; + word-break: break-all; + max-height: 300px; + overflow-y: auto; + line-height: 1.3; +} + /* Panels */ .panel { background: var(--bg-panel); diff --git a/templates/adsb_dashboard.html b/templates/adsb_dashboard.html index d685d70..2a36ea2 100644 --- a/templates/adsb_dashboard.html +++ b/templates/adsb_dashboard.html @@ -4372,6 +4372,74 @@ sudo make install }, pollInterval); } + function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; + } + + function buildVdl2Details(data) { + const avlc = data.avlc || {}; + const src = avlc.src || {}; + const dst = avlc.dst || {}; + const acars = avlc.acars || {}; + const xid = avlc.xid || {}; + const rows = []; + + // Station & frequency + if (data.station) rows.push(['Station', escapeHtml(String(data.station))]); + if (data.freq) rows.push(['Frequency', (data.freq / 1000000).toFixed(3) + ' MHz']); + + // AVLC addressing + if (src.addr) rows.push(['Source', escapeHtml(String(src.addr)) + (src.type ? ` (${escapeHtml(src.type)})` : '')]); + if (dst.addr) rows.push(['Destination', escapeHtml(String(dst.addr)) + (dst.type ? ` (${escapeHtml(dst.type)})` : '')]); + if (avlc.cr) rows.push(['Command/Response', escapeHtml(avlc.cr)]); + if (avlc.frame_type) rows.push(['Frame Type', escapeHtml(avlc.frame_type)]); + + // ACARS fields + if (acars.reg) rows.push(['Registration', escapeHtml(acars.reg)]); + if (acars.flight) rows.push(['Flight', escapeHtml(acars.flight)]); + if (acars.mode) rows.push(['Mode', escapeHtml(acars.mode)]); + if (acars.label) rows.push(['Label', escapeHtml(acars.label)]); + if (acars.blk_id) rows.push(['Block ID', escapeHtml(acars.blk_id)]); + if (acars.ack) rows.push(['ACK', escapeHtml(acars.ack)]); + if (acars.msg_num) rows.push(['Msg Number', escapeHtml(acars.msg_num)]); + if (acars.msg_num_seq) rows.push(['Msg Seq', escapeHtml(acars.msg_num_seq)]); + + // XID fields + if (xid.vdl_params) { + const params = xid.vdl_params; + if (params.ac_location) { + const loc = params.ac_location; + if (loc.lat !== undefined && loc.lon !== undefined) { + rows.push(['Position', `${loc.lat.toFixed(4)}, ${loc.lon.toFixed(4)}`]); + } + } + } + + // Agent name (multi-SDR mode) + if (data.agent_name) rows.push(['Agent', escapeHtml(data.agent_name)]); + + let html = ''; + rows.forEach(([label, value]) => { + html += `
${label}${value}
`; + }); + + // Message body + const msgText = acars.msg_text || ''; + if (msgText) { + html += `
${escapeHtml(msgText)}
`; + } + + // Raw JSON toggle + const rawJson = JSON.stringify(data, null, 2); + const rawId = 'vdl2raw_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); + html += `▸ Show raw JSON`; + html += `
${escapeHtml(rawJson)}
`; + + return html; + } + function addVdl2Message(data) { const container = document.getElementById('vdl2Messages'); @@ -4380,28 +4448,40 @@ sudo make install const msg = document.createElement('div'); msg.className = 'vdl2-message-item'; - msg.style.cssText = 'padding: 6px 8px; border-bottom: 1px solid var(--border-color); font-size: 10px;'; - const station = data.station || ''; const avlc = data.avlc || {}; const src = avlc.src?.addr || ''; - const dst = avlc.dst?.addr || ''; const acars = avlc.acars || {}; const flight = acars.flight || ''; const msgText = acars.msg_text || ''; const time = new Date().toLocaleTimeString(); const freq = data.freq ? (data.freq / 1000000).toFixed(3) : ''; + const label = flight || src || 'VDL2'; + const hasContent = msgText || acars.label || avlc.frame_type; msg.innerHTML = ` -
- ${flight || src || 'VDL2'} - ${time} +
+
+
+ ${escapeHtml(label)} + ${time} +
+
+ ${freq ? freq + ' MHz' : ''}${freq && acars.label ? ' · ' : ''}${acars.label ? 'Label: ' + escapeHtml(acars.label) : ''}${msgText ? ' · ' + escapeHtml(msgText.substring(0, 40)) + (msgText.length > 40 ? '…' : '') : ''} +
+
+ +
+
+ ${buildVdl2Details(data)}
- ${freq ? `
${freq} MHz
` : ''} - ${dst ? `
To: ${dst}
` : ''} - ${msgText ? `
${msgText}
` : ''} `; + msg.addEventListener('click', function(e) { + if (e.target.closest('.vdl2-raw-toggle') || e.target.closest('.vdl2-raw-json')) return; + this.classList.toggle('expanded'); + }); + container.insertBefore(msg, container.firstChild); while (container.children.length > 50) {