Files
trx-rs/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js
T
sjgandClaude Opus 5 0d4c657b97
CI / lint (pull_request) Failing after 2s
CI / test (pull_request) Failing after 1s
CI / frontend (pull_request) Failing after 41s
CI / reuse (pull_request) Failing after 2s
CI / lint (push) Failing after 2s
CI / test (push) Failing after 2s
CI / frontend (push) Failing after 36s
CI / reuse (push) Failing after 1s
[fix](trx-frontend-http): route feature bundles through the host contract
The bookmark fix addressed one instance of a defect the TypeScript
migration left across the feature entries.  app.js stopped being a
classic script, so its top-level declarations are no longer shared
globals, but the converted entries kept reading them as window
properties that nothing publishes.

Restore the broken behavior:

- ais, aprs, hf-aprs read serverLat, serverLon and haversineKm as
  undefined, so every positioned packet rendered an empty distance.
- ais, aprs, hf-aprs, cw, sat, vdes, wefax, wspr called an undefined
  postPath, so clear-history and decoder toggles threw.
- scheduler read authRole as undefined, so the lazy-load path never
  self-initialized and the Settings tab opened an inert scheduler.
- background-decode read authEnabled as undefined, so control gating
  fell back to role-only.
- vchan read fifteen application values and services as undefined:
  mode and bandwidth sync, the out-of-band hint, RX audio restart, and
  the frequency field all silently no-opped on a virtual channel.
- vchan wrapped window.refreshFreqDisplay, capturing an undefined
  original exactly as it did for setRigFrequency, so leaving a channel
  never restored the application's own frequency display.
- _audioChannelOverride was a const that nothing could assign, so RX
  audio always subscribed to the primary channel.
- ftx-family read fmtTime, a helper legacy ft8.js owned locally, so
  decode bar timestamps rendered empty.

Declare the contract once in plugins/host.ts and import it from the
feature entries, rather than restoring globals that
docs/frontend-architecture.md excludes.  trx.state gains jogUnit,
rxActive and audioChannelOverride, and makes lastModeName writable;
trx.core gains the tuning, RDS, WFM, jog and RX audio services the
entries need.  vchan interception moves to an interceptFreqDisplay
service method that refreshFreqDisplay calls, matching the frequency,
mode and bandwidth interception it already registers.

Reading registry-built elements through a strict lookup is the same
defect as in bookmarks: renderTimelineNeedle guards its result, but
schedulerEl throws, so the now-initializing scheduler crashed on the
timeline needle group that its own SVG creates.

Feature tests move onto a shared host fixture, and entries that now
import a common module are bundled through bundleEntry like the other
shared-module entries.  Covers scheduler self-initialization and the
distance path that the bare window reads broke.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdyUjuXejCEfiub675z6cz
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-02 11:20:17 +02:00

270 lines
12 KiB
JavaScript

import {
aprsAgeText,
aprsCategoryLabel,
aprsHexBytes,
aprsPacketCategory,
collapseAprsDuplicates,
normalizeAprsPacket,
renderAprsInfo,
renderLocalAprsSymbol
} from "./chunk-M2I6DH4X.js";
import {
hostCore,
hostState
} from "./chunk-KL66PICH.js";
// 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");
var hfAprsOnlyPosBtn = document.getElementById("hf-aprs-only-pos-btn");
var hfAprsHideCrcBtn = document.getElementById("hf-aprs-hide-crc-btn");
var hfAprsCollapseDupBtn = document.getElementById("hf-aprs-collapse-dup-btn");
var hfAprsTotalCountEl = document.getElementById("hf-aprs-total-count");
var hfAprsVisibleCountEl = document.getElementById("hf-aprs-visible-count");
var hfAprsLatestSeenEl = document.getElementById("hf-aprs-latest-seen");
var hfAprsFilterText = "";
var hfAprsPacketHistory = [];
var hfAprsOnlyPos = false;
var hfAprsHideCrc = false;
var hfAprsCollapseDup = false;
var hfAprsTypeFilter = "all";
function currentHfAprsHistoryRetentionMs() {
return typeof hfAprsWindow.getDecodeHistoryRetentionMs === "function" ? hfAprsWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
}
function pruneHfAprsPacketHistory() {
const cutoffMs = Date.now() - currentHfAprsHistoryRetentionMs();
hfAprsPacketHistory = hfAprsPacketHistory.filter((pkt) => (pkt._tsMs ?? 0) >= cutoffMs);
}
function scheduleHfAprsHistoryRender() {
if (typeof hfAprsWindow.trxScheduleUiFrameJob === "function") {
hfAprsWindow.trxScheduleUiFrameJob("hf-aprs-history", () => {
renderHfAprsHistory();
});
return;
}
renderHfAprsHistory();
}
function hfAprsDistanceText(pkt) {
if (hostState.serverLat == null || hostState.serverLon == null || pkt.lat == null || pkt.lon == null) return "";
const distKm = hostCore.haversineKm(hostState.serverLat, hostState.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 hfAprsFilterMatch(pkt) {
if (hfAprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false;
if (hfAprsHideCrc && !pkt.crcOk) return false;
if (hfAprsTypeFilter !== "all" && aprsPacketCategory(pkt) !== hfAprsTypeFilter) return false;
if (!hfAprsFilterText) 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(hfAprsFilterText);
}
function hfAprsVisiblePackets() {
const packets = hfAprsCollapseDup ? collapseHfAprsDuplicates(hfAprsPacketHistory) : hfAprsPacketHistory;
return packets.filter(hfAprsFilterMatch);
}
var collapseHfAprsDuplicates = collapseAprsDuplicates;
function updateHfAprsSummary() {
const visible = hfAprsVisiblePackets();
if (hfAprsTotalCountEl) {
hfAprsTotalCountEl.textContent = `${hfAprsPacketHistory.length} total`;
}
if (hfAprsVisibleCountEl) {
hfAprsVisibleCountEl.textContent = `${visible.length} shown`;
}
if (hfAprsLatestSeenEl) {
const latest = hfAprsPacketHistory[0];
if (!latest) {
hfAprsLatestSeenEl.textContent = "No packets yet";
} else {
hfAprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`;
}
}
}
function updateHfAprsChipState() {
document.querySelectorAll("[id^='hf-aprs-type-']").forEach((btn) => {
btn.classList.toggle("active", btn.id === `hf-aprs-type-${hfAprsTypeFilter}`);
});
hfAprsOnlyPosBtn?.classList.toggle("active", hfAprsOnlyPos);
hfAprsHideCrcBtn?.classList.toggle("active", hfAprsHideCrc);
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);
}
});
});
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);
}
})();
});
}
return row;
}
function renderHfAprsHistory() {
pruneHfAprsPacketHistory();
if (!hfAprsPacketsEl) {
updateHfAprsSummary();
updateHfAprsChipState();
return;
}
const visible = hfAprsVisiblePackets();
const fragment = document.createDocumentFragment();
for (const [index, packet] of visible.entries()) {
fragment.appendChild(renderHfAprsRow(packet, index === 0));
}
hfAprsPacketsEl.replaceChildren(fragment);
updateHfAprsSummary();
updateHfAprsChipState();
}
function resetHfAprsHistoryView() {
if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
hfAprsPacketHistory = [];
renderHfAprsHistory();
}
function pruneHfAprsHistoryView() {
pruneHfAprsPacketHistory();
renderHfAprsHistory();
}
function addHfAprsPacket(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" });
hfAprsPacketHistory.unshift(pkt);
pruneHfAprsPacketHistory();
scheduleHfAprsHistoryRender();
}
function normalizeServerHfAprsPacket(pkt) {
return normalizeAprsPacket(pkt, hfAprsWindow.getDecodeRigMeta?.() ?? null);
}
function onServerHfAprsBatch(packets) {
if (!Array.isArray(packets) || packets.length === 0) return;
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
const normalized = [];
for (const pkt of packets) {
const next = normalizeServerHfAprsPacket(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" });
normalized.push(next);
}
normalized.reverse();
hfAprsPacketHistory = normalized.concat(hfAprsPacketHistory);
pruneHfAprsPacketHistory();
scheduleHfAprsHistoryRender();
}
var hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn");
hfAprsDecodeToggleBtn?.addEventListener("click", () => {
void (async () => {
try {
await hfAprsWindow.takeSchedulerControlForDecoderDisable?.(hfAprsDecodeToggleBtn);
await hostCore.postPath("/toggle_hf_aprs_decode");
} catch (e) {
console.error("HF APRS toggle failed", e);
}
})();
});
document.getElementById("settings-clear-hf-aprs-history")?.addEventListener("click", () => {
void (async () => {
if (!await hfAprsWindow.trxUi.confirm({ title: "Clear HF APRS history?", message: "All stored HF APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await hostCore.postPath("/clear_hf_aprs_decode");
resetHfAprsHistoryView();
} catch (e) {
console.error("HF APRS history clear failed", e);
}
})();
});
if (hfAprsOnlyPosBtn) {
hfAprsOnlyPosBtn.addEventListener("click", () => {
hfAprsOnlyPos = !hfAprsOnlyPos;
renderHfAprsHistory();
});
}
if (hfAprsHideCrcBtn) {
hfAprsHideCrcBtn.addEventListener("click", () => {
hfAprsHideCrc = !hfAprsHideCrc;
renderHfAprsHistory();
});
}
if (hfAprsCollapseDupBtn) {
hfAprsCollapseDupBtn.addEventListener("click", () => {
hfAprsCollapseDup = !hfAprsCollapseDup;
renderHfAprsHistory();
});
}
["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => {
const btn = document.getElementById(`hf-aprs-type-${type}`);
if (!btn) return;
btn.addEventListener("click", () => {
hfAprsTypeFilter = type;
renderHfAprsHistory();
});
});
if (hfAprsFilterInput) {
hfAprsFilterInput.addEventListener("input", () => {
hfAprsFilterText = hfAprsFilterInput.value.trim().toUpperCase();
renderHfAprsHistory();
});
}
function onServerHfAprs(pkt) {
if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
}
renderHfAprsHistory();
window.trxPluginRuntime.registerDecoder({
id: "hf_aprs",
onMessage: onServerHfAprs,
onBatch: onServerHfAprsBatch,
restore: onServerHfAprsBatch,
reset: resetHfAprsHistoryView,
prune: pruneHfAprsHistoryView
});