Files
intercept/templates/adsb_history.html
2026-02-04 01:10:42 +00:00

775 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ADS-B History // INTERCEPT</title>
{% if offline_settings.fonts_source == 'local' %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/fonts-local.css') }}">
{% else %}
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
{% endif %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/responsive.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/global-nav.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/adsb_history.css') }}">
</head>
<body>
<div class="radar-bg"></div>
<div class="scanline"></div>
<header class="header">
<div class="logo">
ADS-B HISTORY
<span>// INTERCEPT REPORTING</span>
</div>
<div class="status-bar">
<a href="/adsb/dashboard" class="back-link">Live Radar</a>
</div>
</header>
{% set active_mode = 'adsb' %}
{% include 'partials/nav.html' with context %}
<main class="history-shell">
<section class="summary-strip">
<div class="summary-card">
<div class="summary-label">Messages</div>
<div class="summary-value" id="summaryMessages">--</div>
</div>
<div class="summary-card">
<div class="summary-label">Snapshots</div>
<div class="summary-value" id="summarySnapshots">--</div>
</div>
<div class="summary-card">
<div class="summary-label">Aircraft</div>
<div class="summary-value" id="summaryAircraft">--</div>
</div>
<div class="summary-card">
<div class="summary-label">First Seen</div>
<div class="summary-value" id="summaryFirstSeen">--</div>
</div>
<div class="summary-card">
<div class="summary-label">Last Seen</div>
<div class="summary-value" id="summaryLastSeen">--</div>
</div>
</section>
<section class="session-strip">
<div class="session-status">
<div class="status-dot" id="sessionStatusDot"></div>
<div>
<div class="session-label">Tracking</div>
<div class="session-value" id="sessionStatusText">--</div>
</div>
</div>
<div class="session-metric">
<div class="session-label">Uptime</div>
<div class="session-value mono" id="sessionUptime">--:--:--</div>
</div>
<div class="session-metric">
<div class="session-label">Started</div>
<div class="session-value" id="sessionStartedAt">--</div>
</div>
<div class="session-metric">
<div class="session-label">Status</div>
<div class="session-value" id="sessionNotice">Ready</div>
</div>
<div class="session-controls">
<select id="sessionDeviceSelect"></select>
<button class="primary-btn" id="sessionToggleBtn" type="button" onclick="toggleSession()">Start Tracking</button>
</div>
</section>
<section class="controls">
<div class="control-group">
<label for="windowSelect">Window</label>
<select id="windowSelect">
<option value="15">15 minutes</option>
<option value="60" selected>1 hour</option>
<option value="360">6 hours</option>
<option value="1440">24 hours</option>
<option value="10080">7 days</option>
</select>
</div>
<div class="control-group">
<label for="searchInput">Search</label>
<input type="text" id="searchInput" placeholder="ICAO / callsign / registration">
</div>
<div class="control-group">
<label for="limitSelect">Limit</label>
<select id="limitSelect">
<option value="100">100</option>
<option value="200" selected>200</option>
<option value="500">500</option>
<option value="1000">1000</option>
</select>
</div>
<button class="primary-btn" id="refreshBtn">Refresh</button>
<div class="status-pill" id="historyStatus">
{% if history_enabled %}
HISTORY ONLINE
{% else %}
HISTORY DISABLED
{% endif %}
</div>
</section>
<section class="content-grid">
<div class="panel aircraft-panel">
<div class="panel-header">
<span>RECENT AIRCRAFT</span>
<span class="panel-meta" id="aircraftCount">0</span>
</div>
<div class="panel-body">
<table class="aircraft-table">
<thead>
<tr>
<th>ICAO</th>
<th>Callsign</th>
<th>Alt</th>
<th>Speed</th>
<th>Last Seen</th>
</tr>
</thead>
<tbody id="aircraftTableBody">
<tr class="empty-row">
<td colspan="5">No aircraft in this window</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="panel detail-panel">
<div class="panel-header">
<span>DETAIL TIMELINE</span>
<span class="panel-meta" id="detailIcao">--</span>
</div>
<div class="panel-body">
<div class="detail-card">
<div class="detail-title" id="detailTitle">Select an aircraft</div>
<div class="detail-meta" id="detailMeta">---</div>
</div>
<div class="chart-grid">
<div class="chart-card">
<div class="chart-title">Altitude (ft)</div>
<canvas id="altitudeChart"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">Speed (kt)</div>
<canvas id="speedChart"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">Heading (deg)</div>
<canvas id="headingChart"></canvas>
</div>
<div class="chart-card">
<div class="chart-title">Vertical Rate (fpm)</div>
<canvas id="verticalChart"></canvas>
</div>
</div>
<div class="timeline-list" id="timelineList">
<div class="empty-row">No timeline data</div>
</div>
<div class="squawk-list" id="squawkList">
<div class="empty-row">No squawk changes</div>
</div>
</div>
</div>
</section>
</main>
<div class="modal-backdrop" id="aircraftModalBackdrop" aria-hidden="true">
<div class="modal-card" role="dialog" aria-modal="true">
<button class="modal-close" id="aircraftModalClose" aria-label="Close">×</button>
<div class="modal-header">
<div>
<div class="modal-title" id="modalTitle">Aircraft</div>
<div class="modal-subtitle" id="modalSubtitle">--</div>
</div>
<div class="modal-actions">
<button class="nav-btn" id="modalPrev"></button>
<button class="nav-btn" id="modalNext"></button>
</div>
</div>
<div class="modal-body">
<div class="modal-photo">
<img id="modalPhoto" alt="Aircraft photo">
<div class="photo-fallback" id="modalPhotoFallback">No photo</div>
</div>
<div class="modal-details">
<div class="detail-row">
<span>ICAO</span>
<strong id="modalIcao">--</strong>
</div>
<div class="detail-row">
<span>Callsign</span>
<strong id="modalCallsign">--</strong>
</div>
<div class="detail-row">
<span>Registration</span>
<strong id="modalRegistration">--</strong>
</div>
<div class="detail-row">
<span>Type</span>
<strong id="modalType">--</strong>
</div>
<div class="detail-row">
<span>Altitude</span>
<strong id="modalAltitude">--</strong>
</div>
<div class="detail-row">
<span>Speed</span>
<strong id="modalSpeed">--</strong>
</div>
<div class="detail-row">
<span>Heading</span>
<strong id="modalHeading">--</strong>
</div>
<div class="detail-row">
<span>Vertical Rate</span>
<strong id="modalVerticalRate">--</strong>
</div>
<div class="detail-row">
<span>Squawk</span>
<strong id="modalSquawk">--</strong>
</div>
<div class="detail-row">
<span>Position</span>
<strong id="modalPosition">--</strong>
</div>
<div class="detail-row">
<span>Last Seen</span>
<strong id="modalLastSeen">--</strong>
</div>
</div>
</div>
</div>
</div>
<script>
// Bias-T helper (reads from main dashboard localStorage)
function getBiasTEnabled() {
return localStorage.getItem('biasTEnabled') === 'true';
}
const historyEnabled = {{ 'true' if history_enabled else 'false' }};
const summaryMessages = document.getElementById('summaryMessages');
const summarySnapshots = document.getElementById('summarySnapshots');
const summaryAircraft = document.getElementById('summaryAircraft');
const summaryFirstSeen = document.getElementById('summaryFirstSeen');
const summaryLastSeen = document.getElementById('summaryLastSeen');
const aircraftTableBody = document.getElementById('aircraftTableBody');
const aircraftCount = document.getElementById('aircraftCount');
const detailIcao = document.getElementById('detailIcao');
const detailTitle = document.getElementById('detailTitle');
const detailMeta = document.getElementById('detailMeta');
const timelineList = document.getElementById('timelineList');
const squawkList = document.getElementById('squawkList');
const altitudeChart = document.getElementById('altitudeChart');
const speedChart = document.getElementById('speedChart');
const headingChart = document.getElementById('headingChart');
const verticalChart = document.getElementById('verticalChart');
const refreshBtn = document.getElementById('refreshBtn');
const sessionStatusDot = document.getElementById('sessionStatusDot');
const sessionStatusText = document.getElementById('sessionStatusText');
const sessionUptime = document.getElementById('sessionUptime');
const sessionStartedAt = document.getElementById('sessionStartedAt');
const sessionNotice = document.getElementById('sessionNotice');
const sessionDeviceSelect = document.getElementById('sessionDeviceSelect');
const sessionToggleBtn = document.getElementById('sessionToggleBtn');
const windowSelect = document.getElementById('windowSelect');
const searchInput = document.getElementById('searchInput');
const limitSelect = document.getElementById('limitSelect');
let selectedIcao = '';
let sessionStartAt = null;
let sessionTimer = null;
let recentAircraft = [];
let selectedIndex = -1;
const photoCache = new Map();
const modalBackdrop = document.getElementById('aircraftModalBackdrop');
const modalClose = document.getElementById('aircraftModalClose');
const modalPrev = document.getElementById('modalPrev');
const modalNext = document.getElementById('modalNext');
const modalTitle = document.getElementById('modalTitle');
const modalSubtitle = document.getElementById('modalSubtitle');
const modalPhoto = document.getElementById('modalPhoto');
const modalPhotoFallback = document.getElementById('modalPhotoFallback');
const modalIcao = document.getElementById('modalIcao');
const modalCallsign = document.getElementById('modalCallsign');
const modalRegistration = document.getElementById('modalRegistration');
const modalType = document.getElementById('modalType');
const modalAltitude = document.getElementById('modalAltitude');
const modalSpeed = document.getElementById('modalSpeed');
const modalHeading = document.getElementById('modalHeading');
const modalVerticalRate = document.getElementById('modalVerticalRate');
const modalSquawk = document.getElementById('modalSquawk');
const modalPosition = document.getElementById('modalPosition');
const modalLastSeen = document.getElementById('modalLastSeen');
function formatNumber(value) {
if (value === null || value === undefined) {
return '--';
}
return Number(value).toLocaleString();
}
function formatTime(value) {
if (!value) {
return '--';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '--';
}
return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' });
}
function formatDateTime(value) {
if (!value) {
return '--';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '--';
}
return date.toLocaleString();
}
function valueOrDash(value) {
if (value === null || value === undefined || value === '') {
return '--';
}
return value;
}
function formatUptime(seconds) {
if (seconds === null || seconds === undefined) {
return '--:--:--';
}
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
async function loadSummary() {
const sinceMinutes = windowSelect.value;
const resp = await fetch(`/adsb/history/summary?since_minutes=${sinceMinutes}`);
if (!resp.ok) {
return;
}
const data = await resp.json();
summaryMessages.textContent = formatNumber(data.message_count);
summarySnapshots.textContent = formatNumber(data.snapshot_count);
summaryAircraft.textContent = formatNumber(data.aircraft_count);
summaryFirstSeen.textContent = formatTime(data.first_seen);
summaryLastSeen.textContent = formatTime(data.last_seen);
}
async function loadAircraft() {
const sinceMinutes = windowSelect.value;
const limit = limitSelect.value;
const search = encodeURIComponent(searchInput.value.trim());
const resp = await fetch(`/adsb/history/aircraft?since_minutes=${sinceMinutes}&limit=${limit}&search=${search}`);
if (!resp.ok) {
aircraftTableBody.innerHTML = '<tr class="empty-row"><td colspan="5">History database unavailable</td></tr>';
return;
}
const data = await resp.json();
const rows = data.aircraft || [];
recentAircraft = rows;
aircraftCount.textContent = rows.length;
if (!rows.length) {
aircraftTableBody.innerHTML = '<tr class="empty-row"><td colspan="5">No aircraft in this window</td></tr>';
return;
}
aircraftTableBody.innerHTML = rows.map(row => `
<tr class="aircraft-row" data-icao="${row.icao}">
<td class="mono">${row.icao}</td>
<td>${valueOrDash(row.callsign)}</td>
<td>${valueOrDash(row.altitude)}</td>
<td>${valueOrDash(row.speed)}</td>
<td>${formatTime(row.last_seen)}</td>
</tr>
`).join('');
document.querySelectorAll('.aircraft-row').forEach((row, index) => {
row.addEventListener('click', () => {
selectAircraft(row.dataset.icao);
openModal(index);
});
});
}
async function selectAircraft(icao) {
selectedIcao = icao;
detailIcao.textContent = icao || '--';
if (!icao) {
detailTitle.textContent = 'Select an aircraft';
detailMeta.textContent = '---';
timelineList.innerHTML = '<div class="empty-row">No timeline data</div>';
squawkList.innerHTML = '<div class="empty-row">No squawk changes</div>';
drawMetricChart(altitudeChart, [], 'altitude', 'Altitude', 'ft');
drawMetricChart(speedChart, [], 'speed', 'Speed', 'kt');
drawMetricChart(headingChart, [], 'heading', 'Heading', 'deg');
drawMetricChart(verticalChart, [], 'vertical_rate', 'Vertical Rate', 'fpm');
return;
}
await loadTimeline(icao);
}
async function loadTimeline(icao) {
const sinceMinutes = windowSelect.value;
const resp = await fetch(`/adsb/history/timeline?icao=${icao}&since_minutes=${sinceMinutes}`);
if (!resp.ok) {
timelineList.innerHTML = '<div class="empty-row">Timeline unavailable</div>';
return;
}
const data = await resp.json();
const timeline = data.timeline || [];
if (!timeline.length) {
timelineList.innerHTML = '<div class="empty-row">No timeline data</div>';
squawkList.innerHTML = '<div class="empty-row">No squawk changes</div>';
drawMetricChart(altitudeChart, [], 'altitude', 'Altitude', 'ft');
drawMetricChart(speedChart, [], 'speed', 'Speed', 'kt');
drawMetricChart(headingChart, [], 'heading', 'Heading', 'deg');
drawMetricChart(verticalChart, [], 'vertical_rate', 'Vertical Rate', 'fpm');
return;
}
const latest = timeline[timeline.length - 1];
detailTitle.textContent = `${icao} Timeline`;
detailMeta.textContent = `Alt ${valueOrDash(latest.altitude)} ft | Spd ${valueOrDash(latest.speed)} kt | Head ${valueOrDash(latest.heading)}`;
timelineList.innerHTML = timeline.slice(-30).reverse().map(point => `
<div class="timeline-row">
<span>${formatTime(point.captured_at)}</span>
<span>Alt ${valueOrDash(point.altitude)} ft</span>
<span>Spd ${valueOrDash(point.speed)} kt</span>
<span>Hdg ${valueOrDash(point.heading)}</span>
<span>V/S ${valueOrDash(point.vertical_rate)} fpm</span>
</div>
`).join('');
updateSquawkChanges(timeline);
drawMetricChart(altitudeChart, timeline, 'altitude', 'Altitude', 'ft');
drawMetricChart(speedChart, timeline, 'speed', 'Speed', 'kt');
drawMetricChart(headingChart, timeline, 'heading', 'Heading', 'deg');
drawMetricChart(verticalChart, timeline, 'vertical_rate', 'Vertical Rate', 'fpm');
}
function drawMetricChart(canvas, points, field, label, unit) {
const ctx = canvas.getContext('2d');
const width = canvas.clientWidth;
const height = canvas.clientHeight;
canvas.width = width;
canvas.height = height;
ctx.clearRect(0, 0, width, height);
if (!points.length) {
ctx.fillStyle = 'rgba(156, 163, 175, 0.6)';
ctx.font = '12px "JetBrains Mono", monospace';
ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2);
return;
}
const series = points.map(p => p[field]).filter(v => v !== null && v !== undefined);
if (!series.length) {
ctx.fillStyle = 'rgba(156, 163, 175, 0.6)';
ctx.font = '12px "JetBrains Mono", monospace';
ctx.fillText(`No ${label.toLowerCase()} data`, 12, height / 2);
return;
}
const minVal = Math.min(...series);
const maxVal = Math.max(...series);
const range = maxVal - minVal || 1;
const padding = 20;
ctx.strokeStyle = 'rgba(74, 158, 255, 0.4)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(padding, padding);
ctx.lineTo(padding, height - padding);
ctx.lineTo(width - padding, height - padding);
ctx.stroke();
ctx.strokeStyle = 'rgba(74, 158, 255, 0.9)';
ctx.lineWidth = 2;
ctx.beginPath();
let started = false;
const span = Math.max(1, points.length - 1);
points.forEach((point, index) => {
if (point[field] === null || point[field] === undefined) {
return;
}
const x = padding + (index / span) * (width - padding * 2);
const y = height - padding - ((point[field] - minVal) / range) * (height - padding * 2);
if (!started) {
ctx.moveTo(x, y);
started = true;
} else {
ctx.lineTo(x, y);
}
});
if (started) {
ctx.stroke();
}
ctx.fillStyle = 'rgba(226, 232, 240, 0.8)';
ctx.font = '11px "JetBrains Mono", monospace';
ctx.fillText(`${maxVal} ${unit}`, 12, padding);
ctx.fillText(`${minVal} ${unit}`, 12, height - padding);
}
function updateSquawkChanges(points) {
const changes = [];
let lastSquawk = null;
points.forEach(point => {
if (point.squawk && point.squawk !== lastSquawk) {
changes.push({ time: point.captured_at, squawk: point.squawk });
lastSquawk = point.squawk;
}
});
if (!changes.length) {
squawkList.innerHTML = '<div class="empty-row">No squawk changes</div>';
return;
}
squawkList.innerHTML = changes.slice(-10).reverse().map(change => `
<div class="timeline-row">
<span>${formatTime(change.time)}</span>
<span>Squawk ${change.squawk}</span>
</div>
`).join('');
}
async function loadSessionDevices() {
if (!historyEnabled) {
sessionDeviceSelect.innerHTML = '<option value="0">History disabled</option>';
sessionDeviceSelect.disabled = true;
sessionToggleBtn.disabled = true;
return;
}
const resp = await fetch('/devices');
if (!resp.ok) {
sessionDeviceSelect.innerHTML = '<option value="0">No SDR</option>';
return;
}
const devices = await resp.json();
sessionDeviceSelect.innerHTML = '';
if (!devices.length) {
sessionDeviceSelect.innerHTML = '<option value="0">No SDR</option>';
sessionDeviceSelect.disabled = true;
return;
}
devices.forEach((dev, idx) => {
const index = dev.index !== undefined ? dev.index : idx;
const opt = document.createElement('option');
opt.value = index;
opt.textContent = `SDR ${index}: ${dev.name}`;
sessionDeviceSelect.appendChild(opt);
});
sessionDeviceSelect.disabled = false;
}
async function loadSessionStatus() {
const resp = await fetch('/adsb/session');
if (!resp.ok) {
sessionStatusText.textContent = 'UNKNOWN';
sessionStatusDot.classList.remove('active');
sessionNotice.textContent = 'Session unavailable';
return;
}
const data = await resp.json();
if (data.tracking_active) {
sessionStatusText.textContent = 'TRACKING';
sessionStatusDot.classList.add('active');
sessionToggleBtn.textContent = 'Stop Tracking';
sessionToggleBtn.classList.add('stop');
sessionNotice.textContent = 'Live';
} else {
sessionStatusText.textContent = 'STANDBY';
sessionStatusDot.classList.remove('active');
sessionToggleBtn.textContent = 'Start Tracking';
sessionToggleBtn.classList.remove('stop');
sessionNotice.textContent = 'Idle';
}
if (data.session && data.session.started_at) {
sessionStartAt = new Date(data.session.started_at);
sessionStartedAt.textContent = formatDateTime(data.session.started_at);
sessionUptime.textContent = formatUptime(data.uptime_seconds);
} else {
sessionStartAt = null;
sessionStartedAt.textContent = '--';
sessionUptime.textContent = '--:--:--';
}
}
function startSessionTimer() {
if (sessionTimer) {
clearInterval(sessionTimer);
}
sessionTimer = setInterval(() => {
if (!sessionStartAt) {
sessionUptime.textContent = '--:--:--';
return;
}
const seconds = Math.floor((Date.now() - sessionStartAt.getTime()) / 1000);
sessionUptime.textContent = formatUptime(seconds);
}, 1000);
}
async function toggleSession() {
if (!historyEnabled) {
return;
}
sessionToggleBtn.disabled = true;
sessionNotice.textContent = 'Working...';
if (sessionStatusText.textContent === 'TRACKING') {
const resp = await fetch('/adsb/stop', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ source: 'adsb_history' })
});
if (!resp.ok) {
sessionNotice.textContent = 'Stop failed';
}
} else {
const device = parseInt(sessionDeviceSelect.value, 10) || 0;
const resp = await fetch('/adsb/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ device, source: 'adsb_history', bias_t: getBiasTEnabled() })
});
if (!resp.ok) {
sessionNotice.textContent = 'Start failed';
}
}
await loadSessionStatus();
sessionToggleBtn.disabled = false;
}
async function openModal(index) {
if (index < 0 || index >= recentAircraft.length) {
return;
}
selectedIndex = index;
const ac = recentAircraft[index];
modalTitle.textContent = ac.callsign || ac.icao || 'Aircraft';
modalSubtitle.textContent = `${valueOrDash(ac.registration)}${valueOrDash(ac.type_code)}`;
modalIcao.textContent = valueOrDash(ac.icao);
modalCallsign.textContent = valueOrDash(ac.callsign);
modalRegistration.textContent = valueOrDash(ac.registration);
modalType.textContent = [ac.type_desc, ac.type_code].filter(Boolean).join(' • ') || '--';
modalAltitude.textContent = ac.altitude ? `${ac.altitude} ft` : '--';
modalSpeed.textContent = ac.speed ? `${ac.speed} kt` : '--';
modalHeading.textContent = ac.heading !== null && ac.heading !== undefined ? `${ac.heading}°` : '--';
modalVerticalRate.textContent = ac.vertical_rate ? `${ac.vertical_rate} fpm` : '--';
modalSquawk.textContent = valueOrDash(ac.squawk);
if (ac.lat !== null && ac.lat !== undefined && ac.lon !== null && ac.lon !== undefined) {
modalPosition.textContent = `${ac.lat.toFixed(4)}, ${ac.lon.toFixed(4)}`;
} else {
modalPosition.textContent = '--';
}
modalLastSeen.textContent = formatDateTime(ac.last_seen);
await loadPhoto(ac.registration);
modalBackdrop.classList.add('open');
}
function closeModal() {
modalBackdrop.classList.remove('open');
}
async function loadPhoto(registration) {
if (!registration) {
modalPhoto.style.display = 'none';
modalPhotoFallback.style.display = 'flex';
return;
}
if (photoCache.has(registration)) {
const url = photoCache.get(registration);
if (url) {
modalPhoto.src = url;
modalPhoto.style.display = 'block';
modalPhotoFallback.style.display = 'none';
} else {
modalPhoto.style.display = 'none';
modalPhotoFallback.style.display = 'flex';
}
return;
}
try {
const resp = await fetch(`/adsb/aircraft-photo/${encodeURIComponent(registration)}`);
const data = await resp.json();
const url = data && data.thumbnail;
photoCache.set(registration, url || null);
if (url) {
modalPhoto.src = url;
modalPhoto.onerror = () => {
modalPhoto.style.display = 'none';
modalPhotoFallback.style.display = 'flex';
};
modalPhoto.style.display = 'block';
modalPhotoFallback.style.display = 'none';
} else {
modalPhoto.style.display = 'none';
modalPhotoFallback.style.display = 'flex';
}
} catch (err) {
modalPhoto.style.display = 'none';
modalPhotoFallback.style.display = 'flex';
}
}
function moveModal(offset) {
const nextIndex = selectedIndex + offset;
if (nextIndex < 0 || nextIndex >= recentAircraft.length) {
return;
}
openModal(nextIndex);
}
async function refreshAll() {
if (!historyEnabled) {
return;
}
await Promise.all([loadSummary(), loadAircraft(), loadSessionStatus()]);
if (selectedIcao) {
await loadTimeline(selectedIcao);
}
}
refreshBtn.addEventListener('click', refreshAll);
windowSelect.addEventListener('change', refreshAll);
limitSelect.addEventListener('change', refreshAll);
searchInput.addEventListener('input', () => {
clearTimeout(searchInput._debounce);
searchInput._debounce = setTimeout(refreshAll, 350);
});
refreshAll();
loadSessionDevices();
startSessionTimer();
modalClose.addEventListener('click', closeModal);
modalBackdrop.addEventListener('click', (event) => {
if (event.target === modalBackdrop) {
closeModal();
}
});
modalPrev.addEventListener('click', () => moveModal(-1));
modalNext.addEventListener('click', () => moveModal(1));
window.addEventListener('resize', () => {
if (selectedIcao) {
loadTimeline(selectedIcao);
}
});
</script>
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
</body>
</html>