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 fd3f223e..d8643464 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,13 +1,10 @@
import {
aprsAgeText,
- aprsCategoryLabel,
- aprsHexBytes,
aprsPacketCategory,
collapseAprsDuplicates,
normalizeAprsPacket,
- renderAprsInfo,
- renderLocalAprsSymbol
-} from "./chunk-ROTCLFXS.js";
+ renderAprsPacketRow
+} from "./chunk-YAWVT7YC.js";
import {
hostCore,
hostState
@@ -114,51 +111,27 @@ function updateAprsChipState() {
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';
- const symbolHtml = renderLocalAprsSymbol(pkt, escapeAprsHtml);
- 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);
- }
- })();
- });
+async function copyAprsCoords(text) {
+ try {
+ const clipboard = Reflect.get(navigator, "clipboard");
+ if (!clipboard) return;
+ await clipboard.writeText(text);
+ showAprsHint("Coordinates copied", 1200);
+ } catch {
+ showAprsHint("Copy failed", 1500);
}
- return row;
+}
+function renderAprsRow(pkt, isFresh) {
+ return renderAprsPacketRow(pkt, {
+ fresh: isFresh,
+ distance: aprsDistanceText(pkt),
+ onMap: (lat, lon) => {
+ aprsWindow.navigateToAprsMap?.(lat, lon);
+ },
+ onCopy: (text) => {
+ void copyAprsCoords(text);
+ }
+ });
}
function renderAprsHistory() {
pruneAprsPacketHistory();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-ROTCLFXS.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-ROTCLFXS.js
deleted file mode 100644
index 30e308a1..00000000
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-ROTCLFXS.js
+++ /dev/null
@@ -1,156 +0,0 @@
-// src/plugins/aprs-shared.ts
-function aprsPacketCategory(packet) {
- const type = (packet.type ?? "").toLowerCase();
- const info = (packet.info ?? "").toLowerCase();
- if (packet.lat != null && packet.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(timestampMs) {
- if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return "just now";
- const seconds = Math.round(Math.max(0, Date.now() - timestampMs) / 1e3);
- if (seconds < 5) return "just now";
- if (seconds < 60) return `${String(seconds)}s ago`;
- const minutes = Math.round(seconds / 60);
- if (minutes < 60) return `${String(minutes)}m ago`;
- return `${String(Math.round(minutes / 60))}h ago`;
-}
-function aprsPacketSignature(packet) {
- return [
- packet.srcCall ?? "",
- packet.destCall ?? "",
- packet.path ?? "",
- packet.info ?? "",
- packet.type ?? "",
- packet.lat?.toFixed(4) ?? "",
- packet.lon?.toFixed(4) ?? ""
- ].join("|");
-}
-function collapseAprsDuplicates(packets) {
- const seen = /* @__PURE__ */ new Set();
- return packets.filter((packet) => {
- const signature = aprsPacketSignature(packet);
- if (seen.has(signature)) return false;
- seen.add(signature);
- return true;
- });
-}
-function aprsHexBytes(bytes) {
- if (!bytes?.length) return "--";
- return bytes.map((byte) => byte.toString(16).toUpperCase().padStart(2, "0")).join(" ");
-}
-function renderAprsInfo(packet) {
- if (packet.info_bytes?.length) {
- return packet.info_bytes.map((byte) => renderAprsByte(byte)).join("");
- }
- return Array.from(packet.info ?? "", (character) => renderAprsCharacter(character)).join("");
-}
-function renderAprsByte(byte) {
- return byte >= 32 && byte <= 126 ? escapeAprsCharacter(String.fromCharCode(byte)) : `0x${byte.toString(16).toUpperCase().padStart(2, "0")}`;
-}
-function renderAprsCharacter(character) {
- const code = character.charCodeAt(0);
- return code >= 32 && code <= 126 ? escapeAprsCharacter(character) : `0x${code.toString(16).toUpperCase().padStart(2, "0")}`;
-}
-function escapeAprsCharacter(character) {
- if (character === "<") return "<";
- if (character === ">") return ">";
- if (character === "&") return "&";
- if (character === '"') return """;
- return character;
-}
-var APRS_SPRITE_COLUMNS = 16;
-var APRS_SPRITE_CELL_PX = 24;
-var APRS_SPRITE_FIRST_CODE = 33;
-var APRS_SPRITE_LAST_CODE = 126;
-function aprsSpriteOffset(code) {
- if (code.length !== 1) return null;
- const point = code.charCodeAt(0);
- if (point < APRS_SPRITE_FIRST_CODE || point > APRS_SPRITE_LAST_CODE) return null;
- const index = point - APRS_SPRITE_FIRST_CODE;
- const column = index % APRS_SPRITE_COLUMNS;
- const row = Math.floor(index / APRS_SPRITE_COLUMNS);
- return `${String(-column * APRS_SPRITE_CELL_PX)}px ${String(-row * APRS_SPRITE_CELL_PX)}px`;
-}
-function aprsSymbolSprite(symbolTable, symbolCode) {
- if (!symbolTable || !symbolCode) return null;
- const symbolOffset = aprsSpriteOffset(symbolCode);
- if (!symbolOffset) return null;
- if (symbolTable === "/") {
- return {
- className: "aprs-symbol-primary",
- backgroundPosition: symbolOffset,
- label: `Primary APRS symbol ${symbolTable}${symbolCode}`
- };
- }
- if (symbolTable === "\\") {
- return {
- className: "aprs-symbol-alternate",
- backgroundPosition: symbolOffset,
- label: `Alternate APRS symbol ${symbolTable}${symbolCode}`
- };
- }
- const overlayOffset = aprsSpriteOffset(symbolTable);
- if (!overlayOffset) return null;
- return {
- className: "aprs-symbol-overlaid",
- backgroundPosition: `${overlayOffset}, ${symbolOffset}`,
- label: `Alternate APRS symbol \\${symbolCode} with overlay ${symbolTable}`
- };
-}
-function renderLocalAprsSymbol(packet, escapeHtml) {
- if (!packet.symbolTable || !packet.symbolCode) return "";
- const sprite = aprsSymbolSprite(packet.symbolTable, packet.symbolCode);
- if (sprite) {
- return ``;
- }
- const symbol = escapeHtml(packet.symbolCode);
- const table = packet.symbolTable === "/" ? "Primary" : "Alternate";
- return `${symbol}`;
-}
-function normalizeAprsPacket(packet, receiver) {
- return {
- rig_id: packet.rig_id || null,
- receiver,
- srcCall: packet.src_call ?? "",
- destCall: packet.dest_call ?? "",
- path: packet.path ?? "",
- info: packet.info ?? "",
- info_bytes: packet.info_bytes ?? [],
- type: packet.packet_type ?? "",
- crcOk: packet.crc_ok ?? false,
- ts_ms: packet.ts_ms ?? null,
- lat: packet.lat ?? null,
- lon: packet.lon ?? null,
- symbolTable: packet.symbol_table ?? null,
- symbolCode: packet.symbol_code ?? null
- };
-}
-
-export {
- aprsPacketCategory,
- aprsCategoryLabel,
- aprsAgeText,
- collapseAprsDuplicates,
- aprsHexBytes,
- renderAprsInfo,
- aprsSymbolSprite,
- renderLocalAprsSymbol,
- normalizeAprsPacket
-};
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-YAWVT7YC.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-YAWVT7YC.js
new file mode 100644
index 00000000..3d8ea2e7
--- /dev/null
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-YAWVT7YC.js
@@ -0,0 +1,251 @@
+// src/plugins/aprs-shared.ts
+function escapeAprsHtml(value) {
+ return String(value ?? "").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
+}
+function aprsPacketCategory(packet) {
+ const type = (packet.type ?? "").toLowerCase();
+ const info = (packet.info ?? "").toLowerCase();
+ if (packet.lat != null && packet.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(timestampMs) {
+ if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return "just now";
+ const seconds = Math.round(Math.max(0, Date.now() - timestampMs) / 1e3);
+ if (seconds < 5) return "just now";
+ if (seconds < 60) return `${String(seconds)}s ago`;
+ const minutes = Math.round(seconds / 60);
+ if (minutes < 60) return `${String(minutes)}m ago`;
+ return `${String(Math.round(minutes / 60))}h ago`;
+}
+function aprsPacketSignature(packet) {
+ return [
+ packet.srcCall ?? "",
+ packet.destCall ?? "",
+ packet.path ?? "",
+ packet.info ?? "",
+ packet.type ?? "",
+ packet.lat?.toFixed(4) ?? "",
+ packet.lon?.toFixed(4) ?? ""
+ ].join("|");
+}
+function collapseAprsDuplicates(packets) {
+ const seen = /* @__PURE__ */ new Set();
+ return packets.filter((packet) => {
+ const signature = aprsPacketSignature(packet);
+ if (seen.has(signature)) return false;
+ seen.add(signature);
+ return true;
+ });
+}
+function aprsHexBytes(bytes) {
+ if (!bytes?.length) return "--";
+ return bytes.map((byte) => byte.toString(16).toUpperCase().padStart(2, "0")).join(" ");
+}
+function renderAprsInfo(packet) {
+ if (packet.info_bytes?.length) {
+ return packet.info_bytes.map((byte) => renderAprsByte(byte)).join("");
+ }
+ return Array.from(packet.info ?? "", (character) => renderAprsCharacter(character)).join("");
+}
+function renderAprsByte(byte) {
+ return byte >= 32 && byte <= 126 ? escapeAprsCharacter(String.fromCharCode(byte)) : `0x${byte.toString(16).toUpperCase().padStart(2, "0")}`;
+}
+function renderAprsCharacter(character) {
+ const code = character.charCodeAt(0);
+ return code >= 32 && code <= 126 ? escapeAprsCharacter(character) : `0x${code.toString(16).toUpperCase().padStart(2, "0")}`;
+}
+function escapeAprsCharacter(character) {
+ if (character === "<") return "<";
+ if (character === ">") return ">";
+ if (character === "&") return "&";
+ if (character === '"') return """;
+ return character;
+}
+var APRS_SPRITE_COLUMNS = 16;
+var APRS_SPRITE_CELL_PX = 24;
+var APRS_SPRITE_FIRST_CODE = 33;
+var APRS_SPRITE_LAST_CODE = 126;
+function aprsSpriteOffset(code) {
+ if (code.length !== 1) return null;
+ const point = code.charCodeAt(0);
+ if (point < APRS_SPRITE_FIRST_CODE || point > APRS_SPRITE_LAST_CODE) return null;
+ const index = point - APRS_SPRITE_FIRST_CODE;
+ const column = index % APRS_SPRITE_COLUMNS;
+ const row = Math.floor(index / APRS_SPRITE_COLUMNS);
+ return `${String(-column * APRS_SPRITE_CELL_PX)}px ${String(-row * APRS_SPRITE_CELL_PX)}px`;
+}
+function aprsSymbolSprite(symbolTable, symbolCode) {
+ if (!symbolTable || !symbolCode) return null;
+ const symbolOffset = aprsSpriteOffset(symbolCode);
+ if (!symbolOffset) return null;
+ if (symbolTable === "/") {
+ return {
+ className: "aprs-symbol-primary",
+ backgroundPosition: symbolOffset,
+ label: `Primary APRS symbol ${symbolTable}${symbolCode}`
+ };
+ }
+ if (symbolTable === "\\") {
+ return {
+ className: "aprs-symbol-alternate",
+ backgroundPosition: symbolOffset,
+ label: `Alternate APRS symbol ${symbolTable}${symbolCode}`
+ };
+ }
+ const overlayOffset = aprsSpriteOffset(symbolTable);
+ if (!overlayOffset) return null;
+ return {
+ className: "aprs-symbol-overlaid",
+ backgroundPosition: `${overlayOffset}, ${symbolOffset}`,
+ label: `Alternate APRS symbol \\${symbolCode} with overlay ${symbolTable}`
+ };
+}
+function renderLocalAprsSymbol(packet, escapeHtml) {
+ if (!packet.symbolTable || !packet.symbolCode) return "";
+ const sprite = aprsSymbolSprite(packet.symbolTable, packet.symbolCode);
+ if (sprite) {
+ return ``;
+ }
+ const symbol = escapeHtml(packet.symbolCode);
+ const table = packet.symbolTable === "/" ? "Primary" : "Alternate";
+ return `${symbol}`;
+}
+function normalizeAprsPacket(packet, receiver) {
+ return {
+ rig_id: packet.rig_id || null,
+ receiver,
+ srcCall: packet.src_call ?? "",
+ destCall: packet.dest_call ?? "",
+ path: packet.path ?? "",
+ info: packet.info ?? "",
+ info_bytes: packet.info_bytes ?? [],
+ type: packet.packet_type ?? "",
+ crcOk: packet.crc_ok ?? false,
+ ts_ms: packet.ts_ms ?? null,
+ lat: packet.lat ?? null,
+ lon: packet.lon ?? null,
+ symbolTable: packet.symbol_table ?? null,
+ symbolCode: packet.symbol_code ?? null
+ };
+}
+function fahrenheitToCelsius(fahrenheit) {
+ return Math.round((fahrenheit - 32) * 5 / 9 * 10) / 10;
+}
+function summarizeAprsWeather(info) {
+ const parts = [];
+ const temperature = /t(-?\d{2,3})/.exec(info);
+ if (temperature) parts.push(`${fahrenheitToCelsius(Number(temperature[1]))} °C`);
+ const wind = /(\d{3})\/(\d{3})/.exec(info) ?? /c(\d{3}).*?s(\d{3})/.exec(info);
+ if (wind) {
+ const gust = /g(\d{3})/.exec(info);
+ const knots = Number(wind[2]);
+ parts.push(`wind ${Number(wind[1])}° ${knots} kt${gust ? ` gust ${Number(gust[1])}` : ""}`);
+ }
+ const humidity = /h(\d{2})/.exec(info);
+ if (humidity) {
+ const value = Number(humidity[1]);
+ parts.push(`${value === 0 ? 100 : value}% RH`);
+ }
+ const pressure = /b(\d{5})/.exec(info);
+ if (pressure) parts.push(`${(Number(pressure[1]) / 10).toFixed(1)} hPa`);
+ const rain = /r(\d{3})/.exec(info);
+ if (rain && Number(rain[1]) > 0) parts.push(`rain ${(Number(rain[1]) / 100).toFixed(2)}"`);
+ return parts.length ? parts.join(" · ") : null;
+}
+function summarizeAprsTelemetry(info) {
+ const match = /^T#(\d+|MIC)((?:,-?[\d.]*)+)(?:,([01]{8}))?/.exec(info.trim());
+ if (!match?.[2]) return null;
+ const channels = match[2].split(",").filter((value) => value.length > 0);
+ const bits = match[3] ? ` · bits ${match[3]}` : "";
+ return `#${match[1]} · ${channels.join(" ")}${bits}`;
+}
+function summarizeAprsMessage(info) {
+ const match = /^:([^:]{9}):(.*)$/.exec(info);
+ if (!match?.[1] || match[2] == null) return null;
+ const addressee = match[1].trim();
+ const text = match[2].replace(/\{\d+\s*$/, "").trim();
+ return `→ ${addressee}: ${text}`;
+}
+function summarizeAprsCourseSpeed(info) {
+ const match = /(\d{3})\/(\d{3})/.exec(info);
+ if (!match) return null;
+ const knots = Number(match[2]);
+ if (knots === 0) return null;
+ return `${Number(match[1])}° ${knots} kt`;
+}
+function summarizeAprsPayload(packet) {
+ const info = packet.info ?? "";
+ if (!info) return null;
+ const category = aprsPacketCategory(packet);
+ if (category === "message") return summarizeAprsMessage(info);
+ if (category === "weather") return summarizeAprsWeather(info);
+ if (category === "telemetry") return summarizeAprsTelemetry(info);
+ if (category === "position") {
+ const parts = [];
+ if (packet.lat != null && packet.lon != null) {
+ parts.push(`${packet.lat.toFixed(4)}, ${packet.lon.toFixed(4)}`);
+ }
+ const courseSpeed = summarizeAprsCourseSpeed(info);
+ if (courseSpeed) parts.push(courseSpeed);
+ const comment = info.replace(/^[!=@/][^>]*[>_]?/, "").replace(/\d{3}\/\d{3}/, "").trim();
+ if (comment && comment.length <= 60) parts.push(comment);
+ return parts.length ? parts.join(" · ") : null;
+ }
+ const text = info.replace(/^[>;]/, "").trim();
+ return text.length ? text : null;
+}
+function renderAprsPacketRow(packet, options = {}) {
+ const row = document.createElement("details");
+ row.className = "aprs-packet";
+ if (!packet.crcOk) row.classList.add("aprs-packet-crc");
+ if (options.fresh) row.classList.add("aprs-packet-new");
+ const time = packet._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
+ const category = aprsPacketCategory(packet);
+ const summary = summarizeAprsPayload(packet);
+ const hasPosition = packet.lat != null && packet.lon != null;
+ const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(packet.srcCall || "")}`;
+ row.innerHTML = `${escapeAprsHtml(time)}` + (options.badge ? `${escapeAprsHtml(options.badge)}` : "") + renderLocalAprsSymbol(packet, escapeAprsHtml) + `${escapeAprsHtml(packet.srcCall ?? "")}${escapeAprsHtml(aprsCategoryLabel(category))}${summary ? escapeAprsHtml(summary) : renderAprsInfo(packet)}` + (packet.crcOk ? "" : 'CRC') + (options.distance ? `${escapeAprsHtml(options.distance)}` : "") + `${renderAprsInfo(packet)}
` + (packet.info_bytes?.length ? `
${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}
` : "") + `
` + (hasPosition ? `
` : "") + (hasPosition ? `
` : "") + `
QRZ `;
+ row.querySelectorAll("[data-aprs-map]").forEach((element) => {
+ element.addEventListener("click", (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ const [lat, lon] = (element.dataset.aprsMap ?? "").split(",").map(Number);
+ if (Number.isFinite(lat) && Number.isFinite(lon)) options.onMap?.(lat, lon);
+ });
+ });
+ const copyButton = row.querySelector("[data-aprs-copy]");
+ if (copyButton) {
+ copyButton.addEventListener("click", (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ options.onCopy?.(copyButton.dataset.aprsCopy ?? "", copyButton);
+ });
+ }
+ return row;
+}
+
+export {
+ aprsPacketCategory,
+ aprsAgeText,
+ collapseAprsDuplicates,
+ aprsSymbolSprite,
+ normalizeAprsPacket,
+ renderAprsPacketRow
+};
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js
index dc6edd25..8ac8bbed 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js
@@ -1,13 +1,10 @@
import {
aprsAgeText,
- aprsCategoryLabel,
- aprsHexBytes,
aprsPacketCategory,
collapseAprsDuplicates,
normalizeAprsPacket,
- renderAprsInfo,
- renderLocalAprsSymbol
-} from "./chunk-ROTCLFXS.js";
+ renderAprsPacketRow
+} from "./chunk-YAWVT7YC.js";
import {
hostCore,
hostState
@@ -15,7 +12,6 @@ import {
// src/plugins/hf-aprs.ts
var hfAprsWindow = window;
-var escapeHfAprsHtml = (input) => hostCore.escapeMapHtml(input);
var hfAprsStatus = document.getElementById("hf-aprs-status");
var hfAprsPacketsEl = document.getElementById("hf-aprs-packets");
var hfAprsFilterInput = document.getElementById("hf-aprs-filter");
@@ -102,51 +98,27 @@ function updateHfAprsChipState() {
hfAprsCollapseDupBtn?.classList.toggle("active", hfAprsCollapseDup);
}
function renderHfAprsRow(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 ? `${escapeHfAprsHtml(pkt.path)}` : "";
- const crcBadge = pkt.crcOk ? "" : 'CRC Fail';
- const hfBadge = 'HF';
- const symbolHtml = renderLocalAprsSymbol(pkt, escapeHfAprsHtml);
- const posLink = pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}` : "";
- const distance = hfAprsDistanceText(pkt);
- const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;
- row.innerHTML = `${ts}` + hfBadge + symbolHtml + `${escapeHfAprsHtml(pkt.srcCall ?? "")}>${escapeHfAprsHtml(pkt.destCall || "")}${escapeHfAprsHtml(categoryLabel)}` + pathBadge + crcBadge + `
${escapeHfAprsHtml(age)}` + (distance ? `${escapeHfAprsHtml(distance)}` : "") + `${escapeHfAprsHtml(pkt.type || "--")}
${renderAprsInfo(pkt)}` + (posLink ? `${posLink}` : "") + `
` + (pkt.lat != null && pkt.lon != null ? `
` : "") + (pkt.lat != null && pkt.lon != null ? `
` : "") + `
QRZDetails
Source${escapeHfAprsHtml(pkt.srcCall || "--")}Destination${escapeHfAprsHtml(pkt.destCall || "--")}Type${escapeHfAprsHtml(pkt.type || "--")}Path${escapeHfAprsHtml(pkt.path || "--")}Age${escapeHfAprsHtml(age)}CRC${pkt.crcOk ? "OK" : "Failed"}Position${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}Info${escapeHfAprsHtml(pkt.info || "--")}Info Bytes${escapeHfAprsHtml(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 (hfAprsWindow.navigateToAprsMap && typeof lat === "number" && typeof lon === "number" && Number.isFinite(lat) && Number.isFinite(lon)) {
- hfAprsWindow.navigateToAprsMap(lat, lon);
- }
- });
+ return renderAprsPacketRow(pkt, {
+ fresh: isFresh,
+ badge: "HF",
+ distance: hfAprsDistanceText(pkt),
+ onMap: (lat, lon) => {
+ hfAprsWindow.navigateToAprsMap?.(lat, lon);
+ },
+ onCopy: (text) => {
+ void copyHfAprsCoords(text);
+ }
});
- 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);
- hostCore.showHint("Coordinates copied", 1200);
- }
- } catch {
- hostCore.showHint("Copy failed", 1500);
- }
- })();
- });
+}
+async function copyHfAprsCoords(text) {
+ try {
+ const clipboard = Reflect.get(navigator, "clipboard");
+ if (!clipboard) return;
+ await clipboard.writeText(text);
+ hostCore.showHint("Coordinates copied", 1200);
+ } catch {
+ hostCore.showHint("Copy failed", 1500);
}
- return row;
}
function renderHfAprsHistory() {
pruneHfAprsPacketHistory();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js
index ca02c8cb..759ebf1b 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js
@@ -1,6 +1,6 @@
import {
aprsSymbolSprite
-} from "./chunk-ROTCLFXS.js";
+} from "./chunk-YAWVT7YC.js";
// src/map-core.ts
function mapEl(id) {
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
index 977cd9a6..600d963e 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html
@@ -722,20 +722,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
Waiting for server decode
-
-
- Frames
- 0 total
-
-
- Visible
- 0 shown
-
-
- Latest
- No packets yet
-
-
@@ -743,11 +729,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
-
-
+
+
+ 0 shown
+ 0 total
+ No packets yet
+
@@ -757,20 +747,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
Waiting for server decode
-
-
- Frames
- 0 total
-
-
- Visible
- 0 shown
-
-
- Latest
- No packets yet
-
-
@@ -778,11 +754,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
-
-
+
+
+ 0 shown
+ 0 total
+ No packets yet
+
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 adb8716f..7cda2aac 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
@@ -2844,34 +2844,93 @@ body.map-fake-fullscreen-active {
background: var(--input-bg);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
-.aprs-packet { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.82rem; padding: 0.45rem 0.55rem; border-bottom: 1px solid var(--border); line-height: 1.35; }
+/* One line per frame, opening in place. A frame used to be a card five lines
+ tall — timestamp, meta, raw information field, three buttons, a Details
+ panel repeating the row — so five of them filled the panel. */
+.aprs-packet { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.82rem; border-bottom: 1px solid var(--border); line-height: 1.35; }
.aprs-packet:last-child { border-bottom: none; }
+.aprs-line {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.28rem 0.55rem;
+ cursor: pointer;
+ list-style: none;
+ white-space: nowrap;
+}
+.aprs-line::-webkit-details-marker { display: none; }
+.aprs-line:hover {
+ background: color-mix(in srgb, var(--btn-bg) 45%, transparent);
+}
+.aprs-packet[open] > .aprs-line {
+ background: color-mix(in srgb, var(--accent-green) 8%, transparent);
+}
+/* The summary takes the leftover width and ellipsises: a frame is one line
+ whatever its payload, so the column of times and callsigns stays readable. */
+.aprs-line-summary {
+ flex: 1 1 auto;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: var(--text);
+}
+.aprs-line-distance {
+ flex: 0 0 auto;
+ color: var(--text-muted);
+ font-size: 0.75rem;
+}
+.aprs-expanded {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ padding: 0.1rem 0.55rem 0.55rem 2.6rem;
+}
+.aprs-expanded-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.6rem;
+ font-size: 0.75rem;
+ color: var(--text-muted);
+}
+.aprs-expanded-raw,
+.aprs-expanded-bytes {
+ word-break: break-all;
+ font-size: 0.78rem;
+ color: var(--text);
+}
+.aprs-expanded-bytes {
+ color: var(--text-muted);
+ font-size: 0.72rem;
+}
.aprs-packet-new {
animation: aprs-row-flash 1.2s ease;
}
.aprs-packet-crc {
opacity: 0.6;
}
-.aprs-row-head,
-.aprs-row-meta,
-.aprs-row-detail,
.aprs-row-actions {
display: flex;
align-items: center;
gap: 0.45rem;
flex-wrap: wrap;
}
-.aprs-row-head + .aprs-row-meta,
-.aprs-row-meta + .aprs-row-detail,
-.aprs-row-detail + .aprs-row-actions {
- margin-top: 0.2rem;
-}
-.aprs-row-detail {
+/* Counts sit with the filters rather than in three cards of their own: they
+ are one short line of text, and they were taking a fifth of the panel. */
+.aprs-counts {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.6rem;
+ margin-left: auto;
+ font-size: 0.72rem;
color: var(--text-muted);
- font-size: 0.78rem;
+ white-space: nowrap;
}
-.aprs-row-actions {
- margin-top: 0.28rem;
+.aprs-filter-sep {
+ width: 1px;
+ align-self: stretch;
+ min-height: 1.2rem;
+ margin-inline: 0.15rem;
+ background: color-mix(in srgb, var(--border-light) 65%, transparent);
}
.aprs-badge {
display: inline-flex;
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs-shared.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs-shared.ts
index e9efafce..cf7c00d2 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs-shared.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs-shared.ts
@@ -30,6 +30,14 @@ export interface AprsPacket {
symbol_code?: string | null;
}
+function escapeAprsHtml(value: string): string {
+ return String(value ?? "")
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """);
+}
+
export function aprsPacketCategory(packet: AprsPacket): AprsCategory {
const type = (packet.type ?? "").toLowerCase();
const info = (packet.info ?? "").toLowerCase();
@@ -205,3 +213,174 @@ export function normalizeAprsPacket(packet: AprsPacket, receiver: unknown): Aprs
symbolCode: packet.symbol_code ?? null,
};
}
+
+// ── Payload summaries ──────────────────────────────────────────────────────
+// APRS packs its meaning into the information field with a set of one-character
+// type identifiers and fixed-width encodings (APRS 1.0.1, chapters 6-15). The
+// list showed that field as it arrives on the air, so reading a weather report
+// meant decoding "_10090556c220s004g005t077..." by eye. These produce a line
+// of plain text for the common types and leave the raw field to the expanded
+// view, which is still the authority when a summary cannot be made.
+
+/** `t077` → 25.0 °C. APRS carries temperature in whole degrees Fahrenheit. */
+function fahrenheitToCelsius(fahrenheit: number): number {
+ return Math.round(((fahrenheit - 32) * 5 / 9) * 10) / 10;
+}
+
+/** Weather report fields: wind, gust, temperature, rain, humidity, pressure. */
+function summarizeAprsWeather(info: string): string | null {
+ const parts: string[] = [];
+ const temperature = /t(-?\d{2,3})/.exec(info);
+ if (temperature) parts.push(`${fahrenheitToCelsius(Number(temperature[1]))} °C`);
+ const wind = /(\d{3})\/(\d{3})/.exec(info) ?? /c(\d{3}).*?s(\d{3})/.exec(info);
+ if (wind) {
+ const gust = /g(\d{3})/.exec(info);
+ const knots = Number(wind[2]);
+ parts.push(`wind ${Number(wind[1])}° ${knots} kt${gust ? ` gust ${Number(gust[1])}` : ""}`);
+ }
+ const humidity = /h(\d{2})/.exec(info);
+ if (humidity) {
+ const value = Number(humidity[1]);
+ parts.push(`${value === 0 ? 100 : value}% RH`);
+ }
+ const pressure = /b(\d{5})/.exec(info);
+ if (pressure) parts.push(`${(Number(pressure[1]) / 10).toFixed(1)} hPa`);
+ const rain = /r(\d{3})/.exec(info);
+ if (rain && Number(rain[1]) > 0) parts.push(`rain ${(Number(rain[1]) / 100).toFixed(2)}"`);
+ return parts.length ? parts.join(" · ") : null;
+}
+
+/** `T#005,199,000,255,073,123,01101001` → sequence, five channels, eight bits. */
+function summarizeAprsTelemetry(info: string): string | null {
+ const match = /^T#(\d+|MIC)((?:,-?[\d.]*)+)(?:,([01]{8}))?/.exec(info.trim());
+ if (!match?.[2]) return null;
+ const channels = match[2].split(",").filter((value) => value.length > 0);
+ const bits = match[3] ? ` · bits ${match[3]}` : "";
+ return `#${match[1]} · ${channels.join(" ")}${bits}`;
+}
+
+/** `:DEST :text{01` → addressed message text. */
+function summarizeAprsMessage(info: string): string | null {
+ const match = /^:([^:]{9}):(.*)$/.exec(info);
+ if (!match?.[1] || match[2] == null) return null;
+ const addressee = match[1].trim();
+ const text = match[2].replace(/\{\d+\s*$/, "").trim();
+ return `→ ${addressee}: ${text}`;
+}
+
+/** Course/speed appended to a position, as `088/036`. */
+function summarizeAprsCourseSpeed(info: string): string | null {
+ const match = /(\d{3})\/(\d{3})/.exec(info);
+ if (!match) return null;
+ const knots = Number(match[2]);
+ if (knots === 0) return null;
+ return `${Number(match[1])}° ${knots} kt`;
+}
+
+/** Whatever a frame is worth saying in one line, or null to fall back to raw. */
+export function summarizeAprsPayload(packet: AprsPacket): string | null {
+ const info = packet.info ?? "";
+ if (!info) return null;
+ const category = aprsPacketCategory(packet);
+ if (category === "message") return summarizeAprsMessage(info);
+ if (category === "weather") return summarizeAprsWeather(info);
+ if (category === "telemetry") return summarizeAprsTelemetry(info);
+ if (category === "position") {
+ const parts: string[] = [];
+ if (packet.lat != null && packet.lon != null) {
+ parts.push(`${packet.lat.toFixed(4)}, ${packet.lon.toFixed(4)}`);
+ }
+ const courseSpeed = summarizeAprsCourseSpeed(info);
+ if (courseSpeed) parts.push(courseSpeed);
+ // Whatever the station wrote after the position report.
+ const comment = info.replace(/^[!=@/][^>]*[>_]?/, "").replace(/\d{3}\/\d{3}/, "").trim();
+ if (comment && comment.length <= 60) parts.push(comment);
+ return parts.length ? parts.join(" · ") : null;
+ }
+ // Status and anything else: the text it carries, minus its type character.
+ const text = info.replace(/^[>;]/, "").trim();
+ return text.length ? text : null;
+}
+
+// ── Frame row ──────────────────────────────────────────────────────────────
+// Shared by the APRS and HF APRS lists, which had a copy each of the same
+// forty lines of markup and drifted only by one badge.
+
+export interface AprsRowOptions {
+ /** Marks the newest frame so it can flash on arrival. */
+ fresh?: boolean;
+ /** Leading badge, e.g. the band a copy of this list is dedicated to. */
+ badge?: string;
+ /** Distance from the receiver, already formatted, or "" to leave it out. */
+ distance?: string;
+ /** Opens the map on a frame's position. */
+ onMap?: (lat: number, lon: number) => void;
+ /** Puts a frame's coordinates on the clipboard. */
+ onCopy?: (text: string, button: HTMLElement) => void;
+}
+
+export function renderAprsPacketRow(packet: AprsPacket, options: AprsRowOptions = {}): HTMLElement {
+ const row = document.createElement("details");
+ row.className = "aprs-packet";
+ if (!packet.crcOk) row.classList.add("aprs-packet-crc");
+ if (options.fresh) row.classList.add("aprs-packet-new");
+
+ const time = packet._ts
+ || new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
+ const category = aprsPacketCategory(packet);
+ const summary = summarizeAprsPayload(packet);
+ const hasPosition = packet.lat != null && packet.lon != null;
+ const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(packet.srcCall || "")}`;
+
+ row.innerHTML =
+ `` +
+ `${escapeAprsHtml(time)}` +
+ (options.badge ? `${escapeAprsHtml(options.badge)}` : "") +
+ renderLocalAprsSymbol(packet, escapeAprsHtml) +
+ `${escapeAprsHtml(packet.srcCall ?? "")}` +
+ `` +
+ `${escapeAprsHtml(aprsCategoryLabel(category))}` +
+ `${summary ? escapeAprsHtml(summary) : renderAprsInfo(packet)}` +
+ (packet.crcOk ? "" : 'CRC') +
+ (options.distance ? `${escapeAprsHtml(options.distance)}` : "") +
+ `` +
+ `` +
+ `
` +
+ `
${renderAprsInfo(packet)}
` +
+ (packet.info_bytes?.length
+ ? `
${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}
`
+ : "") +
+ `
` +
+ (hasPosition ? `
` : "") +
+ (hasPosition ? `
` : "") +
+ `
QRZ` +
+ `
` +
+ `
`;
+
+ row.querySelectorAll("[data-aprs-map]").forEach((element) => {
+ element.addEventListener("click", (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ const [lat, lon] = (element.dataset.aprsMap ?? "").split(",").map(Number);
+ if (Number.isFinite(lat) && Number.isFinite(lon)) options.onMap?.(lat as number, lon as number);
+ });
+ });
+ const copyButton = row.querySelector("[data-aprs-copy]");
+ if (copyButton) {
+ copyButton.addEventListener("click", (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ options.onCopy?.(copyButton.dataset.aprsCopy ?? "", copyButton);
+ });
+ }
+ return row;
+}
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts
index b306cbc3..45e55674 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/aprs.ts
@@ -6,13 +6,10 @@ import { hostCore, hostState } from "./host.js";
import {
aprsAgeText,
- aprsCategoryLabel,
- aprsHexBytes,
aprsPacketCategory,
collapseAprsDuplicates,
normalizeAprsPacket,
- renderAprsInfo,
- renderLocalAprsSymbol,
+ renderAprsPacketRow,
type AprsPacket,
type AprsTypeFilter,
} from "./aprs-shared";
@@ -141,93 +138,24 @@ function updateAprsChipState() {
aprsCollapseDupBtn?.classList.toggle("active", aprsCollapseDup);
}
-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");
- if (isFresh) row.classList.add("aprs-packet-new");
-
- const ts = pkt._ts || 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';
- const symbolHtml = renderLocalAprsSymbol(pkt, escapeAprsHtml);
- 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") as Clipboard | undefined;
- if (clipboard) {
- await clipboard.writeText(raw);
- showAprsHint("Coordinates copied", 1200);
- }
- } catch {
- showAprsHint("Copy failed", 1500);
- }
- })(); });
+async function copyAprsCoords(text: string): Promise {
+ try {
+ const clipboard = Reflect.get(navigator, "clipboard") as Clipboard | undefined;
+ if (!clipboard) return;
+ await clipboard.writeText(text);
+ showAprsHint("Coordinates copied", 1200);
+ } catch {
+ showAprsHint("Copy failed", 1500);
}
+}
- return row;
+function renderAprsRow(pkt: AprsPacket, isFresh: boolean): HTMLElement {
+ return renderAprsPacketRow(pkt, {
+ fresh: isFresh,
+ distance: aprsDistanceText(pkt),
+ onMap: (lat, lon) => { aprsWindow.navigateToAprsMap?.(lat, lon); },
+ onCopy: (text) => { void copyAprsCoords(text); },
+ });
}
function renderAprsHistory() {
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/hf-aprs.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/hf-aprs.ts
index 3ee88258..3192fe2e 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/hf-aprs.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/hf-aprs.ts
@@ -6,13 +6,10 @@ import { hostCore, hostState } from "./host.js";
import {
aprsAgeText,
- aprsCategoryLabel,
- aprsHexBytes,
aprsPacketCategory,
collapseAprsDuplicates,
normalizeAprsPacket,
- renderAprsInfo,
- renderLocalAprsSymbol,
+ renderAprsPacketRow,
type AprsPacket,
type AprsTypeFilter,
} from "./aprs-shared";
@@ -27,7 +24,6 @@ interface HfAprsBridge {
trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise };
}
const hfAprsWindow = window as unknown as HfAprsBridge;
-const escapeHfAprsHtml = (input: string): string => hostCore.escapeMapHtml(input);
// --- HF APRS Decoder Plugin (server-side decode, 300 baud) ---
const hfAprsStatus = document.getElementById("hf-aprs-status");
@@ -128,95 +124,27 @@ function updateHfAprsChipState() {
hfAprsCollapseDupBtn?.classList.toggle("active", hfAprsCollapseDup);
}
+// HF traffic goes in the same row as VHF, marked with the band it came in on.
+// This was a second copy of the same forty lines of markup.
function renderHfAprsRow(pkt: AprsPacket, isFresh: boolean): HTMLElement {
- 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 || 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 ? `${escapeHfAprsHtml(pkt.path)}` : "";
- const crcBadge = pkt.crcOk ? "" : 'CRC Fail';
- const hfBadge = 'HF';
- const symbolHtml = renderLocalAprsSymbol(pkt, escapeHfAprsHtml);
- const posLink = pkt.lat != null && pkt.lon != null
- ? `${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}`
- : "";
- const distance = hfAprsDistanceText(pkt);
- const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;
-
- row.innerHTML =
- `` +
- `${ts}` +
- hfBadge +
- symbolHtml +
- `${escapeHfAprsHtml(pkt.srcCall ?? "")}` +
- `>${escapeHfAprsHtml(pkt.destCall || "")}` +
- `${escapeHfAprsHtml(categoryLabel)}` +
- pathBadge +
- crcBadge +
- `
` +
- `` +
- `${escapeHfAprsHtml(age)}` +
- (distance ? `${escapeHfAprsHtml(distance)}` : "") +
- `${escapeHfAprsHtml(pkt.type || "--")}` +
- `
` +
- `` +
- `${renderAprsInfo(pkt)}` +
- (posLink ? `${posLink}` : "") +
- `
` +
- `` +
- (pkt.lat != null && pkt.lon != null ? `
` : "") +
- (pkt.lat != null && pkt.lon != null ? `
` : "") +
- `
QRZ` +
- `
` +
- `` +
- `Details
` +
- `` +
- `Source${escapeHfAprsHtml(pkt.srcCall || "--")}` +
- `Destination${escapeHfAprsHtml(pkt.destCall || "--")}` +
- `Type${escapeHfAprsHtml(pkt.type || "--")}` +
- `Path${escapeHfAprsHtml(pkt.path || "--")}` +
- `Age${escapeHfAprsHtml(age)}` +
- `CRC${pkt.crcOk ? "OK" : "Failed"}` +
- `Position${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}` +
- `Info${escapeHfAprsHtml(pkt.info || "--")}` +
- `Info Bytes${escapeHfAprsHtml(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 (hfAprsWindow.navigateToAprsMap && typeof lat === "number" && typeof lon === "number" && Number.isFinite(lat) && Number.isFinite(lon)) {
- hfAprsWindow.navigateToAprsMap(lat, lon);
- }
- });
+ return renderAprsPacketRow(pkt, {
+ fresh: isFresh,
+ badge: "HF",
+ distance: hfAprsDistanceText(pkt),
+ onMap: (lat: number, lon: number) => { hfAprsWindow.navigateToAprsMap?.(lat, lon); },
+ onCopy: (text: string) => { void copyHfAprsCoords(text); },
});
+}
- 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") as Clipboard | undefined;
- if (clipboard) {
- await clipboard.writeText(raw);
- hostCore.showHint("Coordinates copied", 1200);
- }
- } catch {
- hostCore.showHint("Copy failed", 1500);
- }
- })(); });
+async function copyHfAprsCoords(text: string): Promise {
+ try {
+ const clipboard = Reflect.get(navigator, "clipboard") as Clipboard | undefined;
+ if (!clipboard) return;
+ await clipboard.writeText(text);
+ hostCore.showHint("Coordinates copied", 1200);
+ } catch {
+ hostCore.showHint("Copy failed", 1500);
}
-
- return row;
}
function renderHfAprsHistory() {
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs
index 1dd8ed14..ee21064b 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/decode-flow.mjs
@@ -164,3 +164,71 @@ try {
await replay.browser.close();
await historyFixture.close();
}
+
+// The APRS list: one line per frame, with the information field read for the
+// operator rather than shown as it arrives on the air.
+const APRS_FRAMES = [
+ { packet_type: "weather", info: "_10090556c220s004g005t077r000p000P000h50b09900" },
+ { packet_type: "message", info: ":SP2SJG-9 :Hello from the field{01" },
+ { packet_type: "telemetry", info: "T#005,199,000,255,073,123,01101001" },
+ { packet_type: "position", info: "!5421.30N/01839.20E>Test beacon 73", lat: 54.35, lon: 18.65 },
+].map((frame, index) => ({
+ src_call: `SP2SJG-${index}`, dest_call: "APRS", path: "WIDE1-1", crc_ok: true,
+ symbol_table: "/", symbol_code: ">", rig_id: "rig-a", ts_ms: Date.now() - index * 1000,
+ ...frame,
+}));
+
+const aprsFixture = await startWebFixture({ spectrum: true, history: { aprs: APRS_FRAMES } });
+const aprs = await startBrowser(chromium);
+
+try {
+ await aprs.page.setViewportSize({ width: 1400, height: 900 });
+ await aprs.page.goto(`${aprsFixture.origin}/digital-modes`, { waitUntil: "domcontentloaded" });
+ await aprs.page.locator("#tab-digital-modes").waitFor({ state: "visible" });
+ await aprs.page.waitForTimeout(2000);
+ await aprs.page.locator('.sub-tab[data-subtab="aprs"]').click();
+ await aprs.page.waitForTimeout(400);
+
+ const rows = await aprs.page.evaluate(() =>
+ [...document.querySelectorAll("#aprs-packets .aprs-packet")].map((row) => ({
+ tag: row.tagName,
+ height: Math.round(row.getBoundingClientRect().height),
+ type: row.querySelector(".aprs-badge-type")?.textContent?.trim() ?? "",
+ summary: row.querySelector(".aprs-line-summary")?.textContent?.trim() ?? "",
+ })));
+ // Newest first, so find each by the type it carries rather than by position.
+ const summaryOf = (type) => rows.find((row) => row.type === type)?.summary ?? "";
+ assert.equal(rows.length, APRS_FRAMES.length, `rendered ${rows.length} frames`);
+ for (const row of rows) {
+ assert.equal(row.tag, "DETAILS", "a frame is not expandable in place");
+ assert.ok(row.height < 44, `a frame is ${row.height}px tall; it used to be a card of about 140`);
+ }
+ // Each payload read rather than echoed: 25 °C from t077, the addressee from
+ // a message, the sequence from telemetry, the fix from a position report.
+ assert.match(summaryOf("Weather"), /25 °C/, `weather summary: ${summaryOf("Weather")}`);
+ assert.match(summaryOf("Weather"), /990\.0 hPa/, `weather summary: ${summaryOf("Weather")}`);
+ assert.match(summaryOf("Message"), /→ SP2SJG-9: Hello from the field/, `message summary: ${summaryOf("Message")}`);
+ assert.match(summaryOf("Telemetry"), /^#005/, `telemetry summary: ${summaryOf("Telemetry")}`);
+ assert.match(summaryOf("Position"), /54\.3500, 18\.6500/, `position summary: ${summaryOf("Position")}`);
+
+ // The frame as it arrived is still there, one click away.
+ await aprs.page.locator("#aprs-packets .aprs-packet", { hasText: "25 °C" })
+ .locator(".aprs-line").first().click();
+ await aprs.page.waitForTimeout(200);
+ const expanded = await aprs.page.evaluate(() => {
+ const row = document.querySelector("#aprs-packets .aprs-packet[open]");
+ return {
+ open: row.hasAttribute("open"),
+ raw: row.querySelector(".aprs-expanded-raw")?.textContent?.trim() ?? "",
+ meta: row.querySelector(".aprs-expanded-meta")?.textContent ?? "",
+ };
+ });
+ assert.equal(expanded.open, true, "the frame did not open");
+ assert.match(expanded.raw, /^_10090556c220s004g005t077/, `raw frame: ${expanded.raw}`);
+ assert.match(expanded.meta, /WIDE1-1/, `expanded meta: ${expanded.meta}`);
+
+ assert.deepEqual(aprs.runtimeErrors, []);
+} finally {
+ await aprs.browser.close();
+ await aprsFixture.close();
+}