diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js
index 0a8c4212..b4659723 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js
@@ -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("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
+ 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 `${label}`;
-}
-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 `${label}`;
}
- 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 ? `${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}` : "";
- 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 = `
${ts}${nameHtml}${escapeMapHtml(channel.label)}${escapeMapHtml(aisTypeLabel(msg.message_type))}
MMSI ${escapeMapHtml(String(msg.mmsi))}` + (route ? `${escapeMapHtml(route)}` : "") + `${escapeMapHtml(channel.freqText)}
` + (motion ? `${escapeMapHtml(motion)}` : `No motion data`) + (distance ? `${escapeMapHtml(distance)}` : "") + (pos ? `${pos}` : "") + `${escapeMapHtml(aisAgeText(msg._tsMs))}
`;
- 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 = ``;
- for (const msg of messages) {
- const ts = msg._ts ? `${msg._ts}` : "";
- const pin = msg.lat != null && msg.lon != null ? `` : "";
- const name = `${aisDisplayNameHtml(msg)}`;
- 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 += `${ts}${pin}${name}: ${details}
`;
+ 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 ? `${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}` : "";
+ 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 = `${ts}${nameHtml}${escapeAisHtml(channel.label)}${escapeAisHtml(aisTypeLabel(msg.message_type))}
MMSI ${escapeAisHtml(String(msg.mmsi))}` + (route ? `${escapeAisHtml(route)}` : "") + `${escapeAisHtml(channel.freqText)}
` + (motion ? `${escapeAisHtml(motion)}` : `No motion data`) + (distance ? `${escapeAisHtml(distance)}` : "") + (pos ? `${pos}` : "") + `${escapeAisHtml(aisAgeText(msg._tsMs))}
`;
+ 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 = ``;
+ for (const msg of messages) {
+ const ts = msg._ts ? `${msg._ts}` : "";
+ const pin = msg.lat != null && msg.lon != null ? `` : "";
+ const name = `${aisDisplayNameHtml(msg)}`;
+ 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 += `${ts}${pin}${name}: ${details}
`;
+ }
+ 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");
+})();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js
index 36edf444..3f802303 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js
@@ -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");
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs
index b1753331..5a505e65 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs
@@ -22,7 +22,6 @@ await build({
"plugin-loader": path.join(sourceDir, "plugin-loader.ts"),
screenshot: path.join(sourceDir, "screenshot.ts"),
"webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"),
- ais: path.join(sourceDir, "plugins", "ais.js"),
aprs: path.join(sourceDir, "plugins", "aprs.js"),
bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"),
"hf-aprs": path.join(sourceDir, "plugins", "hf-aprs.js"),
@@ -51,6 +50,7 @@ await build({
vdes: path.join(sourceDir, "plugins", "vdes.ts"),
wefax: path.join(sourceDir, "plugins", "wefax.ts"),
"background-decode": path.join(sourceDir, "plugins", "background-decode.ts"),
+ ais: path.join(sourceDir, "plugins", "ais.ts"),
},
outdir: outputDir,
bundle: true,
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts
index 7edc91ce..217606b9 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts
@@ -16,7 +16,7 @@ const pluginGroups: Readonly> = {
const loaded = new Set();
const loading = new Map>();
-const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js"]);
+const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js"]);
function loadLegacyScript(path: string): Promise {
return new Promise((resolve, reject) => {
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ais.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ais.ts
similarity index 58%
rename from src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ais.js
rename to src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ais.ts
index 3f6dae8b..5b5ba892 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ais.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/ais.ts
@@ -2,10 +2,56 @@
//
// SPDX-License-Identifier: GPL-2.0-or-later
+export {};
+
+interface AisMessage {
+ rig_id?: string | null;
+ channel?: string | null;
+ message_type?: number | null;
+ mmsi?: number | null;
+ lat?: number | null;
+ lon?: number | null;
+ sog_knots?: number | null;
+ cog_deg?: number | null;
+ heading_deg?: number | null;
+ vessel_name?: string | null;
+ callsign?: string | null;
+ destination?: string | null;
+ ts_ms?: number | null;
+ _tsMs?: number;
+ _ts?: string;
+}
+interface AisChannelInfo { label: string; badgeClass: string; freqText: string }
+interface AisBridge {
+ getDecodeHistoryRetentionMs?: () => number;
+ trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
+ escapeMapHtml?: (input: string) => string;
+ buildAisVesselUrl?: (mmsi: number | null | undefined) => string | null;
+ serverLat?: number | null;
+ serverLon?: number | null;
+ haversineKm?: (lat1: number, lon1: number, lat2: number, lon2: number) => number;
+ aisMapAddVessel?: (message: AisMessage) => void;
+ clearMapMarkersByType?: (type: string) => void;
+ postPath?: (path: string) => Promise;
+ trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise };
+ updateAisBar?: () => void;
+ clearAisBar?: () => void;
+ resetAisHistoryView?: () => void;
+ onServerAisBatch?: (messages: AisMessage[]) => void;
+ restoreAisHistory?: (messages: AisMessage[]) => void;
+ pruneAisHistoryView?: () => void;
+ onServerAis?: (message: AisMessage) => void;
+ _trxDrainPendingDecode?: (decoder: string) => void;
+}
+const aisWindow = window as unknown as AisBridge;
+const escapeAisHtml = (input: string): string => aisWindow.escapeMapHtml?.(input) ?? input
+ .replaceAll("&", "&").replaceAll("<", "<")
+ .replaceAll(">", ">").replaceAll('"', """);
+
// --- AIS Decoder Plugin (server-side decode) ---
const aisStatus = document.getElementById("ais-status");
const aisMessagesEl = document.getElementById("ais-messages");
-const aisFilterInput = document.getElementById("ais-filter");
+const aisFilterInput = document.getElementById("ais-filter") as HTMLInputElement | null;
const aisBarOverlay = document.getElementById("ais-bar-overlay");
const aisChannelSummaryEl = document.getElementById("ais-channel-summary");
const aisVesselCountEl = document.getElementById("ais-vessel-count");
@@ -14,41 +60,41 @@ const AIS_BAR_WINDOW_MS = 15 * 60 * 1000;
const AIS_DEFAULT_A_HZ = 161_975_000;
const AIS_CHANNEL_SPACING_HZ = 50_000;
let aisFilterText = "";
-let aisMessageHistory = [];
+let aisMessageHistory: AisMessage[] = [];
-function currentAisHistoryRetentionMs() {
- return typeof window.getDecodeHistoryRetentionMs === "function"
- ? window.getDecodeHistoryRetentionMs()
+function currentAisHistoryRetentionMs(): number {
+ return typeof aisWindow.getDecodeHistoryRetentionMs === "function"
+ ? aisWindow.getDecodeHistoryRetentionMs()
: 24 * 60 * 60 * 1000;
}
function pruneAisMessageHistory() {
const cutoffMs = Date.now() - currentAisHistoryRetentionMs();
- aisMessageHistory = aisMessageHistory.filter((msg) => Number(msg?._tsMs) >= cutoffMs);
+ aisMessageHistory = aisMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs);
}
-function scheduleAisUi(key, job) {
- if (typeof window.trxScheduleUiFrameJob === "function") {
- window.trxScheduleUiFrameJob(key, job);
+function scheduleAisUi(key: string, job: () => void): void {
+ if (typeof aisWindow.trxScheduleUiFrameJob === "function") {
+ aisWindow.trxScheduleUiFrameJob(key, job);
return;
}
job();
}
function scheduleAisHistoryRender() {
- scheduleAisUi("ais-history", () => renderAisHistory());
+ scheduleAisUi("ais-history", () => { renderAisHistory(); });
}
function scheduleAisBarUpdate() {
- scheduleAisUi("ais-bar", () => updateAisBar());
+ scheduleAisUi("ais-bar", () => { updateAisBar(); });
}
-function formatAisMhz(freqHz) {
+function formatAisMhz(freqHz: number): string {
return `${(freqHz / 1_000_000).toFixed(3)} MHz`;
}
-function currentAisChannelPlan() {
- const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, "");
+function currentAisChannelPlan(): { aHz: number; bHz: number } {
+ const raw = ((document.getElementById("freq") as HTMLInputElement | null)?.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 {
@@ -57,9 +103,9 @@ function currentAisChannelPlan() {
};
}
-function aisChannelInfo(channel) {
+function aisChannelInfo(channel: string | null | undefined): AisChannelInfo {
const plan = currentAisChannelPlan();
- const ch = String(channel || "").trim().toUpperCase();
+ const ch = (channel ?? "").trim().toUpperCase();
if (ch === "B") {
return {
label: "AIS-B",
@@ -74,18 +120,18 @@ function aisChannelInfo(channel) {
};
}
-function aisDisplayName(msg) {
+function aisDisplayName(msg: AisMessage): string {
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;
+function aisDisplayNameHtml(msg: AisMessage): string {
+ const label = escapeAisHtml(aisDisplayName(msg));
+ const url = aisWindow.buildAisVesselUrl?.(msg.mmsi) ?? null;
if (!url) return label;
- return `${label}`;
+ return `${label}`;
}
-function aisTypeLabel(type) {
+function aisTypeLabel(type: number | null | undefined): string {
switch (Number(type)) {
case 1:
case 2:
@@ -108,8 +154,8 @@ function aisTypeLabel(type) {
}
}
-function aisAgeText(tsMs) {
- if (!Number.isFinite(tsMs)) return "just now";
+function aisAgeText(tsMs: number | undefined): string {
+ if (typeof tsMs !== "number" || !Number.isFinite(tsMs)) return "just now";
const deltaMs = Math.max(0, Date.now() - tsMs);
const seconds = Math.round(deltaMs / 1000);
if (seconds < 5) return "just now";
@@ -120,31 +166,31 @@ function aisAgeText(tsMs) {
return `${hours}h ago`;
}
-function aisMotionText(msg) {
+function aisMotionText(msg: AisMessage): string {
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,
+ 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(" · ");
}
-function aisRouteText(msg) {
+function aisRouteText(msg: AisMessage): string {
return [msg.callsign, msg.destination].filter(Boolean).join(" -> ");
}
-function aisDistanceText(msg) {
- if (serverLat == null || serverLon == null || msg?.lat == null || msg?.lon == null) {
+function aisDistanceText(msg: AisMessage): string {
+ if (aisWindow.serverLat == null || aisWindow.serverLon == null || msg.lat == null || msg.lon == null || !aisWindow.haversineKm) {
return "";
}
- const distKm = haversineKm(serverLat, serverLon, msg.lat, msg.lon);
+ const distKm = aisWindow.haversineKm(aisWindow.serverLat, aisWindow.serverLon, msg.lat, msg.lon);
if (!Number.isFinite(distKm)) return "";
if (distKm < 1) return `${Math.round(distKm * 1000)} m from TRX`;
return `${distKm.toFixed(1)} km from TRX`;
}
-function aisLatestByVessel(messages) {
- const byMmsi = new Map();
+function aisLatestByVessel(messages: AisMessage[]): AisMessage[] {
+ const byMmsi = 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);
@@ -175,7 +221,7 @@ function updateAisSummary() {
}
}
-function renderAisRow(msg) {
+function renderAisRow(msg: AisMessage): HTMLElement {
const row = document.createElement("div");
row.className = "ais-message";
const ts = msg._ts || new Date().toLocaleTimeString([], {
@@ -209,25 +255,25 @@ function renderAisRow(msg) {
`` +
`${ts}` +
`${nameHtml}` +
- `${escapeMapHtml(channel.label)}` +
- `${escapeMapHtml(aisTypeLabel(msg.message_type))}` +
+ `${escapeAisHtml(channel.label)}` +
+ `${escapeAisHtml(aisTypeLabel(msg.message_type))}` +
`
` +
`` +
- `MMSI ${escapeMapHtml(String(msg.mmsi))}` +
- (route ? `${escapeMapHtml(route)}` : "") +
- `${escapeMapHtml(channel.freqText)}` +
+ `MMSI ${escapeAisHtml(String(msg.mmsi))}` +
+ (route ? `${escapeAisHtml(route)}` : "") +
+ `${escapeAisHtml(channel.freqText)}` +
`
` +
`` +
- (motion ? `${escapeMapHtml(motion)}` : `No motion data`) +
- (distance ? `${escapeMapHtml(distance)}` : "") +
+ (motion ? `${escapeAisHtml(motion)}` : `No motion data`) +
+ (distance ? `${escapeAisHtml(distance)}` : "") +
(pos ? `${pos}` : "") +
- `${escapeMapHtml(aisAgeText(msg._tsMs))}` +
+ `${escapeAisHtml(aisAgeText(msg._tsMs))}` +
`
`;
applyAisFilterToRow(row);
return row;
}
-function applyAisFilterToRow(row) {
+function applyAisFilterToRow(row: HTMLElement): void {
if (!aisFilterText) {
row.style.display = "";
return;
@@ -236,19 +282,13 @@ function applyAisFilterToRow(row) {
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 isAis = ((document.getElementById("mode") as HTMLSelectElement | null)?.value || "").toUpperCase() === "AIS";
const cutoffMs = Date.now() - AIS_BAR_WINDOW_MS;
- const recent = aisMessageHistory.filter((msg) => msg._tsMs >= cutoffMs);
+ 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";
@@ -266,12 +306,12 @@ function updateAisBar() {
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)),
+ `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(" · ");
@@ -282,17 +322,17 @@ function updateAisBar() {
aisBarOverlay.innerHTML = html;
aisBarOverlay.style.display = "flex";
}
-window.updateAisBar = updateAisBar;
-window.clearAisBar = function() {
- window.resetAisHistoryView();
+aisWindow.updateAisBar = updateAisBar;
+aisWindow.clearAisBar = function() {
+ aisWindow.resetAisHistoryView?.();
};
-window.resetAisHistoryView = function() {
+aisWindow.resetAisHistoryView = function() {
if (aisMessagesEl) aisMessagesEl.innerHTML = "";
aisMessageHistory = [];
updateAisBar();
renderAisHistory();
- if (window.clearMapMarkersByType) window.clearMapMarkersByType("ais");
+ aisWindow.clearMapMarkersByType?.("ais");
};
function renderAisHistory() {
@@ -302,14 +342,14 @@ function renderAisHistory() {
return;
}
const fragment = document.createDocumentFragment();
- for (let i = 0; i < aisMessageHistory.length; i += 1) {
- fragment.appendChild(renderAisRow(aisMessageHistory[i]));
+ for (const message of aisMessageHistory) {
+ fragment.appendChild(renderAisRow(message));
}
aisMessagesEl.replaceChildren(fragment);
updateAisSummary();
}
-function addAisMessage(msg) {
+function addAisMessage(msg: AisMessage): void {
const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
msg._tsMs = tsMs;
msg._ts = new Date(tsMs).toLocaleTimeString([], {
@@ -323,33 +363,22 @@ function addAisMessage(msg) {
scheduleAisBarUpdate();
scheduleAisHistoryRender();
- if (msg.lat != null && msg.lon != null && window.aisMapAddVessel) {
- window.aisMapAddVessel(msg);
+ if (msg.lat != null && msg.lon != null && aisWindow.aisMapAddVessel) {
+ aisWindow.aisMapAddVessel(msg);
}
}
-function normalizeServerAisMessage(msg) {
+function normalizeServerAisMessage(msg: AisMessage): AisMessage {
return {
+ ...msg,
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) {
+aisWindow.onServerAisBatch = function(messages: AisMessage[]) {
if (!Array.isArray(messages) || messages.length === 0) return;
if (aisStatus) aisStatus.textContent = "Receiving";
- const normalized = [];
+ const normalized: AisMessage[] = [];
for (const msg of messages) {
const next = normalizeServerAisMessage(msg);
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
@@ -359,8 +388,8 @@ window.onServerAisBatch = function(messages) {
minute: "2-digit",
second: "2-digit",
});
- if (next.lat != null && next.lon != null && window.aisMapAddVessel) {
- window.aisMapAddVessel(next);
+ if (next.lat != null && next.lon != null && aisWindow.aisMapAddVessel) {
+ aisWindow.aisMapAddVessel(next);
}
normalized.push(next);
}
@@ -371,25 +400,25 @@ window.onServerAisBatch = function(messages) {
scheduleAisHistoryRender();
};
-window.restoreAisHistory = function(messages) {
- window.onServerAisBatch(messages);
+aisWindow.restoreAisHistory = function(messages: AisMessage[]) {
+ aisWindow.onServerAisBatch?.(messages);
};
-window.pruneAisHistoryView = function() {
+aisWindow.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;
+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 postPath("/clear_ais_decode");
- window.resetAisHistoryView();
+ await aisWindow.postPath?.("/clear_ais_decode");
+ aisWindow.resetAisHistoryView?.();
} catch (e) {
console.error("AIS history clear failed", e);
}
-});
+})(); });
if (aisFilterInput) {
aisFilterInput.addEventListener("input", () => {
@@ -398,10 +427,10 @@ if (aisFilterInput) {
});
}
-window.onServerAis = function(msg) {
+aisWindow.onServerAis = function(msg: AisMessage) {
if (aisStatus) aisStatus.textContent = "Receiving";
addAisMessage(normalizeServerAisMessage(msg));
};
updateAisSummary();
-if (window._trxDrainPendingDecode) window._trxDrainPendingDecode("ais");
+aisWindow._trxDrainPendingDecode?.("ais");
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ais.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ais.test.mjs
new file mode 100644
index 00000000..3a0e2248
--- /dev/null
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ais.test.mjs
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: 2026 Stan Grams
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import test from "node:test";
+import vm from "node:vm";
+
+test("AIS entry forwards positioned vessels with normalized metadata", async () => {
+ const forwarded = [];
+ const window = {
+ trxUi: { confirm: async () => true },
+ aisMapAddVessel: (message) => { forwarded.push(message); },
+ };
+ const context = vm.createContext({
+ window,
+ document: { getElementById: () => null },
+ Date,
+ Number,
+ String,
+ Array,
+ Map,
+ console,
+ });
+ const source = await readFile(new URL("../../assets/web/generated/ais.js", import.meta.url), "utf8");
+ new vm.Script(source).runInContext(context);
+
+ window.onServerAis({ mmsi: 261000001, lat: 54.5, lon: 18.5, channel: "A", ts_ms: Date.now() });
+ assert.equal(forwarded.length, 1);
+ assert.equal(forwarded[0].mmsi, 261000001);
+ assert.equal(forwarded[0].rig_id, null);
+ assert.equal(typeof forwarded[0]._tsMs, "number");
+});