refactor: convert AIS plugin to TypeScript

This commit is contained in:
sjg
2026-08-01 12:39:20 +02:00
parent ec59908e0d
commit d93f784aad
6 changed files with 466 additions and 408 deletions
@@ -1,335 +1,330 @@
"use strict";
const aisStatus = document.getElementById("ais-status");
const aisMessagesEl = document.getElementById("ais-messages");
const aisFilterInput = document.getElementById("ais-filter");
const aisBarOverlay = document.getElementById("ais-bar-overlay");
const aisChannelSummaryEl = document.getElementById("ais-channel-summary");
const aisVesselCountEl = document.getElementById("ais-vessel-count");
const aisLatestSeenEl = document.getElementById("ais-latest-seen");
const AIS_BAR_WINDOW_MS = 15 * 60 * 1e3;
const AIS_DEFAULT_A_HZ = 161975e3;
const AIS_CHANNEL_SPACING_HZ = 5e4;
let aisFilterText = "";
let aisMessageHistory = [];
function currentAisHistoryRetentionMs() {
return typeof window.getDecodeHistoryRetentionMs === "function" ? window.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
}
function pruneAisMessageHistory() {
const cutoffMs = Date.now() - currentAisHistoryRetentionMs();
aisMessageHistory = aisMessageHistory.filter((msg) => Number(msg?._tsMs) >= cutoffMs);
}
function scheduleAisUi(key, job) {
if (typeof window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job);
return;
(() => {
// src/plugins/ais.ts
var aisWindow = window;
var escapeAisHtml = (input) => aisWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
var aisStatus = document.getElementById("ais-status");
var aisMessagesEl = document.getElementById("ais-messages");
var aisFilterInput = document.getElementById("ais-filter");
var aisBarOverlay = document.getElementById("ais-bar-overlay");
var aisChannelSummaryEl = document.getElementById("ais-channel-summary");
var aisVesselCountEl = document.getElementById("ais-vessel-count");
var aisLatestSeenEl = document.getElementById("ais-latest-seen");
var AIS_BAR_WINDOW_MS = 15 * 60 * 1e3;
var AIS_DEFAULT_A_HZ = 161975e3;
var AIS_CHANNEL_SPACING_HZ = 5e4;
var aisFilterText = "";
var aisMessageHistory = [];
function currentAisHistoryRetentionMs() {
return typeof aisWindow.getDecodeHistoryRetentionMs === "function" ? aisWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
}
job();
}
function scheduleAisHistoryRender() {
scheduleAisUi("ais-history", () => renderAisHistory());
}
function scheduleAisBarUpdate() {
scheduleAisUi("ais-bar", () => updateAisBar());
}
function formatAisMhz(freqHz) {
return `${(freqHz / 1e6).toFixed(3)} MHz`;
}
function currentAisChannelPlan() {
const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, "");
const aHz = raw ? Number(raw) : AIS_DEFAULT_A_HZ;
const safeAHz = Number.isFinite(aHz) && aHz > 0 ? aHz : AIS_DEFAULT_A_HZ;
return {
aHz: safeAHz,
bHz: safeAHz + AIS_CHANNEL_SPACING_HZ
};
}
function aisChannelInfo(channel) {
const plan = currentAisChannelPlan();
const ch = String(channel || "").trim().toUpperCase();
if (ch === "B") {
function pruneAisMessageHistory() {
const cutoffMs = Date.now() - currentAisHistoryRetentionMs();
aisMessageHistory = aisMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs);
}
function scheduleAisUi(key, job) {
if (typeof aisWindow.trxScheduleUiFrameJob === "function") {
aisWindow.trxScheduleUiFrameJob(key, job);
return;
}
job();
}
function scheduleAisHistoryRender() {
scheduleAisUi("ais-history", () => {
renderAisHistory();
});
}
function scheduleAisBarUpdate() {
scheduleAisUi("ais-bar", () => {
updateAisBar();
});
}
function formatAisMhz(freqHz) {
return `${(freqHz / 1e6).toFixed(3)} MHz`;
}
function currentAisChannelPlan() {
const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, "");
const aHz = raw ? Number(raw) : AIS_DEFAULT_A_HZ;
const safeAHz = Number.isFinite(aHz) && aHz > 0 ? aHz : AIS_DEFAULT_A_HZ;
return {
label: "AIS-B",
badgeClass: "ais-badge ais-badge-channel-b",
freqText: formatAisMhz(plan.bHz)
aHz: safeAHz,
bHz: safeAHz + AIS_CHANNEL_SPACING_HZ
};
}
return {
label: "AIS-A",
badgeClass: "ais-badge ais-badge-channel-a",
freqText: formatAisMhz(plan.aHz)
};
}
function aisDisplayName(msg) {
return msg.vessel_name || msg.callsign || `MMSI ${msg.mmsi}`;
}
function aisDisplayNameHtml(msg) {
const label = escapeMapHtml(aisDisplayName(msg));
const url = window.buildAisVesselUrl ? window.buildAisVesselUrl(msg?.mmsi) : null;
if (!url) return label;
return `<a class="title-link" href="${escapeMapHtml(url)}" target="_blank" rel="noopener">${label}</a>`;
}
function aisTypeLabel(type) {
switch (Number(type)) {
case 1:
case 2:
case 3:
return "Class A Position";
case 4:
return "Base Station";
case 5:
return "Static/Voyage";
case 18:
return "Class B Position";
case 19:
return "Class B Extended";
case 21:
return "Aid to Nav";
case 24:
return "Class B Static";
default:
return `Type ${type ?? "--"}`;
function aisChannelInfo(channel) {
const plan = currentAisChannelPlan();
const ch = (channel ?? "").trim().toUpperCase();
if (ch === "B") {
return {
label: "AIS-B",
badgeClass: "ais-badge ais-badge-channel-b",
freqText: formatAisMhz(plan.bHz)
};
}
return {
label: "AIS-A",
badgeClass: "ais-badge ais-badge-channel-a",
freqText: formatAisMhz(plan.aHz)
};
}
}
function aisAgeText(tsMs) {
if (!Number.isFinite(tsMs)) return "just now";
const deltaMs = Math.max(0, Date.now() - tsMs);
const seconds = Math.round(deltaMs / 1e3);
if (seconds < 5) return "just now";
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.round(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.round(minutes / 60);
return `${hours}h ago`;
}
function aisMotionText(msg) {
const parts = [
msg.sog_knots != null ? `${Number(msg.sog_knots).toFixed(1)} kn` : null,
msg.cog_deg != null ? `${Number(msg.cog_deg).toFixed(1)}° COG` : null,
msg.heading_deg != null ? `${Number(msg.heading_deg).toFixed(0)}° HDG` : null
].filter(Boolean);
return parts.join(" · ");
}
function aisRouteText(msg) {
return [msg.callsign, msg.destination].filter(Boolean).join(" -> ");
}
function aisDistanceText(msg) {
if (serverLat == null || serverLon == null || msg?.lat == null || msg?.lon == null) {
return "";
function aisDisplayName(msg) {
return msg.vessel_name || msg.callsign || `MMSI ${msg.mmsi}`;
}
const distKm = haversineKm(serverLat, serverLon, msg.lat, msg.lon);
if (!Number.isFinite(distKm)) return "";
if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`;
return `${distKm.toFixed(1)} km from TRX`;
}
function aisLatestByVessel(messages) {
const byMmsi = /* @__PURE__ */ new Map();
for (const msg of messages) {
const key = Number.isFinite(msg.mmsi) ? String(msg.mmsi) : `${msg.channel || "?"}:${msg._tsMs || 0}`;
if (!byMmsi.has(key)) byMmsi.set(key, msg);
function aisDisplayNameHtml(msg) {
const label = escapeAisHtml(aisDisplayName(msg));
const url = aisWindow.buildAisVesselUrl?.(msg.mmsi) ?? null;
if (!url) return label;
return `<a class="title-link" href="${escapeAisHtml(url)}" target="_blank" rel="noopener">${label}</a>`;
}
return Array.from(byMmsi.values());
}
function updateAisSummary() {
const plan = currentAisChannelPlan();
if (aisChannelSummaryEl) {
aisChannelSummaryEl.textContent = `A ${formatAisMhz(plan.aHz)} · B ${formatAisMhz(plan.bHz)}`;
}
const vessels = aisLatestByVessel(aisMessageHistory);
if (aisVesselCountEl) {
const count = vessels.length;
aisVesselCountEl.textContent = `${count} vessel${count === 1 ? "" : "s"}`;
}
if (aisLatestSeenEl) {
const latest = aisMessageHistory[0];
if (!latest) {
aisLatestSeenEl.textContent = "No traffic yet";
} else {
const channel = aisChannelInfo(latest.channel);
aisLatestSeenEl.textContent = `${channel.label} ${aisAgeText(latest._tsMs)}`;
function aisTypeLabel(type) {
switch (Number(type)) {
case 1:
case 2:
case 3:
return "Class A Position";
case 4:
return "Base Station";
case 5:
return "Static/Voyage";
case 18:
return "Class B Position";
case 19:
return "Class B Extended";
case 21:
return "Aid to Nav";
case 24:
return "Class B Static";
default:
return `Type ${type ?? "--"}`;
}
}
}
function renderAisRow(msg) {
const row = document.createElement("div");
row.className = "ais-message";
const ts = msg._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
const name = aisDisplayName(msg);
const nameHtml = aisDisplayNameHtml(msg);
const channel = aisChannelInfo(msg.channel);
const motion = aisMotionText(msg);
const route = aisRouteText(msg);
const distance = aisDistanceText(msg);
const pos = msg.lat != null && msg.lon != null ? `<a class="ais-pos-link" href="javascript:void(0)" onclick="window.navigateToAprsMap(${msg.lat},${msg.lon})">${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}</a>` : "";
row.dataset.filterText = [
name,
msg.mmsi,
msg.channel,
channel.label,
msg.vessel_name,
msg.callsign,
msg.destination,
aisTypeLabel(msg.message_type)
].filter(Boolean).join(" ").toUpperCase();
row.innerHTML = `<div class="ais-row-head"><span class="ais-time">${ts}</span><span class="ais-call">${nameHtml}</span><span class="${channel.badgeClass}">${escapeMapHtml(channel.label)}</span><span class="ais-badge ais-badge-type">${escapeMapHtml(aisTypeLabel(msg.message_type))}</span></div><div class="ais-row-meta"><span>MMSI ${escapeMapHtml(String(msg.mmsi))}</span>` + (route ? `<span class="ais-meta-text">${escapeMapHtml(route)}</span>` : "") + `<span class="ais-meta-text">${escapeMapHtml(channel.freqText)}</span></div><div class="ais-row-detail">` + (motion ? `<span>${escapeMapHtml(motion)}</span>` : `<span>No motion data</span>`) + (distance ? `<span>${escapeMapHtml(distance)}</span>` : "") + (pos ? `<span>${pos}</span>` : "") + `<span>${escapeMapHtml(aisAgeText(msg._tsMs))}</span></div>`;
applyAisFilterToRow(row);
return row;
}
function applyAisFilterToRow(row) {
if (!aisFilterText) {
row.style.display = "";
return;
function aisAgeText(tsMs) {
if (typeof tsMs !== "number" || !Number.isFinite(tsMs)) return "just now";
const deltaMs = Math.max(0, Date.now() - tsMs);
const seconds = Math.round(deltaMs / 1e3);
if (seconds < 5) return "just now";
if (seconds < 60) return `${seconds}s ago`;
const minutes = Math.round(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.round(minutes / 60);
return `${hours}h ago`;
}
const message = row.dataset.filterText || "";
row.style.display = message.includes(aisFilterText) ? "" : "none";
}
function applyAisFilterToAll() {
if (!aisMessagesEl) return;
const rows = aisMessagesEl.querySelectorAll(".ais-message");
rows.forEach((row) => applyAisFilterToRow(row));
}
function updateAisBar() {
if (!aisBarOverlay) return;
updateAisSummary();
const isAis = (document.getElementById("mode")?.value || "").toUpperCase() === "AIS";
const cutoffMs = Date.now() - AIS_BAR_WINDOW_MS;
const recent = aisMessageHistory.filter((msg) => msg._tsMs >= cutoffMs);
const messages = aisLatestByVessel(recent).slice(0, 8);
if (!isAis || messages.length === 0) {
aisBarOverlay.style.display = "none";
aisBarOverlay.innerHTML = "";
return;
function aisMotionText(msg) {
const parts = [
msg.sog_knots != null ? `${msg.sog_knots.toFixed(1)} kn` : null,
msg.cog_deg != null ? `${msg.cog_deg.toFixed(1)}° COG` : null,
msg.heading_deg != null ? `${msg.heading_deg.toFixed(0)}° HDG` : null
].filter(Boolean);
return parts.join(" · ");
}
let html = `<div class="aprs-bar-header"><span class="aprs-bar-title"><span class="aprs-bar-title-word">AIS</span><span class="aprs-bar-title-word">Live</span></span><span class="aprs-bar-clear-wrap"><span class="aprs-bar-clear" role="button" tabindex="0" onclick="window.clearAisBar()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();window.clearAisBar();}" aria-label="Clear AIS overlay">Clear</span></span><span class="aprs-bar-window">Last 15 minutes</span></div>`;
for (const msg of messages) {
const ts = msg._ts ? `<span class="aprs-bar-time">${msg._ts}</span>` : "";
const pin = msg.lat != null && msg.lon != null ? `<button class="aprs-bar-pin" title="${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}" onclick="window.navigateToAprsMap(${msg.lat},${msg.lon})">📍</button>` : "";
const name = `<span class="ais-call">${aisDisplayNameHtml(msg)}</span>`;
const channel = aisChannelInfo(msg.channel);
const distance = aisDistanceText(msg);
const details = [
`MMSI ${escapeMapHtml(String(msg.mmsi))}`,
escapeMapHtml(channel.label),
msg.sog_knots != null ? `${Number(msg.sog_knots).toFixed(1)} kn` : null,
msg.cog_deg != null ? `${Number(msg.cog_deg).toFixed(1)}°` : null,
distance ? escapeMapHtml(distance) : null,
escapeMapHtml(aisAgeText(msg._tsMs))
].filter(Boolean).join(" · ");
html += `<div class="aprs-bar-frame"><div class="aprs-bar-frame-main">${ts}${pin}${name}: ${details}</div></div>`;
function aisRouteText(msg) {
return [msg.callsign, msg.destination].filter(Boolean).join(" -> ");
}
aisBarOverlay.innerHTML = html;
aisBarOverlay.style.display = "flex";
}
window.updateAisBar = updateAisBar;
window.clearAisBar = function() {
window.resetAisHistoryView();
};
window.resetAisHistoryView = function() {
if (aisMessagesEl) aisMessagesEl.innerHTML = "";
aisMessageHistory = [];
updateAisBar();
renderAisHistory();
if (window.clearMapMarkersByType) window.clearMapMarkersByType("ais");
};
function renderAisHistory() {
pruneAisMessageHistory();
if (!aisMessagesEl) {
updateAisSummary();
return;
function aisDistanceText(msg) {
if (aisWindow.serverLat == null || aisWindow.serverLon == null || msg.lat == null || msg.lon == null || !aisWindow.haversineKm) {
return "";
}
const distKm = aisWindow.haversineKm(aisWindow.serverLat, aisWindow.serverLon, msg.lat, msg.lon);
if (!Number.isFinite(distKm)) return "";
if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`;
return `${distKm.toFixed(1)} km from TRX`;
}
const fragment = document.createDocumentFragment();
for (let i = 0; i < aisMessageHistory.length; i += 1) {
fragment.appendChild(renderAisRow(aisMessageHistory[i]));
function aisLatestByVessel(messages) {
const byMmsi = /* @__PURE__ */ new Map();
for (const msg of messages) {
const key = Number.isFinite(msg.mmsi) ? String(msg.mmsi) : `${msg.channel || "?"}:${msg._tsMs || 0}`;
if (!byMmsi.has(key)) byMmsi.set(key, msg);
}
return Array.from(byMmsi.values());
}
aisMessagesEl.replaceChildren(fragment);
updateAisSummary();
}
function addAisMessage(msg) {
const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
msg._tsMs = tsMs;
msg._ts = new Date(tsMs).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
aisMessageHistory.unshift(msg);
pruneAisMessageHistory();
scheduleAisBarUpdate();
scheduleAisHistoryRender();
if (msg.lat != null && msg.lon != null && window.aisMapAddVessel) {
window.aisMapAddVessel(msg);
function updateAisSummary() {
const plan = currentAisChannelPlan();
if (aisChannelSummaryEl) {
aisChannelSummaryEl.textContent = `A ${formatAisMhz(plan.aHz)} · B ${formatAisMhz(plan.bHz)}`;
}
const vessels = aisLatestByVessel(aisMessageHistory);
if (aisVesselCountEl) {
const count = vessels.length;
aisVesselCountEl.textContent = `${count} vessel${count === 1 ? "" : "s"}`;
}
if (aisLatestSeenEl) {
const latest = aisMessageHistory[0];
if (!latest) {
aisLatestSeenEl.textContent = "No traffic yet";
} else {
const channel = aisChannelInfo(latest.channel);
aisLatestSeenEl.textContent = `${channel.label} ${aisAgeText(latest._tsMs)}`;
}
}
}
}
function normalizeServerAisMessage(msg) {
return {
rig_id: msg.rig_id || null,
channel: msg.channel,
message_type: msg.message_type,
mmsi: msg.mmsi,
lat: msg.lat,
lon: msg.lon,
sog_knots: msg.sog_knots,
cog_deg: msg.cog_deg,
heading_deg: msg.heading_deg,
vessel_name: msg.vessel_name,
callsign: msg.callsign,
destination: msg.destination,
ts_ms: msg.ts_ms
};
}
window.onServerAisBatch = function(messages) {
if (!Array.isArray(messages) || messages.length === 0) return;
if (aisStatus) aisStatus.textContent = "Receiving";
const normalized = [];
for (const msg of messages) {
const next = normalizeServerAisMessage(msg);
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
next._tsMs = tsMs;
next._ts = new Date(tsMs).toLocaleTimeString([], {
function renderAisRow(msg) {
const row = document.createElement("div");
row.className = "ais-message";
const ts = msg._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
if (next.lat != null && next.lon != null && window.aisMapAddVessel) {
window.aisMapAddVessel(next);
const name = aisDisplayName(msg);
const nameHtml = aisDisplayNameHtml(msg);
const channel = aisChannelInfo(msg.channel);
const motion = aisMotionText(msg);
const route = aisRouteText(msg);
const distance = aisDistanceText(msg);
const pos = msg.lat != null && msg.lon != null ? `<a class="ais-pos-link" href="javascript:void(0)" onclick="window.navigateToAprsMap(${msg.lat},${msg.lon})">${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}</a>` : "";
row.dataset.filterText = [
name,
msg.mmsi,
msg.channel,
channel.label,
msg.vessel_name,
msg.callsign,
msg.destination,
aisTypeLabel(msg.message_type)
].filter(Boolean).join(" ").toUpperCase();
row.innerHTML = `<div class="ais-row-head"><span class="ais-time">${ts}</span><span class="ais-call">${nameHtml}</span><span class="${channel.badgeClass}">${escapeAisHtml(channel.label)}</span><span class="ais-badge ais-badge-type">${escapeAisHtml(aisTypeLabel(msg.message_type))}</span></div><div class="ais-row-meta"><span>MMSI ${escapeAisHtml(String(msg.mmsi))}</span>` + (route ? `<span class="ais-meta-text">${escapeAisHtml(route)}</span>` : "") + `<span class="ais-meta-text">${escapeAisHtml(channel.freqText)}</span></div><div class="ais-row-detail">` + (motion ? `<span>${escapeAisHtml(motion)}</span>` : `<span>No motion data</span>`) + (distance ? `<span>${escapeAisHtml(distance)}</span>` : "") + (pos ? `<span>${pos}</span>` : "") + `<span>${escapeAisHtml(aisAgeText(msg._tsMs))}</span></div>`;
applyAisFilterToRow(row);
return row;
}
function applyAisFilterToRow(row) {
if (!aisFilterText) {
row.style.display = "";
return;
}
normalized.push(next);
const message = row.dataset.filterText || "";
row.style.display = message.includes(aisFilterText) ? "" : "none";
}
normalized.reverse();
aisMessageHistory = normalized.concat(aisMessageHistory);
pruneAisMessageHistory();
scheduleAisBarUpdate();
scheduleAisHistoryRender();
};
window.restoreAisHistory = function(messages) {
window.onServerAisBatch(messages);
};
window.pruneAisHistoryView = function() {
pruneAisMessageHistory();
updateAisBar();
renderAisHistory();
};
document.getElementById("settings-clear-ais-history")?.addEventListener("click", async () => {
if (!await window.trxUi.confirm({ title: "Clear AIS history?", message: "All stored AIS messages will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await postPath("/clear_ais_decode");
window.resetAisHistoryView();
} catch (e) {
console.error("AIS history clear failed", e);
function updateAisBar() {
if (!aisBarOverlay) return;
updateAisSummary();
const isAis = (document.getElementById("mode")?.value || "").toUpperCase() === "AIS";
const cutoffMs = Date.now() - AIS_BAR_WINDOW_MS;
const recent = aisMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs);
const messages = aisLatestByVessel(recent).slice(0, 8);
if (!isAis || messages.length === 0) {
aisBarOverlay.style.display = "none";
aisBarOverlay.innerHTML = "";
return;
}
let html = `<div class="aprs-bar-header"><span class="aprs-bar-title"><span class="aprs-bar-title-word">AIS</span><span class="aprs-bar-title-word">Live</span></span><span class="aprs-bar-clear-wrap"><span class="aprs-bar-clear" role="button" tabindex="0" onclick="window.clearAisBar()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();window.clearAisBar();}" aria-label="Clear AIS overlay">Clear</span></span><span class="aprs-bar-window">Last 15 minutes</span></div>`;
for (const msg of messages) {
const ts = msg._ts ? `<span class="aprs-bar-time">${msg._ts}</span>` : "";
const pin = msg.lat != null && msg.lon != null ? `<button class="aprs-bar-pin" title="${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}" onclick="window.navigateToAprsMap(${msg.lat},${msg.lon})">📍</button>` : "";
const name = `<span class="ais-call">${aisDisplayNameHtml(msg)}</span>`;
const channel = aisChannelInfo(msg.channel);
const distance = aisDistanceText(msg);
const details = [
`MMSI ${escapeAisHtml(String(msg.mmsi))}`,
escapeAisHtml(channel.label),
msg.sog_knots != null ? `${msg.sog_knots.toFixed(1)} kn` : null,
msg.cog_deg != null ? `${msg.cog_deg.toFixed(1)}°` : null,
distance ? escapeAisHtml(distance) : null,
escapeAisHtml(aisAgeText(msg._tsMs))
].filter(Boolean).join(" · ");
html += `<div class="aprs-bar-frame"><div class="aprs-bar-frame-main">${ts}${pin}${name}: ${details}</div></div>`;
}
aisBarOverlay.innerHTML = html;
aisBarOverlay.style.display = "flex";
}
});
if (aisFilterInput) {
aisFilterInput.addEventListener("input", () => {
aisFilterText = aisFilterInput.value.trim().toUpperCase();
aisWindow.updateAisBar = updateAisBar;
aisWindow.clearAisBar = function() {
aisWindow.resetAisHistoryView?.();
};
aisWindow.resetAisHistoryView = function() {
if (aisMessagesEl) aisMessagesEl.innerHTML = "";
aisMessageHistory = [];
updateAisBar();
renderAisHistory();
aisWindow.clearMapMarkersByType?.("ais");
};
function renderAisHistory() {
pruneAisMessageHistory();
if (!aisMessagesEl) {
updateAisSummary();
return;
}
const fragment = document.createDocumentFragment();
for (const message of aisMessageHistory) {
fragment.appendChild(renderAisRow(message));
}
aisMessagesEl.replaceChildren(fragment);
updateAisSummary();
}
function addAisMessage(msg) {
const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
msg._tsMs = tsMs;
msg._ts = new Date(tsMs).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
aisMessageHistory.unshift(msg);
pruneAisMessageHistory();
scheduleAisBarUpdate();
scheduleAisHistoryRender();
if (msg.lat != null && msg.lon != null && aisWindow.aisMapAddVessel) {
aisWindow.aisMapAddVessel(msg);
}
}
function normalizeServerAisMessage(msg) {
return {
...msg,
rig_id: msg.rig_id || null
};
}
aisWindow.onServerAisBatch = function(messages) {
if (!Array.isArray(messages) || messages.length === 0) return;
if (aisStatus) aisStatus.textContent = "Receiving";
const normalized = [];
for (const msg of messages) {
const next = normalizeServerAisMessage(msg);
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
next._tsMs = tsMs;
next._ts = new Date(tsMs).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
if (next.lat != null && next.lon != null && aisWindow.aisMapAddVessel) {
aisWindow.aisMapAddVessel(next);
}
normalized.push(next);
}
normalized.reverse();
aisMessageHistory = normalized.concat(aisMessageHistory);
pruneAisMessageHistory();
scheduleAisBarUpdate();
scheduleAisHistoryRender();
};
aisWindow.restoreAisHistory = function(messages) {
aisWindow.onServerAisBatch?.(messages);
};
aisWindow.pruneAisHistoryView = function() {
pruneAisMessageHistory();
updateAisBar();
renderAisHistory();
};
document.getElementById("settings-clear-ais-history")?.addEventListener("click", () => {
void (async () => {
if (!await aisWindow.trxUi.confirm({ title: "Clear AIS history?", message: "All stored AIS messages will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await aisWindow.postPath?.("/clear_ais_decode");
aisWindow.resetAisHistoryView?.();
} catch (e) {
console.error("AIS history clear failed", e);
}
})();
});
}
window.onServerAis = function(msg) {
if (aisStatus) aisStatus.textContent = "Receiving";
addAisMessage(normalizeServerAisMessage(msg));
};
updateAisSummary();
if (window._trxDrainPendingDecode) window._trxDrainPendingDecode("ais");
if (aisFilterInput) {
aisFilterInput.addEventListener("input", () => {
aisFilterText = aisFilterInput.value.trim().toUpperCase();
renderAisHistory();
});
}
aisWindow.onServerAis = function(msg) {
if (aisStatus) aisStatus.textContent = "Receiving";
addAisMessage(normalizeServerAisMessage(msg));
};
updateAisSummary();
aisWindow._trxDrainPendingDecode?.("ais");
})();
@@ -10,7 +10,7 @@ const pluginGroups = {
};
const loaded = /* @__PURE__ */ new Set();
const loading = /* @__PURE__ */ new Map();
const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js"]);
const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js"]);
function loadLegacyScript(path) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");