diff --git a/static/css/adsb_dashboard.css b/static/css/adsb_dashboard.css index 63728bb..1b44f56 100644 --- a/static/css/adsb_dashboard.css +++ b/static/css/adsb_dashboard.css @@ -592,101 +592,175 @@ body { background: rgba(74, 158, 255, 0.08); } -.vdl2-message-item.expanded { - background: rgba(74, 158, 255, 0.05); +/* VDL2 Message Modal */ +.vdl2-modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + animation: vdl2ModalFadeIn 0.15s ease; } -.vdl2-msg-summary { +@keyframes vdl2ModalFadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.vdl2-modal { + background: var(--bg-panel, #1a1a2e); + border: 1px solid var(--accent-cyan, #4a9eff); + border-radius: 8px; + width: 520px; + max-width: 90vw; + max-height: 80vh; + display: flex; + flex-direction: column; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 1px var(--accent-cyan, #4a9eff); + animation: vdl2ModalSlideIn 0.15s ease; +} + +@keyframes vdl2ModalSlideIn { + from { opacity: 0; transform: scale(0.95) translateY(10px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +.vdl2-modal-header { 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; + padding: 14px 18px; + border-bottom: 1px solid var(--border-color); flex-shrink: 0; - margin-left: 6px; } -.vdl2-message-item.expanded .vdl2-expand-icon { - transform: rotate(90deg); +.vdl2-modal-title { + font-size: 14px; + font-weight: 700; + color: var(--accent-cyan, #4a9eff); + letter-spacing: 0.5px; } -.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 { +.vdl2-modal-time { + font-size: 11px; 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-modal-close { + background: none; + border: 1px solid var(--border-color); + color: var(--text-muted); + width: 28px; + height: 28px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s; + margin-left: 12px; } -.vdl2-msg-body { - margin-top: 4px; - padding: 4px 6px; - background: rgba(0, 0, 0, 0.2); - border-radius: 3px; - font-family: var(--font-mono); +.vdl2-modal-close:hover { + background: rgba(239, 68, 68, 0.15); + border-color: var(--accent-red, #ef4444); + color: var(--accent-red, #ef4444); +} + +.vdl2-modal-body { + padding: 16px 18px; + overflow-y: auto; + flex: 1; + min-height: 0; +} + +.vdl2-modal-section { + margin-bottom: 14px; +} + +.vdl2-modal-section:last-child { + margin-bottom: 0; +} + +.vdl2-modal-section-title { font-size: 9px; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 1.5px; + margin-bottom: 8px; +} + +.vdl2-modal-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px 16px; +} + +.vdl2-modal-field { + display: flex; + flex-direction: column; + gap: 1px; +} + +.vdl2-modal-field-label { + font-size: 10px; + color: var(--text-dim); +} + +.vdl2-modal-field-value { + font-size: 12px; + color: var(--text-primary); + font-weight: 500; +} + +.vdl2-modal-msg-body { + padding: 10px 12px; + background: rgba(0, 0, 0, 0.25); + border-radius: 4px; + font-family: var(--font-mono); + font-size: 12px; color: var(--text-primary); white-space: pre-wrap; word-break: break-word; - line-height: 1.4; - max-height: 200px; + line-height: 1.5; + max-height: 250px; overflow-y: auto; } -.vdl2-raw-toggle { +.vdl2-modal-raw-toggle { display: inline-block; - margin-top: 4px; - font-size: 8px; - color: var(--accent-cyan); + margin-top: 10px; + font-size: 10px; + color: var(--accent-cyan, #4a9eff); cursor: pointer; opacity: 0.7; + transition: opacity 0.15s; } -.vdl2-raw-toggle:hover { +.vdl2-modal-raw-toggle:hover { opacity: 1; } -.vdl2-raw-json { +.vdl2-modal-raw-json { display: none; - margin-top: 4px; - padding: 4px 6px; + margin-top: 8px; + padding: 10px 12px; background: rgba(0, 0, 0, 0.3); - border-radius: 3px; + border: 1px solid var(--border-color); + border-radius: 4px; font-family: var(--font-mono); - font-size: 8px; + font-size: 11px; color: var(--text-dim); white-space: pre-wrap; word-break: break-all; max-height: 300px; overflow-y: auto; - line-height: 1.3; + line-height: 1.4; } /* Panels */ diff --git a/templates/adsb_dashboard.html b/templates/adsb_dashboard.html index 2a36ea2..078c7e7 100644 --- a/templates/adsb_dashboard.html +++ b/templates/adsb_dashboard.html @@ -4378,66 +4378,131 @@ sudo make install return div.innerHTML; } - function buildVdl2Details(data) { + function showVdl2Modal(data, timeStr) { + // Remove any existing modal + const existing = document.querySelector('.vdl2-modal-overlay'); + if (existing) existing.remove(); + const avlc = data.avlc || {}; const src = avlc.src || {}; const dst = avlc.dst || {}; const acars = avlc.acars || {}; const xid = avlc.xid || {}; - const rows = []; + const flight = acars.flight || ''; + const freq = data.freq ? (data.freq / 1000000).toFixed(3) + ' MHz' : ''; + const title = flight || src.addr || 'VDL2 Message'; - // Station & frequency - if (data.station) rows.push(['Station', escapeHtml(String(data.station))]); - if (data.freq) rows.push(['Frequency', (data.freq / 1000000).toFixed(3) + ' MHz']); + // Build connection fields + let connectionHtml = ''; + const connFields = []; + if (data.station) connFields.push(['Station', String(data.station)]); + if (freq) connFields.push(['Frequency', freq]); + if (src.addr) connFields.push(['Source', String(src.addr) + (src.type ? ` (${src.type})` : '')]); + if (dst.addr) connFields.push(['Destination', String(dst.addr) + (dst.type ? ` (${dst.type})` : '')]); + if (avlc.cr) connFields.push(['Cmd/Response', avlc.cr]); + if (avlc.frame_type) connFields.push(['Frame Type', avlc.frame_type]); + if (data.agent_name) connFields.push(['Agent', data.agent_name]); + if (connFields.length) { + connectionHtml = `
+
Connection
+
${connFields.map(([l, v]) => + `
${escapeHtml(l)}${escapeHtml(v)}
` + ).join('')}
+
`; + } - // 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)]); + // Build ACARS fields + let acarsHtml = ''; + const acarsFields = []; + if (acars.reg) acarsFields.push(['Registration', acars.reg]); + if (acars.flight) acarsFields.push(['Flight', acars.flight]); + if (acars.mode) acarsFields.push(['Mode', acars.mode]); + if (acars.label) acarsFields.push(['Label', acars.label]); + if (acars.blk_id) acarsFields.push(['Block ID', acars.blk_id]); + if (acars.ack) acarsFields.push(['ACK', acars.ack]); + if (acars.msg_num) acarsFields.push(['Msg Number', acars.msg_num]); + if (acars.msg_num_seq) acarsFields.push(['Msg Seq', acars.msg_num_seq]); + if (acarsFields.length) { + acarsHtml = `
+
ACARS
+
${acarsFields.map(([l, v]) => + `
${escapeHtml(l)}${escapeHtml(v)}
` + ).join('')}
+
`; + } - // 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)}`]); - } + // Position from XID + let posHtml = ''; + if (xid.vdl_params?.ac_location) { + const loc = xid.vdl_params.ac_location; + if (loc.lat !== undefined && loc.lon !== undefined) { + posHtml = `
+
Position
+
+
Latitude${loc.lat.toFixed(4)}
+
Longitude${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 || ''; + let bodyHtml = ''; if (msgText) { - html += `
${escapeHtml(msgText)}
`; + bodyHtml = `
+
Message
+
${escapeHtml(msgText)}
+
`; } - // Raw JSON toggle + // Raw JSON 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)}
`; + const rawId = 'vdl2modal_raw_' + Date.now(); + const rawHtml = `
+ ▸ Show raw JSON +
${escapeHtml(rawJson)}
+
`; - return html; + const overlay = document.createElement('div'); + overlay.className = 'vdl2-modal-overlay'; + overlay.innerHTML = ` +
+
+
+
${escapeHtml(title)}
+
${escapeHtml(timeStr)}${freq ? ' · ' + escapeHtml(freq) : ''}
+
+ +
+
+ ${connectionHtml} + ${acarsHtml} + ${posHtml} + ${bodyHtml} + ${rawHtml} +
+
+ `; + + // Close handlers + overlay.querySelector('.vdl2-modal-close').addEventListener('click', () => overlay.remove()); + overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); + + // Raw JSON toggle + const rawToggle = overlay.querySelector(`#${rawId}_toggle`); + const rawEl = overlay.querySelector(`#${rawId}`); + rawToggle.addEventListener('click', () => { + const show = rawEl.style.display !== 'block'; + rawEl.style.display = show ? 'block' : 'none'; + rawToggle.innerHTML = show ? '▾ Hide raw JSON' : '▸ Show raw JSON'; + }); + + // Escape key + const onKey = (e) => { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', onKey); } }; + document.addEventListener('keydown', onKey); + + document.body.appendChild(overlay); } function addVdl2Message(data) { @@ -4457,30 +4522,18 @@ sudo make install 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 = ` -
-
-
- ${escapeHtml(label)} - ${time} -
-
- ${freq ? freq + ' MHz' : ''}${freq && acars.label ? ' · ' : ''}${acars.label ? 'Label: ' + escapeHtml(acars.label) : ''}${msgText ? ' · ' + escapeHtml(msgText.substring(0, 40)) + (msgText.length > 40 ? '…' : '') : ''} -
-
- +
+ ${escapeHtml(label)} + ${time}
-
- ${buildVdl2Details(data)} +
+ ${freq ? freq + ' MHz' : ''}${freq && acars.label ? ' · ' : ''}${acars.label ? 'Label: ' + escapeHtml(acars.label) : ''}${msgText ? ' · ' + escapeHtml(msgText.substring(0, 50)) + (msgText.length > 50 ? '…' : '') : ''}
`; - msg.addEventListener('click', function(e) { - if (e.target.closest('.vdl2-raw-toggle') || e.target.closest('.vdl2-raw-json')) return; - this.classList.toggle('expanded'); - }); + msg.addEventListener('click', () => showVdl2Modal(data, time)); container.insertBefore(msg, container.firstChild);