[feat](trx-frontend-http): add dedicated WSPR plugin tab

Expose a WSPR subtab in the Plugins view with its own controls and
message list, wire a dedicated wspr.js asset endpoint, and route WSPR
decode events to the new panel.

This makes WSPR visible in the HTTP frontend instead of reusing the
FT8 panel for WSPR messages.

Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-02-12 23:18:36 +01:00
parent 6d2d647511
commit 7b75049f4f
6 changed files with 104 additions and 12 deletions
@@ -0,0 +1,57 @@
// --- WSPR Decoder Plugin (server-side decode) ---
const wsprStatus = document.getElementById("wspr-status");
const wsprMessagesEl = document.getElementById("wspr-messages");
const WSPR_MAX_MESSAGES = 200;
function fmtWsprTime(tsMs) {
if (!tsMs) return "--:--:--";
return new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
}
function renderWsprRow(msg) {
const row = document.createElement("div");
row.className = "ft8-row";
const snr = Number.isFinite(msg.snr_db) ? msg.snr_db.toFixed(1) : "--";
const dt = Number.isFinite(msg.dt_s) ? msg.dt_s.toFixed(2) : "--";
const baseHz = Number.isFinite(window.ft8BaseHz) ? window.ft8BaseHz : null;
const rfHz = Number.isFinite(msg.freq_hz) && Number.isFinite(baseHz) ? (baseHz + msg.freq_hz) : null;
const freq = Number.isFinite(rfHz) ? rfHz.toFixed(0) : "--";
const message = (msg.message || "").toString();
row.innerHTML = `<span class="ft8-time">${fmtWsprTime(msg.ts_ms)}</span><span class="ft8-snr">${snr}</span><span class="ft8-dt">${dt}</span><span class="ft8-freq">${freq}</span><span class="ft8-msg">${escapeWsprHtml(message)}</span>`;
return row;
}
function addWsprMessage(msg) {
wsprMessagesEl.prepend(renderWsprRow(msg));
while (wsprMessagesEl.children.length > WSPR_MAX_MESSAGES) {
wsprMessagesEl.removeChild(wsprMessagesEl.lastChild);
}
}
function escapeWsprHtml(input) {
return input
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll("\"", "&quot;");
}
document.getElementById("wspr-decode-toggle-btn").addEventListener("click", async () => {
try { await postPath("/toggle_wspr_decode"); } catch (e) { console.error("WSPR toggle failed", e); }
});
document.getElementById("wspr-clear-btn").addEventListener("click", async () => {
wsprMessagesEl.innerHTML = "";
try { await postPath("/clear_wspr_decode"); } catch (e) { console.error("WSPR clear failed", e); }
});
window.onServerWspr = function(msg) {
wsprStatus.textContent = "Receiving";
addWsprMessage({
ts_ms: msg.ts_ms,
snr_db: msg.snr_db,
dt_s: msg.dt_s,
freq_hz: msg.freq_hz,
message: msg.message,
});
};