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 += `