[fix](trx-frontend-http): route feature bundles through the host contract
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

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>
This commit was merged in pull request #23.
This commit is contained in:
sjg
2026-08-02 11:20:17 +02:00
co-authored by Claude Opus 5
parent 23dbcac5b6
commit 0d4c657b97
50 changed files with 687 additions and 398 deletions
@@ -1,6 +1,11 @@
import {
hostCore,
hostState
} from "./chunk-KL66PICH.js";
// src/plugins/ais.ts
var aisWindow = window;
var escapeAisHtml = (input) => aisWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
var escapeAisHtml = (input) => hostCore.escapeMapHtml(input);
var aisStatus = document.getElementById("ais-status");
var aisMessagesEl = document.getElementById("ais-messages");
var aisFilterInput = document.getElementById("ais-filter");
@@ -119,10 +124,10 @@ function aisRouteText(msg) {
return [msg.callsign, msg.destination].filter(Boolean).join(" -> ");
}
function aisDistanceText(msg) {
if (aisWindow.serverLat == null || aisWindow.serverLon == null || msg.lat == null || msg.lon == null || !aisWindow.haversineKm) {
if (hostState.serverLat == null || hostState.serverLon == null || msg.lat == null || msg.lon == null) {
return "";
}
const distKm = aisWindow.haversineKm(aisWindow.serverLat, aisWindow.serverLon, msg.lat, msg.lon);
const distKm = hostCore.haversineKm(hostState.serverLat, hostState.serverLon, msg.lat, msg.lon);
if (!Number.isFinite(distKm)) return "";
if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`;
return `${distKm.toFixed(1)} km from TRX`;
@@ -303,7 +308,7 @@ document.getElementById("settings-clear-ais-history")?.addEventListener("click",
void (async () => {
if (!await aisWindow.trxUi.confirm({ title: "Clear AIS history?", message: "All stored AIS messages will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await aisWindow.postPath?.("/clear_ais_decode");
await hostCore.postPath("/clear_ais_decode");
resetAisHistoryView();
} catch (e) {
console.error("AIS history clear failed", e);
@@ -3206,6 +3206,7 @@ function refreshWavelengthDisplay(hz) {
wavelengthEl.textContent = formatWavelength(hz);
}
function refreshFreqDisplay() {
if (window.trx?.modules.vchan?.interceptFreqDisplay()) return;
if (lastFreqHz == null || freqDirty) return;
freqEl.value = formatFrequencyForStep(lastFreqHz, jogUnit);
refreshWavelengthDisplay(lastFreqHz);
@@ -5775,6 +5776,19 @@ Object.defineProperties(trxState, {
} },
lastModeName: { get() {
return lastModeName;
}, set(v) {
lastModeName = v;
} },
jogUnit: { get() {
return jogUnit;
} },
rxActive: { get() {
return rxActive;
} },
audioChannelOverride: { get() {
return _audioChannelOverride;
}, set(v) {
_audioChannelOverride = v;
} },
lastSpectrumData: { get() {
return lastSpectrumData;
@@ -5832,6 +5846,16 @@ var trxCore = Object.freeze({
syncBandwidthInput,
scheduleSpectrumDraw,
onDecoderRegistryReady,
formatFreqForStep: formatFrequencyForStep,
refreshFreqDisplay,
setJogDivisor,
mwDefaultsForMode,
resetRdsDisplay,
positionRdsPsOverlay,
updateWfmControls,
updateSdrSquelchControlVisibility,
startRxAudio,
stopRxAudio,
latLonToMaidenhead,
locatorToLatLon,
haversineKm,
@@ -8,12 +8,16 @@ import {
renderAprsInfo,
renderLocalAprsSymbol
} from "./chunk-M2I6DH4X.js";
import {
hostCore,
hostState
} from "./chunk-KL66PICH.js";
// src/plugins/aprs.ts
var aprsWindow = window;
var escapeAprsHtml = (input) => aprsWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
var escapeAprsHtml = (input) => hostCore.escapeMapHtml(input);
var showAprsHint = (message, durationMs) => {
aprsWindow.showHint?.(message, durationMs);
hostCore.showHint(message, durationMs);
};
var aprsStatus = document.getElementById("aprs-status");
var aprsPacketsEl = document.getElementById("aprs-packets");
@@ -58,8 +62,8 @@ function scheduleAprsBarUpdate() {
});
}
function aprsDistanceText(pkt) {
if (aprsWindow.serverLat == null || aprsWindow.serverLon == null || pkt.lat == null || pkt.lon == null || !aprsWindow.haversineKm) return "";
const distKm = aprsWindow.haversineKm(aprsWindow.serverLat, aprsWindow.serverLon, pkt.lat, pkt.lon);
if (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`;
@@ -260,7 +264,7 @@ document.getElementById("settings-clear-aprs-history")?.addEventListener("click"
void (async () => {
if (!await aprsWindow.trxUi.confirm({ title: "Clear APRS history?", message: "All stored APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await aprsWindow.postPath?.("/clear_aprs_decode");
await hostCore.postPath("/clear_aprs_decode");
resetAprsHistoryView();
} catch (e) {
console.error("APRS history clear failed", e);
@@ -1,3 +1,7 @@
import {
hostState
} from "./chunk-KL66PICH.js";
// src/plugins/background-decode.ts
var bgdWindow = window;
(function() {
@@ -104,7 +108,7 @@ var bgdWindow = window;
}
setCheckbox("background-decode-enabled", currentConfig.enabled);
renderBookmarkChecklist();
const isControl = backgroundDecodeRole === "control" || bgdWindow.authEnabled === false;
const isControl = backgroundDecodeRole === "control" || hostState.authEnabled === false;
const panel = document.getElementById("background-decode-panel");
if (panel) {
panel.querySelectorAll("input, select, button.sch-write").forEach(function(el) {
@@ -1,7 +1,10 @@
import {
hostCore,
hostState
} from "./chunk-KL66PICH.js";
// src/plugins/bookmarks.ts
var bridge = window;
var trxState = bridge.trx.state;
var trxCore = bridge.trx.core;
function bmEl(id) {
const element = document.getElementById(id);
if (!element) throw new Error(`Missing bookmark element #${id}`);
@@ -39,7 +42,7 @@ function bmEsc(str) {
return d.innerHTML;
}
function bmCanControl() {
return !trxState.authEnabled || trxState.authRole === "control";
return !hostState.authEnabled || hostState.authRole === "control";
}
function bmSyncAccess() {
const canCtrl = bmCanControl();
@@ -49,7 +52,7 @@ function bmSyncAccess() {
if (selectAllBtn) selectAllBtn.style.display = canCtrl ? "" : "none";
}
function bmListScope() {
return trxState.lastActiveRigId || "general";
return hostState.lastActiveRigId || "general";
}
async function bmFetchOverlay() {
const overlayScope = bmListScope();
@@ -65,7 +68,7 @@ async function bmFetchOverlay() {
if (typeof bridge.syncBookmarkMapLocators === "function") {
bridge.syncBookmarkMapLocators(bmOverlayList);
}
trxCore.scheduleSpectrumDraw();
hostCore.scheduleSpectrumDraw();
}
async function bmFetch(categoryFilter) {
let url = "/bookmarks";
@@ -188,11 +191,11 @@ function bmChangePage(delta) {
bmRender(bmFilteredList);
}
function bmReadDecoders() {
return trxState.decoderRegistry.filter((d) => d.bookmark_selectable).filter((d) => bmOptionalEl("bm-dec-" + d.id)?.checked).map((d) => d.id);
return hostState.decoderRegistry.filter((d) => d.bookmark_selectable).filter((d) => bmOptionalEl("bm-dec-" + d.id)?.checked).map((d) => d.id);
}
function bmWriteDecoders(decoders) {
const set = new Set(decoders || []);
trxState.decoderRegistry.filter((d) => d.bookmark_selectable).forEach((d) => {
hostState.decoderRegistry.filter((d) => d.bookmark_selectable).forEach((d) => {
const el = bmOptionalEl("bm-dec-" + d.id);
if (el) el.checked = set.has(d.id);
});
@@ -201,7 +204,7 @@ function bmBuildDecoderCheckboxes() {
const container = bmEl("bm-decoder-checkboxes");
if (!container) return;
container.innerHTML = "";
trxState.decoderRegistry.filter((d) => d.bookmark_selectable).forEach((d) => {
hostState.decoderRegistry.filter((d) => d.bookmark_selectable).forEach((d) => {
const label = document.createElement("label");
label.className = "bm-decoder-check";
label.innerHTML = '<input type="checkbox" id="bm-dec-' + d.id + '" value="' + d.id + '" /> ' + d.label;
@@ -231,17 +234,17 @@ function bmCloseForm() {
if (wrap) wrap.style.display = "none";
}
function bmPrefillFromStatus() {
const freqHz = trxState.lastFreqHz;
const freqHz = hostState.lastFreqHz;
if (freqHz != null && Number.isFinite(freqHz)) {
bmEl("bm-freq").value = String(Math.round(freqHz));
}
if (trxState.lastModeName) {
bmEl("bm-mode").value = trxState.lastModeName;
if (hostState.lastModeName) {
bmEl("bm-mode").value = hostState.lastModeName;
}
if (trxState.currentBandwidthHz > 0) {
bmEl("bm-bw").value = String(Math.round(trxState.currentBandwidthHz));
if (hostState.currentBandwidthHz > 0) {
bmEl("bm-bw").value = String(Math.round(hostState.currentBandwidthHz));
}
const activeDecoders = trxState.decoderRegistry.filter((d) => d.bookmark_selectable && d.activation === "toggle").filter((d) => bmOptionalEl(d.id + "-decode-toggle-btn")?.dataset.enabled === "true").map((d) => d.id);
const activeDecoders = hostState.decoderRegistry.filter((d) => d.bookmark_selectable && d.activation === "toggle").filter((d) => bmOptionalEl(d.id + "-decode-toggle-btn")?.dataset.enabled === "true").map((d) => d.id);
bmWriteDecoders(activeDecoders);
}
async function bmSave(e) {
@@ -327,36 +330,36 @@ function bmApply(bm) {
modeEl.value = (bm.mode || "").toUpperCase();
}
if (bm.bandwidth_hz) {
trxState.currentBandwidthHz = bm.bandwidth_hz;
trxCore.syncBandwidthInput(bm.bandwidth_hz);
hostState.currentBandwidthHz = bm.bandwidth_hz;
hostCore.syncBandwidthInput(bm.bandwidth_hz);
}
trxCore.armOptimisticFrequency(bm.freq_hz);
trxCore.applyLocalTunedFrequency(bm.freq_hz, true);
if (trxState.lastSpectrumData) {
trxCore.scheduleSpectrumDraw();
hostCore.armOptimisticFrequency(bm.freq_hz);
hostCore.applyLocalTunedFrequency(bm.freq_hz, true);
if (hostState.lastSpectrumData) {
hostCore.scheduleSpectrumDraw();
}
const tunePromise = (async () => {
await bridge.trx.modules.vchan?.takeSchedulerControl();
const onVirtual = await bridge.trx.modules.vchan?.interceptMode(bm.mode) ?? false;
if (!onVirtual) {
await trxCore.postPath("/set_mode?mode=" + encodeURIComponent(bm.mode));
await hostCore.postPath("/set_mode?mode=" + encodeURIComponent(bm.mode));
}
if (bm.bandwidth_hz) {
const bwHandledByVchan = await bridge.trx.modules.vchan?.interceptBandwidth(bm.bandwidth_hz) ?? false;
if (!bwHandledByVchan) {
await trxCore.postPath(`/set_bandwidth?hz=${bm.bandwidth_hz}`);
await hostCore.postPath(`/set_bandwidth?hz=${bm.bandwidth_hz}`);
}
}
trxCore.setRigFrequency(bm.freq_hz);
hostCore.setRigFrequency(bm.freq_hz);
})();
const hasDecoders = Array.isArray(bm.decoders) && bm.decoders.length > 0;
const modeUp = (bm.mode || "").toUpperCase();
const allToggleDecoders = trxState.decoderRegistry.filter(
const allToggleDecoders = hostState.decoderRegistry.filter(
(d) => d.activation === "toggle"
);
const decoderPromise = allToggleDecoders.length ? (async () => {
let statusUrl = "/status";
const rigId = trxState.lastActiveRigId;
const rigId = hostState.lastActiveRigId;
if (rigId) {
statusUrl += "?remote=" + encodeURIComponent(rigId);
}
@@ -377,7 +380,7 @@ function bmApply(bm) {
wanted = currentlyOn;
}
if (wanted !== currentlyOn) {
toggles.push(trxCore.postPath("/toggle_" + d.id.replace(/-/g, "_") + "_decode"));
toggles.push(hostCore.postPath("/toggle_" + d.id.replace(/-/g, "_") + "_decode"));
}
}
if (toggles.length) await Promise.all(toggles);
@@ -427,8 +430,8 @@ function bmUpdateSelectionUi() {
function bmPopulateMoveTarget() {
const sel = bmEl("bm-move-target");
if (!sel) return;
const rigIds = trxState.lastRigIds;
const displayNames = trxState.lastRigDisplayNames;
const rigIds = hostState.lastRigIds;
const displayNames = hostState.lastRigDisplayNames;
const prev = sel.value;
sel.innerHTML = "";
if (bmScope !== "general") {
@@ -533,8 +536,8 @@ async function bmDeleteSelected() {
function bmPopulateScopePicker() {
const picker = bmEl("bm-scope-picker");
if (!picker) return;
const rigIds = trxState.lastRigIds;
const displayNames = trxState.lastRigDisplayNames;
const rigIds = hostState.lastRigIds;
const displayNames = hostState.lastRigDisplayNames;
const prev = picker.value;
while (picker.options.length > 1) picker.remove(1);
rigIds.forEach((id) => {
@@ -553,7 +556,7 @@ function bmPopulateScopePicker() {
(function initBookmarks() {
bmSyncAccess();
bmBuildDecoderCheckboxes();
trxCore.onDecoderRegistryReady(bmBuildDecoderCheckboxes);
hostCore.onDecoderRegistryReady(bmBuildDecoderCheckboxes);
bmPopulateScopePicker();
const scopePicker = bmEl("bm-scope-picker");
if (scopePicker) {
@@ -0,0 +1,9 @@
// src/plugins/host.ts
var host = window;
var hostState = host.trx.state;
var hostCore = host.trx.core;
export {
hostState,
hostCore
};
@@ -1,5 +1,13 @@
import {
hostCore
} from "./chunk-KL66PICH.js";
// src/plugins/ftx-family.ts
var bridge = window;
function formatBarTime(timestampMs) {
if (!timestampMs) return "--:--:--";
return new Date(timestampMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
}
function finiteNumber(value) {
const number = typeof value === "number" ? value : Number(value);
return Number.isFinite(number) ? number : null;
@@ -198,7 +206,7 @@ function initializeFtxDecoder(config) {
let html = "";
for (const message of recent) {
const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
const time = timestamp === null ? "" : `<span class="aprs-bar-time">${bridge.fmtTime?.(timestamp) ?? ""}</span>`;
const time = timestamp === null ? "" : `<span class="aprs-bar-time">${formatBarTime(timestamp)}</span>`;
const snr = finiteNumber(message.snr_db);
const delta = finiteNumber(message.dt_s);
const frequency = displayFrequency(message.freq_hz);
@@ -243,7 +251,7 @@ function initializeFtxDecoder(config) {
void (async () => {
try {
await bridge.takeSchedulerControlForDecoderDisable?.(toggle);
await bridge.postPath?.(`/toggle_${id}_decode`);
await hostCore.postPath(`/toggle_${id}_decode`);
} catch (error) {
console.error(`${label} toggle failed`, error);
}
@@ -253,7 +261,7 @@ function initializeFtxDecoder(config) {
void (async () => {
if (!await bridge.trxUi.confirm({ title: `Clear ${label} history?`, message: `All stored ${label} decodes will be permanently removed.`, confirmLabel: "Clear history" })) return;
try {
await bridge.postPath?.(`/clear_${id}_decode`);
await hostCore.postPath(`/clear_${id}_decode`);
reset();
} catch (error) {
console.error(`${label} history clear failed`, error);
@@ -1,3 +1,7 @@
import {
hostCore
} from "./chunk-KL66PICH.js";
// src/plugins/cw.ts
var cwWindow = window;
var cwStatusEl = document.getElementById("cw-status");
@@ -25,7 +29,7 @@ var cwBarCurrentLine = null;
var cwBarDismissedAtMs = 0;
var cwAutoLocalOverride = null;
function escapeCwHtml(input) {
return cwWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
return hostCore.escapeMapHtml(input);
}
function applyCwAutoUi(enabled) {
if (cwAutoInput) cwAutoInput.checked = enabled;
@@ -246,7 +250,7 @@ async function setCwTone(tone, { syncInput = true } = {}) {
cwToneInput.value = String(clamped);
}
try {
await cwWindow.postPath?.(`/set_cw_tone?tone_hz=${encodeURIComponent(clamped)}`);
await hostCore.postPath(`/set_cw_tone?tone_hz=${encodeURIComponent(clamped)}`);
} catch (e) {
console.error("CW tone set failed", e);
}
@@ -259,7 +263,7 @@ if (cwAutoInput) {
cwAutoLocalOverride = enabled;
applyCwAutoUi(enabled);
try {
await cwWindow.postPath?.(`/set_cw_auto?enabled=${enabled ? "true" : "false"}`);
await hostCore.postPath(`/set_cw_auto?enabled=${enabled ? "true" : "false"}`);
drawCwTonePicker();
} catch (error) {
console.error("CW auto toggle failed", error);
@@ -276,7 +280,7 @@ if (cwWpmInput) {
const wpm = clampCwWpm(cwWpmInput.value);
cwWpmInput.value = String(wpm);
try {
await cwWindow.postPath?.(`/set_cw_wpm?wpm=${encodeURIComponent(wpm)}`);
await hostCore.postPath(`/set_cw_wpm?wpm=${encodeURIComponent(wpm)}`);
} catch (error) {
console.error("CW WPM set failed", error);
}
@@ -312,7 +316,7 @@ document.getElementById("settings-clear-cw-history")?.addEventListener("click",
void (async () => {
if (!await cwWindow.trxUi.confirm({ title: "Clear CW history?", message: "All stored CW decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await cwWindow.postPath?.("/clear_cw_decode");
await hostCore.postPath("/clear_cw_decode");
resetCwHistoryView();
} catch (error) {
console.error("CW history clear failed", error);
@@ -1,6 +1,7 @@
import {
initializeFtxDecoder
} from "./chunk-SGMG5LG2.js";
} from "./chunk-O2Y7YEVQ.js";
import "./chunk-KL66PICH.js";
// src/plugins/ft2.ts
initializeFtxDecoder({ id: "ft2", label: "FT2", periodMs: 3750 });
@@ -1,6 +1,7 @@
import {
initializeFtxDecoder
} from "./chunk-SGMG5LG2.js";
} from "./chunk-O2Y7YEVQ.js";
import "./chunk-KL66PICH.js";
// src/plugins/ft4.ts
initializeFtxDecoder({ id: "ft4", label: "FT4", periodMs: 7500 });
@@ -2,7 +2,8 @@ import {
initializeFt8FamilyBar,
initializeFtxDecoder,
installFtxCompatibilityHelpers
} from "./chunk-SGMG5LG2.js";
} from "./chunk-O2Y7YEVQ.js";
import "./chunk-KL66PICH.js";
// src/plugins/ft8.ts
installFtxCompatibilityHelpers();
@@ -8,10 +8,14 @@ import {
renderAprsInfo,
renderLocalAprsSymbol
} from "./chunk-M2I6DH4X.js";
import {
hostCore,
hostState
} from "./chunk-KL66PICH.js";
// src/plugins/hf-aprs.ts
var hfAprsWindow = window;
var escapeHfAprsHtml = (input) => hfAprsWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
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");
@@ -44,8 +48,8 @@ function scheduleHfAprsHistoryRender() {
renderHfAprsHistory();
}
function hfAprsDistanceText(pkt) {
if (hfAprsWindow.serverLat == null || hfAprsWindow.serverLon == null || pkt.lat == null || pkt.lon == null || !hfAprsWindow.haversineKm) return "";
const distKm = hfAprsWindow.haversineKm(hfAprsWindow.serverLat, hfAprsWindow.serverLon, pkt.lat, pkt.lon);
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`;
@@ -134,10 +138,10 @@ function renderHfAprsRow(pkt, isFresh) {
const clipboard = Reflect.get(navigator, "clipboard");
if (clipboard) {
await clipboard.writeText(raw);
hfAprsWindow.showHint?.("Coordinates copied", 1200);
hostCore.showHint("Coordinates copied", 1200);
}
} catch {
hfAprsWindow.showHint?.("Copy failed", 1500);
hostCore.showHint("Copy failed", 1500);
}
})();
});
@@ -201,7 +205,7 @@ hfAprsDecodeToggleBtn?.addEventListener("click", () => {
void (async () => {
try {
await hfAprsWindow.takeSchedulerControlForDecoderDisable?.(hfAprsDecodeToggleBtn);
await hfAprsWindow.postPath?.("/toggle_hf_aprs_decode");
await hostCore.postPath("/toggle_hf_aprs_decode");
} catch (e) {
console.error("HF APRS toggle failed", e);
}
@@ -211,7 +215,7 @@ document.getElementById("settings-clear-hf-aprs-history")?.addEventListener("cli
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 hfAprsWindow.postPath?.("/clear_hf_aprs_decode");
await hostCore.postPath("/clear_hf_aprs_decode");
resetHfAprsHistoryView();
} catch (e) {
console.error("HF APRS history clear failed", e);
@@ -1,3 +1,7 @@
import {
hostCore
} from "./chunk-KL66PICH.js";
// src/plugins/sat.ts
var satWindow = window;
var satDom = {
@@ -243,7 +247,7 @@ lrptDecodeToggleBtn?.addEventListener("click", () => {
void (async () => {
try {
await satWindow.takeSchedulerControlForDecoderDisable?.(lrptDecodeToggleBtn);
await satWindow.postPath?.("/toggle_lrpt_decode");
await hostCore.postPath("/toggle_lrpt_decode");
} catch (e) {
console.error("LRPT toggle failed", e);
}
@@ -264,7 +268,7 @@ document.getElementById("settings-clear-sat-history")?.addEventListener("click",
void (async () => {
if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await satWindow.postPath?.("/clear_lrpt_decode");
await hostCore.postPath("/clear_lrpt_decode");
resetSatHistoryView();
} catch (e) {
console.error("Weather satellite history clear failed", e);
@@ -1,3 +1,7 @@
import {
hostState
} from "./chunk-KL66PICH.js";
// src/plugins/scheduler.ts
var schedulerWindow = window;
var wiredElements = /* @__PURE__ */ new WeakSet();
@@ -6,6 +10,9 @@ function schedulerEl(id) {
if (!element) throw new Error(`Missing scheduler element #${id}`);
return element;
}
function schedulerOptionalEl(id) {
return document.getElementById(id);
}
(function() {
"use strict";
let schedulerRole = null;
@@ -359,8 +366,8 @@ function schedulerEl(id) {
renderSatelliteSection();
if (mode === "grayline" && currentConfig && currentConfig.grayline) {
const gl = currentConfig.grayline;
const lat = gl.lat ?? schedulerWindow.serverLat ?? "";
const lon = gl.lon ?? schedulerWindow.serverLon ?? "";
const lat = gl.lat ?? hostState.serverLat ?? "";
const lon = gl.lon ?? hostState.serverLon ?? "";
setInputValue("scheduler-gl-lat", lat != null ? lat : "");
setInputValue("scheduler-gl-lon", lon != null ? lon : "");
const gridEl = schedulerEl("scheduler-gl-grid");
@@ -373,8 +380,8 @@ function schedulerEl(id) {
renderBookmarkSelect("scheduler-gl-dusk", gl.dusk_bookmark_id);
renderBookmarkSelect("scheduler-gl-night", gl.night_bookmark_id);
} else if (mode === "grayline") {
const lat = schedulerWindow.serverLat ?? "";
const lon = schedulerWindow.serverLon ?? "";
const lat = hostState.serverLat ?? "";
const lon = hostState.serverLon ?? "";
setInputValue("scheduler-gl-lat", lat != null ? lat : "");
setInputValue("scheduler-gl-lon", lon != null ? lon : "");
const gridEl2 = schedulerEl("scheduler-gl-grid");
@@ -624,7 +631,7 @@ function schedulerEl(id) {
return '<line class="sch-timeline-needle" x1="' + x.toFixed(1) + '" y1="2" x2="' + x.toFixed(1) + '" y2="38" /><polygon class="sch-timeline-needle-head" points="' + (x - 3).toFixed(1) + ",2 " + (x + 3).toFixed(1) + ",2 " + x.toFixed(1) + ',6" />';
}
function renderTimelineNeedle() {
const g = schedulerEl("sch-timeline-needle-g");
const g = schedulerOptionalEl("sch-timeline-needle-g");
if (g) g.innerHTML = timelineNeedleSvg();
}
function schInlineEdit(tr, entry, idx) {
@@ -1213,8 +1220,8 @@ function schedulerEl(id) {
markDirty: markSchedulerDirty
};
schedulerWindow.trx.modules.scheduler = schedulerService;
if (schedulerWindow.authRole != null) {
initScheduler(schedulerWindow.lastActiveRigId ?? null, schedulerWindow.authRole);
if (hostState.authRole != null) {
initScheduler(hostState.lastActiveRigId, hostState.authRole);
wireSchedulerEvents();
}
})();
@@ -1,3 +1,8 @@
import {
hostCore,
hostState
} from "./chunk-KL66PICH.js";
// src/plugins/vchan.ts
var vchanWindow = window;
var vchanSessionId = null;
@@ -63,7 +68,7 @@ function vchanStartSchedulerReleasePolling() {
}
async function vchanToggleSchedulerRelease() {
if (!vchanSessionId) return;
const rigId = vchanRigId || vchanWindow.lastActiveRigId || null;
const rigId = vchanRigId || hostState.lastActiveRigId || null;
try {
const resp = await fetch("/scheduler-control", {
method: "PUT",
@@ -161,14 +166,12 @@ function vchanRender() {
});
picker.appendChild(addBtn);
vchanSyncAccentUI();
if (vchanWindow.updateDocumentTitle && vchanWindow.activeChannelRds) {
vchanWindow.updateDocumentTitle(vchanWindow.activeChannelRds());
}
hostCore.updateDocumentTitle(hostCore.activeChannelRds());
vchanRenderSchedulerRelease();
}
async function vchanAllocate() {
if (!vchanSessionId || !vchanRigId) return;
const freqHz = typeof vchanWindow.lastFreqHz === "number" && vchanWindow.lastFreqHz > 0 ? vchanWindow.lastFreqHz : 0;
const freqHz = typeof hostState.lastFreqHz === "number" && hostState.lastFreqHz > 0 ? hostState.lastFreqHz : 0;
const modeEl = document.getElementById("mode");
const mode = modeEl ? modeEl.value || "USB" : "USB";
try {
@@ -251,11 +254,11 @@ async function vchanSubscribe(channelId) {
}
function vchanReconnectAudio() {
const ch = vchanIsOnVirtual() ? vchanActiveChannel() : null;
vchanWindow._audioChannelOverride = ch?.id ?? null;
if (!vchanWindow.rxActive) return;
vchanWindow.stopRxAudio?.();
hostState.audioChannelOverride = ch?.id ?? null;
if (!hostState.rxActive) return;
hostCore.stopRxAudio();
setTimeout(() => {
vchanWindow.startRxAudio?.();
hostCore.startRxAudio();
}, 300);
}
function vchanApplyCapabilities(caps) {
@@ -276,11 +279,7 @@ function vchanUpdateFreqDisplay() {
if (!ch) return;
const el = document.getElementById("freq");
if (!el) return;
if (vchanWindow.formatFreqForStep && typeof vchanWindow.jogUnit === "number") {
el.value = vchanWindow.formatFreqForStep(ch.freq_hz, vchanWindow.jogUnit);
} else {
el.value = (ch.freq_hz / 1e6).toFixed(6).replace(/\.?0+$/, "");
}
el.value = hostCore.formatFreqForStep(ch.freq_hz, hostState.jogUnit);
}
function vchanSyncModeDisplay() {
const modeEl = document.getElementById("mode");
@@ -290,21 +289,21 @@ function vchanSyncModeDisplay() {
if (ch && ch.mode) modeEl.value = ch.mode.toUpperCase();
}
const modeUpper = (modeEl.value || "").toUpperCase();
if (typeof vchanWindow.lastModeName === "string") {
if (modeUpper === "WFM" && vchanWindow.lastModeName !== "WFM") {
vchanWindow.setJogDivisor?.(10);
vchanWindow.resetRdsDisplay?.();
} else if (modeUpper !== "WFM" && vchanWindow.lastModeName === "WFM") {
vchanWindow.resetRdsDisplay?.();
if (typeof hostState.lastModeName === "string") {
if (modeUpper === "WFM" && hostState.lastModeName !== "WFM") {
hostCore.setJogDivisor(10);
hostCore.resetRdsDisplay();
} else if (modeUpper !== "WFM" && hostState.lastModeName === "WFM") {
hostCore.resetRdsDisplay();
}
vchanWindow.lastModeName = modeUpper;
hostState.lastModeName = modeUpper;
}
vchanWindow.updateWfmControls?.();
vchanWindow.updateSdrSquelchControlVisibility?.();
hostCore.updateWfmControls();
hostCore.updateSdrSquelchControlVisibility();
if (vchanWindow.refreshRdsUi) {
vchanWindow.refreshRdsUi();
} else {
vchanWindow.positionRdsPsOverlay?.();
hostCore.positionRdsPsOverlay();
}
}
function vchanSyncBwDisplay() {
@@ -314,12 +313,12 @@ function vchanSyncBwDisplay() {
const bwEl = document.getElementById("spectrum-bw-input");
if (!bwEl) return;
let bwHz = ch.bandwidth_hz || 0;
if (bwHz === 0 && vchanWindow.mwDefaultsForMode) {
bwHz = vchanWindow.mwDefaultsForMode(ch.mode)[0] || 0;
if (bwHz === 0) {
bwHz = hostCore.mwDefaultsForMode(ch.mode)[0] || 0;
}
if (bwHz > 0) {
bwEl.value = (bwHz / 1e3).toFixed(3).replace(/\.?0+$/, "");
vchanWindow.currentBandwidthHz = bwHz;
hostState.currentBandwidthHz = bwHz;
}
}
function vchanSyncAccentUI() {
@@ -333,25 +332,20 @@ function vchanSyncAccentUI() {
vchanSyncModeDisplay();
vchanSyncBwDisplay();
} else {
origRefreshFreqDisplay?.();
}
if (vchanWindow.updateDocumentTitle && vchanWindow.activeChannelRds) {
vchanWindow.updateDocumentTitle(vchanWindow.activeChannelRds());
hostCore.refreshFreqDisplay();
}
hostCore.updateDocumentTitle(hostCore.activeChannelRds());
}
var origRefreshFreqDisplay = null;
function vchanSetChannelFreq(freqHz) {
if (!vchanRigId || !vchanActiveId) return;
if (vchanWindow.lastSpectrumData && vchanWindow.lastSpectrumData.sample_rate > 0) {
const halfSpan = vchanWindow.lastSpectrumData.sample_rate / 2;
const center = vchanWindow.lastSpectrumData.center_hz;
if (hostState.lastSpectrumData && hostState.lastSpectrumData.sample_rate > 0) {
const halfSpan = hostState.lastSpectrumData.sample_rate / 2;
const center = hostState.lastSpectrumData.center_hz;
if (Math.abs(freqHz - center) > halfSpan) {
if (vchanWindow.showHint) {
vchanWindow.showHint(
`Out of SDR bandwidth (center ${(center / 1e6).toFixed(3)} MHz ±${(halfSpan / 1e3).toFixed(0)} kHz)`,
3e3
);
}
hostCore.showHint(
`Out of SDR bandwidth (center ${(center / 1e6).toFixed(3)} MHz ±${(halfSpan / 1e3).toFixed(0)} kHz)`,
3e3
);
return;
}
}
@@ -413,13 +407,17 @@ async function vchanInterceptBandwidth(bwHz) {
}
function vchanInterceptFrequency(freqHz) {
if (!vchanIsOnVirtual()) return false;
const core = vchanWindow.trx?.core;
const targetHz = Math.round(freqHz);
core?.armOptimisticFrequency(targetHz);
core?.applyLocalTunedFrequency(targetHz);
hostCore.armOptimisticFrequency(targetHz);
hostCore.applyLocalTunedFrequency(targetHz);
vchanSetChannelFreq(freqHz);
return true;
}
function vchanInterceptFreqDisplay() {
if (!vchanIsOnVirtual()) return false;
vchanUpdateFreqDisplay();
return true;
}
vchanWindow.trx ??= {};
vchanWindow.trx.modules ??= {};
vchanWindow.trx.modules.vchan = {
@@ -437,6 +435,7 @@ vchanWindow.trx.modules.vchan = {
interceptMode: vchanInterceptMode,
interceptBandwidth: vchanInterceptBandwidth,
interceptFrequency: vchanInterceptFrequency,
interceptFreqDisplay: vchanInterceptFreqDisplay,
takeSchedulerControl: vchanTakeSchedulerControl,
releaseToScheduler: vchanToggleSchedulerRelease
};
@@ -450,13 +449,3 @@ vchanWindow.trx.modules.vchan = {
vchanStartSchedulerReleasePolling();
vchanRenderSchedulerRelease();
})();
(function() {
origRefreshFreqDisplay = vchanWindow.refreshFreqDisplay ?? null;
vchanWindow.refreshFreqDisplay = function() {
if (vchanIsOnVirtual()) {
vchanUpdateFreqDisplay();
return;
}
origRefreshFreqDisplay?.();
};
})();
@@ -1,6 +1,10 @@
import {
hostCore
} from "./chunk-KL66PICH.js";
// src/plugins/vdes.ts
var vdesWindow = window;
var escapeVdesHtml = (input) => vdesWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
var escapeVdesHtml = (input) => hostCore.escapeMapHtml(input);
var vdesStatus = document.getElementById("vdes-status");
var vdesMessagesEl = document.getElementById("vdes-messages");
var vdesFilterInput = document.getElementById("vdes-filter");
@@ -231,7 +235,7 @@ document.getElementById("settings-clear-vdes-history")?.addEventListener("click"
void (async () => {
if (!await vdesWindow.trxUi.confirm({ title: "Clear VDES history?", message: "All stored VDES decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await vdesWindow.postPath?.("/clear_vdes_decode");
await hostCore.postPath("/clear_vdes_decode");
resetVdesHistoryView();
} catch (e) {
console.error("VDES history clear failed", e);
@@ -1,3 +1,7 @@
import {
hostCore
} from "./chunk-KL66PICH.js";
// src/plugins/wefax.ts
var wefaxWindow = window;
var wefaxDom = {
@@ -299,7 +303,7 @@ if (wefaxDom.toggleBtn) {
if (wefaxWindow.takeSchedulerControlForDecoderDisable) {
await wefaxWindow.takeSchedulerControlForDecoderDisable(toggleButton);
}
await wefaxWindow.postPath?.("/toggle_wefax_decode");
await hostCore.postPath("/toggle_wefax_decode");
} catch (e) {
console.error("WEFAX toggle failed", e);
}
@@ -310,7 +314,7 @@ if (wefaxDom.clearBtn) {
wefaxDom.clearBtn.addEventListener("click", () => {
void (async () => {
try {
await wefaxWindow.postPath?.("/clear_wefax_decode");
await hostCore.postPath("/clear_wefax_decode");
resetWefaxHistoryView();
} catch (e) {
console.error("WEFAX clear failed", e);
@@ -1,3 +1,7 @@
import {
hostCore
} from "./chunk-KL66PICH.js";
// src/plugins/wspr.ts
var wsprWindow = window;
var wsprStatus = document.getElementById("wspr-status");
@@ -229,7 +233,7 @@ wsprDecodeToggleBtn?.addEventListener("click", () => {
void (async () => {
try {
await wsprWindow.takeSchedulerControlForDecoderDisable?.(wsprDecodeToggleBtn);
await wsprWindow.postPath?.("/toggle_wspr_decode");
await hostCore.postPath("/toggle_wspr_decode");
} catch (error) {
console.error("WSPR toggle failed", error);
}
@@ -239,7 +243,7 @@ document.getElementById("settings-clear-wspr-history")?.addEventListener("click"
void (async () => {
if (!await wsprWindow.trxUi.confirm({ title: "Clear WSPR history?", message: "All stored WSPR decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
try {
await wsprWindow.postPath?.("/clear_wspr_decode");
await hostCore.postPath("/clear_wspr_decode");
resetWsprHistoryView();
} catch (error) {
console.error("WSPR history clear failed", error);