mirror of
https://github.com/smittix/intercept.git
synced 2026-06-08 14:11:54 -07:00
feat(adsb): improve ACARS/VDL2 panels with history, clear, smooth updates, and translation
- Persist ACARS/VDL2 messages across page refresh via new /acars/messages and /vdl2/messages endpoints backed by FlightCorrelator - Add clear buttons to ACARS/VDL2 sidebars and right-panel datalink section with /acars/clear and /vdl2/clear endpoints - Fix right-panel DATALINK MESSAGES flickering by diffing innerHTML before updating, with opacity transition for smooth refreshes - Add aircraft deselect toggle (click selected aircraft again to deselect) - Enrich VDL2 messages with ACARS label translation (label_description, message_type, parsed fields) matching existing ACARS translator Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -443,6 +443,26 @@ def stream_acars() -> Response:
|
||||
return response
|
||||
|
||||
|
||||
@acars_bp.route('/messages')
|
||||
def get_acars_messages() -> Response:
|
||||
"""Get recent ACARS messages from correlator (for history reload)."""
|
||||
from utils.flight_correlator import get_flight_correlator
|
||||
limit = request.args.get('limit', 50, type=int)
|
||||
limit = max(1, min(limit, 200))
|
||||
msgs = get_flight_correlator().get_recent_messages('acars', limit)
|
||||
return jsonify(msgs)
|
||||
|
||||
|
||||
@acars_bp.route('/clear', methods=['POST'])
|
||||
def clear_acars_messages() -> Response:
|
||||
"""Clear stored ACARS messages and reset counter."""
|
||||
global acars_message_count
|
||||
from utils.flight_correlator import get_flight_correlator
|
||||
get_flight_correlator().clear_acars()
|
||||
acars_message_count = 0
|
||||
return jsonify({'status': 'cleared'})
|
||||
|
||||
|
||||
@acars_bp.route('/frequencies')
|
||||
def get_frequencies() -> Response:
|
||||
"""Get default ACARS frequencies."""
|
||||
|
||||
@@ -79,6 +79,22 @@ def stream_vdl2_output(process: subprocess.Popen, is_text_mode: bool = False) ->
|
||||
data['type'] = 'vdl2'
|
||||
data['timestamp'] = datetime.utcnow().isoformat() + 'Z'
|
||||
|
||||
# Enrich embedded ACARS payload with translated label
|
||||
try:
|
||||
vdl2_inner = data.get('vdl2', data)
|
||||
acars_payload = (vdl2_inner.get('avlc') or {}).get('acars')
|
||||
if acars_payload and acars_payload.get('label'):
|
||||
from utils.acars_translator import translate_message
|
||||
translation = translate_message({
|
||||
'label': acars_payload.get('label'),
|
||||
'text': acars_payload.get('msg_text', ''),
|
||||
})
|
||||
acars_payload['label_description'] = translation['label_description']
|
||||
acars_payload['message_type'] = translation['message_type']
|
||||
acars_payload['parsed'] = translation['parsed']
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Update stats
|
||||
vdl2_message_count += 1
|
||||
vdl2_last_message_time = time.time()
|
||||
@@ -370,6 +386,26 @@ def stream_vdl2() -> Response:
|
||||
return response
|
||||
|
||||
|
||||
@vdl2_bp.route('/messages')
|
||||
def get_vdl2_messages() -> Response:
|
||||
"""Get recent VDL2 messages from correlator (for history reload)."""
|
||||
from utils.flight_correlator import get_flight_correlator
|
||||
limit = request.args.get('limit', 50, type=int)
|
||||
limit = max(1, min(limit, 200))
|
||||
msgs = get_flight_correlator().get_recent_messages('vdl2', limit)
|
||||
return jsonify(msgs)
|
||||
|
||||
|
||||
@vdl2_bp.route('/clear', methods=['POST'])
|
||||
def clear_vdl2_messages() -> Response:
|
||||
"""Clear stored VDL2 messages and reset counter."""
|
||||
global vdl2_message_count
|
||||
from utils.flight_correlator import get_flight_correlator
|
||||
get_flight_correlator().clear_vdl2()
|
||||
vdl2_message_count = 0
|
||||
return jsonify({'status': 'cleared'})
|
||||
|
||||
|
||||
@vdl2_bp.route('/frequencies')
|
||||
def get_frequencies() -> Response:
|
||||
"""Get default VDL2 frequencies."""
|
||||
|
||||
+115
-18
@@ -171,9 +171,14 @@
|
||||
<div id="acarsFreqSelector" style="display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px; font-size: 9px;">
|
||||
<!-- Frequency checkboxes populated by JS -->
|
||||
</div>
|
||||
<button class="acars-btn" id="acarsToggleBtn" onclick="toggleAcars()" style="width: 100%;">
|
||||
▶ START ACARS
|
||||
</button>
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<button class="acars-btn" id="acarsToggleBtn" onclick="toggleAcars()" style="flex: 1;">
|
||||
▶ START ACARS
|
||||
</button>
|
||||
<button class="acars-btn" id="acarsClearBtn" onclick="clearAcarsMessages()" title="Clear messages" style="padding: 4px 8px; font-size: 9px; opacity: 0.7;">
|
||||
CLEAR
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="acars-messages" id="acarsMessages">
|
||||
<div class="no-aircraft" style="padding: 20px; text-align: center;">
|
||||
@@ -221,9 +226,14 @@
|
||||
<div id="vdl2FreqSelector" style="display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px; font-size: 9px;">
|
||||
<!-- Frequency checkboxes populated by JS -->
|
||||
</div>
|
||||
<button class="vdl2-btn" id="vdl2ToggleBtn" onclick="toggleVdl2()" style="width: 100%;">
|
||||
▶ START VDL2
|
||||
</button>
|
||||
<div style="display: flex; gap: 5px;">
|
||||
<button class="vdl2-btn" id="vdl2ToggleBtn" onclick="toggleVdl2()" style="flex: 1;">
|
||||
▶ START VDL2
|
||||
</button>
|
||||
<button class="vdl2-btn" id="vdl2ClearBtn" onclick="clearVdl2Messages()" title="Clear messages" style="padding: 4px 8px; font-size: 9px; opacity: 0.7;">
|
||||
CLEAR
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vdl2-messages" id="vdl2Messages">
|
||||
<div class="no-aircraft" style="padding: 20px; text-align: center;">
|
||||
@@ -2786,6 +2796,31 @@ sudo make install</code>
|
||||
}
|
||||
|
||||
function selectAircraft(icao) {
|
||||
// Toggle: clicking the same aircraft deselects it
|
||||
if (selectedIcao === icao) {
|
||||
const prev = selectedIcao;
|
||||
selectedIcao = null;
|
||||
// Reset previous marker icon
|
||||
if (prev && markers[prev] && aircraft[prev]) {
|
||||
const ac = aircraft[prev];
|
||||
const militaryInfo = isMilitaryAircraft(prev, ac.callsign);
|
||||
const rotation = Math.round((ac.heading || 0) / 5) * 5;
|
||||
const color = militaryInfo.military ? '#556b2f' : getAltitudeColor(ac.altitude);
|
||||
const iconType = getAircraftIconType(ac.type_code, militaryInfo.military);
|
||||
markers[prev].setIcon(createMarkerIcon(rotation, color, iconType, false));
|
||||
if (markerState[prev]) markerState[prev].isSelected = false;
|
||||
}
|
||||
renderAircraftList();
|
||||
showAircraftDetails(null);
|
||||
updateFlightLookupBtn();
|
||||
highlightSidebarMessages(null);
|
||||
if (acarsMessageTimer) {
|
||||
clearInterval(acarsMessageTimer);
|
||||
acarsMessageTimer = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const prevSelected = selectedIcao;
|
||||
selectedIcao = icao;
|
||||
|
||||
@@ -2915,8 +2950,11 @@ sudo make install</code>
|
||||
</div>
|
||||
</div>
|
||||
<div id="aircraftAcarsSection" style="margin-top:12px;">
|
||||
<div style="font-size:10px;font-weight:600;color:var(--accent-cyan);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;border-top:1px solid var(--border-color);padding-top:8px;">Datalink Messages</div>
|
||||
<div id="aircraftAcarsMessages" style="font-size:10px;color:var(--text-dim);">Loading...</div>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;font-size:10px;font-weight:600;color:var(--accent-cyan);text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;border-top:1px solid var(--border-color);padding-top:8px;">
|
||||
<span>Datalink Messages</span>
|
||||
<button onclick="clearAircraftMessages()" title="Clear datalink messages" style="background:none;border:1px solid var(--border-color);color:var(--text-muted);cursor:pointer;font-size:9px;padding:1px 5px;border-radius:3px;font-family:var(--font-mono);line-height:1;">✕</button>
|
||||
</div>
|
||||
<div id="aircraftAcarsMessages" style="font-size:10px;color:var(--text-dim);transition:opacity 0.15s ease;">Loading...</div>
|
||||
</div>`;
|
||||
|
||||
// Fetch aircraft photo if registration is available
|
||||
@@ -2945,18 +2983,23 @@ sudo make install</code>
|
||||
const container = document.getElementById('aircraftAcarsMessages');
|
||||
if (!container) return;
|
||||
const msgs = (data.acars || []).concat(data.vdl2 || []);
|
||||
let newHtml;
|
||||
if (msgs.length === 0) {
|
||||
container.innerHTML = '<span style="color:var(--text-muted);font-style:italic;">No messages yet</span>';
|
||||
return;
|
||||
newHtml = '<span style="color:var(--text-muted);font-style:italic;">No messages yet</span>';
|
||||
} else {
|
||||
msgs.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
||||
newHtml = msgs.slice(0, 20).map(renderAcarsCard).join('');
|
||||
}
|
||||
// Only update DOM if content actually changed
|
||||
if (container.innerHTML !== newHtml) {
|
||||
container.style.opacity = '0.7';
|
||||
setTimeout(() => {
|
||||
container.innerHTML = newHtml;
|
||||
container.style.opacity = '1';
|
||||
}, 150);
|
||||
}
|
||||
// Sort newest first
|
||||
msgs.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
|
||||
container.innerHTML = msgs.slice(0, 20).map(renderAcarsCard).join('');
|
||||
})
|
||||
.catch(() => {
|
||||
const container = document.getElementById('aircraftAcarsMessages');
|
||||
if (container) container.innerHTML = '<span style="color:var(--text-muted);">—</span>';
|
||||
});
|
||||
.catch(() => { /* keep existing content on error */ });
|
||||
}
|
||||
|
||||
doFetch();
|
||||
@@ -3532,6 +3575,17 @@ sudo make install</code>
|
||||
document.getElementById('acarsToggleBtn').classList.add('active');
|
||||
document.getElementById('acarsPanelIndicator').classList.add('active');
|
||||
startAcarsStream(false);
|
||||
// Reload message history from backend
|
||||
fetch('/acars/messages?limit=50')
|
||||
.then(r => r.json())
|
||||
.then(msgs => {
|
||||
if (!msgs || !msgs.length) return;
|
||||
// Add oldest first so newest ends up on top
|
||||
for (let i = msgs.length - 1; i >= 0; i--) {
|
||||
addAcarsMessage(msgs[i]);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -3887,6 +3941,33 @@ sudo make install</code>
|
||||
}
|
||||
}
|
||||
|
||||
function clearAcarsMessages() {
|
||||
fetch('/acars/clear', { method: 'POST' }).catch(() => {});
|
||||
const container = document.getElementById('acarsMessages');
|
||||
container.innerHTML = '<div class="no-aircraft" style="padding: 20px; text-align: center;"><div style="font-size: 10px; color: var(--text-muted);">No ACARS messages</div><div style="font-size: 9px; color: var(--text-dim); margin-top: 5px;">Start ACARS to receive aircraft datalink messages</div></div>';
|
||||
acarsMessageCount = 0;
|
||||
document.getElementById('acarsCount').textContent = '0';
|
||||
}
|
||||
|
||||
function clearVdl2Messages() {
|
||||
fetch('/vdl2/clear', { method: 'POST' }).catch(() => {});
|
||||
const container = document.getElementById('vdl2Messages');
|
||||
container.innerHTML = '<div class="no-aircraft" style="padding: 20px; text-align: center;"><div style="font-size: 10px; color: var(--text-muted);">No VDL2 messages</div><div style="font-size: 9px; color: var(--text-dim); margin-top: 5px;">Start VDL2 to receive digital datalink messages</div></div>';
|
||||
vdl2MessageCount = 0;
|
||||
document.getElementById('vdl2Count').textContent = '0';
|
||||
}
|
||||
|
||||
function clearAircraftMessages() {
|
||||
const container = document.getElementById('aircraftAcarsMessages');
|
||||
if (container) {
|
||||
container.innerHTML = '<span style="color:var(--text-muted);font-style:italic;">No messages yet</span>';
|
||||
}
|
||||
if (acarsMessageTimer) {
|
||||
clearInterval(acarsMessageTimer);
|
||||
acarsMessageTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Populate ACARS device selector
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
fetch('/devices')
|
||||
@@ -4213,13 +4294,14 @@ sudo make install</code>
|
||||
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.label) acarsFields.push(['Label', acars.label + (acars.label_description ? ' — ' + acars.label_description : '')]);
|
||||
if (acars.sublabel) acarsFields.push(['Sublabel', acars.sublabel]);
|
||||
if (acars.blk_id) acarsFields.push(['Block ID', acars.blk_id]);
|
||||
if (acars.ack) acarsFields.push(['ACK', acars.ack === '!' ? 'NAK' : 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 (acars.more != null) acarsFields.push(['More Follows', acars.more ? 'Yes' : 'No']);
|
||||
if (acars.message_type && acars.message_type !== 'other') acarsFields.push(['Message Type', acars.message_type.replace(/_/g, ' ')]);
|
||||
const acarsHtml = buildSection('ACARS', acarsFields);
|
||||
|
||||
// XID fields
|
||||
@@ -4352,11 +4434,16 @@ sudo make install</code>
|
||||
(matchedIcao ? ' cursor: pointer;' : '');
|
||||
const linkIcon = matchedIcao ? '<span style="color:var(--accent-green);font-size:9px;margin-left:3px;" title="Tracked on map">✈</span>' : '';
|
||||
|
||||
const acarsLabelDesc = acars.label_description || '';
|
||||
const acarsMsgType = acars.message_type || 'other';
|
||||
const vdl2TypeBadge = typeof getAcarsTypeBadge === 'function' && acars.label ? getAcarsTypeBadge(acarsMsgType) : '';
|
||||
|
||||
msg.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; margin-bottom: 2px;">
|
||||
<span style="color: var(--accent-cyan); font-weight: bold;">${escapeHtml(label)}${linkIcon}</span>
|
||||
<span style="color: var(--text-muted);">${time}</span>
|
||||
</div>
|
||||
${acarsLabelDesc ? `<div style="margin-top: 2px;">${vdl2TypeBadge} <span style="color: var(--text-primary);">${escapeHtml(acarsLabelDesc)}</span></div>` : ''}
|
||||
<div style="color: var(--text-dim); font-size: 9px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
${freq ? freq + ' MHz' : ''}${freq && acars.label ? ' · ' : ''}${acars.label ? 'Label: ' + escapeHtml(acars.label) : ''}${msgText ? ' · ' + escapeHtml(msgText.substring(0, 50)) + (msgText.length > 50 ? '…' : '') : ''}
|
||||
</div>
|
||||
@@ -4430,6 +4517,16 @@ sudo make install</code>
|
||||
document.getElementById('vdl2ToggleBtn').classList.add('active');
|
||||
document.getElementById('vdl2PanelIndicator').classList.add('active');
|
||||
startVdl2Stream(false);
|
||||
// Reload message history from backend
|
||||
fetch('/vdl2/messages?limit=50')
|
||||
.then(r => r.json())
|
||||
.then(msgs => {
|
||||
if (!msgs || !msgs.length) return;
|
||||
for (let i = msgs.length - 1; i >= 0; i--) {
|
||||
addVdl2Message(msgs[i]);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
@@ -81,6 +81,21 @@ class FlightCorrelator:
|
||||
"""Return message without internal correlation fields."""
|
||||
return {k: v for k, v in msg.items() if not k.startswith('_corr_')}
|
||||
|
||||
def get_recent_messages(self, msg_type: str = 'acars', limit: int = 50) -> list[dict]:
|
||||
"""Return the most recent messages (newest first)."""
|
||||
source = self._acars_messages if msg_type == 'acars' else self._vdl2_messages
|
||||
msgs = [self._clean_msg(m) for m in source]
|
||||
msgs.reverse()
|
||||
return msgs[:limit]
|
||||
|
||||
def clear_acars(self) -> None:
|
||||
"""Clear all stored ACARS messages."""
|
||||
self._acars_messages.clear()
|
||||
|
||||
def clear_vdl2(self) -> None:
|
||||
"""Clear all stored VDL2 messages."""
|
||||
self._vdl2_messages.clear()
|
||||
|
||||
@property
|
||||
def acars_count(self) -> int:
|
||||
return len(self._acars_messages)
|
||||
|
||||
Reference in New Issue
Block a user