diff --git a/routes/acars.py b/routes/acars.py
index db05df0..8976781 100644
--- a/routes/acars.py
+++ b/routes/acars.py
@@ -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."""
diff --git a/routes/vdl2.py b/routes/vdl2.py
index ca2200a..637571d 100644
--- a/routes/vdl2.py
+++ b/routes/vdl2.py
@@ -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."""
diff --git a/templates/adsb_dashboard.html b/templates/adsb_dashboard.html
index 1d91ef4..61623c8 100644
--- a/templates/adsb_dashboard.html
+++ b/templates/adsb_dashboard.html
@@ -171,9 +171,14 @@
@@ -221,9 +226,14 @@
-
+
+
+
+
@@ -2786,6 +2796,31 @@ sudo make install
}
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
`;
// Fetch aircraft photo if registration is available
@@ -2945,18 +2983,23 @@ sudo make install
const container = document.getElementById('aircraftAcarsMessages');
if (!container) return;
const msgs = (data.acars || []).concat(data.vdl2 || []);
+ let newHtml;
if (msgs.length === 0) {
- container.innerHTML = '
No messages yet';
- return;
+ newHtml = '
No messages yet';
+ } 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 = '
—';
- });
+ .catch(() => { /* keep existing content on error */ });
}
doFetch();
@@ -3532,6 +3575,17 @@ sudo make install
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
}
}
+ function clearAcarsMessages() {
+ fetch('/acars/clear', { method: 'POST' }).catch(() => {});
+ const container = document.getElementById('acarsMessages');
+ container.innerHTML = '
No ACARS messages
Start ACARS to receive aircraft datalink messages
';
+ acarsMessageCount = 0;
+ document.getElementById('acarsCount').textContent = '0';
+ }
+
+ function clearVdl2Messages() {
+ fetch('/vdl2/clear', { method: 'POST' }).catch(() => {});
+ const container = document.getElementById('vdl2Messages');
+ container.innerHTML = '
No VDL2 messages
Start VDL2 to receive digital datalink messages
';
+ vdl2MessageCount = 0;
+ document.getElementById('vdl2Count').textContent = '0';
+ }
+
+ function clearAircraftMessages() {
+ const container = document.getElementById('aircraftAcarsMessages');
+ if (container) {
+ container.innerHTML = '
No messages yet';
+ }
+ if (acarsMessageTimer) {
+ clearInterval(acarsMessageTimer);
+ acarsMessageTimer = null;
+ }
+ }
+
// Populate ACARS device selector
document.addEventListener('DOMContentLoaded', () => {
fetch('/devices')
@@ -4213,13 +4294,14 @@ sudo make install
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
(matchedIcao ? ' cursor: pointer;' : '');
const linkIcon = matchedIcao ? '
✈' : '';
+ const acarsLabelDesc = acars.label_description || '';
+ const acarsMsgType = acars.message_type || 'other';
+ const vdl2TypeBadge = typeof getAcarsTypeBadge === 'function' && acars.label ? getAcarsTypeBadge(acarsMsgType) : '';
+
msg.innerHTML = `
${escapeHtml(label)}${linkIcon}
${time}
+ ${acarsLabelDesc ? `
${vdl2TypeBadge} ${escapeHtml(acarsLabelDesc)}
` : ''}
${freq ? freq + ' MHz' : ''}${freq && acars.label ? ' · ' : ''}${acars.label ? 'Label: ' + escapeHtml(acars.label) : ''}${msgText ? ' · ' + escapeHtml(msgText.substring(0, 50)) + (msgText.length > 50 ? '…' : '') : ''}
@@ -4430,6 +4517,16 @@ sudo make install
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(() => {});
diff --git a/utils/flight_correlator.py b/utils/flight_correlator.py
index a95b4f4..4e37b84 100644
--- a/utils/flight_correlator.py
+++ b/utils/flight_correlator.py
@@ -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)