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>
273 lines
12 KiB
JavaScript
273 lines
12 KiB
JavaScript
import {
|
|
hostCore
|
|
} from "./chunk-KL66PICH.js";
|
|
|
|
// src/plugins/vdes.ts
|
|
var vdesWindow = window;
|
|
var escapeVdesHtml = (input) => hostCore.escapeMapHtml(input);
|
|
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;
|
|
}
|
|
function pruneVdesMessageHistory() {
|
|
const cutoffMs = Date.now() - currentVdesHistoryRetentionMs();
|
|
vdesMessageHistory = vdesMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs);
|
|
}
|
|
function scheduleVdesUi(key, job) {
|
|
if (typeof vdesWindow.trxScheduleUiFrameJob === "function") {
|
|
vdesWindow.trxScheduleUiFrameJob(key, job);
|
|
return;
|
|
}
|
|
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 (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`;
|
|
}
|
|
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();
|
|
}
|
|
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";
|
|
}
|
|
}
|
|
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"
|
|
});
|
|
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 = `<div class="vdes-row-head"><span class="vdes-time">${ts}</span><span class="vdes-call">${escapeVdesHtml(title)}</span><span class="vdes-badge">${escapeVdesHtml(label)}</span>` + (labelText ? `<span class="vdes-badge">${escapeVdesHtml(labelText)}</span>` : "") + (linkText ? `<span class="vdes-badge">${escapeVdesHtml(linkText)}</span>` : "") + (srcText ? `<span class="vdes-badge">${escapeVdesHtml(srcText)}</span>` : "") + (dstText ? `<span class="vdes-badge">${escapeVdesHtml(dstText)}</span>` : "") + (syncText ? `<span class="vdes-badge">${escapeVdesHtml(syncText)}</span>` : "") + (phaseText ? `<span class="vdes-badge">${escapeVdesHtml(phaseText)}</span>` : "") + `<span class="vdes-badge">T${escapeVdesHtml(String(msg.message_type ?? "--"))}</span></div><div class="vdes-row-meta"><span>${escapeVdesHtml(currentVdesCenterText())}</span><span>${escapeVdesHtml(`${msg.bit_len || 0} bits`)}</span>` + (sessionText ? `<span>${escapeVdesHtml(sessionText)}</span>` : "") + (asmText ? `<span>${escapeVdesHtml(asmText)}</span>` : "") + (countText ? `<span>${escapeVdesHtml(countText)}</span>` : "") + (ackText ? `<span>${escapeVdesHtml(ackText)}</span>` : "") + (cqiText ? `<span>${escapeVdesHtml(cqiText)}</span>` : "") + (info ? `<span>${escapeVdesHtml(info)}</span>` : "") + (fecText ? `<span>${escapeVdesHtml(fecText)}</span>` : "") + `<span>${escapeVdesHtml(vdesAgeText(msg._tsMs))}</span></div><div class="vdes-row-detail">` + (previewText ? `<span>${escapeVdesHtml(previewText)}</span>` : "") + (previewText ? `<span>·</span>` : "") + `<span class="vdes-raw">${escapeVdesHtml(rawHex)}</span></div>`;
|
|
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;
|
|
}
|
|
let html = `<div class="aprs-bar-header"><span class="aprs-bar-title"><span class="aprs-bar-title-word">VDES</span><span class="aprs-bar-title-word">Live</span></span><span class="aprs-bar-clear-wrap"><span class="aprs-bar-clear" role="button" tabindex="0" onclick="window.clearVdesBar()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();window.clearVdesBar();}" aria-label="Clear VDES overlay">Clear</span></span><span class="aprs-bar-window">Last 15 minutes</span></div>`;
|
|
for (const msg of messages) {
|
|
const ts = msg._ts ? `<span class="aprs-bar-time">${msg._ts}</span>` : "";
|
|
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 += `<div class="aprs-bar-frame"><div class="aprs-bar-frame-main">${ts}<span class="vdes-call">${title}</span> <span class="vdes-badge">${label}</span>: ${detail}</div></div>`;
|
|
}
|
|
vdesBarOverlay.innerHTML = html;
|
|
vdesBarOverlay.style.display = "flex";
|
|
}
|
|
vdesWindow.updateVdesBar = updateVdesBar;
|
|
vdesWindow.clearVdesBar = function() {
|
|
resetVdesHistoryView();
|
|
};
|
|
function resetVdesHistoryView() {
|
|
if (vdesMessagesEl) vdesMessagesEl.innerHTML = "";
|
|
vdesMessageHistory = [];
|
|
updateVdesBar();
|
|
renderVdesHistory();
|
|
}
|
|
function renderVdesHistory() {
|
|
pruneVdesMessageHistory();
|
|
if (!vdesMessagesEl) {
|
|
updateVdesSummary();
|
|
return;
|
|
}
|
|
const fragment = document.createDocumentFragment();
|
|
for (const message of vdesMessageHistory) {
|
|
fragment.appendChild(renderVdesRow(message));
|
|
}
|
|
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 {
|
|
...msg,
|
|
rig_id: msg.rig_id || null
|
|
};
|
|
}
|
|
function onServerVdesBatch(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();
|
|
}
|
|
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 hostCore.postPath("/clear_vdes_decode");
|
|
resetVdesHistoryView();
|
|
} catch (e) {
|
|
console.error("VDES history clear failed", e);
|
|
}
|
|
})();
|
|
});
|
|
if (vdesFilterInput) {
|
|
vdesFilterInput.addEventListener("input", () => {
|
|
vdesFilterText = vdesFilterInput.value.trim().toUpperCase();
|
|
renderVdesHistory();
|
|
});
|
|
}
|
|
function onServerVdes(msg) {
|
|
if (vdesStatus) vdesStatus.textContent = "Receiving";
|
|
const next = normalizeServerVdesMessage(msg);
|
|
addVdesMessage(next);
|
|
if (next.lat != null && next.lon != null && vdesWindow.vdesMapAddPoint) {
|
|
vdesWindow.vdesMapAddPoint(next);
|
|
}
|
|
}
|
|
function pruneVdesHistoryView() {
|
|
pruneVdesMessageHistory();
|
|
updateVdesBar();
|
|
renderVdesHistory();
|
|
}
|
|
updateVdesSummary();
|
|
window.trxPluginRuntime.registerDecoder({
|
|
id: "vdes",
|
|
onMessage: onServerVdes,
|
|
onBatch: onServerVdesBatch,
|
|
restore: onServerVdesBatch,
|
|
reset: resetVdesHistoryView,
|
|
prune: pruneVdesHistoryView
|
|
});
|