diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js
index 34c90fe4..06e4b640 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js
@@ -1,407 +1,419 @@
"use strict";
-const aprsStatus = document.getElementById("aprs-status");
-const aprsPacketsEl = document.getElementById("aprs-packets");
-const aprsFilterInput = document.getElementById("aprs-filter");
-const aprsBarOverlay = document.getElementById("aprs-bar-overlay");
-const aprsOnlyPosBtn = document.getElementById("aprs-only-pos-btn");
-const aprsHideCrcBtn = document.getElementById("aprs-hide-crc-btn");
-const aprsCollapseDupBtn = document.getElementById("aprs-collapse-dup-btn");
-const aprsTotalCountEl = document.getElementById("aprs-total-count");
-const aprsVisibleCountEl = document.getElementById("aprs-visible-count");
-const aprsLatestSeenEl = document.getElementById("aprs-latest-seen");
-const APRS_BAR_WINDOW_MS = 15 * 60 * 1e3;
-let aprsFilterText = "";
-let aprsPacketHistory = [];
-let aprsBarDismissedAtMs = 0;
-let aprsOnlyPos = false;
-let aprsHideCrc = false;
-let aprsCollapseDup = false;
-let aprsTypeFilter = "all";
-function currentAprsHistoryRetentionMs() {
- return typeof window.getDecodeHistoryRetentionMs === "function" ? window.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
-}
-function pruneAprsPacketHistory() {
- const cutoffMs = Date.now() - currentAprsHistoryRetentionMs();
- aprsPacketHistory = aprsPacketHistory.filter((pkt) => Number(pkt?._tsMs) >= cutoffMs);
-}
-function scheduleAprsUi(key, job) {
- if (typeof window.trxScheduleUiFrameJob === "function") {
- window.trxScheduleUiFrameJob(key, job);
- return;
+(() => {
+ // src/plugins/aprs.ts
+ var aprsWindow = window;
+ var escapeAprsHtml = (input) => aprsWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
+ var showAprsHint = (message, durationMs) => {
+ aprsWindow.showHint?.(message, durationMs);
+ };
+ var aprsStatus = document.getElementById("aprs-status");
+ var aprsPacketsEl = document.getElementById("aprs-packets");
+ var aprsFilterInput = document.getElementById("aprs-filter");
+ var aprsBarOverlay = document.getElementById("aprs-bar-overlay");
+ var aprsOnlyPosBtn = document.getElementById("aprs-only-pos-btn");
+ var aprsHideCrcBtn = document.getElementById("aprs-hide-crc-btn");
+ var aprsCollapseDupBtn = document.getElementById("aprs-collapse-dup-btn");
+ var aprsTotalCountEl = document.getElementById("aprs-total-count");
+ var aprsVisibleCountEl = document.getElementById("aprs-visible-count");
+ var aprsLatestSeenEl = document.getElementById("aprs-latest-seen");
+ var APRS_BAR_WINDOW_MS = 15 * 60 * 1e3;
+ var aprsFilterText = "";
+ var aprsPacketHistory = [];
+ var aprsBarDismissedAtMs = 0;
+ var aprsOnlyPos = false;
+ var aprsHideCrc = false;
+ var aprsCollapseDup = false;
+ var aprsTypeFilter = "all";
+ function currentAprsHistoryRetentionMs() {
+ return typeof aprsWindow.getDecodeHistoryRetentionMs === "function" ? aprsWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
}
- job();
-}
-function scheduleAprsHistoryRender() {
- scheduleAprsUi("aprs-history", () => renderAprsHistory());
-}
-function scheduleAprsBarUpdate() {
- scheduleAprsUi("aprs-bar", () => updateAprsBar());
-}
-function renderAprsInfo(pkt) {
- const bytes = Array.isArray(pkt.info_bytes) ? pkt.info_bytes : null;
- if (bytes && bytes.length > 0) {
- let out2 = "";
- for (let i = 0; i < bytes.length; i++) {
- const b = bytes[i];
- if (b >= 32 && b <= 126) {
- const ch = String.fromCharCode(b);
- if (ch === "<") out2 += "<";
- else if (ch === ">") out2 += ">";
- else if (ch === "&") out2 += "&";
- else if (ch === '"') out2 += """;
- else out2 += ch;
- } else {
- const hex = b.toString(16).toUpperCase().padStart(2, "0");
- out2 += `0x${hex}`;
- }
+ function pruneAprsPacketHistory() {
+ const cutoffMs = Date.now() - currentAprsHistoryRetentionMs();
+ aprsPacketHistory = aprsPacketHistory.filter((pkt) => (pkt._tsMs ?? 0) >= cutoffMs);
+ }
+ function scheduleAprsUi(key, job) {
+ if (typeof aprsWindow.trxScheduleUiFrameJob === "function") {
+ aprsWindow.trxScheduleUiFrameJob(key, job);
+ return;
}
- return out2;
+ job();
}
- const str = pkt.info || "";
- let out = "";
- for (let i = 0; i < str.length; i++) {
- const code = str.charCodeAt(i);
- if (code >= 32 && code <= 126) {
- const ch = str[i];
- if (ch === "<") out += "<";
- else if (ch === ">") out += ">";
- else if (ch === "&") out += "&";
- else if (ch === '"') out += """;
- else out += ch;
- } else {
- const hex = code.toString(16).toUpperCase().padStart(2, "0");
- out += `0x${hex}`;
- }
- }
- return out;
-}
-function aprsPacketCategory(pkt) {
- const type = String(pkt.type || "").toLowerCase();
- const info = String(pkt.info || "").toLowerCase();
- if (pkt.lat != null && pkt.lon != null || type.includes("position")) return "position";
- if (type.includes("message") || info.startsWith(":")) return "message";
- if (type.includes("weather") || info.startsWith("_")) return "weather";
- if (type.includes("telemetry") || info.startsWith("t#")) return "telemetry";
- return "other";
-}
-function aprsCategoryLabel(category) {
- switch (category) {
- case "position":
- return "Position";
- case "message":
- return "Message";
- case "weather":
- return "Weather";
- case "telemetry":
- return "Telemetry";
- default:
- return "Other";
- }
-}
-function aprsAgeText(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 aprsDistanceText(pkt) {
- if (serverLat == null || serverLon == null || pkt.lat == null || pkt.lon == null) return "";
- const distKm = haversineKm(serverLat, serverLon, pkt.lat, pkt.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 aprsPacketSignature(pkt) {
- return [
- pkt.srcCall || "",
- pkt.destCall || "",
- pkt.path || "",
- pkt.info || "",
- pkt.type || "",
- pkt.lat != null ? pkt.lat.toFixed(4) : "",
- pkt.lon != null ? pkt.lon.toFixed(4) : ""
- ].join("|");
-}
-function aprsHexBytes(bytes) {
- if (!Array.isArray(bytes) || bytes.length === 0) return "--";
- return bytes.map((b) => Number(b).toString(16).toUpperCase().padStart(2, "0")).join(" ");
-}
-function aprsFilterMatch(pkt) {
- if (aprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false;
- if (aprsHideCrc && !pkt.crcOk) return false;
- if (aprsTypeFilter !== "all" && aprsPacketCategory(pkt) !== aprsTypeFilter) return false;
- if (!aprsFilterText) return true;
- const haystack = [
- pkt.srcCall,
- pkt.destCall,
- pkt.path,
- pkt.info,
- pkt.type,
- pkt.lat != null ? pkt.lat.toFixed(4) : "",
- pkt.lon != null ? pkt.lon.toFixed(4) : "",
- aprsPacketCategory(pkt)
- ].filter(Boolean).join(" ").toUpperCase();
- return haystack.includes(aprsFilterText);
-}
-function aprsVisiblePackets() {
- const packets = aprsCollapseDup ? collapseAprsDuplicates(aprsPacketHistory) : aprsPacketHistory;
- return packets.filter(aprsFilterMatch);
-}
-function collapseAprsDuplicates(packets) {
- const seen = /* @__PURE__ */ new Set();
- const out = [];
- for (const pkt of packets) {
- const key = aprsPacketSignature(pkt);
- if (seen.has(key)) continue;
- seen.add(key);
- out.push(pkt);
- }
- return out;
-}
-function updateAprsSummary() {
- const visible = aprsVisiblePackets();
- if (aprsTotalCountEl) {
- aprsTotalCountEl.textContent = `${aprsPacketHistory.length} total`;
- }
- if (aprsVisibleCountEl) {
- aprsVisibleCountEl.textContent = `${visible.length} shown`;
- }
- if (aprsLatestSeenEl) {
- const latest = aprsPacketHistory[0];
- if (!latest) {
- aprsLatestSeenEl.textContent = "No packets yet";
- } else {
- aprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`;
- }
- }
-}
-function updateAprsChipState() {
- document.querySelectorAll("[id^='aprs-type-']").forEach((btn) => {
- btn.classList.toggle("active", btn.id === `aprs-type-${aprsTypeFilter}`);
- });
- aprsOnlyPosBtn?.classList.toggle("active", aprsOnlyPos);
- aprsHideCrcBtn?.classList.toggle("active", aprsHideCrc);
- aprsCollapseDupBtn?.classList.toggle("active", aprsCollapseDup);
-}
-function renderAprsRow(pkt, isFresh) {
- const row = document.createElement("div");
- row.className = "aprs-packet";
- if (!pkt.crcOk) row.classList.add("aprs-packet-crc");
- if (isFresh) row.classList.add("aprs-packet-new");
- const ts = pkt._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
- const age = aprsAgeText(pkt._tsMs);
- const category = aprsPacketCategory(pkt);
- const categoryLabel = aprsCategoryLabel(category);
- const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`;
- const pathBadge = pkt.path ? `${escapeMapHtml(pkt.path)}` : "";
- const crcBadge = pkt.crcOk ? "" : 'CRC Fail';
- let symbolHtml = "";
- if (pkt.symbolTable && pkt.symbolCode) {
- const sheet = pkt.symbolTable === "/" ? 0 : 1;
- const code = pkt.symbolCode.charCodeAt(0) - 33;
- const col = code % 16;
- const row2 = Math.floor(code / 16);
- const bgX = -(col * 24);
- const bgY = -(row2 * 24);
- symbolHtml = ``;
- }
- const posLink = pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}` : "";
- const distance = aprsDistanceText(pkt);
- const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;
- row.innerHTML = `
${ts}` + symbolHtml + `${escapeMapHtml(pkt.srcCall)}>${escapeMapHtml(pkt.destCall || "")}${escapeMapHtml(categoryLabel)}` + pathBadge + crcBadge + `
${escapeMapHtml(age)}` + (distance ? `${escapeMapHtml(distance)}` : "") + `${escapeMapHtml(pkt.type || "--")}
${renderAprsInfo(pkt)}` + (posLink ? `${posLink}` : "") + `
` + (pkt.lat != null && pkt.lon != null ? `
` : "") + (pkt.lat != null && pkt.lon != null ? `
` : "") + `
QRZDetails
Source${escapeMapHtml(pkt.srcCall || "--")}Destination${escapeMapHtml(pkt.destCall || "--")}Type${escapeMapHtml(pkt.type || "--")}Path${escapeMapHtml(pkt.path || "--")}Age${escapeMapHtml(age)}CRC${pkt.crcOk ? "OK" : "Failed"}Position${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}Info${escapeMapHtml(pkt.info || "--")}Info Bytes${escapeMapHtml(aprsHexBytes(pkt.info_bytes))}
`;
- row.querySelectorAll("[data-aprs-map]").forEach((el) => {
- el.addEventListener("click", (evt) => {
- evt.preventDefault();
- const raw = String(el.dataset.aprsMap || "");
- const [lat, lon] = raw.split(",").map(Number);
- if (window.navigateToAprsMap && Number.isFinite(lat) && Number.isFinite(lon)) {
- window.navigateToAprsMap(lat, lon);
- }
+ function scheduleAprsHistoryRender() {
+ scheduleAprsUi("aprs-history", () => {
+ renderAprsHistory();
});
- });
- const copyBtn = row.querySelector("[data-aprs-copy]");
- if (copyBtn) {
- copyBtn.addEventListener("click", async () => {
- const raw = String(copyBtn.dataset.aprsCopy || "");
- try {
- if (navigator.clipboard?.writeText) {
- await navigator.clipboard.writeText(raw);
- showHint("Coordinates copied", 1200);
+ }
+ function scheduleAprsBarUpdate() {
+ scheduleAprsUi("aprs-bar", () => {
+ updateAprsBar();
+ });
+ }
+ function renderAprsInfo(pkt) {
+ const bytes = Array.isArray(pkt.info_bytes) ? pkt.info_bytes : null;
+ if (bytes && bytes.length > 0) {
+ let out2 = "";
+ for (const b of bytes) {
+ if (b >= 32 && b <= 126) {
+ const ch = String.fromCharCode(b);
+ if (ch === "<") out2 += "<";
+ else if (ch === ">") out2 += ">";
+ else if (ch === "&") out2 += "&";
+ else if (ch === '"') out2 += """;
+ else out2 += ch;
+ } else {
+ const hex = b.toString(16).toUpperCase().padStart(2, "0");
+ out2 += `0x${hex}`;
}
- } catch (_e) {
- showHint("Copy failed", 1500);
}
- });
+ return out2;
+ }
+ const str = pkt.info || "";
+ let out = "";
+ for (let i = 0; i < str.length; i++) {
+ const code = str.charCodeAt(i);
+ if (code >= 32 && code <= 126) {
+ const ch = str.charAt(i);
+ if (ch === "<") out += "<";
+ else if (ch === ">") out += ">";
+ else if (ch === "&") out += "&";
+ else if (ch === '"') out += """;
+ else out += ch;
+ } else {
+ const hex = code.toString(16).toUpperCase().padStart(2, "0");
+ out += `0x${hex}`;
+ }
+ }
+ return out;
}
- return row;
-}
-function renderAprsHistory() {
- pruneAprsPacketHistory();
- if (!aprsPacketsEl) {
+ function aprsPacketCategory(pkt) {
+ const type = (pkt.type ?? "").toLowerCase();
+ const info = (pkt.info ?? "").toLowerCase();
+ if (pkt.lat != null && pkt.lon != null || type.includes("position")) return "position";
+ if (type.includes("message") || info.startsWith(":")) return "message";
+ if (type.includes("weather") || info.startsWith("_")) return "weather";
+ if (type.includes("telemetry") || info.startsWith("t#")) return "telemetry";
+ return "other";
+ }
+ function aprsCategoryLabel(category) {
+ switch (category) {
+ case "position":
+ return "Position";
+ case "message":
+ return "Message";
+ case "weather":
+ return "Weather";
+ case "telemetry":
+ return "Telemetry";
+ default:
+ return "Other";
+ }
+ }
+ function aprsAgeText(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`;
+ }
+ function aprsDistanceText(pkt) {
+ if (aprsWindow.serverLat == null || aprsWindow.serverLon == null || pkt.lat == null || pkt.lon == null || !aprsWindow.haversineKm) return "";
+ const distKm = aprsWindow.haversineKm(aprsWindow.serverLat, aprsWindow.serverLon, pkt.lat, pkt.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 aprsPacketSignature(pkt) {
+ return [
+ pkt.srcCall || "",
+ pkt.destCall || "",
+ pkt.path || "",
+ pkt.info || "",
+ pkt.type || "",
+ pkt.lat != null ? pkt.lat.toFixed(4) : "",
+ pkt.lon != null ? pkt.lon.toFixed(4) : ""
+ ].join("|");
+ }
+ function aprsHexBytes(bytes) {
+ if (!Array.isArray(bytes) || bytes.length === 0) return "--";
+ return bytes.map((b) => b.toString(16).toUpperCase().padStart(2, "0")).join(" ");
+ }
+ function aprsFilterMatch(pkt) {
+ if (aprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false;
+ if (aprsHideCrc && !pkt.crcOk) return false;
+ if (aprsTypeFilter !== "all" && aprsPacketCategory(pkt) !== aprsTypeFilter) return false;
+ if (!aprsFilterText) return true;
+ const haystack = [
+ pkt.srcCall,
+ pkt.destCall,
+ pkt.path,
+ pkt.info,
+ pkt.type,
+ pkt.lat != null ? pkt.lat.toFixed(4) : "",
+ pkt.lon != null ? pkt.lon.toFixed(4) : "",
+ aprsPacketCategory(pkt)
+ ].filter(Boolean).join(" ").toUpperCase();
+ return haystack.includes(aprsFilterText);
+ }
+ function aprsVisiblePackets() {
+ const packets = aprsCollapseDup ? collapseAprsDuplicates(aprsPacketHistory) : aprsPacketHistory;
+ return packets.filter(aprsFilterMatch);
+ }
+ function collapseAprsDuplicates(packets) {
+ const seen = /* @__PURE__ */ new Set();
+ const out = [];
+ for (const pkt of packets) {
+ const key = aprsPacketSignature(pkt);
+ if (seen.has(key)) continue;
+ seen.add(key);
+ out.push(pkt);
+ }
+ return out;
+ }
+ function updateAprsSummary() {
+ const visible = aprsVisiblePackets();
+ if (aprsTotalCountEl) {
+ aprsTotalCountEl.textContent = `${aprsPacketHistory.length} total`;
+ }
+ if (aprsVisibleCountEl) {
+ aprsVisibleCountEl.textContent = `${visible.length} shown`;
+ }
+ if (aprsLatestSeenEl) {
+ const latest = aprsPacketHistory[0];
+ if (!latest) {
+ aprsLatestSeenEl.textContent = "No packets yet";
+ } else {
+ aprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`;
+ }
+ }
+ }
+ function updateAprsChipState() {
+ document.querySelectorAll("[id^='aprs-type-']").forEach((btn) => {
+ btn.classList.toggle("active", btn.id === `aprs-type-${aprsTypeFilter}`);
+ });
+ aprsOnlyPosBtn?.classList.toggle("active", aprsOnlyPos);
+ aprsHideCrcBtn?.classList.toggle("active", aprsHideCrc);
+ aprsCollapseDupBtn?.classList.toggle("active", aprsCollapseDup);
+ }
+ function renderAprsRow(pkt, isFresh) {
+ const row = document.createElement("div");
+ row.className = "aprs-packet";
+ if (!pkt.crcOk) row.classList.add("aprs-packet-crc");
+ if (isFresh) row.classList.add("aprs-packet-new");
+ const ts = pkt._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
+ const age = aprsAgeText(pkt._tsMs);
+ const category = aprsPacketCategory(pkt);
+ const categoryLabel = aprsCategoryLabel(category);
+ const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`;
+ const pathBadge = pkt.path ? `${escapeAprsHtml(pkt.path)}` : "";
+ const crcBadge = pkt.crcOk ? "" : 'CRC Fail';
+ let symbolHtml = "";
+ if (pkt.symbolTable && pkt.symbolCode) {
+ const symbol = escapeAprsHtml(pkt.symbolCode);
+ const table = pkt.symbolTable === "/" ? "Primary" : "Alternate";
+ symbolHtml = `${symbol}`;
+ }
+ const posLink = pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}` : "";
+ const distance = aprsDistanceText(pkt);
+ const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;
+ row.innerHTML = `${ts}` + symbolHtml + `${escapeAprsHtml(pkt.srcCall ?? "")}>${escapeAprsHtml(pkt.destCall || "")}${escapeAprsHtml(categoryLabel)}` + pathBadge + crcBadge + `
${escapeAprsHtml(age)}` + (distance ? `${escapeAprsHtml(distance)}` : "") + `${escapeAprsHtml(pkt.type || "--")}
${renderAprsInfo(pkt)}` + (posLink ? `${posLink}` : "") + `
` + (pkt.lat != null && pkt.lon != null ? `
` : "") + (pkt.lat != null && pkt.lon != null ? `
` : "") + `
QRZDetails
Source${escapeAprsHtml(pkt.srcCall || "--")}Destination${escapeAprsHtml(pkt.destCall || "--")}Type${escapeAprsHtml(pkt.type || "--")}Path${escapeAprsHtml(pkt.path || "--")}Age${escapeAprsHtml(age)}CRC${pkt.crcOk ? "OK" : "Failed"}Position${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}Info${escapeAprsHtml(pkt.info || "--")}Info Bytes${escapeAprsHtml(aprsHexBytes(pkt.info_bytes))}
`;
+ row.querySelectorAll("[data-aprs-map]").forEach((el) => {
+ el.addEventListener("click", (evt) => {
+ evt.preventDefault();
+ const raw = el.dataset.aprsMap ?? "";
+ const [lat, lon] = raw.split(",").map(Number);
+ if (aprsWindow.navigateToAprsMap && typeof lat === "number" && typeof lon === "number" && Number.isFinite(lat) && Number.isFinite(lon)) {
+ aprsWindow.navigateToAprsMap(lat, lon);
+ }
+ });
+ });
+ const copyBtn = row.querySelector("[data-aprs-copy]");
+ if (copyBtn) {
+ copyBtn.addEventListener("click", () => {
+ void (async () => {
+ const raw = copyBtn.dataset.aprsCopy ?? "";
+ try {
+ const clipboard = Reflect.get(navigator, "clipboard");
+ if (clipboard) {
+ await clipboard.writeText(raw);
+ showAprsHint("Coordinates copied", 1200);
+ }
+ } catch {
+ showAprsHint("Copy failed", 1500);
+ }
+ })();
+ });
+ }
+ return row;
+ }
+ function renderAprsHistory() {
+ pruneAprsPacketHistory();
+ if (!aprsPacketsEl) {
+ updateAprsSummary();
+ updateAprsChipState();
+ return;
+ }
+ const visible = aprsVisiblePackets();
+ const fragment = document.createDocumentFragment();
+ for (const [index, packet] of visible.entries()) {
+ fragment.appendChild(renderAprsRow(packet, index === 0));
+ }
+ aprsPacketsEl.replaceChildren(fragment);
updateAprsSummary();
updateAprsChipState();
- return;
}
- const visible = aprsVisiblePackets();
- const fragment = document.createDocumentFragment();
- for (let i = 0; i < visible.length; i++) {
- fragment.appendChild(renderAprsRow(visible[i], i === 0));
- }
- aprsPacketsEl.replaceChildren(fragment);
- updateAprsSummary();
- updateAprsChipState();
-}
-function updateAprsBar() {
- if (!aprsBarOverlay) return;
- const isPkt = (document.getElementById("mode")?.value || "").toUpperCase() === "PKT";
- const cutoffMs = Date.now() - APRS_BAR_WINDOW_MS;
- const okFrames = aprsPacketHistory.filter((p) => p.crcOk && p._tsMs >= cutoffMs);
- const frames = collapseAprsDuplicates(okFrames).slice(0, 8);
- const newestTsMs = frames.reduce((latest, pkt) => Math.max(latest, Number(pkt._tsMs) || 0), 0);
- if (!isPkt || frames.length === 0 || newestTsMs <= aprsBarDismissedAtMs) {
- aprsBarOverlay.style.display = "none";
- aprsBarOverlay.innerHTML = "";
- return;
- }
- let html = ``;
- for (const pkt of frames) {
- const ts = pkt._ts ? `${pkt._ts}` : "";
- const call = `${escapeMapHtml(pkt.srcCall)}`;
- const dest = escapeMapHtml(pkt.destCall || "");
- const info = escapeMapHtml(pkt.info || "");
- const pin = pkt.lat != null && pkt.lon != null ? `` : "";
- html += `${ts}${pin}${call}>${dest}: ${info}
`;
- }
- aprsBarOverlay.innerHTML = html;
- aprsBarOverlay.style.display = "flex";
-}
-window.updateAprsBar = updateAprsBar;
-window.clearAprsBar = function() {
- window.resetAprsHistoryView();
-};
-window.closeAprsBar = function() {
- aprsBarDismissedAtMs = Date.now();
- if (aprsBarOverlay) {
- aprsBarOverlay.style.display = "none";
- aprsBarOverlay.innerHTML = "";
- }
-};
-window.resetAprsHistoryView = function() {
- if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
- aprsPacketHistory = [];
- updateAprsBar();
- renderAprsHistory();
- if (window.clearMapMarkersByType) window.clearMapMarkersByType("aprs");
-};
-window.pruneAprsHistoryView = function() {
- pruneAprsPacketHistory();
- updateAprsBar();
- renderAprsHistory();
-};
-function addAprsPacket(pkt) {
- const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
- pkt._tsMs = tsMs;
- pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
- aprsPacketHistory.unshift(pkt);
- pruneAprsPacketHistory();
- if (pkt.lat != null && pkt.lon != null && window.aprsMapAddStation) {
- window.aprsMapAddStation(pkt.srcCall, pkt.lat, pkt.lon, pkt.info, pkt.symbolTable, pkt.symbolCode, pkt);
- }
- if (pkt.crcOk) scheduleAprsBarUpdate();
- scheduleAprsHistoryRender();
-}
-function normalizeServerAprsPacket(pkt) {
- return {
- rig_id: pkt.rig_id || null,
- receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null,
- srcCall: pkt.src_call,
- destCall: pkt.dest_call,
- path: pkt.path,
- info: pkt.info,
- info_bytes: pkt.info_bytes,
- type: pkt.packet_type,
- crcOk: pkt.crc_ok,
- ts_ms: pkt.ts_ms,
- lat: pkt.lat,
- lon: pkt.lon,
- symbolTable: pkt.symbol_table,
- symbolCode: pkt.symbol_code
- };
-}
-window.onServerAprsBatch = function(packets) {
- if (!Array.isArray(packets) || packets.length === 0) return;
- aprsStatus.textContent = "Receiving";
- const normalized = [];
- let hasCrcOk = false;
- for (const pkt of packets) {
- const next = normalizeServerAprsPacket(pkt);
- 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 && window.aprsMapAddStation) {
- window.aprsMapAddStation(next.srcCall, next.lat, next.lon, next.info, next.symbolTable, next.symbolCode, next);
+ function updateAprsBar() {
+ if (!aprsBarOverlay) return;
+ const isPkt = (document.getElementById("mode")?.value || "").toUpperCase() === "PKT";
+ const cutoffMs = Date.now() - APRS_BAR_WINDOW_MS;
+ const okFrames = aprsPacketHistory.filter((p) => p.crcOk && (p._tsMs ?? 0) >= cutoffMs);
+ const frames = collapseAprsDuplicates(okFrames).slice(0, 8);
+ const newestTsMs = frames.reduce((latest, pkt) => Math.max(latest, Number(pkt._tsMs) || 0), 0);
+ if (!isPkt || frames.length === 0 || newestTsMs <= aprsBarDismissedAtMs) {
+ aprsBarOverlay.style.display = "none";
+ aprsBarOverlay.innerHTML = "";
+ return;
}
- if (next.crcOk) hasCrcOk = true;
- normalized.push(next);
+ let html = ``;
+ for (const pkt of frames) {
+ const ts = pkt._ts ? `${pkt._ts}` : "";
+ const call = `${escapeAprsHtml(pkt.srcCall ?? "")}`;
+ const dest = escapeAprsHtml(pkt.destCall || "");
+ const info = escapeAprsHtml(pkt.info || "");
+ const pin = pkt.lat != null && pkt.lon != null ? `` : "";
+ html += `${ts}${pin}${call}>${dest}: ${info}
`;
+ }
+ aprsBarOverlay.innerHTML = html;
+ aprsBarOverlay.style.display = "flex";
}
- normalized.reverse();
- aprsPacketHistory = normalized.concat(aprsPacketHistory);
- pruneAprsPacketHistory();
- if (hasCrcOk) scheduleAprsBarUpdate();
- scheduleAprsHistoryRender();
-};
-window.restoreAprsHistory = function(packets) {
- window.onServerAprsBatch(packets);
-};
-document.getElementById("settings-clear-aprs-history")?.addEventListener("click", async () => {
- if (!await window.trxUi.confirm({ title: "Clear APRS history?", message: "All stored APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
- try {
- await postPath("/clear_aprs_decode");
- window.resetAprsHistoryView();
- } catch (e) {
- console.error("APRS history clear failed", e);
+ aprsWindow.updateAprsBar = updateAprsBar;
+ aprsWindow.clearAprsBar = function() {
+ aprsWindow.resetAprsHistoryView?.();
+ };
+ aprsWindow.closeAprsBar = function() {
+ aprsBarDismissedAtMs = Date.now();
+ if (aprsBarOverlay) {
+ aprsBarOverlay.style.display = "none";
+ aprsBarOverlay.innerHTML = "";
+ }
+ };
+ aprsWindow.resetAprsHistoryView = function() {
+ if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
+ aprsPacketHistory = [];
+ updateAprsBar();
+ renderAprsHistory();
+ aprsWindow.clearMapMarkersByType?.("aprs");
+ };
+ aprsWindow.pruneAprsHistoryView = function() {
+ pruneAprsPacketHistory();
+ updateAprsBar();
+ renderAprsHistory();
+ };
+ function addAprsPacket(pkt) {
+ const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
+ pkt._tsMs = tsMs;
+ pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
+ aprsPacketHistory.unshift(pkt);
+ pruneAprsPacketHistory();
+ if (pkt.lat != null && pkt.lon != null && aprsWindow.aprsMapAddStation) {
+ aprsWindow.aprsMapAddStation(pkt.srcCall ?? "", pkt.lat, pkt.lon, pkt.info ?? "", pkt.symbolTable, pkt.symbolCode, pkt);
+ }
+ if (pkt.crcOk) scheduleAprsBarUpdate();
+ scheduleAprsHistoryRender();
}
-});
-if (aprsOnlyPosBtn) {
- aprsOnlyPosBtn.addEventListener("click", () => {
- aprsOnlyPos = !aprsOnlyPos;
- renderAprsHistory();
+ function normalizeServerAprsPacket(pkt) {
+ return {
+ rig_id: pkt.rig_id || null,
+ receiver: aprsWindow.getDecodeRigMeta?.() ?? null,
+ srcCall: pkt.src_call ?? "",
+ destCall: pkt.dest_call ?? "",
+ path: pkt.path ?? "",
+ info: pkt.info ?? "",
+ info_bytes: pkt.info_bytes ?? [],
+ type: pkt.packet_type ?? "",
+ crcOk: pkt.crc_ok ?? false,
+ ts_ms: pkt.ts_ms ?? null,
+ lat: pkt.lat ?? null,
+ lon: pkt.lon ?? null,
+ symbolTable: pkt.symbol_table ?? null,
+ symbolCode: pkt.symbol_code ?? null
+ };
+ }
+ aprsWindow.onServerAprsBatch = function(packets) {
+ if (!Array.isArray(packets) || packets.length === 0) return;
+ if (aprsStatus) aprsStatus.textContent = "Receiving";
+ const normalized = [];
+ let hasCrcOk = false;
+ for (const pkt of packets) {
+ const next = normalizeServerAprsPacket(pkt);
+ 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 && aprsWindow.aprsMapAddStation) {
+ aprsWindow.aprsMapAddStation(next.srcCall ?? "", next.lat, next.lon, next.info ?? "", next.symbolTable, next.symbolCode, next);
+ }
+ if (next.crcOk) hasCrcOk = true;
+ normalized.push(next);
+ }
+ normalized.reverse();
+ aprsPacketHistory = normalized.concat(aprsPacketHistory);
+ pruneAprsPacketHistory();
+ if (hasCrcOk) scheduleAprsBarUpdate();
+ scheduleAprsHistoryRender();
+ };
+ aprsWindow.restoreAprsHistory = function(packets) {
+ aprsWindow.onServerAprsBatch?.(packets);
+ };
+ document.getElementById("settings-clear-aprs-history")?.addEventListener("click", () => {
+ void (async () => {
+ if (!await aprsWindow.trxUi.confirm({ title: "Clear APRS history?", message: "All stored APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
+ try {
+ await aprsWindow.postPath?.("/clear_aprs_decode");
+ aprsWindow.resetAprsHistoryView?.();
+ } catch (e) {
+ console.error("APRS history clear failed", e);
+ }
+ })();
});
-}
-if (aprsHideCrcBtn) {
- aprsHideCrcBtn.addEventListener("click", () => {
- aprsHideCrc = !aprsHideCrc;
- renderAprsHistory();
+ if (aprsOnlyPosBtn) {
+ aprsOnlyPosBtn.addEventListener("click", () => {
+ aprsOnlyPos = !aprsOnlyPos;
+ renderAprsHistory();
+ });
+ }
+ if (aprsHideCrcBtn) {
+ aprsHideCrcBtn.addEventListener("click", () => {
+ aprsHideCrc = !aprsHideCrc;
+ renderAprsHistory();
+ });
+ }
+ if (aprsCollapseDupBtn) {
+ aprsCollapseDupBtn.addEventListener("click", () => {
+ aprsCollapseDup = !aprsCollapseDup;
+ renderAprsHistory();
+ });
+ }
+ ["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => {
+ const btn = document.getElementById(`aprs-type-${type}`);
+ if (!btn) return;
+ btn.addEventListener("click", () => {
+ aprsTypeFilter = type;
+ renderAprsHistory();
+ });
});
-}
-if (aprsCollapseDupBtn) {
- aprsCollapseDupBtn.addEventListener("click", () => {
- aprsCollapseDup = !aprsCollapseDup;
- renderAprsHistory();
- });
-}
-["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => {
- const btn = document.getElementById(`aprs-type-${type}`);
- if (!btn) return;
- btn.addEventListener("click", () => {
- aprsTypeFilter = type;
- renderAprsHistory();
- });
-});
-if (aprsFilterInput) {
- aprsFilterInput.addEventListener("input", () => {
- aprsFilterText = aprsFilterInput.value.trim().toUpperCase();
- renderAprsHistory();
- });
-}
-window.onServerAprs = function(pkt) {
- aprsStatus.textContent = "Receiving";
- addAprsPacket(normalizeServerAprsPacket(pkt));
-};
-renderAprsHistory();
-if (window._trxDrainPendingDecode) window._trxDrainPendingDecode("aprs");
+ if (aprsFilterInput) {
+ aprsFilterInput.addEventListener("input", () => {
+ aprsFilterText = aprsFilterInput.value.trim().toUpperCase();
+ renderAprsHistory();
+ });
+ }
+ aprsWindow.onServerAprs = function(pkt) {
+ if (aprsStatus) aprsStatus.textContent = "Receiving";
+ addAprsPacket(normalizeServerAprsPacket(pkt));
+ };
+ renderAprsHistory();
+ aprsWindow._trxDrainPendingDecode?.("aprs");
+})();
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 3f802303..9f5535aa 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", "/ais.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", "/aprs.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/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
index f5e18207..480ba033 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
@@ -2521,6 +2521,7 @@ body.map-fake-fullscreen-active {
word-break: break-all;
}
.aprs-symbol { display: inline-block; width: 24px; height: 24px; background-size: 384px 192px; vertical-align: middle; margin-right: 0.3rem; }
+.aprs-symbol-local { border: 1px solid var(--border); border-radius: 4px; background: var(--surface-raised); color: var(--text); font: 700 0.8rem/22px ui-monospace, monospace; text-align: center; }
.aprs-pos { color: var(--accent-green); text-decoration: none; margin-left: 0.3rem; font-size: 0.8rem; }
.aprs-pos:hover { text-decoration: underline; }
.leaflet-popup-content-wrapper, .leaflet-popup-tip { background: color-mix(in srgb, var(--card-bg) 84%, transparent) !important; color: var(--text) !important; box-shadow: 0 3px 14px rgba(0,0,0,0.45) !important; }
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 5a505e65..ecfdf169 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"),
- aprs: path.join(sourceDir, "plugins", "aprs.js"),
bookmarks: path.join(sourceDir, "plugins", "bookmarks.js"),
"hf-aprs": path.join(sourceDir, "plugins", "hf-aprs.js"),
sat: path.join(sourceDir, "plugins", "sat.js"),
@@ -51,6 +50,7 @@ await build({
wefax: path.join(sourceDir, "plugins", "wefax.ts"),
"background-decode": path.join(sourceDir, "plugins", "background-decode.ts"),
ais: path.join(sourceDir, "plugins", "ais.ts"),
+ aprs: path.join(sourceDir, "plugins", "aprs.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 217606b9..8b836047 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", "/ais.js"]);
+const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.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/aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts
similarity index 59%
rename from src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.js
rename to src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts
index de616b61..70dc6f38 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts
@@ -2,10 +2,68 @@
//
// SPDX-License-Identifier: GPL-2.0-or-later
+export {};
+
+type AprsCategory = "position" | "message" | "weather" | "telemetry" | "other";
+type AprsTypeFilter = "all" | AprsCategory;
+interface AprsPacket {
+ rig_id?: string | null;
+ receiver?: unknown;
+ srcCall?: string;
+ destCall?: string;
+ path?: string;
+ info?: string;
+ info_bytes?: number[];
+ type?: string;
+ crcOk?: boolean;
+ ts_ms?: number | null;
+ lat?: number | null;
+ lon?: number | null;
+ symbolTable?: string | null;
+ symbolCode?: string | null;
+ _tsMs?: number;
+ _ts?: string;
+ src_call?: string;
+ dest_call?: string;
+ packet_type?: string;
+ crc_ok?: boolean;
+ symbol_table?: string | null;
+ symbol_code?: string | null;
+}
+interface AprsBridge {
+ getDecodeHistoryRetentionMs?: () => number;
+ trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
+ serverLat?: number | null;
+ serverLon?: number | null;
+ haversineKm?: (lat1: number, lon1: number, lat2: number, lon2: number) => number;
+ escapeMapHtml?: (input: string) => string;
+ navigateToAprsMap?: (lat: number, lon: number) => void;
+ showHint?: (message: string, durationMs: number) => void;
+ clearMapMarkersByType?: (type: string) => void;
+ aprsMapAddStation?: (call: string, lat: number, lon: number, info: string, symbolTable: string | null | undefined, symbolCode: string | null | undefined, packet: AprsPacket) => void;
+ getDecodeRigMeta?: () => unknown;
+ postPath?: (path: string) => Promise;
+ trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise };
+ updateAprsBar?: () => void;
+ clearAprsBar?: () => void;
+ closeAprsBar?: () => void;
+ resetAprsHistoryView?: () => void;
+ pruneAprsHistoryView?: () => void;
+ onServerAprsBatch?: (packets: AprsPacket[]) => void;
+ restoreAprsHistory?: (packets: AprsPacket[]) => void;
+ onServerAprs?: (packet: AprsPacket) => void;
+ _trxDrainPendingDecode?: (decoder: string) => void;
+}
+const aprsWindow = window as unknown as AprsBridge;
+const escapeAprsHtml = (input: string): string => aprsWindow.escapeMapHtml?.(input) ?? input
+ .replaceAll("&", "&").replaceAll("<", "<")
+ .replaceAll(">", ">").replaceAll('"', """);
+const showAprsHint = (message: string, durationMs: number): void => { aprsWindow.showHint?.(message, durationMs); };
+
// --- APRS Decoder Plugin (server-side decode) ---
const aprsStatus = document.getElementById("aprs-status");
const aprsPacketsEl = document.getElementById("aprs-packets");
-const aprsFilterInput = document.getElementById("aprs-filter");
+const aprsFilterInput = document.getElementById("aprs-filter") as HTMLInputElement | null;
const aprsBarOverlay = document.getElementById("aprs-bar-overlay");
const aprsOnlyPosBtn = document.getElementById("aprs-only-pos-btn");
const aprsHideCrcBtn = document.getElementById("aprs-hide-crc-btn");
@@ -15,46 +73,45 @@ const aprsVisibleCountEl = document.getElementById("aprs-visible-count");
const aprsLatestSeenEl = document.getElementById("aprs-latest-seen");
const APRS_BAR_WINDOW_MS = 15 * 60 * 1000;
let aprsFilterText = "";
-let aprsPacketHistory = [];
+let aprsPacketHistory: AprsPacket[] = [];
let aprsBarDismissedAtMs = 0;
let aprsOnlyPos = false;
let aprsHideCrc = false;
let aprsCollapseDup = false;
-let aprsTypeFilter = "all";
+let aprsTypeFilter: AprsTypeFilter = "all";
-function currentAprsHistoryRetentionMs() {
- return typeof window.getDecodeHistoryRetentionMs === "function"
- ? window.getDecodeHistoryRetentionMs()
+function currentAprsHistoryRetentionMs(): number {
+ return typeof aprsWindow.getDecodeHistoryRetentionMs === "function"
+ ? aprsWindow.getDecodeHistoryRetentionMs()
: 24 * 60 * 60 * 1000;
}
function pruneAprsPacketHistory() {
const cutoffMs = Date.now() - currentAprsHistoryRetentionMs();
- aprsPacketHistory = aprsPacketHistory.filter((pkt) => Number(pkt?._tsMs) >= cutoffMs);
+ aprsPacketHistory = aprsPacketHistory.filter((pkt) => (pkt._tsMs ?? 0) >= cutoffMs);
}
-function scheduleAprsUi(key, job) {
- if (typeof window.trxScheduleUiFrameJob === "function") {
- window.trxScheduleUiFrameJob(key, job);
+function scheduleAprsUi(key: string, job: () => void): void {
+ if (typeof aprsWindow.trxScheduleUiFrameJob === "function") {
+ aprsWindow.trxScheduleUiFrameJob(key, job);
return;
}
job();
}
function scheduleAprsHistoryRender() {
- scheduleAprsUi("aprs-history", () => renderAprsHistory());
+ scheduleAprsUi("aprs-history", () => { renderAprsHistory(); });
}
function scheduleAprsBarUpdate() {
- scheduleAprsUi("aprs-bar", () => updateAprsBar());
+ scheduleAprsUi("aprs-bar", () => { updateAprsBar(); });
}
-function renderAprsInfo(pkt) {
+function renderAprsInfo(pkt: AprsPacket): string {
const bytes = Array.isArray(pkt.info_bytes) ? pkt.info_bytes : null;
if (bytes && bytes.length > 0) {
let out = "";
- for (let i = 0; i < bytes.length; i++) {
- const b = bytes[i];
+ for (const b of bytes) {
if (b >= 0x20 && b <= 0x7e) {
const ch = String.fromCharCode(b);
if (ch === "<") out += "<";
@@ -74,7 +131,7 @@ function renderAprsInfo(pkt) {
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
if (code >= 0x20 && code <= 0x7e) {
- const ch = str[i];
+ const ch = str.charAt(i);
if (ch === "<") out += "<";
else if (ch === ">") out += ">";
else if (ch === "&") out += "&";
@@ -88,9 +145,9 @@ function renderAprsInfo(pkt) {
return out;
}
-function aprsPacketCategory(pkt) {
- const type = String(pkt.type || "").toLowerCase();
- const info = String(pkt.info || "").toLowerCase();
+function aprsPacketCategory(pkt: AprsPacket): AprsCategory {
+ const type = (pkt.type ?? "").toLowerCase();
+ const info = (pkt.info ?? "").toLowerCase();
if (pkt.lat != null && pkt.lon != null || type.includes("position")) return "position";
if (type.includes("message") || info.startsWith(":")) return "message";
if (type.includes("weather") || info.startsWith("_")) return "weather";
@@ -98,7 +155,7 @@ function aprsPacketCategory(pkt) {
return "other";
}
-function aprsCategoryLabel(category) {
+function aprsCategoryLabel(category: AprsCategory): string {
switch (category) {
case "position": return "Position";
case "message": return "Message";
@@ -108,8 +165,8 @@ function aprsCategoryLabel(category) {
}
}
-function aprsAgeText(tsMs) {
- if (!Number.isFinite(tsMs)) return "just now";
+function aprsAgeText(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,15 +177,15 @@ function aprsAgeText(tsMs) {
return `${hours}h ago`;
}
-function aprsDistanceText(pkt) {
- if (serverLat == null || serverLon == null || pkt.lat == null || pkt.lon == null) return "";
- const distKm = haversineKm(serverLat, serverLon, pkt.lat, pkt.lon);
+function aprsDistanceText(pkt: AprsPacket): string {
+ if (aprsWindow.serverLat == null || aprsWindow.serverLon == null || pkt.lat == null || pkt.lon == null || !aprsWindow.haversineKm) return "";
+ const distKm = aprsWindow.haversineKm(aprsWindow.serverLat, aprsWindow.serverLon, pkt.lat, pkt.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 aprsPacketSignature(pkt) {
+function aprsPacketSignature(pkt: AprsPacket): string {
return [
pkt.srcCall || "",
pkt.destCall || "",
@@ -140,12 +197,12 @@ function aprsPacketSignature(pkt) {
].join("|");
}
-function aprsHexBytes(bytes) {
+function aprsHexBytes(bytes: number[] | undefined): string {
if (!Array.isArray(bytes) || bytes.length === 0) return "--";
- return bytes.map((b) => Number(b).toString(16).toUpperCase().padStart(2, "0")).join(" ");
+ return bytes.map((b) => b.toString(16).toUpperCase().padStart(2, "0")).join(" ");
}
-function aprsFilterMatch(pkt) {
+function aprsFilterMatch(pkt: AprsPacket): boolean {
if (aprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false;
if (aprsHideCrc && !pkt.crcOk) return false;
if (aprsTypeFilter !== "all" && aprsPacketCategory(pkt) !== aprsTypeFilter) return false;
@@ -166,14 +223,14 @@ function aprsFilterMatch(pkt) {
return haystack.includes(aprsFilterText);
}
-function aprsVisiblePackets() {
+function aprsVisiblePackets(): AprsPacket[] {
const packets = aprsCollapseDup ? collapseAprsDuplicates(aprsPacketHistory) : aprsPacketHistory;
return packets.filter(aprsFilterMatch);
}
-function collapseAprsDuplicates(packets) {
+function collapseAprsDuplicates(packets: AprsPacket[]): AprsPacket[] {
const seen = new Set();
- const out = [];
+ const out: AprsPacket[] = [];
for (const pkt of packets) {
const key = aprsPacketSignature(pkt);
if (seen.has(key)) continue;
@@ -210,7 +267,7 @@ function updateAprsChipState() {
aprsCollapseDupBtn?.classList.toggle("active", aprsCollapseDup);
}
-function renderAprsRow(pkt, isFresh) {
+function renderAprsRow(pkt: AprsPacket, isFresh: boolean): HTMLElement {
const row = document.createElement("div");
row.className = "aprs-packet";
if (!pkt.crcOk) row.classList.add("aprs-packet-crc");
@@ -221,17 +278,13 @@ function renderAprsRow(pkt, isFresh) {
const category = aprsPacketCategory(pkt);
const categoryLabel = aprsCategoryLabel(category);
const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`;
- const pathBadge = pkt.path ? `${escapeMapHtml(pkt.path)}` : "";
+ const pathBadge = pkt.path ? `${escapeAprsHtml(pkt.path)}` : "";
const crcBadge = pkt.crcOk ? "" : 'CRC Fail';
let symbolHtml = "";
if (pkt.symbolTable && pkt.symbolCode) {
- const sheet = pkt.symbolTable === "/" ? 0 : 1;
- const code = pkt.symbolCode.charCodeAt(0) - 33;
- const col = code % 16;
- const row2 = Math.floor(code / 16);
- const bgX = -(col * 24);
- const bgY = -(row2 * 24);
- symbolHtml = ``;
+ const symbol = escapeAprsHtml(pkt.symbolCode);
+ const table = pkt.symbolTable === "/" ? "Primary" : "Alternate";
+ symbolHtml = `${symbol}`;
}
const posLink = pkt.lat != null && pkt.lon != null
? `${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}`
@@ -243,19 +296,19 @@ function renderAprsRow(pkt, isFresh) {
`` +
`${ts}` +
symbolHtml +
- `${escapeMapHtml(pkt.srcCall)}` +
- `>${escapeMapHtml(pkt.destCall || "")}` +
- `${escapeMapHtml(categoryLabel)}` +
+ `${escapeAprsHtml(pkt.srcCall ?? "")}` +
+ `>${escapeAprsHtml(pkt.destCall || "")}` +
+ `${escapeAprsHtml(categoryLabel)}` +
pathBadge +
crcBadge +
`
` +
`` +
- `${escapeMapHtml(age)}` +
- (distance ? `${escapeMapHtml(distance)}` : "") +
- `${escapeMapHtml(pkt.type || "--")}` +
+ `${escapeAprsHtml(age)}` +
+ (distance ? `${escapeAprsHtml(distance)}` : "") +
+ `${escapeAprsHtml(pkt.type || "--")}` +
`
` +
`` +
- `${renderAprsInfo(pkt)}` +
+ `${renderAprsInfo(pkt)}` +
(posLink ? `${posLink}` : "") +
`
` +
`` +
@@ -266,42 +319,43 @@ function renderAprsRow(pkt, isFresh) {
`
` +
`Details
` +
`` +
- `Source${escapeMapHtml(pkt.srcCall || "--")}` +
- `Destination${escapeMapHtml(pkt.destCall || "--")}` +
- `Type${escapeMapHtml(pkt.type || "--")}` +
- `Path${escapeMapHtml(pkt.path || "--")}` +
- `Age${escapeMapHtml(age)}` +
+ `Source${escapeAprsHtml(pkt.srcCall || "--")}` +
+ `Destination${escapeAprsHtml(pkt.destCall || "--")}` +
+ `Type${escapeAprsHtml(pkt.type || "--")}` +
+ `Path${escapeAprsHtml(pkt.path || "--")}` +
+ `Age${escapeAprsHtml(age)}` +
`CRC${pkt.crcOk ? "OK" : "Failed"}` +
`Position${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}` +
- `Info${escapeMapHtml(pkt.info || "--")}` +
- `Info Bytes${escapeMapHtml(aprsHexBytes(pkt.info_bytes))}` +
+ `Info${escapeAprsHtml(pkt.info || "--")}` +
+ `Info Bytes${escapeAprsHtml(aprsHexBytes(pkt.info_bytes))}` +
`
` +
` `;
- row.querySelectorAll("[data-aprs-map]").forEach((el) => {
+ row.querySelectorAll
("[data-aprs-map]").forEach((el) => {
el.addEventListener("click", (evt) => {
evt.preventDefault();
- const raw = String(el.dataset.aprsMap || "");
+ const raw = el.dataset.aprsMap ?? "";
const [lat, lon] = raw.split(",").map(Number);
- if (window.navigateToAprsMap && Number.isFinite(lat) && Number.isFinite(lon)) {
- window.navigateToAprsMap(lat, lon);
+ if (aprsWindow.navigateToAprsMap && typeof lat === "number" && typeof lon === "number" && Number.isFinite(lat) && Number.isFinite(lon)) {
+ aprsWindow.navigateToAprsMap(lat, lon);
}
});
});
- const copyBtn = row.querySelector("[data-aprs-copy]");
+ const copyBtn = row.querySelector("[data-aprs-copy]");
if (copyBtn) {
- copyBtn.addEventListener("click", async () => {
- const raw = String(copyBtn.dataset.aprsCopy || "");
+ copyBtn.addEventListener("click", () => { void (async () => {
+ const raw = copyBtn.dataset.aprsCopy ?? "";
try {
- if (navigator.clipboard?.writeText) {
- await navigator.clipboard.writeText(raw);
- showHint("Coordinates copied", 1200);
+ const clipboard = Reflect.get(navigator, "clipboard") as Clipboard | undefined;
+ if (clipboard) {
+ await clipboard.writeText(raw);
+ showAprsHint("Coordinates copied", 1200);
}
- } catch (_e) {
- showHint("Copy failed", 1500);
+ } catch {
+ showAprsHint("Copy failed", 1500);
}
- });
+ })(); });
}
return row;
@@ -316,8 +370,8 @@ function renderAprsHistory() {
}
const visible = aprsVisiblePackets();
const fragment = document.createDocumentFragment();
- for (let i = 0; i < visible.length; i++) {
- fragment.appendChild(renderAprsRow(visible[i], i === 0));
+ for (const [index, packet] of visible.entries()) {
+ fragment.appendChild(renderAprsRow(packet, index === 0));
}
aprsPacketsEl.replaceChildren(fragment);
updateAprsSummary();
@@ -326,9 +380,9 @@ function renderAprsHistory() {
function updateAprsBar() {
if (!aprsBarOverlay) return;
- const isPkt = (document.getElementById("mode")?.value || "").toUpperCase() === "PKT";
+ const isPkt = ((document.getElementById("mode") as HTMLSelectElement | null)?.value || "").toUpperCase() === "PKT";
const cutoffMs = Date.now() - APRS_BAR_WINDOW_MS;
- const okFrames = aprsPacketHistory.filter((p) => p.crcOk && p._tsMs >= cutoffMs);
+ const okFrames = aprsPacketHistory.filter((p) => p.crcOk && (p._tsMs ?? 0) >= cutoffMs);
const frames = collapseAprsDuplicates(okFrames).slice(0, 8);
const newestTsMs = frames.reduce((latest, pkt) => Math.max(latest, Number(pkt._tsMs) || 0), 0);
if (!isPkt || frames.length === 0 || newestTsMs <= aprsBarDismissedAtMs) {
@@ -339,9 +393,9 @@ function updateAprsBar() {
let html = '';
for (const pkt of frames) {
const ts = pkt._ts ? `${pkt._ts}` : "";
- const call = `${escapeMapHtml(pkt.srcCall)}`;
- const dest = escapeMapHtml(pkt.destCall || "");
- const info = escapeMapHtml(pkt.info || "");
+ const call = `${escapeAprsHtml(pkt.srcCall ?? "")}`;
+ const dest = escapeAprsHtml(pkt.destCall || "");
+ const info = escapeAprsHtml(pkt.info || "");
const pin = pkt.lat != null && pkt.lon != null
? ``
: "";
@@ -352,11 +406,11 @@ function updateAprsBar() {
aprsBarOverlay.innerHTML = html;
aprsBarOverlay.style.display = "flex";
}
-window.updateAprsBar = updateAprsBar;
-window.clearAprsBar = function() {
- window.resetAprsHistoryView();
+aprsWindow.updateAprsBar = updateAprsBar;
+aprsWindow.clearAprsBar = function() {
+ aprsWindow.resetAprsHistoryView?.();
};
-window.closeAprsBar = function() {
+aprsWindow.closeAprsBar = function() {
aprsBarDismissedAtMs = Date.now();
if (aprsBarOverlay) {
aprsBarOverlay.style.display = "none";
@@ -364,21 +418,21 @@ window.closeAprsBar = function() {
}
};
-window.resetAprsHistoryView = function() {
+aprsWindow.resetAprsHistoryView = function() {
if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
aprsPacketHistory = [];
updateAprsBar();
renderAprsHistory();
- if (window.clearMapMarkersByType) window.clearMapMarkersByType("aprs");
+ aprsWindow.clearMapMarkersByType?.("aprs");
};
-window.pruneAprsHistoryView = function() {
+aprsWindow.pruneAprsHistoryView = function() {
pruneAprsPacketHistory();
updateAprsBar();
renderAprsHistory();
};
-function addAprsPacket(pkt) {
+function addAprsPacket(pkt: AprsPacket): void {
const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
pkt._tsMs = tsMs;
pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
@@ -386,8 +440,8 @@ function addAprsPacket(pkt) {
aprsPacketHistory.unshift(pkt);
pruneAprsPacketHistory();
- if (pkt.lat != null && pkt.lon != null && window.aprsMapAddStation) {
- window.aprsMapAddStation(pkt.srcCall, pkt.lat, pkt.lon, pkt.info, pkt.symbolTable, pkt.symbolCode, pkt);
+ if (pkt.lat != null && pkt.lon != null && aprsWindow.aprsMapAddStation) {
+ aprsWindow.aprsMapAddStation(pkt.srcCall ?? "", pkt.lat, pkt.lon, pkt.info ?? "", pkt.symbolTable, pkt.symbolCode, pkt);
}
if (pkt.crcOk) scheduleAprsBarUpdate();
@@ -395,37 +449,37 @@ function addAprsPacket(pkt) {
scheduleAprsHistoryRender();
}
-function normalizeServerAprsPacket(pkt) {
+function normalizeServerAprsPacket(pkt: AprsPacket): AprsPacket {
return {
rig_id: pkt.rig_id || null,
- receiver: window.getDecodeRigMeta ? window.getDecodeRigMeta() : null,
- srcCall: pkt.src_call,
- destCall: pkt.dest_call,
- path: pkt.path,
- info: pkt.info,
- info_bytes: pkt.info_bytes,
- type: pkt.packet_type,
- crcOk: pkt.crc_ok,
- ts_ms: pkt.ts_ms,
- lat: pkt.lat,
- lon: pkt.lon,
- symbolTable: pkt.symbol_table,
- symbolCode: pkt.symbol_code,
+ receiver: aprsWindow.getDecodeRigMeta?.() ?? null,
+ srcCall: pkt.src_call ?? "",
+ destCall: pkt.dest_call ?? "",
+ path: pkt.path ?? "",
+ info: pkt.info ?? "",
+ info_bytes: pkt.info_bytes ?? [],
+ type: pkt.packet_type ?? "",
+ crcOk: pkt.crc_ok ?? false,
+ ts_ms: pkt.ts_ms ?? null,
+ lat: pkt.lat ?? null,
+ lon: pkt.lon ?? null,
+ symbolTable: pkt.symbol_table ?? null,
+ symbolCode: pkt.symbol_code ?? null,
};
}
-window.onServerAprsBatch = function(packets) {
+aprsWindow.onServerAprsBatch = function(packets: AprsPacket[]) {
if (!Array.isArray(packets) || packets.length === 0) return;
- aprsStatus.textContent = "Receiving";
- const normalized = [];
+ if (aprsStatus) aprsStatus.textContent = "Receiving";
+ const normalized: AprsPacket[] = [];
let hasCrcOk = false;
for (const pkt of packets) {
const next = normalizeServerAprsPacket(pkt);
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 && window.aprsMapAddStation) {
- window.aprsMapAddStation(next.srcCall, next.lat, next.lon, next.info, next.symbolTable, next.symbolCode, next);
+ if (next.lat != null && next.lon != null && aprsWindow.aprsMapAddStation) {
+ aprsWindow.aprsMapAddStation(next.srcCall ?? "", next.lat, next.lon, next.info ?? "", next.symbolTable, next.symbolCode, next);
}
if (next.crcOk) hasCrcOk = true;
normalized.push(next);
@@ -437,19 +491,19 @@ window.onServerAprsBatch = function(packets) {
scheduleAprsHistoryRender();
};
-window.restoreAprsHistory = function(packets) {
- window.onServerAprsBatch(packets);
+aprsWindow.restoreAprsHistory = function(packets: AprsPacket[]) {
+ aprsWindow.onServerAprsBatch?.(packets);
};
-document.getElementById("settings-clear-aprs-history")?.addEventListener("click", async () => {
- if (!await window.trxUi.confirm({ title: "Clear APRS history?", message: "All stored APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
+document.getElementById("settings-clear-aprs-history")?.addEventListener("click", () => { void (async () => {
+ if (!await aprsWindow.trxUi.confirm({ title: "Clear APRS history?", message: "All stored APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
- await postPath("/clear_aprs_decode");
- window.resetAprsHistoryView();
+ await aprsWindow.postPath?.("/clear_aprs_decode");
+ aprsWindow.resetAprsHistoryView?.();
} catch (e) {
console.error("APRS history clear failed", e);
}
-});
+})(); });
if (aprsOnlyPosBtn) {
aprsOnlyPosBtn.addEventListener("click", () => {
@@ -472,7 +526,7 @@ if (aprsCollapseDupBtn) {
});
}
-["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => {
+(["all", "position", "message", "weather", "telemetry", "other"] as const).forEach((type) => {
const btn = document.getElementById(`aprs-type-${type}`);
if (!btn) return;
btn.addEventListener("click", () => {
@@ -489,10 +543,10 @@ if (aprsFilterInput) {
}
// --- Server-side APRS decode handler ---
-window.onServerAprs = function(pkt) {
- aprsStatus.textContent = "Receiving";
+aprsWindow.onServerAprs = function(pkt: AprsPacket) {
+ if (aprsStatus) aprsStatus.textContent = "Receiving";
addAprsPacket(normalizeServerAprsPacket(pkt));
};
renderAprsHistory();
-if (window._trxDrainPendingDecode) window._trxDrainPendingDecode("aprs");
+aprsWindow._trxDrainPendingDecode?.("aprs");
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/aprs.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/aprs.test.mjs
new file mode 100644
index 00000000..4571966f
--- /dev/null
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/aprs.test.mjs
@@ -0,0 +1,37 @@
+// 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("APRS entry normalizes positioned packets without remote symbol assets", async () => {
+ const forwarded = [];
+ const window = {
+ trxUi: { confirm: async () => true },
+ aprsMapAddStation: (...args) => { forwarded.push(args); },
+ };
+ const context = vm.createContext({
+ window,
+ document: { getElementById: () => null, querySelectorAll: () => [] },
+ navigator: {},
+ Date,
+ Number,
+ String,
+ Array,
+ Set,
+ Reflect,
+ console,
+ });
+ const source = await readFile(new URL("../../assets/web/generated/aprs.js", import.meta.url), "utf8");
+ assert.equal(source.includes("raw.githubusercontent.com"), false);
+ new vm.Script(source).runInContext(context);
+
+ window.onServerAprs({ src_call: "SP0ABC", dest_call: "APRS", crc_ok: true, lat: 54.5, lon: 18.5, symbol_table: "/", symbol_code: ">" });
+ assert.equal(forwarded.length, 1);
+ assert.equal(forwarded[0][0], "SP0ABC");
+ assert.equal(forwarded[0][1], 54.5);
+ assert.equal(forwarded[0][6].rig_id, null);
+});