[fix](trx-frontend-http): restore bookmark host contract
CI / lint (pull_request) Failing after 1s
CI / test (pull_request) Failing after 1s
CI / frontend (pull_request) Failing after 42s
CI / reuse (pull_request) Failing after 0s

The TypeScript migration turned app.js from a classic script into an ES
module, so its top-level declarations stopped being shared globals.
bookmarks.ts was converted verbatim and kept reading them as window
properties, which app.ts no longer publishes.

Every bookmark interaction read undefined: the Add Bookmark and Select
All buttons stayed hidden because the auth check saw no authEnabled or
authRole, per-rig scopes were missing from the scope picker and the move
target, decoder checkboxes were never built, and Tune threw on
bridge.postPath before issuing a single request.

Extend the typed window.trx host contract instead of restoring globals,
as docs/frontend-architecture.md closes the standalone window property
list.  trx.state publishes authEnabled; trx.core publishes
setRigFrequency, applyLocalTunedFrequency, armOptimisticFrequency,
syncBandwidthInput, scheduleSpectrumDraw, and onDecoderRegistryReady.

Replace the vchan setRigFrequency wrapper with an interceptFrequency
service method, matching interceptMode and interceptBandwidth.  The
wrapper captured an undefined original and silently dropped every tune;
routing interception through setRigFrequency also restores virtual
channel redirection for the application's own tuning.

Read registry-built elements through bmOptionalEl, since bmEl throws and
the decoder checkboxes and decode toggle buttons are legitimately absent
until the registry arrives.

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 is contained in:
sjg
2026-08-02 10:15:56 +02:00
co-authored by Claude Opus 5
parent 695434942f
commit 23dbcac5b6
8 changed files with 324 additions and 217 deletions
@@ -1,10 +1,15 @@
// 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}`);
return element;
}
function bmOptionalEl(id) {
return document.getElementById(id);
}
function errorMessage(error) {
return error instanceof Error ? error.message : String(error);
}
@@ -34,7 +39,7 @@ function bmEsc(str) {
return d.innerHTML;
}
function bmCanControl() {
return typeof bridge.authEnabled !== "undefined" && !bridge.authEnabled || typeof bridge.authRole !== "undefined" && bridge.authRole === "control";
return !trxState.authEnabled || trxState.authRole === "control";
}
function bmSyncAccess() {
const canCtrl = bmCanControl();
@@ -44,8 +49,7 @@ function bmSyncAccess() {
if (selectAllBtn) selectAllBtn.style.display = canCtrl ? "" : "none";
}
function bmListScope() {
const rig = typeof bridge.lastActiveRigId !== "undefined" ? bridge.lastActiveRigId : null;
return rig || "general";
return trxState.lastActiveRigId || "general";
}
async function bmFetchOverlay() {
const overlayScope = bmListScope();
@@ -61,7 +65,7 @@ async function bmFetchOverlay() {
if (typeof bridge.syncBookmarkMapLocators === "function") {
bridge.syncBookmarkMapLocators(bmOverlayList);
}
if (typeof bridge.scheduleSpectrumDraw === "function") bridge.scheduleSpectrumDraw();
trxCore.scheduleSpectrumDraw();
}
async function bmFetch(categoryFilter) {
let url = "/bookmarks";
@@ -184,12 +188,12 @@ function bmChangePage(delta) {
bmRender(bmFilteredList);
}
function bmReadDecoders() {
return (bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable).filter((d) => bmEl("bm-dec-" + d.id)?.checked).map((d) => d.id);
return trxState.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 || []);
(bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable).forEach((d) => {
const el = bmEl("bm-dec-" + d.id);
trxState.decoderRegistry.filter((d) => d.bookmark_selectable).forEach((d) => {
const el = bmOptionalEl("bm-dec-" + d.id);
if (el) el.checked = set.has(d.id);
});
}
@@ -197,7 +201,7 @@ function bmBuildDecoderCheckboxes() {
const container = bmEl("bm-decoder-checkboxes");
if (!container) return;
container.innerHTML = "";
(bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable).forEach((d) => {
trxState.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;
@@ -227,19 +231,17 @@ function bmCloseForm() {
if (wrap) wrap.style.display = "none";
}
function bmPrefillFromStatus() {
if (typeof bridge.lastFreqHz === "number" && Number.isFinite(bridge.lastFreqHz)) {
bmEl("bm-freq").value = String(Math.round(bridge.lastFreqHz));
const freqHz = trxState.lastFreqHz;
if (freqHz != null && Number.isFinite(freqHz)) {
bmEl("bm-freq").value = String(Math.round(freqHz));
}
if (typeof bridge.lastModeName === "string" && bridge.lastModeName) {
bmEl("bm-mode").value = bridge.lastModeName;
if (trxState.lastModeName) {
bmEl("bm-mode").value = trxState.lastModeName;
}
if (typeof bridge.currentBandwidthHz === "number" && bridge.currentBandwidthHz > 0) {
bmEl("bm-bw").value = String(Math.round(bridge.currentBandwidthHz));
if (trxState.currentBandwidthHz > 0) {
bmEl("bm-bw").value = String(Math.round(trxState.currentBandwidthHz));
}
const activeDecoders = (bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable && d.activation === "toggle").filter((d) => {
const btn = bmEl(d.id + "-decode-toggle-btn");
return btn && btn.dataset.enabled === "true";
}).map((d) => d.id);
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);
bmWriteDecoders(activeDecoders);
}
async function bmSave(e) {
@@ -320,55 +322,43 @@ async function bmDelete(id) {
}
function bmApply(bm) {
try {
if (typeof bridge.modeEl !== "undefined" && bridge.modeEl) {
bridge.modeEl.value = (bm.mode || "").toUpperCase();
const modeEl = document.getElementById("mode");
if (modeEl) {
modeEl.value = (bm.mode || "").toUpperCase();
}
if (bm.bandwidth_hz) {
if (typeof bridge.currentBandwidthHz !== "undefined") {
bridge.currentBandwidthHz = bm.bandwidth_hz;
}
bridge.currentBandwidthHz = bm.bandwidth_hz;
if (typeof bridge.syncBandwidthInput === "function") {
bridge.syncBandwidthInput(bm.bandwidth_hz);
}
trxState.currentBandwidthHz = bm.bandwidth_hz;
trxCore.syncBandwidthInput(bm.bandwidth_hz);
}
if (typeof bridge.applyLocalTunedFrequency === "function") {
if (typeof bridge._freqOptimisticSeq !== "undefined") {
++bridge._freqOptimisticSeq;
bridge._freqOptimisticHz = bm.freq_hz;
}
bridge.applyLocalTunedFrequency(bm.freq_hz, true);
}
if (typeof bridge.scheduleSpectrumDraw === "function" && typeof bridge.lastSpectrumData !== "undefined" && bridge.lastSpectrumData) {
bridge.scheduleSpectrumDraw();
trxCore.armOptimisticFrequency(bm.freq_hz);
trxCore.applyLocalTunedFrequency(bm.freq_hz, true);
if (trxState.lastSpectrumData) {
trxCore.scheduleSpectrumDraw();
}
const tunePromise = (async () => {
await bridge.trx?.modules?.vchan?.takeSchedulerControl();
const onVirtual = await bridge.trx?.modules?.vchan?.interceptMode(bm.mode) ?? false;
await bridge.trx.modules.vchan?.takeSchedulerControl();
const onVirtual = await bridge.trx.modules.vchan?.interceptMode(bm.mode) ?? false;
if (!onVirtual) {
await bridge.postPath("/set_mode?mode=" + encodeURIComponent(bm.mode));
await trxCore.postPath("/set_mode?mode=" + encodeURIComponent(bm.mode));
}
if (bm.bandwidth_hz) {
const bwHandledByVchan = await bridge.trx?.modules?.vchan?.interceptBandwidth(bm.bandwidth_hz) ?? false;
const bwHandledByVchan = await bridge.trx.modules.vchan?.interceptBandwidth(bm.bandwidth_hz) ?? false;
if (!bwHandledByVchan) {
await bridge.postPath(`/set_bandwidth?hz=${bm.bandwidth_hz}`);
await trxCore.postPath(`/set_bandwidth?hz=${bm.bandwidth_hz}`);
}
}
if (typeof bridge.setRigFrequency === "function") {
await bridge.setRigFrequency(bm.freq_hz);
} else {
await bridge.postPath(`/set_freq?hz=${bm.freq_hz}`);
}
trxCore.setRigFrequency(bm.freq_hz);
})();
const hasDecoders = Array.isArray(bm.decoders) && bm.decoders.length > 0;
const modeUp = (bm.mode || "").toUpperCase();
const allToggleDecoders = (bridge.decoderRegistry || []).filter(
const allToggleDecoders = trxState.decoderRegistry.filter(
(d) => d.activation === "toggle"
);
const decoderPromise = allToggleDecoders.length ? (async () => {
let statusUrl = "/status";
if (typeof bridge.lastActiveRigId !== "undefined" && bridge.lastActiveRigId) {
statusUrl += "?remote=" + encodeURIComponent(bridge.lastActiveRigId);
const rigId = trxState.lastActiveRigId;
if (rigId) {
statusUrl += "?remote=" + encodeURIComponent(rigId);
}
const statusResp = await fetch(statusUrl);
if (!statusResp.ok) return;
@@ -387,7 +377,7 @@ function bmApply(bm) {
wanted = currentlyOn;
}
if (wanted !== currentlyOn) {
toggles.push(bridge.postPath("/toggle_" + d.id.replace(/-/g, "_") + "_decode"));
toggles.push(trxCore.postPath("/toggle_" + d.id.replace(/-/g, "_") + "_decode"));
}
}
if (toggles.length) await Promise.all(toggles);
@@ -399,8 +389,6 @@ function bmApply(bm) {
console.error("Failed to apply bookmark:", err);
}
}
bridge.trx ??= {};
bridge.trx.modules ??= {};
bridge.trx.modules.bookmarks = {
get overlayList() {
return bmOverlayList;
@@ -439,8 +427,8 @@ function bmUpdateSelectionUi() {
function bmPopulateMoveTarget() {
const sel = bmEl("bm-move-target");
if (!sel) return;
const rigIds = typeof bridge.lastRigIds !== "undefined" && Array.isArray(bridge.lastRigIds) ? bridge.lastRigIds : [];
const displayNames = typeof bridge.lastRigDisplayNames !== "undefined" ? bridge.lastRigDisplayNames : {};
const rigIds = trxState.lastRigIds;
const displayNames = trxState.lastRigDisplayNames;
const prev = sel.value;
sel.innerHTML = "";
if (bmScope !== "general") {
@@ -545,8 +533,8 @@ async function bmDeleteSelected() {
function bmPopulateScopePicker() {
const picker = bmEl("bm-scope-picker");
if (!picker) return;
const rigIds = typeof bridge.lastRigIds !== "undefined" && Array.isArray(bridge.lastRigIds) ? bridge.lastRigIds : [];
const displayNames = typeof bridge.lastRigDisplayNames !== "undefined" ? bridge.lastRigDisplayNames : {};
const rigIds = trxState.lastRigIds;
const displayNames = trxState.lastRigDisplayNames;
const prev = picker.value;
while (picker.options.length > 1) picker.remove(1);
rigIds.forEach((id) => {
@@ -565,9 +553,7 @@ function bmPopulateScopePicker() {
(function initBookmarks() {
bmSyncAccess();
bmBuildDecoderCheckboxes();
if (typeof bridge.onDecoderRegistryReady === "function") {
bridge.onDecoderRegistryReady(bmBuildDecoderCheckboxes);
}
trxCore.onDecoderRegistryReady(bmBuildDecoderCheckboxes);
bmPopulateScopePicker();
const scopePicker = bmEl("bm-scope-picker");
if (scopePicker) {