diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js
index aab2aeb9..99fa5838 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js
@@ -10,7 +10,7 @@ const pluginGroups = {
};
const loaded = /* @__PURE__ */ new Set();
const loading = /* @__PURE__ */ new Map();
-const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js"]);
+const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js"]);
function loadLegacyScript(path) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vdes.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vdes.js
index 73b4eda3..abad6484 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vdes.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vdes.js
@@ -1,282 +1,267 @@
"use strict";
-const vdesStatus = document.getElementById("vdes-status");
-const vdesMessagesEl = document.getElementById("vdes-messages");
-const vdesFilterInput = document.getElementById("vdes-filter");
-const vdesBarOverlay = document.getElementById("vdes-bar-overlay");
-const vdesChannelSummaryEl = document.getElementById("vdes-channel-summary");
-const vdesFrameCountEl = document.getElementById("vdes-frame-count");
-const vdesLatestSeenEl = document.getElementById("vdes-latest-seen");
-const VDES_BAR_WINDOW_MS = 15 * 60 * 1e3;
-let vdesFilterText = "";
-let vdesMessageHistory = [];
-function currentVdesHistoryRetentionMs() {
- return typeof window.getDecodeHistoryRetentionMs === "function" ? window.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
-}
-function pruneVdesMessageHistory() {
- const cutoffMs = Date.now() - currentVdesHistoryRetentionMs();
- vdesMessageHistory = vdesMessageHistory.filter((msg) => Number(msg?._tsMs) >= cutoffMs);
-}
-function scheduleVdesUi(key, job) {
- if (typeof window.trxScheduleUiFrameJob === "function") {
- window.trxScheduleUiFrameJob(key, job);
- return;
+(() => {
+ // src/plugins/vdes.ts
+ var vdesWindow = window;
+ var escapeVdesHtml = (input) => vdesWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
+ var vdesStatus = document.getElementById("vdes-status");
+ var vdesMessagesEl = document.getElementById("vdes-messages");
+ var vdesFilterInput = document.getElementById("vdes-filter");
+ var vdesBarOverlay = document.getElementById("vdes-bar-overlay");
+ var vdesChannelSummaryEl = document.getElementById("vdes-channel-summary");
+ var vdesFrameCountEl = document.getElementById("vdes-frame-count");
+ var vdesLatestSeenEl = document.getElementById("vdes-latest-seen");
+ var VDES_BAR_WINDOW_MS = 15 * 60 * 1e3;
+ var vdesFilterText = "";
+ var vdesMessageHistory = [];
+ function currentVdesHistoryRetentionMs() {
+ return typeof vdesWindow.getDecodeHistoryRetentionMs === "function" ? vdesWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
}
- job();
-}
-function scheduleVdesHistoryRender() {
- scheduleVdesUi("vdes-history", () => renderVdesHistory());
-}
-function scheduleVdesBarUpdate() {
- scheduleVdesUi("vdes-bar", () => updateVdesBar());
-}
-function currentVdesCenterText() {
- const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, "");
- const hz = raw ? Number(raw) : 0;
- if (!Number.isFinite(hz) || hz <= 0) return "100 kHz centered on tuned frequency";
- return `100 kHz @ ${(hz / 1e6).toFixed(3)} MHz`;
-}
-function vdesAgeText(tsMs) {
- if (!Number.isFinite(tsMs)) return "just now";
- const deltaMs = Math.max(0, Date.now() - tsMs);
- const seconds = Math.round(deltaMs / 1e3);
- if (seconds < 5) return "just now";
- if (seconds < 60) return `${seconds}s ago`;
- const minutes = Math.round(seconds / 60);
- if (minutes < 60) return `${minutes}m ago`;
- const hours = Math.round(minutes / 60);
- return `${hours}h ago`;
-}
-function vdesHexPreview(rawBytes) {
- if (!Array.isArray(rawBytes) || rawBytes.length === 0) return "--";
- return rawBytes.slice(0, 20).map((value) => Number(value).toString(16).padStart(2, "0")).join(" ").toUpperCase();
-}
-function updateVdesSummary() {
- pruneVdesMessageHistory();
- if (vdesChannelSummaryEl) {
- vdesChannelSummaryEl.textContent = currentVdesCenterText();
+ function pruneVdesMessageHistory() {
+ const cutoffMs = Date.now() - currentVdesHistoryRetentionMs();
+ vdesMessageHistory = vdesMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs);
}
- if (vdesFrameCountEl) {
- const count = vdesMessageHistory.length;
- vdesFrameCountEl.textContent = `${count} burst${count === 1 ? "" : "s"}`;
+ function scheduleVdesUi(key, job) {
+ if (typeof vdesWindow.trxScheduleUiFrameJob === "function") {
+ vdesWindow.trxScheduleUiFrameJob(key, job);
+ return;
+ }
+ job();
}
- if (vdesLatestSeenEl) {
- const latest = vdesMessageHistory[0];
- vdesLatestSeenEl.textContent = latest ? vdesAgeText(latest._tsMs) : "No traffic yet";
+ function scheduleVdesHistoryRender() {
+ scheduleVdesUi("vdes-history", () => {
+ renderVdesHistory();
+ });
}
-}
-function applyVdesFilterToRow(row) {
- if (!vdesFilterText) {
- row.style.display = "";
- return;
+ function scheduleVdesBarUpdate() {
+ scheduleVdesUi("vdes-bar", () => {
+ updateVdesBar();
+ });
}
- const text = row.dataset.filterText || "";
- row.style.display = text.includes(vdesFilterText) ? "" : "none";
-}
-function applyVdesFilterToAll() {
- if (!vdesMessagesEl) return;
- vdesMessagesEl.querySelectorAll(".vdes-message").forEach((row) => applyVdesFilterToRow(row));
-}
-function renderVdesRow(msg) {
- const row = document.createElement("div");
- row.className = "vdes-message";
- const ts = msg._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit"
- });
- const title = msg.vessel_name || "VDES Burst";
- const label = msg.callsign || "VDES";
- const info = msg.destination || "";
- const labelText = msg.message_label || "";
- const linkText = Number.isFinite(msg.link_id) ? `LID ${msg.link_id}` : "";
- const syncText = Number.isFinite(msg.sync_score) ? `Sync ${(Number(msg.sync_score) * 100).toFixed(0)}%` : "";
- const phaseText = Number.isFinite(msg.phase_rotation) ? `R${Number(msg.phase_rotation)}` : "";
- const fecText = msg.fec_state || "";
- const srcText = Number.isFinite(msg.source_id) ? `SRC ${Number(msg.source_id)}` : "";
- const dstText = Number.isFinite(msg.destination_id) ? `DST ${Number(msg.destination_id)}` : "";
- const sessionText = Number.isFinite(msg.session_id) ? `S${Number(msg.session_id)}` : "";
- const asmText = Number.isFinite(msg.asm_identifier) ? `ASM ${Number(msg.asm_identifier)}` : "";
- const countText = Number.isFinite(msg.data_count) ? `${Number(msg.data_count)} data bits` : "";
- const ackText = Number.isFinite(msg.ack_nack_mask) ? `ACK 0x${Number(msg.ack_nack_mask).toString(16).toUpperCase().padStart(4, "0")}` : "";
- const cqiText = Number.isFinite(msg.channel_quality) ? `CQ ${Number(msg.channel_quality)}` : "";
- const previewText = msg.payload_preview || "";
- const rawHex = vdesHexPreview(msg.raw_bytes);
- row.dataset.filterText = [
- title,
- label,
- labelText,
- info,
- srcText,
- dstText,
- sessionText,
- asmText,
- countText,
- ackText,
- cqiText,
- previewText,
- linkText,
- syncText,
- phaseText,
- fecText,
- rawHex,
- msg.message_type,
- msg.bit_len
- ].filter(Boolean).join(" ").toUpperCase();
- row.innerHTML = `
${ts}${escapeMapHtml(title)}${escapeMapHtml(label)}` + (labelText ? `${escapeMapHtml(labelText)}` : "") + (linkText ? `${escapeMapHtml(linkText)}` : "") + (srcText ? `${escapeMapHtml(srcText)}` : "") + (dstText ? `${escapeMapHtml(dstText)}` : "") + (syncText ? `${escapeMapHtml(syncText)}` : "") + (phaseText ? `${escapeMapHtml(phaseText)}` : "") + `T${escapeMapHtml(String(msg.message_type ?? "--"))}
${escapeMapHtml(currentVdesCenterText())}${escapeMapHtml(`${msg.bit_len || 0} bits`)}` + (sessionText ? `${escapeMapHtml(sessionText)}` : "") + (asmText ? `${escapeMapHtml(asmText)}` : "") + (countText ? `${escapeMapHtml(countText)}` : "") + (ackText ? `${escapeMapHtml(ackText)}` : "") + (cqiText ? `${escapeMapHtml(cqiText)}` : "") + (info ? `${escapeMapHtml(info)}` : "") + (fecText ? `${escapeMapHtml(fecText)}` : "") + `${escapeMapHtml(vdesAgeText(msg._tsMs))}
` + (previewText ? `${escapeMapHtml(previewText)}` : "") + (previewText ? `·` : "") + `${escapeMapHtml(rawHex)}
`;
- applyVdesFilterToRow(row);
- return row;
-}
-function updateVdesBar() {
- if (!vdesBarOverlay) return;
- updateVdesSummary();
- const isVdes = (document.getElementById("mode")?.value || "").toUpperCase() === "VDES";
- const cutoffMs = Date.now() - VDES_BAR_WINDOW_MS;
- const messages = vdesMessageHistory.filter((msg) => msg._tsMs >= cutoffMs).slice(0, 6);
- if (!isVdes || messages.length === 0) {
- vdesBarOverlay.style.display = "none";
- vdesBarOverlay.innerHTML = "";
- return;
+ function currentVdesCenterText() {
+ const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, "");
+ const hz = raw ? Number(raw) : 0;
+ if (!Number.isFinite(hz) || hz <= 0) return "100 kHz centered on tuned frequency";
+ return `100 kHz @ ${(hz / 1e6).toFixed(3)} MHz`;
}
- let html = ``;
- for (const msg of messages) {
- const ts = msg._ts ? `${msg._ts}` : "";
- const label = escapeMapHtml(msg.callsign || "VDES");
- const title = escapeMapHtml(msg.vessel_name || "Burst");
- const detail = [
- `${msg.bit_len || 0} bits`,
- msg.message_label ? escapeMapHtml(msg.message_label) : null,
- Number.isFinite(msg.source_id) ? `src ${Number(msg.source_id)}` : null,
- Number.isFinite(msg.destination_id) ? `dst ${Number(msg.destination_id)}` : null,
- Number.isFinite(msg.link_id) ? `LID ${Number(msg.link_id)}` : null,
- Number.isFinite(msg.asm_identifier) ? `ASM ${Number(msg.asm_identifier)}` : null,
- Number.isFinite(msg.sync_score) ? `sync ${(Number(msg.sync_score) * 100).toFixed(0)}%` : null,
- Number.isFinite(msg.phase_rotation) ? `rot ${Number(msg.phase_rotation)}` : null,
- msg.destination ? escapeMapHtml(msg.destination) : null,
- escapeMapHtml(vdesAgeText(msg._tsMs))
- ].filter(Boolean).join(" · ");
- html += `${ts}${title} ${label}: ${detail}
`;
+ function vdesAgeText(tsMs) {
+ if (typeof tsMs !== "number" || !Number.isFinite(tsMs)) return "just now";
+ const deltaMs = Math.max(0, Date.now() - tsMs);
+ const seconds = Math.round(deltaMs / 1e3);
+ if (seconds < 5) return "just now";
+ if (seconds < 60) return `${seconds}s ago`;
+ const minutes = Math.round(seconds / 60);
+ if (minutes < 60) return `${minutes}m ago`;
+ const hours = Math.round(minutes / 60);
+ return `${hours}h ago`;
}
- vdesBarOverlay.innerHTML = html;
- vdesBarOverlay.style.display = "flex";
-}
-window.updateVdesBar = updateVdesBar;
-window.clearVdesBar = function() {
- window.resetVdesHistoryView();
-};
-window.resetVdesHistoryView = function() {
- if (vdesMessagesEl) vdesMessagesEl.innerHTML = "";
- vdesMessageHistory = [];
- updateVdesBar();
- renderVdesHistory();
-};
-function renderVdesHistory() {
- pruneVdesMessageHistory();
- if (!vdesMessagesEl) {
- updateVdesSummary();
- return;
+ function vdesHexPreview(rawBytes) {
+ if (!Array.isArray(rawBytes) || rawBytes.length === 0) return "--";
+ return rawBytes.slice(0, 20).map((value) => value.toString(16).padStart(2, "0")).join(" ").toUpperCase();
}
- const fragment = document.createDocumentFragment();
- for (let i = 0; i < vdesMessageHistory.length; i += 1) {
- fragment.appendChild(renderVdesRow(vdesMessageHistory[i]));
+ function updateVdesSummary() {
+ pruneVdesMessageHistory();
+ if (vdesChannelSummaryEl) {
+ vdesChannelSummaryEl.textContent = currentVdesCenterText();
+ }
+ if (vdesFrameCountEl) {
+ const count = vdesMessageHistory.length;
+ vdesFrameCountEl.textContent = `${count} burst${count === 1 ? "" : "s"}`;
+ }
+ if (vdesLatestSeenEl) {
+ const latest = vdesMessageHistory[0];
+ vdesLatestSeenEl.textContent = latest ? vdesAgeText(latest._tsMs) : "No traffic yet";
+ }
}
- vdesMessagesEl.replaceChildren(fragment);
- updateVdesSummary();
-}
-function addVdesMessage(msg) {
- const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
- msg._tsMs = tsMs;
- msg._ts = new Date(tsMs).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit"
- });
- vdesMessageHistory.unshift(msg);
- pruneVdesMessageHistory();
- scheduleVdesBarUpdate();
- scheduleVdesHistoryRender();
-}
-function normalizeServerVdesMessage(msg) {
- return {
- rig_id: msg.rig_id || null,
- message_type: msg.message_type,
- bit_len: msg.bit_len,
- raw_bytes: msg.raw_bytes,
- lat: msg.lat,
- lon: msg.lon,
- vessel_name: msg.vessel_name,
- callsign: msg.callsign,
- destination: msg.destination,
- message_label: msg.message_label,
- session_id: msg.session_id,
- source_id: msg.source_id,
- destination_id: msg.destination_id,
- data_count: msg.data_count,
- asm_identifier: msg.asm_identifier,
- ack_nack_mask: msg.ack_nack_mask,
- channel_quality: msg.channel_quality,
- payload_preview: msg.payload_preview,
- link_id: msg.link_id,
- sync_score: msg.sync_score,
- sync_errors: msg.sync_errors,
- phase_rotation: msg.phase_rotation,
- fec_state: msg.fec_state,
- ts_ms: msg.ts_ms
- };
-}
-window.onServerVdesBatch = function(messages) {
- if (!Array.isArray(messages) || messages.length === 0) return;
- if (vdesStatus) vdesStatus.textContent = "Receiving";
- const normalized = [];
- for (const msg of messages) {
- const next = normalizeServerVdesMessage(msg);
- const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
- next._tsMs = tsMs;
- next._ts = new Date(tsMs).toLocaleTimeString([], {
+ function applyVdesFilterToRow(row) {
+ if (!vdesFilterText) {
+ row.style.display = "";
+ return;
+ }
+ const text = row.dataset.filterText || "";
+ row.style.display = text.includes(vdesFilterText) ? "" : "none";
+ }
+ function renderVdesRow(msg) {
+ const row = document.createElement("div");
+ row.className = "vdes-message";
+ const ts = msg._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
- if (next.lat != null && next.lon != null && window.vdesMapAddPoint) {
- window.vdesMapAddPoint(next);
+ const title = msg.vessel_name || "VDES Burst";
+ const label = msg.callsign || "VDES";
+ const info = msg.destination || "";
+ const labelText = msg.message_label || "";
+ const linkText = Number.isFinite(msg.link_id) ? `LID ${msg.link_id}` : "";
+ const syncText = Number.isFinite(msg.sync_score) ? `Sync ${(Number(msg.sync_score) * 100).toFixed(0)}%` : "";
+ const phaseText = Number.isFinite(msg.phase_rotation) ? `R${Number(msg.phase_rotation)}` : "";
+ const fecText = msg.fec_state || "";
+ const srcText = Number.isFinite(msg.source_id) ? `SRC ${Number(msg.source_id)}` : "";
+ const dstText = Number.isFinite(msg.destination_id) ? `DST ${Number(msg.destination_id)}` : "";
+ const sessionText = Number.isFinite(msg.session_id) ? `S${Number(msg.session_id)}` : "";
+ const asmText = Number.isFinite(msg.asm_identifier) ? `ASM ${Number(msg.asm_identifier)}` : "";
+ const countText = Number.isFinite(msg.data_count) ? `${Number(msg.data_count)} data bits` : "";
+ const ackText = Number.isFinite(msg.ack_nack_mask) ? `ACK 0x${Number(msg.ack_nack_mask).toString(16).toUpperCase().padStart(4, "0")}` : "";
+ const cqiText = Number.isFinite(msg.channel_quality) ? `CQ ${Number(msg.channel_quality)}` : "";
+ const previewText = msg.payload_preview || "";
+ const rawHex = vdesHexPreview(msg.raw_bytes);
+ row.dataset.filterText = [
+ title,
+ label,
+ labelText,
+ info,
+ srcText,
+ dstText,
+ sessionText,
+ asmText,
+ countText,
+ ackText,
+ cqiText,
+ previewText,
+ linkText,
+ syncText,
+ phaseText,
+ fecText,
+ rawHex,
+ msg.message_type,
+ msg.bit_len
+ ].filter(Boolean).join(" ").toUpperCase();
+ row.innerHTML = `${ts}${escapeVdesHtml(title)}${escapeVdesHtml(label)}` + (labelText ? `${escapeVdesHtml(labelText)}` : "") + (linkText ? `${escapeVdesHtml(linkText)}` : "") + (srcText ? `${escapeVdesHtml(srcText)}` : "") + (dstText ? `${escapeVdesHtml(dstText)}` : "") + (syncText ? `${escapeVdesHtml(syncText)}` : "") + (phaseText ? `${escapeVdesHtml(phaseText)}` : "") + `T${escapeVdesHtml(String(msg.message_type ?? "--"))}
${escapeVdesHtml(currentVdesCenterText())}${escapeVdesHtml(`${msg.bit_len || 0} bits`)}` + (sessionText ? `${escapeVdesHtml(sessionText)}` : "") + (asmText ? `${escapeVdesHtml(asmText)}` : "") + (countText ? `${escapeVdesHtml(countText)}` : "") + (ackText ? `${escapeVdesHtml(ackText)}` : "") + (cqiText ? `${escapeVdesHtml(cqiText)}` : "") + (info ? `${escapeVdesHtml(info)}` : "") + (fecText ? `${escapeVdesHtml(fecText)}` : "") + `${escapeVdesHtml(vdesAgeText(msg._tsMs))}
` + (previewText ? `${escapeVdesHtml(previewText)}` : "") + (previewText ? `·` : "") + `${escapeVdesHtml(rawHex)}
`;
+ applyVdesFilterToRow(row);
+ return row;
+ }
+ function updateVdesBar() {
+ if (!vdesBarOverlay) return;
+ updateVdesSummary();
+ const isVdes = (document.getElementById("mode")?.value || "").toUpperCase() === "VDES";
+ const cutoffMs = Date.now() - VDES_BAR_WINDOW_MS;
+ const messages = vdesMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs).slice(0, 6);
+ if (!isVdes || messages.length === 0) {
+ vdesBarOverlay.style.display = "none";
+ vdesBarOverlay.innerHTML = "";
+ return;
}
- normalized.push(next);
+ let html = ``;
+ for (const msg of messages) {
+ const ts = msg._ts ? `${msg._ts}` : "";
+ const label = escapeVdesHtml(msg.callsign || "VDES");
+ const title = escapeVdesHtml(msg.vessel_name || "Burst");
+ const detail = [
+ `${msg.bit_len || 0} bits`,
+ msg.message_label ? escapeVdesHtml(msg.message_label) : null,
+ Number.isFinite(msg.source_id) ? `src ${Number(msg.source_id)}` : null,
+ Number.isFinite(msg.destination_id) ? `dst ${Number(msg.destination_id)}` : null,
+ Number.isFinite(msg.link_id) ? `LID ${Number(msg.link_id)}` : null,
+ Number.isFinite(msg.asm_identifier) ? `ASM ${Number(msg.asm_identifier)}` : null,
+ Number.isFinite(msg.sync_score) ? `sync ${(Number(msg.sync_score) * 100).toFixed(0)}%` : null,
+ Number.isFinite(msg.phase_rotation) ? `rot ${Number(msg.phase_rotation)}` : null,
+ msg.destination ? escapeVdesHtml(msg.destination) : null,
+ escapeVdesHtml(vdesAgeText(msg._tsMs))
+ ].filter(Boolean).join(" · ");
+ html += `${ts}${title} ${label}: ${detail}
`;
+ }
+ vdesBarOverlay.innerHTML = html;
+ vdesBarOverlay.style.display = "flex";
}
- normalized.reverse();
- vdesMessageHistory = normalized.concat(vdesMessageHistory);
- pruneVdesMessageHistory();
- scheduleVdesBarUpdate();
- scheduleVdesHistoryRender();
-};
-window.restoreVdesHistory = function(messages) {
- window.onServerVdesBatch(messages);
-};
-document.getElementById("settings-clear-vdes-history")?.addEventListener("click", async () => {
- if (!await window.trxUi.confirm({ title: "Clear VDES history?", message: "All stored VDES decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
- try {
- await postPath("/clear_vdes_decode");
- window.resetVdesHistoryView();
- } catch (e) {
- console.error("VDES history clear failed", e);
- }
-});
-if (vdesFilterInput) {
- vdesFilterInput.addEventListener("input", () => {
- vdesFilterText = vdesFilterInput.value.trim().toUpperCase();
+ vdesWindow.updateVdesBar = updateVdesBar;
+ vdesWindow.clearVdesBar = function() {
+ vdesWindow.resetVdesHistoryView?.();
+ };
+ vdesWindow.resetVdesHistoryView = function() {
+ if (vdesMessagesEl) vdesMessagesEl.innerHTML = "";
+ vdesMessageHistory = [];
+ updateVdesBar();
renderVdesHistory();
- });
-}
-window.onServerVdes = function(msg) {
- if (vdesStatus) vdesStatus.textContent = "Receiving";
- const next = normalizeServerVdesMessage(msg);
- addVdesMessage(next);
- if (next.lat != null && next.lon != null && window.vdesMapAddPoint) {
- window.vdesMapAddPoint(next);
+ };
+ function renderVdesHistory() {
+ pruneVdesMessageHistory();
+ if (!vdesMessagesEl) {
+ updateVdesSummary();
+ return;
+ }
+ const fragment = document.createDocumentFragment();
+ for (const message of vdesMessageHistory) {
+ fragment.appendChild(renderVdesRow(message));
+ }
+ vdesMessagesEl.replaceChildren(fragment);
+ updateVdesSummary();
}
-};
-window.pruneVdesHistoryView = function() {
- pruneVdesMessageHistory();
- updateVdesBar();
- renderVdesHistory();
-};
-updateVdesSummary();
-if (window._trxDrainPendingDecode) window._trxDrainPendingDecode("vdes");
+ function addVdesMessage(msg) {
+ const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
+ msg._tsMs = tsMs;
+ msg._ts = new Date(tsMs).toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit"
+ });
+ vdesMessageHistory.unshift(msg);
+ pruneVdesMessageHistory();
+ scheduleVdesBarUpdate();
+ scheduleVdesHistoryRender();
+ }
+ function normalizeServerVdesMessage(msg) {
+ return {
+ ...msg,
+ rig_id: msg.rig_id || null
+ };
+ }
+ vdesWindow.onServerVdesBatch = function(messages) {
+ if (!Array.isArray(messages) || messages.length === 0) return;
+ if (vdesStatus) vdesStatus.textContent = "Receiving";
+ const normalized = [];
+ for (const msg of messages) {
+ const next = normalizeServerVdesMessage(msg);
+ 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"
+ });
+ if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) {
+ vdesWindow.vdesMapAddPoint(next);
+ }
+ normalized.push(next);
+ }
+ normalized.reverse();
+ vdesMessageHistory = normalized.concat(vdesMessageHistory);
+ pruneVdesMessageHistory();
+ scheduleVdesBarUpdate();
+ scheduleVdesHistoryRender();
+ };
+ vdesWindow.restoreVdesHistory = function(messages) {
+ vdesWindow.onServerVdesBatch?.(messages);
+ };
+ 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");
+ vdesWindow.resetVdesHistoryView?.();
+ } catch (e) {
+ console.error("VDES history clear failed", e);
+ }
+ })();
+ });
+ if (vdesFilterInput) {
+ vdesFilterInput.addEventListener("input", () => {
+ vdesFilterText = vdesFilterInput.value.trim().toUpperCase();
+ renderVdesHistory();
+ });
+ }
+ vdesWindow.onServerVdes = function(msg) {
+ if (vdesStatus) vdesStatus.textContent = "Receiving";
+ const next = normalizeServerVdesMessage(msg);
+ addVdesMessage(next);
+ if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) {
+ vdesWindow.vdesMapAddPoint(next);
+ }
+ };
+ vdesWindow.pruneVdesHistoryView = function() {
+ pruneVdesMessageHistory();
+ updateVdesBar();
+ renderVdesHistory();
+ };
+ updateVdesSummary();
+ vdesWindow._trxDrainPendingDecode?.("vdes");
+})();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs
index 53e313bd..6ce24053 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/build.mjs
@@ -31,7 +31,6 @@ await build({
"sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.js"),
scheduler: path.join(sourceDir, "plugins", "scheduler.js"),
vchan: path.join(sourceDir, "plugins", "vchan.js"),
- vdes: path.join(sourceDir, "plugins", "vdes.js"),
wefax: path.join(sourceDir, "plugins", "wefax.js"),
},
outdir: outputDir,
@@ -51,6 +50,7 @@ await build({
cw: path.join(sourceDir, "plugins", "cw.ts"),
ft8: path.join(sourceDir, "plugins", "ft8.ts"),
"leaflet-ais-tracksymbol": path.join(sourceDir, "leaflet-ais-tracksymbol.ts"),
+ vdes: path.join(sourceDir, "plugins", "vdes.ts"),
},
outdir: outputDir,
bundle: true,
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts
index cdf6c769..2cc0a37a 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts
@@ -16,7 +16,7 @@ const pluginGroups: Readonly> = {
const loaded = new Set();
const loading = new Map>();
-const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js"]);
+const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js"]);
function loadLegacyScript(path: string): Promise {
return new Promise((resolve, reject) => {
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.ts
similarity index 56%
rename from src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.js
rename to src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.ts
index eac37c1c..25cbd75f 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/vdes.ts
@@ -2,54 +2,107 @@
//
// SPDX-License-Identifier: GPL-2.0-or-later
+export {};
+
+interface VdesMessage {
+ rig_id?: string | null;
+ message_type?: number;
+ bit_len?: number;
+ raw_bytes?: number[];
+ lat?: number | null;
+ lon?: number | null;
+ vessel_name?: string | null;
+ callsign?: string | null;
+ destination?: string | null;
+ message_label?: string | null;
+ session_id?: number | null;
+ source_id?: number | null;
+ destination_id?: number | null;
+ data_count?: number | null;
+ asm_identifier?: number | null;
+ ack_nack_mask?: number | null;
+ channel_quality?: number | null;
+ payload_preview?: string | null;
+ link_id?: number | null;
+ sync_score?: number | null;
+ sync_errors?: number | null;
+ phase_rotation?: number | null;
+ fec_state?: string | null;
+ ts_ms?: number | null;
+ _tsMs?: number;
+ _ts?: string;
+}
+
+interface VdesBridge {
+ getDecodeHistoryRetentionMs?: () => number;
+ trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
+ escapeMapHtml?: (input: string) => string;
+ postPath?: (path: string) => Promise;
+ trxUi: { confirm(options: { title: string; message: string; confirmLabel: string }): Promise };
+ vdesMapAddPoint?: (message: VdesMessage) => void;
+ updateVdesBar?: () => void;
+ clearVdesBar?: () => void;
+ resetVdesHistoryView?: () => void;
+ onServerVdesBatch?: (messages: VdesMessage[]) => void;
+ restoreVdesHistory?: (messages: VdesMessage[]) => void;
+ onServerVdes?: (message: VdesMessage) => void;
+ pruneVdesHistoryView?: () => void;
+ _trxDrainPendingDecode?: (decoder: string) => void;
+}
+
+const vdesWindow = window as unknown as VdesBridge;
+const escapeVdesHtml = (input: string): string => vdesWindow.escapeMapHtml?.(input) ?? input
+ .replaceAll("&", "&").replaceAll("<", "<")
+ .replaceAll(">", ">").replaceAll('"', """);
+
// --- VDES Decoder Plugin (server-side decode) ---
const vdesStatus = document.getElementById("vdes-status");
const vdesMessagesEl = document.getElementById("vdes-messages");
-const vdesFilterInput = document.getElementById("vdes-filter");
+const vdesFilterInput = document.getElementById("vdes-filter") as HTMLInputElement | null;
const vdesBarOverlay = document.getElementById("vdes-bar-overlay");
const vdesChannelSummaryEl = document.getElementById("vdes-channel-summary");
const vdesFrameCountEl = document.getElementById("vdes-frame-count");
const vdesLatestSeenEl = document.getElementById("vdes-latest-seen");
const VDES_BAR_WINDOW_MS = 15 * 60 * 1000;
let vdesFilterText = "";
-let vdesMessageHistory = [];
+let vdesMessageHistory: VdesMessage[] = [];
-function currentVdesHistoryRetentionMs() {
- return typeof window.getDecodeHistoryRetentionMs === "function"
- ? window.getDecodeHistoryRetentionMs()
+function currentVdesHistoryRetentionMs(): number {
+ return typeof vdesWindow.getDecodeHistoryRetentionMs === "function"
+ ? vdesWindow.getDecodeHistoryRetentionMs()
: 24 * 60 * 60 * 1000;
}
function pruneVdesMessageHistory() {
const cutoffMs = Date.now() - currentVdesHistoryRetentionMs();
- vdesMessageHistory = vdesMessageHistory.filter((msg) => Number(msg?._tsMs) >= cutoffMs);
+ vdesMessageHistory = vdesMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs);
}
-function scheduleVdesUi(key, job) {
- if (typeof window.trxScheduleUiFrameJob === "function") {
- window.trxScheduleUiFrameJob(key, job);
+function scheduleVdesUi(key: string, job: () => void): void {
+ if (typeof vdesWindow.trxScheduleUiFrameJob === "function") {
+ vdesWindow.trxScheduleUiFrameJob(key, job);
return;
}
job();
}
function scheduleVdesHistoryRender() {
- scheduleVdesUi("vdes-history", () => renderVdesHistory());
+ scheduleVdesUi("vdes-history", () => { renderVdesHistory(); });
}
function scheduleVdesBarUpdate() {
- scheduleVdesUi("vdes-bar", () => updateVdesBar());
+ scheduleVdesUi("vdes-bar", () => { updateVdesBar(); });
}
function currentVdesCenterText() {
- const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, "");
+ const raw = ((document.getElementById("freq") as HTMLInputElement | null)?.value || "").replace(/[^\d]/g, "");
const hz = raw ? Number(raw) : 0;
if (!Number.isFinite(hz) || hz <= 0) return "100 kHz centered on tuned frequency";
return `100 kHz @ ${(hz / 1_000_000).toFixed(3)} MHz`;
}
-function vdesAgeText(tsMs) {
- if (!Number.isFinite(tsMs)) return "just now";
+function vdesAgeText(tsMs: number | undefined): string {
+ if (typeof tsMs !== "number" || !Number.isFinite(tsMs)) return "just now";
const deltaMs = Math.max(0, Date.now() - tsMs);
const seconds = Math.round(deltaMs / 1000);
if (seconds < 5) return "just now";
@@ -60,11 +113,11 @@ function vdesAgeText(tsMs) {
return `${hours}h ago`;
}
-function vdesHexPreview(rawBytes) {
+function vdesHexPreview(rawBytes: number[] | undefined): string {
if (!Array.isArray(rawBytes) || rawBytes.length === 0) return "--";
return rawBytes
.slice(0, 20)
- .map((value) => Number(value).toString(16).padStart(2, "0"))
+ .map((value) => value.toString(16).padStart(2, "0"))
.join(" ")
.toUpperCase();
}
@@ -84,7 +137,7 @@ function updateVdesSummary() {
}
}
-function applyVdesFilterToRow(row) {
+function applyVdesFilterToRow(row: HTMLElement): void {
if (!vdesFilterText) {
row.style.display = "";
return;
@@ -93,12 +146,7 @@ function applyVdesFilterToRow(row) {
row.style.display = text.includes(vdesFilterText) ? "" : "none";
}
-function applyVdesFilterToAll() {
- if (!vdesMessagesEl) return;
- vdesMessagesEl.querySelectorAll(".vdes-message").forEach((row) => applyVdesFilterToRow(row));
-}
-
-function renderVdesRow(msg) {
+function renderVdesRow(msg: VdesMessage): HTMLElement {
const row = document.createElement("div");
row.className = "vdes-message";
const ts = msg._ts || new Date().toLocaleTimeString([], {
@@ -150,32 +198,32 @@ function renderVdesRow(msg) {
row.innerHTML =
`` +
`${ts}` +
- `${escapeMapHtml(title)}` +
- `${escapeMapHtml(label)}` +
- (labelText ? `${escapeMapHtml(labelText)}` : "") +
- (linkText ? `${escapeMapHtml(linkText)}` : "") +
- (srcText ? `${escapeMapHtml(srcText)}` : "") +
- (dstText ? `${escapeMapHtml(dstText)}` : "") +
- (syncText ? `${escapeMapHtml(syncText)}` : "") +
- (phaseText ? `${escapeMapHtml(phaseText)}` : "") +
- `T${escapeMapHtml(String(msg.message_type ?? "--"))}` +
+ `${escapeVdesHtml(title)}` +
+ `${escapeVdesHtml(label)}` +
+ (labelText ? `${escapeVdesHtml(labelText)}` : "") +
+ (linkText ? `${escapeVdesHtml(linkText)}` : "") +
+ (srcText ? `${escapeVdesHtml(srcText)}` : "") +
+ (dstText ? `${escapeVdesHtml(dstText)}` : "") +
+ (syncText ? `${escapeVdesHtml(syncText)}` : "") +
+ (phaseText ? `${escapeVdesHtml(phaseText)}` : "") +
+ `T${escapeVdesHtml(String(msg.message_type ?? "--"))}` +
`
` +
`` +
- `${escapeMapHtml(currentVdesCenterText())}` +
- `${escapeMapHtml(`${msg.bit_len || 0} bits`)}` +
- (sessionText ? `${escapeMapHtml(sessionText)}` : "") +
- (asmText ? `${escapeMapHtml(asmText)}` : "") +
- (countText ? `${escapeMapHtml(countText)}` : "") +
- (ackText ? `${escapeMapHtml(ackText)}` : "") +
- (cqiText ? `${escapeMapHtml(cqiText)}` : "") +
- (info ? `${escapeMapHtml(info)}` : "") +
- (fecText ? `${escapeMapHtml(fecText)}` : "") +
- `${escapeMapHtml(vdesAgeText(msg._tsMs))}` +
+ `${escapeVdesHtml(currentVdesCenterText())}` +
+ `${escapeVdesHtml(`${msg.bit_len || 0} bits`)}` +
+ (sessionText ? `${escapeVdesHtml(sessionText)}` : "") +
+ (asmText ? `${escapeVdesHtml(asmText)}` : "") +
+ (countText ? `${escapeVdesHtml(countText)}` : "") +
+ (ackText ? `${escapeVdesHtml(ackText)}` : "") +
+ (cqiText ? `${escapeVdesHtml(cqiText)}` : "") +
+ (info ? `${escapeVdesHtml(info)}` : "") +
+ (fecText ? `${escapeVdesHtml(fecText)}` : "") +
+ `${escapeVdesHtml(vdesAgeText(msg._tsMs))}` +
`
` +
`` +
- (previewText ? `${escapeMapHtml(previewText)}` : "") +
+ (previewText ? `${escapeVdesHtml(previewText)}` : "") +
(previewText ? `·` : "") +
- `${escapeMapHtml(rawHex)}` +
+ `${escapeVdesHtml(rawHex)}` +
`
`;
applyVdesFilterToRow(row);
return row;
@@ -184,9 +232,9 @@ function renderVdesRow(msg) {
function updateVdesBar() {
if (!vdesBarOverlay) return;
updateVdesSummary();
- const isVdes = (document.getElementById("mode")?.value || "").toUpperCase() === "VDES";
+ const isVdes = ((document.getElementById("mode") as HTMLSelectElement | null)?.value || "").toUpperCase() === "VDES";
const cutoffMs = Date.now() - VDES_BAR_WINDOW_MS;
- const messages = vdesMessageHistory.filter((msg) => msg._tsMs >= cutoffMs).slice(0, 6);
+ const messages = vdesMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs).slice(0, 6);
if (!isVdes || messages.length === 0) {
vdesBarOverlay.style.display = "none";
vdesBarOverlay.innerHTML = "";
@@ -196,19 +244,19 @@ function updateVdesBar() {
let html = '';
for (const msg of messages) {
const ts = msg._ts ? `${msg._ts}` : "";
- const label = escapeMapHtml(msg.callsign || "VDES");
- const title = escapeMapHtml(msg.vessel_name || "Burst");
+ const label = escapeVdesHtml(msg.callsign || "VDES");
+ const title = escapeVdesHtml(msg.vessel_name || "Burst");
const detail = [
`${msg.bit_len || 0} bits`,
- msg.message_label ? escapeMapHtml(msg.message_label) : null,
+ msg.message_label ? escapeVdesHtml(msg.message_label) : null,
Number.isFinite(msg.source_id) ? `src ${Number(msg.source_id)}` : null,
Number.isFinite(msg.destination_id) ? `dst ${Number(msg.destination_id)}` : null,
Number.isFinite(msg.link_id) ? `LID ${Number(msg.link_id)}` : null,
Number.isFinite(msg.asm_identifier) ? `ASM ${Number(msg.asm_identifier)}` : null,
Number.isFinite(msg.sync_score) ? `sync ${(Number(msg.sync_score) * 100).toFixed(0)}%` : null,
Number.isFinite(msg.phase_rotation) ? `rot ${Number(msg.phase_rotation)}` : null,
- msg.destination ? escapeMapHtml(msg.destination) : null,
- escapeMapHtml(vdesAgeText(msg._tsMs)),
+ msg.destination ? escapeVdesHtml(msg.destination) : null,
+ escapeVdesHtml(vdesAgeText(msg._tsMs)),
]
.filter(Boolean)
.join(" · ");
@@ -217,12 +265,12 @@ function updateVdesBar() {
vdesBarOverlay.innerHTML = html;
vdesBarOverlay.style.display = "flex";
}
-window.updateVdesBar = updateVdesBar;
-window.clearVdesBar = function() {
- window.resetVdesHistoryView();
+vdesWindow.updateVdesBar = updateVdesBar;
+vdesWindow.clearVdesBar = function() {
+ vdesWindow.resetVdesHistoryView?.();
};
-window.resetVdesHistoryView = function() {
+vdesWindow.resetVdesHistoryView = function() {
if (vdesMessagesEl) vdesMessagesEl.innerHTML = "";
vdesMessageHistory = [];
updateVdesBar();
@@ -236,14 +284,14 @@ function renderVdesHistory() {
return;
}
const fragment = document.createDocumentFragment();
- for (let i = 0; i < vdesMessageHistory.length; i += 1) {
- fragment.appendChild(renderVdesRow(vdesMessageHistory[i]));
+ for (const message of vdesMessageHistory) {
+ fragment.appendChild(renderVdesRow(message));
}
vdesMessagesEl.replaceChildren(fragment);
updateVdesSummary();
}
-function addVdesMessage(msg) {
+function addVdesMessage(msg: VdesMessage): void {
const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
msg._tsMs = tsMs;
msg._ts = new Date(tsMs).toLocaleTimeString([], {
@@ -258,39 +306,17 @@ function addVdesMessage(msg) {
scheduleVdesHistoryRender();
}
-function normalizeServerVdesMessage(msg) {
+function normalizeServerVdesMessage(msg: VdesMessage): VdesMessage {
return {
+ ...msg,
rig_id: msg.rig_id || null,
- message_type: msg.message_type,
- bit_len: msg.bit_len,
- raw_bytes: msg.raw_bytes,
- lat: msg.lat,
- lon: msg.lon,
- vessel_name: msg.vessel_name,
- callsign: msg.callsign,
- destination: msg.destination,
- message_label: msg.message_label,
- session_id: msg.session_id,
- source_id: msg.source_id,
- destination_id: msg.destination_id,
- data_count: msg.data_count,
- asm_identifier: msg.asm_identifier,
- ack_nack_mask: msg.ack_nack_mask,
- channel_quality: msg.channel_quality,
- payload_preview: msg.payload_preview,
- link_id: msg.link_id,
- sync_score: msg.sync_score,
- sync_errors: msg.sync_errors,
- phase_rotation: msg.phase_rotation,
- fec_state: msg.fec_state,
- ts_ms: msg.ts_ms,
};
}
-window.onServerVdesBatch = function(messages) {
+vdesWindow.onServerVdesBatch = function(messages: VdesMessage[]) {
if (!Array.isArray(messages) || messages.length === 0) return;
if (vdesStatus) vdesStatus.textContent = "Receiving";
- const normalized = [];
+ const normalized: VdesMessage[] = [];
for (const msg of messages) {
const next = normalizeServerVdesMessage(msg);
const tsMs = Number.isFinite(next.ts_ms) ? Number(next.ts_ms) : Date.now();
@@ -300,8 +326,8 @@ window.onServerVdesBatch = function(messages) {
minute: "2-digit",
second: "2-digit",
});
- if (next.lat != null && next.lon != null && window.vdesMapAddPoint) {
- window.vdesMapAddPoint(next);
+ if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) {
+ vdesWindow.vdesMapAddPoint(next);
}
normalized.push(next);
}
@@ -312,19 +338,19 @@ window.onServerVdesBatch = function(messages) {
scheduleVdesHistoryRender();
};
-window.restoreVdesHistory = function(messages) {
- window.onServerVdesBatch(messages);
+vdesWindow.restoreVdesHistory = function(messages: VdesMessage[]) {
+ vdesWindow.onServerVdesBatch?.(messages);
};
-document.getElementById("settings-clear-vdes-history")?.addEventListener("click", async () => {
- if (!await window.trxUi.confirm({ title: "Clear VDES history?", message: "All stored VDES decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
+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 postPath("/clear_vdes_decode");
- window.resetVdesHistoryView();
+ await vdesWindow.postPath?.("/clear_vdes_decode");
+ vdesWindow.resetVdesHistoryView?.();
} catch (e) {
console.error("VDES history clear failed", e);
}
-});
+})(); });
if (vdesFilterInput) {
vdesFilterInput.addEventListener("input", () => {
@@ -333,20 +359,20 @@ if (vdesFilterInput) {
});
}
-window.onServerVdes = function(msg) {
+vdesWindow.onServerVdes = function(msg: VdesMessage) {
if (vdesStatus) vdesStatus.textContent = "Receiving";
const next = normalizeServerVdesMessage(msg);
addVdesMessage(next);
- if (next.lat != null && next.lon != null && window.vdesMapAddPoint) {
- window.vdesMapAddPoint(next);
+ if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) {
+ vdesWindow.vdesMapAddPoint(next);
}
};
-window.pruneVdesHistoryView = function() {
+vdesWindow.pruneVdesHistoryView = function() {
pruneVdesMessageHistory();
updateVdesBar();
renderVdesHistory();
};
updateVdesSummary();
-if (window._trxDrainPendingDecode) window._trxDrainPendingDecode("vdes");
+vdesWindow._trxDrainPendingDecode?.("vdes");
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/vdes.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/vdes.test.mjs
new file mode 100644
index 00000000..35d30769
--- /dev/null
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/vdes.test.mjs
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: 2026 Stan Grams
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import test from "node:test";
+import vm from "node:vm";
+
+test("VDES entry normalizes and forwards positioned server messages", async () => {
+ const forwarded = [];
+ const window = {
+ trxUi: { confirm: async () => true },
+ vdesMapAddPoint: (message) => { forwarded.push(message); },
+ };
+ const context = vm.createContext({
+ window,
+ document: { getElementById: () => null },
+ Date,
+ Number,
+ String,
+ Array,
+ console,
+ });
+ const source = await readFile(new URL("../../assets/web/generated/vdes.js", import.meta.url), "utf8");
+ new vm.Script(source).runInContext(context);
+
+ window.onServerVdes({ callsign: "SP0ABC", lat: 54.5, lon: 18.5, bit_len: 120, ts_ms: Date.now() });
+ assert.equal(forwarded.length, 1);
+ assert.equal(forwarded[0].callsign, "SP0ABC");
+ assert.equal(forwarded[0].rig_id, null);
+ assert.equal(typeof forwarded[0]._tsMs, "number");
+});