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 ? `` : "") + `QRZ
Details
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 ? `` : "") + `QRZ
Details
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 = `
APRSLiveLast 15 minutesClear
`; - 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 = `
APRSLiveLast 15 minutesClear
`; + 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 = '
APRSLiveLast 15 minutesClear
'; 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); +});