[feat](trx-frontend-http): make the APRS list a log, and read the payloads
CI / lint (push) Successful in 2m24s
CI / test (push) Successful in 8m12s
CI / frontend (push) Successful in 3m41s
CI / reuse (push) Successful in 2s

A frame was a card five lines tall — timestamp, a meta line, the
information field as it arrived on the air, three buttons, and a Details
panel repeating the four things already on the row — so five frames
filled the panel and the payload was left to be decoded by eye.

A frame is one line now: time, station, type, and what the frame says.
It opens in place for the path, the CRC, the raw field, its bytes and
the actions.  Twenty-one frames fit where five did.

And the information field is read rather than echoed.  Weather reports
give temperature, wind, humidity and pressure; telemetry gives its
sequence and channels; a message gives its addressee and text; a
position gives the fix, course and speed, and the comment the station
wrote.  Anything that cannot be summarised falls back to the raw field,
which is in the expanded view either way.

HF APRS had a copy of the same forty lines of markup, differing by one
badge, so both now build their rows from one function in the shared
module — the CSS is shared between them and this would have broken it
otherwise.  Both headers lose their three summary cards for a line of
counts beside the filters, which frees another fifth of the panel.

Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
sjg
2026-08-04 23:12:02 +02:00
parent 67a7bace4e
commit 37987b2779
11 changed files with 661 additions and 479 deletions
@@ -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 ? `<span class="aprs-badge">${escapeAprsHtml(pkt.path)}</span>` : "";
const crcBadge = pkt.crcOk ? "" : '<span class="aprs-badge aprs-badge-crc">CRC Fail</span>';
const symbolHtml = renderLocalAprsSymbol(pkt, escapeAprsHtml);
const posLink = pkt.lat != null && pkt.lon != null ? `<a class="aprs-pos" href="javascript:void(0)" data-aprs-map="${pkt.lat},${pkt.lon}">${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}</a>` : "";
const distance = aprsDistanceText(pkt);
const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;
row.innerHTML = `<div class="aprs-row-head"><span class="aprs-time">${ts}</span>` + symbolHtml + `<span class="aprs-call">${escapeAprsHtml(pkt.srcCall ?? "")}</span><span>&gt;${escapeAprsHtml(pkt.destCall || "")}</span><span class="${categoryClass}">${escapeAprsHtml(categoryLabel)}</span>` + pathBadge + crcBadge + `</div><div class="aprs-row-meta"><span class="aprs-meta-text">${escapeAprsHtml(age)}</span>` + (distance ? `<span class="aprs-meta-text">${escapeAprsHtml(distance)}</span>` : "") + `<span class="aprs-meta-text">${escapeAprsHtml(pkt.type || "--")}</span></div><div class="aprs-row-detail"><span title="${escapeAprsHtml(pkt.type || "")}">${renderAprsInfo(pkt)}</span>` + (posLink ? `<span>${posLink}</span>` : "") + `</div><div class="aprs-row-actions">` + (pkt.lat != null && pkt.lon != null ? `<button class="aprs-inline-btn" type="button" data-aprs-map="${pkt.lat},${pkt.lon}">Map</button>` : "") + (pkt.lat != null && pkt.lon != null ? `<button class="aprs-inline-btn" type="button" data-aprs-copy="${pkt.lat},${pkt.lon}">Copy Coords</button>` : "") + `<a class="aprs-inline-btn" href="${qrzHref}" target="_blank" rel="noopener">QRZ</a></div><details class="aprs-details"><summary>Details</summary><div class="aprs-details-grid"><span class="aprs-detail-label">Source</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.srcCall || "--")}</span><span class="aprs-detail-label">Destination</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.destCall || "--")}</span><span class="aprs-detail-label">Type</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.type || "--")}</span><span class="aprs-detail-label">Path</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.path || "--")}</span><span class="aprs-detail-label">Age</span><span class="aprs-detail-value">${escapeAprsHtml(age)}</span><span class="aprs-detail-label">CRC</span><span class="aprs-detail-value">${pkt.crcOk ? "OK" : "Failed"}</span><span class="aprs-detail-label">Position</span><span class="aprs-detail-value">${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}</span><span class="aprs-detail-label">Info</span><span class="aprs-detail-value">${escapeAprsHtml(pkt.info || "--")}</span><span class="aprs-detail-label">Info Bytes</span><span class="aprs-detail-value">${escapeAprsHtml(aprsHexBytes(pkt.info_bytes))}</span></div></details>`;
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();
@@ -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)) : `<span class="aprs-byte">0x${byte.toString(16).toUpperCase().padStart(2, "0")}</span>`;
}
function renderAprsCharacter(character) {
const code = character.charCodeAt(0);
return code >= 32 && code <= 126 ? escapeAprsCharacter(character) : `<span class="aprs-byte">0x${code.toString(16).toUpperCase().padStart(2, "0")}</span>`;
}
function escapeAprsCharacter(character) {
if (character === "<") return "&lt;";
if (character === ">") return "&gt;";
if (character === "&") return "&amp;";
if (character === '"') return "&quot;";
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 `<span class="aprs-symbol ${sprite.className}" role="img" style="background-position:${sprite.backgroundPosition}" title="${escapeHtml(sprite.label)}" aria-label="${escapeHtml(sprite.label)}"></span>`;
}
const symbol = escapeHtml(packet.symbolCode);
const table = packet.symbolTable === "/" ? "Primary" : "Alternate";
return `<span class="aprs-symbol aprs-symbol-local" title="${table} APRS symbol ${symbol}">${symbol}</span>`;
}
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
};
@@ -0,0 +1,251 @@
// src/plugins/aprs-shared.ts
function escapeAprsHtml(value) {
return String(value ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
}
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)) : `<span class="aprs-byte">0x${byte.toString(16).toUpperCase().padStart(2, "0")}</span>`;
}
function renderAprsCharacter(character) {
const code = character.charCodeAt(0);
return code >= 32 && code <= 126 ? escapeAprsCharacter(character) : `<span class="aprs-byte">0x${code.toString(16).toUpperCase().padStart(2, "0")}</span>`;
}
function escapeAprsCharacter(character) {
if (character === "<") return "&lt;";
if (character === ">") return "&gt;";
if (character === "&") return "&amp;";
if (character === '"') return "&quot;";
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 `<span class="aprs-symbol ${sprite.className}" role="img" style="background-position:${sprite.backgroundPosition}" title="${escapeHtml(sprite.label)}" aria-label="${escapeHtml(sprite.label)}"></span>`;
}
const symbol = escapeHtml(packet.symbolCode);
const table = packet.symbolTable === "/" ? "Primary" : "Alternate";
return `<span class="aprs-symbol aprs-symbol-local" title="${table} APRS symbol ${symbol}">${symbol}</span>`;
}
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 = `<summary class="aprs-line"><span class="aprs-time">${escapeAprsHtml(time)}</span>` + (options.badge ? `<span class="aprs-badge aprs-badge-band">${escapeAprsHtml(options.badge)}</span>` : "") + renderLocalAprsSymbol(packet, escapeAprsHtml) + `<span class="aprs-call">${escapeAprsHtml(packet.srcCall ?? "")}</span><span class="aprs-badge aprs-badge-type aprs-badge-type-${category}">${escapeAprsHtml(aprsCategoryLabel(category))}</span><span class="aprs-line-summary">${summary ? escapeAprsHtml(summary) : renderAprsInfo(packet)}</span>` + (packet.crcOk ? "" : '<span class="aprs-badge aprs-badge-crc">CRC</span>') + (options.distance ? `<span class="aprs-line-distance">${escapeAprsHtml(options.distance)}</span>` : "") + `</summary><div class="aprs-expanded"><div class="aprs-expanded-meta"><span>&gt;${escapeAprsHtml(packet.destCall || "--")}</span><span>${escapeAprsHtml(packet.path || "no path")}</span><span>${escapeAprsHtml(aprsAgeText(packet._tsMs))}</span><span>CRC ${packet.crcOk ? "ok" : "failed"}</span>` + (hasPosition ? `<a class="aprs-pos" href="javascript:void(0)" data-aprs-map="${packet.lat},${packet.lon}">${packet.lat?.toFixed(5)}, ${packet.lon?.toFixed(5)}</a>` : "") + `</div><div class="aprs-expanded-raw">${renderAprsInfo(packet)}</div>` + (packet.info_bytes?.length ? `<div class="aprs-expanded-bytes">${escapeAprsHtml(aprsHexBytes(packet.info_bytes))}</div>` : "") + `<div class="aprs-row-actions">` + (hasPosition ? `<button class="aprs-inline-btn" type="button" data-aprs-map="${packet.lat},${packet.lon}">Map</button>` : "") + (hasPosition ? `<button class="aprs-inline-btn" type="button" data-aprs-copy="${packet.lat},${packet.lon}">Copy Coords</button>` : "") + `<a class="aprs-inline-btn" href="${qrzHref}" target="_blank" rel="noopener">QRZ</a></div></div>`;
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
};
@@ -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 ? `<span class="aprs-badge">${escapeHfAprsHtml(pkt.path)}</span>` : "";
const crcBadge = pkt.crcOk ? "" : '<span class="aprs-badge aprs-badge-crc">CRC Fail</span>';
const hfBadge = '<span class="aprs-badge" style="background:var(--accent-alt,#f59e0b);color:#000">HF</span>';
const symbolHtml = renderLocalAprsSymbol(pkt, escapeHfAprsHtml);
const posLink = pkt.lat != null && pkt.lon != null ? `<a class="aprs-pos" href="javascript:void(0)" data-aprs-map="${pkt.lat},${pkt.lon}">${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}</a>` : "";
const distance = hfAprsDistanceText(pkt);
const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;
row.innerHTML = `<div class="aprs-row-head"><span class="aprs-time">${ts}</span>` + hfBadge + symbolHtml + `<span class="aprs-call">${escapeHfAprsHtml(pkt.srcCall ?? "")}</span><span>&gt;${escapeHfAprsHtml(pkt.destCall || "")}</span><span class="${categoryClass}">${escapeHfAprsHtml(categoryLabel)}</span>` + pathBadge + crcBadge + `</div><div class="aprs-row-meta"><span class="aprs-meta-text">${escapeHfAprsHtml(age)}</span>` + (distance ? `<span class="aprs-meta-text">${escapeHfAprsHtml(distance)}</span>` : "") + `<span class="aprs-meta-text">${escapeHfAprsHtml(pkt.type || "--")}</span></div><div class="aprs-row-detail"><span title="${escapeHfAprsHtml(pkt.type || "")}">${renderAprsInfo(pkt)}</span>` + (posLink ? `<span>${posLink}</span>` : "") + `</div><div class="aprs-row-actions">` + (pkt.lat != null && pkt.lon != null ? `<button class="aprs-inline-btn" type="button" data-aprs-map="${pkt.lat},${pkt.lon}">Map</button>` : "") + (pkt.lat != null && pkt.lon != null ? `<button class="aprs-inline-btn" type="button" data-aprs-copy="${pkt.lat},${pkt.lon}">Copy Coords</button>` : "") + `<a class="aprs-inline-btn" href="${qrzHref}" target="_blank" rel="noopener">QRZ</a></div><details class="aprs-details"><summary>Details</summary><div class="aprs-details-grid"><span class="aprs-detail-label">Source</span><span class="aprs-detail-value">${escapeHfAprsHtml(pkt.srcCall || "--")}</span><span class="aprs-detail-label">Destination</span><span class="aprs-detail-value">${escapeHfAprsHtml(pkt.destCall || "--")}</span><span class="aprs-detail-label">Type</span><span class="aprs-detail-value">${escapeHfAprsHtml(pkt.type || "--")}</span><span class="aprs-detail-label">Path</span><span class="aprs-detail-value">${escapeHfAprsHtml(pkt.path || "--")}</span><span class="aprs-detail-label">Age</span><span class="aprs-detail-value">${escapeHfAprsHtml(age)}</span><span class="aprs-detail-label">CRC</span><span class="aprs-detail-value">${pkt.crcOk ? "OK" : "Failed"}</span><span class="aprs-detail-label">Position</span><span class="aprs-detail-value">${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}</span><span class="aprs-detail-label">Info</span><span class="aprs-detail-value">${escapeHfAprsHtml(pkt.info || "--")}</span><span class="aprs-detail-label">Info Bytes</span><span class="aprs-detail-value">${escapeHfAprsHtml(aprsHexBytes(pkt.info_bytes))}</span></div></details>`;
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();
@@ -1,6 +1,6 @@
import {
aprsSymbolSprite
} from "./chunk-ROTCLFXS.js";
} from "./chunk-YAWVT7YC.js";
// src/map-core.ts
function mapEl(id) {