Files
trx-rs/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wefax.js
T
sjgandClaude Opus 5 bf3bcc8a84
CI / lint (pull_request) Successful in 2m24s
CI / test (pull_request) Successful in 8m34s
CI / test (push) Successful in 7m47s
CI / frontend (pull_request) Successful in 4m28s
CI / reuse (pull_request) Successful in 5s
CI / lint (push) Successful in 2m21s
CI / frontend (push) Successful in 3m37s
CI / reuse (push) Successful in 6s
[fix](trx-frontend-http): show one rig at a time on the digital modes page
A client connected to several rigs decodes all of them at once, and the
decode stream carries every rig's traffic to every browser.  The decoder
panels listed all of it: a station a background rig copied on another band
appeared in the APRS list next to the selected rig's, the vessel counts and
the "latest seen" lines counted both, the status lines said "Receiving"
because some other rig was, and the CW pane interleaved two rigs into one
stream of text that read as neither.

The page is about the rig the operator selected — the one whose spectrum is
on screen and whose audio is playing — so each panel now shows what that rig
heard: rows, counts, latest-seen, status, the live picture a WEFAX or SSTV
frame is painting, and the CW pane.

Nothing is dropped on the way in.  The map is the whole station's view, has
its own rig filter, and would empty out if the plugins stopped feeding it, so
the histories still hold every rig and the map still plots them.  That also
means a switch loses nothing: the runtime gained a rerender hook, which the
rig switch calls, and switching back brings the other rig's traffic up again.
The CW pane is the exception — a running stream of text cannot be unpicked
after the fact — so it starts empty on the rig switched to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-07 08:11:13 +02:00

351 lines
12 KiB
JavaScript

import {
forActiveRig,
isActiveRigDecode
} from "./chunk-S57W63QN.js";
import {
hostCore
} from "./chunk-KL66PICH.js";
// src/plugins/wefax.ts
var wefaxWindow = window;
var wefaxDom = {
status: document.getElementById("wefax-status"),
liveView: document.getElementById("wefax-live-view"),
historyView: document.getElementById("wefax-history-view"),
liveContainer: document.getElementById("wefax-live-container"),
liveInfo: document.getElementById("wefax-live-info"),
liveCanvas: document.getElementById("wefax-live-canvas"),
liveLatest: document.getElementById("wefax-live-latest"),
historyList: document.getElementById("wefax-history-list"),
historyCount: document.getElementById("wefax-history-count"),
filterInput: document.getElementById("wefax-filter"),
sortSelect: document.getElementById("wefax-sort"),
toggleBtn: document.getElementById("wefax-decode-toggle-btn"),
clearBtn: document.getElementById("wefax-clear-btn"),
viewLiveBtn: document.getElementById("wefax-view-live"),
viewHistoryBtn: document.getElementById("wefax-view-history")
};
var wefaxImageHistory = [];
var WEFAX_MAX_IMAGES = 100;
var wefaxLiveCtx = null;
var wefaxLiveLineCount = 0;
var wefaxLivePixelsPerLine = 1809;
var wefaxActiveView = "live";
var wefaxFilterText = "";
function currentWefaxHistoryRetentionMs() {
return wefaxWindow.getDecodeHistoryRetentionMs?.() ?? 24 * 60 * 60 * 1e3;
}
function pruneWefaxHistory() {
const cutoff = Date.now() - currentWefaxHistoryRetentionMs();
wefaxImageHistory = wefaxImageHistory.filter(function(m) {
return (m._tsMs || 0) > cutoff;
});
}
function escapeHtml(s) {
return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function scheduleWefaxUi(key, job) {
if (typeof wefaxWindow.trxScheduleUiFrameJob === "function") {
wefaxWindow.trxScheduleUiFrameJob(key, job);
return;
}
job();
}
function switchWefaxView(view) {
wefaxActiveView = view;
if (wefaxDom.liveView) wefaxDom.liveView.style.display = view === "live" ? "" : "none";
if (wefaxDom.historyView) wefaxDom.historyView.style.display = view === "history" ? "" : "none";
[wefaxDom.viewLiveBtn, wefaxDom.viewHistoryBtn].forEach(function(btn) {
if (btn) btn.classList.remove("sat-view-active");
});
if (view === "live" && wefaxDom.viewLiveBtn) wefaxDom.viewLiveBtn.classList.add("sat-view-active");
if (view === "history" && wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.classList.add("sat-view-active");
if (view === "history") renderWefaxHistoryTable();
}
if (wefaxDom.viewLiveBtn) wefaxDom.viewLiveBtn.addEventListener("click", function() {
switchWefaxView("live");
});
if (wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.addEventListener("click", function() {
switchWefaxView("history");
});
function resetLiveCanvas(pixelsPerLine) {
const canvas = wefaxDom.liveCanvas;
if (!canvas) return;
wefaxLivePixelsPerLine = pixelsPerLine;
wefaxLiveLineCount = 0;
canvas.width = pixelsPerLine;
canvas.height = 800;
wefaxLiveCtx = canvas.getContext("2d");
if (!wefaxLiveCtx) return;
wefaxLiveCtx.fillStyle = "#000";
wefaxLiveCtx.fillRect(0, 0, canvas.width, canvas.height);
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "";
}
function paintLine(lineBytes) {
const canvas = wefaxDom.liveCanvas;
if (!wefaxLiveCtx || !canvas) return;
const y = wefaxLiveLineCount;
if (y >= canvas.height) {
const old = wefaxLiveCtx.getImageData(0, 0, canvas.width, canvas.height);
canvas.height *= 2;
wefaxLiveCtx = canvas.getContext("2d");
if (!wefaxLiveCtx) return;
wefaxLiveCtx.putImageData(old, 0, 0);
}
const w = wefaxLivePixelsPerLine;
const imgData = wefaxLiveCtx.createImageData(w, 1);
const d = imgData.data;
for (let x = 0; x < w; x++) {
const v = lineBytes[x] ?? 0;
const i = x * 4;
d[i] = v;
d[i + 1] = v;
d[i + 2] = v;
d[i + 3] = 255;
}
wefaxLiveCtx.putImageData(imgData, 0, y);
wefaxLiveLineCount++;
}
function wefaxRigImages() {
return forActiveRig(wefaxImageHistory);
}
function renderWefaxLatestCard() {
if (!wefaxDom.liveLatest) return;
const rigImages = wefaxRigImages();
if (rigImages.length === 0) {
wefaxDom.liveLatest.innerHTML = '<div style="color:var(--text-muted);font-size:0.82rem;">No images decoded yet. Enable the decoder and tune to a WEFAX station.</div>';
return;
}
const img = rigImages[0];
if (!img) return;
const ts = img._ts || "--";
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
const meta = [
`${String(img.ioc ?? "--")} IOC`,
`${String(img.lpm ?? "--")} LPM`,
`${String(img.line_count ?? 0)} lines`,
`${date} ${ts}`
].join(" · ");
const imgSrc = img._dataUrl ? img._dataUrl : img.path ? "/images/" + escapeHtml(img.path.split("/").pop()) : null;
let html = '<div class="sat-latest-card">';
html += '<div class="sat-latest-title">Latest decoded image</div>';
html += '<div class="sat-latest-meta">' + escapeHtml(meta) + "</div>";
if (imgSrc) {
html += '<a href="' + imgSrc + '" target="_blank" style="font-size:0.8rem;color:var(--accent);display:inline-block;margin-top:0.25rem;">View full image</a>';
}
html += "</div>";
wefaxDom.liveLatest.innerHTML = html;
}
function getWefaxFilteredHistory() {
let items = wefaxRigImages();
if (wefaxFilterText) {
items = items.filter(function(i) {
const haystack = [
String(i.ioc || ""),
String(i.lpm || ""),
String(i.line_count || "")
].join(" ").toUpperCase();
return haystack.indexOf(wefaxFilterText) >= 0;
});
}
const sortVal = wefaxDom.sortSelect ? wefaxDom.sortSelect.value : "newest";
if (sortVal === "oldest") items = items.slice().reverse();
return items;
}
function renderWefaxHistoryRow(img) {
const row = document.createElement("div");
row.className = "sat-history-row";
const ts = img._ts || "--";
const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: "short", day: "numeric" }) : "";
const ioc = img.ioc || "--";
const lpm = img.lpm || "--";
const lines = img.line_count || 0;
const imgSrc = img._dataUrl ? img._dataUrl : img.path ? "/images/" + escapeHtml(img.path.split("/").pop()) : null;
const link = imgSrc ? '<a href="' + imgSrc + '" target="_blank" style="color:var(--accent);">View</a>' : "--";
row.innerHTML = [
"<span>" + escapeHtml(date + " " + ts) + "</span>",
"<span>" + escapeHtml(String(ioc)) + "</span>",
"<span>" + escapeHtml(String(lpm)) + "</span>",
`<span>${String(lines)}</span>`,
"<span>" + link + "</span>"
].join("");
return row;
}
function renderWefaxHistoryTable() {
if (!wefaxDom.historyList) return;
pruneWefaxHistory();
const items = getWefaxFilteredHistory();
const fragment = document.createDocumentFragment();
for (const item of items) {
fragment.appendChild(renderWefaxHistoryRow(item));
}
wefaxDom.historyList.replaceChildren(fragment);
if (wefaxDom.historyCount) {
const total = wefaxRigImages().length;
const shown = items.length;
wefaxDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? `${String(total)} image${total === 1 ? "" : "s"}` : `${String(shown)} of ${String(total)} images`;
}
}
function addWefaxImage(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"
});
const canvas = wefaxDom.liveCanvas;
if (wefaxLiveCtx && canvas && wefaxLiveLineCount > 0) {
const trimmed = wefaxLiveCtx.getImageData(0, 0, canvas.width, wefaxLiveLineCount);
canvas.height = wefaxLiveLineCount;
wefaxLiveCtx = canvas.getContext("2d");
if (!wefaxLiveCtx) return;
wefaxLiveCtx.putImageData(trimmed, 0, 0);
try {
msg._dataUrl = canvas.toDataURL("image/png");
} catch {
}
}
wefaxImageHistory.unshift(msg);
if (wefaxImageHistory.length > WEFAX_MAX_IMAGES) {
wefaxImageHistory = wefaxImageHistory.slice(0, WEFAX_MAX_IMAGES);
}
scheduleWefaxUi("wefax-latest", renderWefaxLatestCard);
if (wefaxActiveView === "history") {
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
}
}
function onServerWefaxProgress(msg) {
if (!isActiveRigDecode(msg.rig_id)) return;
if (msg.state && !msg.line_data) {
if (wefaxDom.status) {
wefaxDom.status.textContent = msg.state;
wefaxDom.status.style.color = msg.state.indexOf("Idle") === 0 ? "" : "var(--text-accent)";
}
return;
}
if ((msg.line_count ?? 0) <= 1 || !wefaxLiveCtx) {
resetLiveCanvas(msg.pixels_per_line || 1809);
}
if (msg.line_data) {
const binary = atob(msg.line_data);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
paintLine(bytes);
}
if (wefaxDom.liveInfo) {
wefaxDom.liveInfo.textContent = `Line ${String(msg.line_count ?? 0)} · ${String(msg.ioc ?? "--")} IOC · ${String(msg.lpm ?? "--")} LPM`;
}
if (wefaxDom.status) {
wefaxDom.status.textContent = `Receiving — line ${String(msg.line_count ?? 0)}`;
wefaxDom.status.style.color = "var(--text-accent)";
}
}
function onServerWefax(msg) {
addWefaxImage(msg);
if (!isActiveRigDecode(msg.rig_id)) return;
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
if (wefaxDom.status) {
wefaxDom.status.textContent = `Complete — ${String(msg.line_count ?? 0)} lines`;
wefaxDom.status.style.color = "";
}
}
function restoreWefaxHistory(messages) {
if (!messages.length) return;
for (const message of messages) {
const tsMs = Number.isFinite(message.ts_ms) ? Number(message.ts_ms) : Date.now();
message._tsMs = tsMs;
message._ts = new Date(tsMs).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
}
wefaxImageHistory = messages.concat(wefaxImageHistory);
pruneWefaxHistory();
scheduleWefaxUi("wefax-latest", renderWefaxLatestCard);
if (wefaxActiveView === "history") {
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
}
}
function pruneWefaxHistoryView() {
pruneWefaxHistory();
renderWefaxHistoryTable();
renderWefaxLatestCard();
}
function resetWefaxHistoryView() {
wefaxImageHistory = [];
if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = "";
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
wefaxLiveCtx = null;
wefaxLiveLineCount = 0;
renderWefaxLatestCard();
renderWefaxHistoryTable();
if (wefaxDom.status) {
wefaxDom.status.textContent = "Idle";
wefaxDom.status.style.color = "";
}
}
if (wefaxDom.filterInput) {
const filterInput = wefaxDom.filterInput;
wefaxDom.filterInput.addEventListener("input", function() {
wefaxFilterText = filterInput.value.trim().toUpperCase();
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
});
}
if (wefaxDom.sortSelect) {
wefaxDom.sortSelect.addEventListener("change", function() {
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
});
}
wefaxWindow.syncWefaxToggle = function(enabled) {
if (!wefaxDom.toggleBtn) return;
wefaxDom.toggleBtn.dataset.enabled = enabled ? "true" : "false";
wefaxDom.toggleBtn.textContent = enabled ? "Disable WEFAX" : "Enable WEFAX";
wefaxDom.toggleBtn.style.borderColor = enabled ? "#00d17f" : "";
wefaxDom.toggleBtn.style.color = enabled ? "#00d17f" : "";
};
if (wefaxDom.toggleBtn) {
const toggleButton = wefaxDom.toggleBtn;
wefaxDom.toggleBtn.addEventListener("click", () => {
void (async () => {
try {
if (wefaxWindow.takeSchedulerControlForDecoderDisable) {
await wefaxWindow.takeSchedulerControlForDecoderDisable(toggleButton);
}
await hostCore.postPath("/toggle_wefax_decode");
} catch (e) {
console.error("WEFAX toggle failed", e);
}
})();
});
}
if (wefaxDom.clearBtn) {
wefaxDom.clearBtn.addEventListener("click", () => {
void (async () => {
try {
await hostCore.postPath("/clear_wefax_decode");
resetWefaxHistoryView();
} catch (e) {
console.error("WEFAX clear failed", e);
}
})();
});
}
renderWefaxLatestCard();
wefaxWindow.trxPluginRuntime.registerDecoder({
id: "wefax",
onMessage: onServerWefax,
restore: restoreWefaxHistory,
prune: pruneWefaxHistoryView,
reset: resetWefaxHistoryView,
rerender: () => {
renderWefaxLatestCard();
renderWefaxHistoryTable();
}
});
wefaxWindow.trxPluginRuntime.registerDecoder({
id: "wefax_progress",
onMessage: onServerWefaxProgress
});