fix: address PR #145 review issues

- Escape ac.icao, callsign, typeCode with escapeHtml() in aircraft card (XSS)
- Add linking comments between duplicated IATA_TO_ICAO mappings
- VDL2 sidebar: single-click selects aircraft, double-click opens modal
- Remove stale ICAOs from acarsAircraftIcaos in cleanupOldAircraft()
- Add null guard to drawPolarPlot() in weather-satellite.js
- Move deferred imports (translate_message, get_flight_correlator) to module level
- Check all frequency checkboxes by default on initial load
- Remove extra blank lines and uncertain MC/MCO airline code entry
- Add TODO comments linking duplicated renderAcarsCard implementations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-03-01 20:42:14 +00:00
parent b5c3d71247
commit a154601e86
7 changed files with 51 additions and 55 deletions

View File

@@ -15,21 +15,23 @@ import time
from datetime import datetime from datetime import datetime
from typing import Any, Generator from typing import Any, Generator
from flask import Blueprint, jsonify, request, Response from flask import Blueprint, Response, jsonify, request
import app as app_module import app as app_module
from utils.logging import sensor_logger as logger from utils.acars_translator import translate_message
from utils.validation import validate_device_index, validate_gain, validate_ppm
from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout
from utils.event_pipeline import process_event
from utils.constants import ( from utils.constants import (
PROCESS_START_WAIT,
PROCESS_TERMINATE_TIMEOUT, PROCESS_TERMINATE_TIMEOUT,
SSE_KEEPALIVE_INTERVAL, SSE_KEEPALIVE_INTERVAL,
SSE_QUEUE_TIMEOUT, SSE_QUEUE_TIMEOUT,
PROCESS_START_WAIT,
) )
from utils.event_pipeline import process_event
from utils.flight_correlator import get_flight_correlator
from utils.logging import sensor_logger as logger
from utils.process import register_process, unregister_process from utils.process import register_process, unregister_process
from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout
from utils.validation import validate_device_index, validate_gain, validate_ppm
acars_bp = Blueprint('acars', __name__, url_prefix='/acars') acars_bp = Blueprint('acars', __name__, url_prefix='/acars')
@@ -126,7 +128,6 @@ def stream_acars_output(process: subprocess.Popen, is_text_mode: bool = False) -
# Enrich with translated label and parsed fields # Enrich with translated label and parsed fields
try: try:
from utils.acars_translator import translate_message
translation = translate_message(data) translation = translate_message(data)
data['label_description'] = translation['label_description'] data['label_description'] = translation['label_description']
data['message_type'] = translation['message_type'] data['message_type'] = translation['message_type']
@@ -142,7 +143,6 @@ def stream_acars_output(process: subprocess.Popen, is_text_mode: bool = False) -
# Feed flight correlator # Feed flight correlator
try: try:
from utils.flight_correlator import get_flight_correlator
get_flight_correlator().add_acars_message(data) get_flight_correlator().add_acars_message(data)
except Exception: except Exception:
pass pass
@@ -452,11 +452,9 @@ def stream_acars() -> Response:
return response return response
@acars_bp.route('/messages') @acars_bp.route('/messages')
def get_acars_messages() -> Response: def get_acars_messages() -> Response:
"""Get recent ACARS messages from correlator (for history reload).""" """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 = request.args.get('limit', 50, type=int)
limit = max(1, min(limit, 200)) limit = max(1, min(limit, 200))
msgs = get_flight_correlator().get_recent_messages('acars', limit) msgs = get_flight_correlator().get_recent_messages('acars', limit)
@@ -467,7 +465,6 @@ def get_acars_messages() -> Response:
def clear_acars_messages() -> Response: def clear_acars_messages() -> Response:
"""Clear stored ACARS messages and reset counter.""" """Clear stored ACARS messages and reset counter."""
global acars_message_count, acars_last_message_time global acars_message_count, acars_last_message_time
from utils.flight_correlator import get_flight_correlator
get_flight_correlator().clear_acars() get_flight_correlator().clear_acars()
acars_message_count = 0 acars_message_count = 0
acars_last_message_time = None acars_last_message_time = None

View File

@@ -13,8 +13,7 @@ import time
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Generator from typing import Any, Generator
from flask import Blueprint, jsonify, request, Response, render_template from flask import Blueprint, Response, jsonify, make_response, render_template, request
from flask import make_response
# psycopg2 is optional - only needed for PostgreSQL history persistence # psycopg2 is optional - only needed for PostgreSQL history persistence
try: try:
@@ -28,39 +27,38 @@ except ImportError:
import app as app_module import app as app_module
from config import ( from config import (
ADSB_AUTO_START,
ADSB_DB_HOST, ADSB_DB_HOST,
ADSB_DB_NAME, ADSB_DB_NAME,
ADSB_DB_PASSWORD, ADSB_DB_PASSWORD,
ADSB_DB_PORT, ADSB_DB_PORT,
ADSB_DB_USER, ADSB_DB_USER,
ADSB_AUTO_START,
ADSB_HISTORY_ENABLED, ADSB_HISTORY_ENABLED,
SHARED_OBSERVER_LOCATION_ENABLED, SHARED_OBSERVER_LOCATION_ENABLED,
) )
from utils.logging import adsb_logger as logger from utils import aircraft_db
from utils.process import write_dump1090_pid, clear_dump1090_pid, cleanup_stale_dump1090 from utils.acars_translator import translate_message
from utils.validation import ( from utils.adsb_history import _ensure_adsb_schema, adsb_history_writer, adsb_snapshot_writer
validate_device_index, validate_gain,
validate_rtl_tcp_host, validate_rtl_tcp_port
)
from utils.sse import format_sse
from utils.event_pipeline import process_event
from utils.sdr import SDRFactory, SDRType
from utils.constants import ( from utils.constants import (
ADSB_SBS_PORT, ADSB_SBS_PORT,
ADSB_TERMINATE_TIMEOUT, ADSB_TERMINATE_TIMEOUT,
PROCESS_TERMINATE_TIMEOUT,
SBS_SOCKET_TIMEOUT,
SBS_RECONNECT_DELAY,
SOCKET_BUFFER_SIZE,
SSE_KEEPALIVE_INTERVAL,
SSE_QUEUE_TIMEOUT,
SOCKET_CONNECT_TIMEOUT,
ADSB_UPDATE_INTERVAL, ADSB_UPDATE_INTERVAL,
DUMP1090_START_WAIT, DUMP1090_START_WAIT,
PROCESS_TERMINATE_TIMEOUT,
SBS_RECONNECT_DELAY,
SBS_SOCKET_TIMEOUT,
SOCKET_BUFFER_SIZE,
SOCKET_CONNECT_TIMEOUT,
SSE_KEEPALIVE_INTERVAL,
SSE_QUEUE_TIMEOUT,
) )
from utils import aircraft_db from utils.event_pipeline import process_event
from utils.adsb_history import adsb_history_writer, adsb_snapshot_writer, _ensure_adsb_schema from utils.flight_correlator import get_flight_correlator
from utils.logging import adsb_logger as logger
from utils.process import cleanup_stale_dump1090, clear_dump1090_pid, write_dump1090_pid
from utils.sdr import SDRFactory, SDRType
from utils.sse import format_sse
from utils.validation import validate_device_index, validate_gain, validate_rtl_tcp_host, validate_rtl_tcp_port
adsb_bp = Blueprint('adsb', __name__, url_prefix='/adsb') adsb_bp = Blueprint('adsb', __name__, url_prefix='/adsb')
@@ -1247,14 +1245,12 @@ def get_aircraft_messages(icao: str):
callsign = aircraft.get('callsign') if aircraft else None callsign = aircraft.get('callsign') if aircraft else None
registration = aircraft.get('registration') if aircraft else None registration = aircraft.get('registration') if aircraft else None
from utils.flight_correlator import get_flight_correlator
messages = get_flight_correlator().get_messages_for_aircraft( messages = get_flight_correlator().get_messages_for_aircraft(
icao=icao.upper(), callsign=callsign, registration=registration icao=icao.upper(), callsign=callsign, registration=registration
) )
# Backfill translation on messages missing label_description # Backfill translation on messages missing label_description
try: try:
from utils.acars_translator import translate_message
for msg in messages.get('acars', []): for msg in messages.get('acars', []):
if not msg.get('label_description'): if not msg.get('label_description'):
translation = translate_message(msg) translation = translate_message(msg)

View File

@@ -15,21 +15,23 @@ import time
from datetime import datetime from datetime import datetime
from typing import Any, Generator from typing import Any, Generator
from flask import Blueprint, jsonify, request, Response from flask import Blueprint, Response, jsonify, request
import app as app_module import app as app_module
from utils.logging import sensor_logger as logger from utils.acars_translator import translate_message
from utils.validation import validate_device_index, validate_gain, validate_ppm
from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout
from utils.event_pipeline import process_event
from utils.constants import ( from utils.constants import (
PROCESS_START_WAIT,
PROCESS_TERMINATE_TIMEOUT, PROCESS_TERMINATE_TIMEOUT,
SSE_KEEPALIVE_INTERVAL, SSE_KEEPALIVE_INTERVAL,
SSE_QUEUE_TIMEOUT, SSE_QUEUE_TIMEOUT,
PROCESS_START_WAIT,
) )
from utils.event_pipeline import process_event
from utils.flight_correlator import get_flight_correlator
from utils.logging import sensor_logger as logger
from utils.process import register_process, unregister_process from utils.process import register_process, unregister_process
from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout
from utils.validation import validate_device_index, validate_gain, validate_ppm
vdl2_bp = Blueprint('vdl2', __name__, url_prefix='/vdl2') vdl2_bp = Blueprint('vdl2', __name__, url_prefix='/vdl2')
@@ -85,7 +87,6 @@ def stream_vdl2_output(process: subprocess.Popen, is_text_mode: bool = False) ->
vdl2_inner = data.get('vdl2', data) vdl2_inner = data.get('vdl2', data)
acars_payload = (vdl2_inner.get('avlc') or {}).get('acars') acars_payload = (vdl2_inner.get('avlc') or {}).get('acars')
if acars_payload and acars_payload.get('label'): if acars_payload and acars_payload.get('label'):
from utils.acars_translator import translate_message
translation = translate_message({ translation = translate_message({
'label': acars_payload.get('label'), 'label': acars_payload.get('label'),
'text': acars_payload.get('msg_text', ''), 'text': acars_payload.get('msg_text', ''),
@@ -104,7 +105,6 @@ def stream_vdl2_output(process: subprocess.Popen, is_text_mode: bool = False) ->
# Feed flight correlator # Feed flight correlator
try: try:
from utils.flight_correlator import get_flight_correlator
get_flight_correlator().add_vdl2_message(data) get_flight_correlator().add_vdl2_message(data)
except Exception: except Exception:
pass pass
@@ -396,7 +396,6 @@ def stream_vdl2() -> Response:
@vdl2_bp.route('/messages') @vdl2_bp.route('/messages')
def get_vdl2_messages() -> Response: def get_vdl2_messages() -> Response:
"""Get recent VDL2 messages from correlator (for history reload).""" """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 = request.args.get('limit', 50, type=int)
limit = max(1, min(limit, 200)) limit = max(1, min(limit, 200))
msgs = get_flight_correlator().get_recent_messages('vdl2', limit) msgs = get_flight_correlator().get_recent_messages('vdl2', limit)
@@ -407,7 +406,6 @@ def get_vdl2_messages() -> Response:
def clear_vdl2_messages() -> Response: def clear_vdl2_messages() -> Response:
"""Clear stored VDL2 messages and reset counter.""" """Clear stored VDL2 messages and reset counter."""
global vdl2_message_count, vdl2_last_message_time global vdl2_message_count, vdl2_last_message_time
from utils.flight_correlator import get_flight_correlator
get_flight_correlator().clear_vdl2() get_flight_correlator().clear_vdl2()
vdl2_message_count = 0 vdl2_message_count = 0
vdl2_last_message_time = None vdl2_last_message_time = None

View File

@@ -727,6 +727,7 @@ const WeatherSat = (function() {
* Draw polar plot for a pass trajectory * Draw polar plot for a pass trajectory
*/ */
function drawPolarPlot(pass) { function drawPolarPlot(pass) {
if (!pass) return;
const canvas = document.getElementById('wxsatPolarCanvas'); const canvas = document.getElementById('wxsatPolarCanvas');
if (!canvas) return; if (!canvas) return;

View File

@@ -2864,8 +2864,8 @@ sudo make install</code>
return ` return `
<div class="aircraft-header"> <div class="aircraft-header">
<span class="aircraft-callsign">${callsign}${badge}${acarsIndicator}${agentBadge}</span> <span class="aircraft-callsign">${escapeHtml(callsign)}${badge}${acarsIndicator}${agentBadge}</span>
<span class="aircraft-icao">${typeCode ? typeCode + ' • ' : ''}${ac.icao}</span> <span class="aircraft-icao">${typeCode ? escapeHtml(typeCode) + ' • ' : ''}${escapeHtml(ac.icao)}</span>
</div> </div>
<div class="aircraft-details"> <div class="aircraft-details">
<div class="aircraft-detail"> <div class="aircraft-detail">
@@ -3242,6 +3242,7 @@ sudo make install</code>
return '<span style="display:inline-block;padding:1px 5px;border-radius:3px;font-size:8px;font-weight:700;color:#000;background:' + color + ';">' + lbl + '</span>'; return '<span style="display:inline-block;padding:1px 5px;border-radius:3px;font-size:8px;font-weight:700;color:#000;background:' + color + ';">' + lbl + '</span>';
} }
// TODO: Similar to renderAcarsMainCard in partials/modes/acars.html — consider unifying
function renderAcarsCard(msg) { function renderAcarsCard(msg) {
const type = msg.message_type || 'other'; const type = msg.message_type || 'other';
const badge = getAcarsTypeBadge(type); const badge = getAcarsTypeBadge(type);
@@ -3346,6 +3347,7 @@ sudo make install</code>
cleanupTrail(icao); cleanupTrail(icao);
delete aircraft[icao]; delete aircraft[icao];
delete alertedAircraft[icao]; delete alertedAircraft[icao];
if (typeof acarsAircraftIcaos !== 'undefined') acarsAircraftIcaos.delete(icao);
needsUpdate = true; needsUpdate = true;
if (selectedIcao === icao) { if (selectedIcao === icao) {
@@ -3811,8 +3813,8 @@ sudo make install</code>
}); });
container.innerHTML = freqs.map((freq, i) => { container.innerHTML = freqs.map((freq, i) => {
// On initial load, only check the first (primary) frequency; otherwise preserve state // On initial load, check all frequencies; otherwise preserve state
const checked = previouslyChecked.size === 0 ? (i === 0 ? 'checked' : '') : (previouslyChecked.has(freq) ? 'checked' : ''); const checked = previouslyChecked.size === 0 || previouslyChecked.has(freq) ? 'checked' : '';
return ` return `
<label style="display: flex; align-items: center; gap: 3px; padding: 2px 6px; background: var(--bg-secondary); border-radius: 3px; cursor: pointer;"> <label style="display: flex; align-items: center; gap: 3px; padding: 2px 6px; background: var(--bg-secondary); border-radius: 3px; cursor: pointer;">
<input type="checkbox" class="acars-freq-cb" value="${freq}" ${checked} style="margin: 0; cursor: pointer;"> <input type="checkbox" class="acars-freq-cb" value="${freq}" ${checked} style="margin: 0; cursor: pointer;">
@@ -4040,6 +4042,7 @@ sudo make install</code>
const acarsAircraftIcaos = new Set(); const acarsAircraftIcaos = new Set();
// IATA (2-letter) → ICAO (3-letter) airline code mapping // IATA (2-letter) → ICAO (3-letter) airline code mapping
// NOTE: Duplicated from utils/airline_codes.py — keep both in sync
const IATA_TO_ICAO = { const IATA_TO_ICAO = {
'AA':'AAL','DL':'DAL','UA':'UAL','WN':'SWA','B6':'JBU','AS':'ASA', 'AA':'AAL','DL':'DAL','UA':'UAL','WN':'SWA','B6':'JBU','AS':'ASA',
'NK':'NKS','F9':'FFT','G4':'AAY','HA':'HAL','SY':'SCX','WS':'WJA', 'NK':'NKS','F9':'FFT','G4':'AAY','HA':'HAL','SY':'SCX','WS':'WJA',
@@ -4696,8 +4699,9 @@ sudo make install</code>
`; `;
if (matchedIcao) { if (matchedIcao) {
msg.addEventListener('click', (e) => { e.stopPropagation(); selectAircraft(matchedIcao); }); msg.addEventListener('click', () => selectAircraft(matchedIcao));
msg.title = 'Click to locate ' + label + ' on map'; msg.addEventListener('dblclick', () => showVdl2Modal(data, time));
msg.title = 'Click to locate on map, double-click for details';
} else { } else {
msg.addEventListener('click', () => showVdl2Modal(data, time)); msg.addEventListener('click', () => showVdl2Modal(data, time));
} }

View File

@@ -111,7 +111,7 @@
container.querySelectorAll('input:checked').forEach(cb => previouslyChecked.add(cb.value)); container.querySelectorAll('input:checked').forEach(cb => previouslyChecked.add(cb.value));
container.innerHTML = freqs.map((freq, i) => { container.innerHTML = freqs.map((freq, i) => {
const checked = previouslyChecked.size === 0 ? (i === 0 ? 'checked' : '') : (previouslyChecked.has(freq) ? 'checked' : ''); const checked = previouslyChecked.size === 0 || previouslyChecked.has(freq) ? 'checked' : '';
return ` return `
<label style="display: flex; align-items: center; gap: 3px; padding: 2px 6px; background: var(--bg-secondary); border-radius: 3px; cursor: pointer;"> <label style="display: flex; align-items: center; gap: 3px; padding: 2px 6px; background: var(--bg-secondary); border-radius: 3px; cursor: pointer;">
<input type="checkbox" class="acars-main-freq-cb" value="${freq}" ${checked} style="margin: 0; cursor: pointer;"> <input type="checkbox" class="acars-main-freq-cb" value="${freq}" ${checked} style="margin: 0; cursor: pointer;">
@@ -188,6 +188,7 @@
return `<span style="display:inline-block;padding:1px 5px;border-radius:3px;font-size:8px;font-weight:700;color:#000;background:${color};">${lbl}</span>`; return `<span style="display:inline-block;padding:1px 5px;border-radius:3px;font-size:8px;font-weight:700;color:#000;background:${color};">${lbl}</span>`;
} }
// TODO: Similar to renderAcarsCard in templates/adsb_dashboard.html — consider unifying
function renderAcarsMainCard(data) { function renderAcarsMainCard(data) {
const flight = escapeHtml(data.flight || 'UNKNOWN'); const flight = escapeHtml(data.flight || 'UNKNOWN');
const type = data.message_type || 'other'; const type = data.message_type || 'other';

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import re import re
# IATA (2-letter) → ICAO (3-letter) mapping for common airlines # IATA (2-letter) → ICAO (3-letter) mapping for common airlines
# NOTE: Duplicated in templates/adsb_dashboard.html (JS IATA_TO_ICAO) — keep both in sync
IATA_TO_ICAO: dict[str, str] = { IATA_TO_ICAO: dict[str, str] = {
# North America — Major # North America — Major
"AA": "AAL", # American Airlines "AA": "AAL", # American Airlines
@@ -112,8 +113,6 @@ IATA_TO_ICAO: dict[str, str] = {
"AV": "AVA", # Avianca "AV": "AVA", # Avianca
"CM": "CMP", # Copa Airlines "CM": "CMP", # Copa Airlines
"AR": "ARG", # Aerolíneas Argentinas "AR": "ARG", # Aerolíneas Argentinas
# ACARS-specific addressing codes
"MC": "MCO", # Possible: some ACARS systems use MC
} }
# Build reverse mapping (ICAO → IATA) # Build reverse mapping (ICAO → IATA)