feat: Replace VDL2 inline expand with modal for easier reading

Message cards now open a centered modal overlay on click with organized
sections (Connection, ACARS, Position, Message) in a readable grid
layout. Includes raw JSON toggle, closes via X button, backdrop click,
or Escape key.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-16 21:49:40 +00:00
parent 85bf31c10b
commit 2142ea7638
2 changed files with 244 additions and 117 deletions
+131 -57
View File
@@ -592,101 +592,175 @@ body {
background: rgba(74, 158, 255, 0.08); background: rgba(74, 158, 255, 0.08);
} }
.vdl2-message-item.expanded { /* VDL2 Message Modal */
background: rgba(74, 158, 255, 0.05); .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; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} padding: 14px 18px;
border-bottom: 1px solid var(--border-color);
.vdl2-msg-summary .vdl2-expand-icon {
color: var(--text-dim);
font-size: 8px;
transition: transform 0.2s;
flex-shrink: 0; flex-shrink: 0;
margin-left: 6px;
} }
.vdl2-message-item.expanded .vdl2-expand-icon { .vdl2-modal-title {
transform: rotate(90deg); font-size: 14px;
font-weight: 700;
color: var(--accent-cyan, #4a9eff);
letter-spacing: 0.5px;
} }
.vdl2-msg-details { .vdl2-modal-time {
display: none; font-size: 11px;
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); color: var(--text-muted);
flex-shrink: 0;
margin-right: 8px;
} }
.vdl2-detail-value { .vdl2-modal-close {
color: var(--text-primary); background: none;
text-align: right; border: 1px solid var(--border-color);
word-break: break-word; 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 { .vdl2-modal-close:hover {
margin-top: 4px; background: rgba(239, 68, 68, 0.15);
padding: 4px 6px; border-color: var(--accent-red, #ef4444);
background: rgba(0, 0, 0, 0.2); color: var(--accent-red, #ef4444);
border-radius: 3px; }
font-family: var(--font-mono);
.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-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); color: var(--text-primary);
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
line-height: 1.4; line-height: 1.5;
max-height: 200px; max-height: 250px;
overflow-y: auto; overflow-y: auto;
} }
.vdl2-raw-toggle { .vdl2-modal-raw-toggle {
display: inline-block; display: inline-block;
margin-top: 4px; margin-top: 10px;
font-size: 8px; font-size: 10px;
color: var(--accent-cyan); color: var(--accent-cyan, #4a9eff);
cursor: pointer; cursor: pointer;
opacity: 0.7; opacity: 0.7;
transition: opacity 0.15s;
} }
.vdl2-raw-toggle:hover { .vdl2-modal-raw-toggle:hover {
opacity: 1; opacity: 1;
} }
.vdl2-raw-json { .vdl2-modal-raw-json {
display: none; display: none;
margin-top: 4px; margin-top: 8px;
padding: 4px 6px; padding: 10px 12px;
background: rgba(0, 0, 0, 0.3); 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-family: var(--font-mono);
font-size: 8px; font-size: 11px;
color: var(--text-dim); color: var(--text-dim);
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
max-height: 300px; max-height: 300px;
overflow-y: auto; overflow-y: auto;
line-height: 1.3; line-height: 1.4;
} }
/* Panels */ /* Panels */
+113 -60
View File
@@ -4378,66 +4378,131 @@ sudo make install</code>
return div.innerHTML; 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 avlc = data.avlc || {};
const src = avlc.src || {}; const src = avlc.src || {};
const dst = avlc.dst || {}; const dst = avlc.dst || {};
const acars = avlc.acars || {}; const acars = avlc.acars || {};
const xid = avlc.xid || {}; 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 // Build connection fields
if (data.station) rows.push(['Station', escapeHtml(String(data.station))]); let connectionHtml = '';
if (data.freq) rows.push(['Frequency', (data.freq / 1000000).toFixed(3) + ' MHz']); 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 = `<div class="vdl2-modal-section">
<div class="vdl2-modal-section-title">Connection</div>
<div class="vdl2-modal-grid">${connFields.map(([l, v]) =>
`<div class="vdl2-modal-field"><span class="vdl2-modal-field-label">${escapeHtml(l)}</span><span class="vdl2-modal-field-value">${escapeHtml(v)}</span></div>`
).join('')}</div>
</div>`;
}
// AVLC addressing // Build ACARS fields
if (src.addr) rows.push(['Source', escapeHtml(String(src.addr)) + (src.type ? ` (${escapeHtml(src.type)})` : '')]); let acarsHtml = '';
if (dst.addr) rows.push(['Destination', escapeHtml(String(dst.addr)) + (dst.type ? ` (${escapeHtml(dst.type)})` : '')]); const acarsFields = [];
if (avlc.cr) rows.push(['Command/Response', escapeHtml(avlc.cr)]); if (acars.reg) acarsFields.push(['Registration', acars.reg]);
if (avlc.frame_type) rows.push(['Frame Type', escapeHtml(avlc.frame_type)]); 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 = `<div class="vdl2-modal-section">
<div class="vdl2-modal-section-title">ACARS</div>
<div class="vdl2-modal-grid">${acarsFields.map(([l, v]) =>
`<div class="vdl2-modal-field"><span class="vdl2-modal-field-label">${escapeHtml(l)}</span><span class="vdl2-modal-field-value">${escapeHtml(v)}</span></div>`
).join('')}</div>
</div>`;
}
// ACARS fields // Position from XID
if (acars.reg) rows.push(['Registration', escapeHtml(acars.reg)]); let posHtml = '';
if (acars.flight) rows.push(['Flight', escapeHtml(acars.flight)]); if (xid.vdl_params?.ac_location) {
if (acars.mode) rows.push(['Mode', escapeHtml(acars.mode)]); const loc = xid.vdl_params.ac_location;
if (acars.label) rows.push(['Label', escapeHtml(acars.label)]); if (loc.lat !== undefined && loc.lon !== undefined) {
if (acars.blk_id) rows.push(['Block ID', escapeHtml(acars.blk_id)]); posHtml = `<div class="vdl2-modal-section">
if (acars.ack) rows.push(['ACK', escapeHtml(acars.ack)]); <div class="vdl2-modal-section-title">Position</div>
if (acars.msg_num) rows.push(['Msg Number', escapeHtml(acars.msg_num)]); <div class="vdl2-modal-grid">
if (acars.msg_num_seq) rows.push(['Msg Seq', escapeHtml(acars.msg_num_seq)]); <div class="vdl2-modal-field"><span class="vdl2-modal-field-label">Latitude</span><span class="vdl2-modal-field-value">${loc.lat.toFixed(4)}</span></div>
<div class="vdl2-modal-field"><span class="vdl2-modal-field-label">Longitude</span><span class="vdl2-modal-field-value">${loc.lon.toFixed(4)}</span></div>
// XID fields </div>
if (xid.vdl_params) { </div>`;
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 += `<div class="vdl2-detail-row"><span class="vdl2-detail-label">${label}</span><span class="vdl2-detail-value">${value}</span></div>`;
});
// Message body // Message body
const msgText = acars.msg_text || ''; const msgText = acars.msg_text || '';
let bodyHtml = '';
if (msgText) { if (msgText) {
html += `<div class="vdl2-msg-body">${escapeHtml(msgText)}</div>`; bodyHtml = `<div class="vdl2-modal-section">
<div class="vdl2-modal-section-title">Message</div>
<div class="vdl2-modal-msg-body">${escapeHtml(msgText)}</div>
</div>`;
} }
// Raw JSON toggle // Raw JSON
const rawJson = JSON.stringify(data, null, 2); const rawJson = JSON.stringify(data, null, 2);
const rawId = 'vdl2raw_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5); const rawId = 'vdl2modal_raw_' + Date.now();
html += `<span class="vdl2-raw-toggle" onclick="event.stopPropagation(); var el=document.getElementById('${rawId}'); el.style.display=el.style.display==='block'?'none':'block'; this.textContent=el.style.display==='block'?'▾ Hide raw JSON':'▸ Show raw JSON';">▸ Show raw JSON</span>`; const rawHtml = `<div class="vdl2-modal-section">
html += `<div class="vdl2-raw-json" id="${rawId}">${escapeHtml(rawJson)}</div>`; <span class="vdl2-modal-raw-toggle" id="${rawId}_toggle">&#9656; Show raw JSON</span>
<div class="vdl2-modal-raw-json" id="${rawId}">${escapeHtml(rawJson)}</div>
</div>`;
return html; const overlay = document.createElement('div');
overlay.className = 'vdl2-modal-overlay';
overlay.innerHTML = `
<div class="vdl2-modal">
<div class="vdl2-modal-header">
<div>
<div class="vdl2-modal-title">${escapeHtml(title)}</div>
<div class="vdl2-modal-time">${escapeHtml(timeStr)}${freq ? ' · ' + escapeHtml(freq) : ''}</div>
</div>
<button class="vdl2-modal-close" title="Close">&times;</button>
</div>
<div class="vdl2-modal-body">
${connectionHtml}
${acarsHtml}
${posHtml}
${bodyHtml}
${rawHtml}
</div>
</div>
`;
// 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 ? '&#9662; Hide raw JSON' : '&#9656; 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) { function addVdl2Message(data) {
@@ -4457,30 +4522,18 @@ sudo make install</code>
const time = new Date().toLocaleTimeString(); const time = new Date().toLocaleTimeString();
const freq = data.freq ? (data.freq / 1000000).toFixed(3) : ''; const freq = data.freq ? (data.freq / 1000000).toFixed(3) : '';
const label = flight || src || 'VDL2'; const label = flight || src || 'VDL2';
const hasContent = msgText || acars.label || avlc.frame_type;
msg.innerHTML = ` msg.innerHTML = `
<div class="vdl2-msg-summary"> <div style="display: flex; justify-content: space-between; margin-bottom: 2px;">
<div style="flex: 1; min-width: 0;"> <span style="color: var(--accent-cyan); font-weight: bold;">${escapeHtml(label)}</span>
<div style="display: flex; justify-content: space-between; margin-bottom: 2px;"> <span style="color: var(--text-muted);">${time}</span>
<span style="color: var(--accent-cyan); font-weight: bold;">${escapeHtml(label)}</span>
<span style="color: var(--text-muted);">${time}</span>
</div>
<div style="color: var(--text-dim); font-size: 9px;">
${freq ? freq + ' MHz' : ''}${freq && acars.label ? ' · ' : ''}${acars.label ? 'Label: ' + escapeHtml(acars.label) : ''}${msgText ? ' · ' + escapeHtml(msgText.substring(0, 40)) + (msgText.length > 40 ? '…' : '') : ''}
</div>
</div>
<span class="vdl2-expand-icon">&#9654;</span>
</div> </div>
<div class="vdl2-msg-details"> <div style="color: var(--text-dim); font-size: 9px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
${buildVdl2Details(data)} ${freq ? freq + ' MHz' : ''}${freq && acars.label ? ' · ' : ''}${acars.label ? 'Label: ' + escapeHtml(acars.label) : ''}${msgText ? ' · ' + escapeHtml(msgText.substring(0, 50)) + (msgText.length > 50 ? '…' : '') : ''}
</div> </div>
`; `;
msg.addEventListener('click', function(e) { msg.addEventListener('click', () => showVdl2Modal(data, time));
if (e.target.closest('.vdl2-raw-toggle') || e.target.closest('.vdl2-raw-json')) return;
this.classList.toggle('expanded');
});
container.insertBefore(msg, container.firstChild); container.insertBefore(msg, container.firstChild);