refactor: convert WEFAX plugin to TypeScript

This commit is contained in:
sjg
2026-08-01 12:33:38 +02:00
parent 0ab0f80986
commit 7251ec276d
6 changed files with 491 additions and 392 deletions
@@ -10,7 +10,7 @@ const pluginGroups = {
}; };
const loaded = /* @__PURE__ */ new Set(); const loaded = /* @__PURE__ */ new Set();
const loading = /* @__PURE__ */ new Map(); const loading = /* @__PURE__ */ new Map();
const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js"]); const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js"]);
function loadLegacyScript(path) { function loadLegacyScript(path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const script = document.createElement("script"); const script = document.createElement("script");
@@ -1,304 +1,324 @@
"use strict"; "use strict";
var wefaxDom = { (() => {
status: document.getElementById("wefax-status"), // src/plugins/wefax.ts
liveView: document.getElementById("wefax-live-view"), var wefaxWindow = window;
historyView: document.getElementById("wefax-history-view"), var wefaxDom = {
liveContainer: document.getElementById("wefax-live-container"), status: document.getElementById("wefax-status"),
liveInfo: document.getElementById("wefax-live-info"), liveView: document.getElementById("wefax-live-view"),
liveCanvas: document.getElementById("wefax-live-canvas"), historyView: document.getElementById("wefax-history-view"),
liveLatest: document.getElementById("wefax-live-latest"), liveContainer: document.getElementById("wefax-live-container"),
historyList: document.getElementById("wefax-history-list"), liveInfo: document.getElementById("wefax-live-info"),
historyCount: document.getElementById("wefax-history-count"), liveCanvas: document.getElementById("wefax-live-canvas"),
filterInput: document.getElementById("wefax-filter"), liveLatest: document.getElementById("wefax-live-latest"),
sortSelect: document.getElementById("wefax-sort"), historyList: document.getElementById("wefax-history-list"),
toggleBtn: document.getElementById("wefax-decode-toggle-btn"), historyCount: document.getElementById("wefax-history-count"),
clearBtn: document.getElementById("wefax-clear-btn"), filterInput: document.getElementById("wefax-filter"),
viewLiveBtn: document.getElementById("wefax-view-live"), sortSelect: document.getElementById("wefax-sort"),
viewHistoryBtn: document.getElementById("wefax-view-history") toggleBtn: document.getElementById("wefax-decode-toggle-btn"),
}; clearBtn: document.getElementById("wefax-clear-btn"),
var wefaxImageHistory = []; viewLiveBtn: document.getElementById("wefax-view-live"),
var WEFAX_MAX_IMAGES = 100; viewHistoryBtn: document.getElementById("wefax-view-history")
var wefaxLiveCtx = null; };
var wefaxLiveLineCount = 0; var wefaxImageHistory = [];
var wefaxLivePixelsPerLine = 1809; var WEFAX_MAX_IMAGES = 100;
var wefaxActiveView = "live"; var wefaxLiveCtx = null;
var wefaxFilterText = ""; var wefaxLiveLineCount = 0;
function currentWefaxHistoryRetentionMs() { var wefaxLivePixelsPerLine = 1809;
return window.getDecodeHistoryRetentionMs ? window.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3; var wefaxActiveView = "live";
} var wefaxFilterText = "";
function pruneWefaxHistory() { function currentWefaxHistoryRetentionMs() {
var cutoff = Date.now() - currentWefaxHistoryRetentionMs(); return wefaxWindow.getDecodeHistoryRetentionMs?.() ?? 24 * 60 * 60 * 1e3;
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 window.trxScheduleUiFrameJob === "function") {
window.trxScheduleUiFrameJob(key, job);
return;
} }
job(); function pruneWefaxHistory() {
} const cutoff = Date.now() - currentWefaxHistoryRetentionMs();
function switchWefaxView(view) { wefaxImageHistory = wefaxImageHistory.filter(function(m) {
wefaxActiveView = view; return (m._tsMs || 0) > cutoff;
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) {
wefaxLivePixelsPerLine = pixelsPerLine;
wefaxLiveLineCount = 0;
wefaxDom.liveCanvas.width = pixelsPerLine;
wefaxDom.liveCanvas.height = 800;
wefaxLiveCtx = wefaxDom.liveCanvas.getContext("2d");
wefaxLiveCtx.fillStyle = "#000";
wefaxLiveCtx.fillRect(0, 0, wefaxDom.liveCanvas.width, wefaxDom.liveCanvas.height);
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "";
}
function paintLine(lineBytes) {
if (!wefaxLiveCtx) return;
var y = wefaxLiveLineCount;
if (y >= wefaxDom.liveCanvas.height) {
var old = wefaxLiveCtx.getImageData(0, 0, wefaxDom.liveCanvas.width, wefaxDom.liveCanvas.height);
wefaxDom.liveCanvas.height *= 2;
wefaxLiveCtx.putImageData(old, 0, 0);
}
var w = wefaxLivePixelsPerLine;
var imgData = wefaxLiveCtx.createImageData(w, 1);
var d = imgData.data;
for (var x = 0; x < w; x++) {
var v = x < lineBytes.length ? lineBytes[x] : 0;
var 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 renderWefaxLatestCard() {
if (!wefaxDom.liveLatest) return;
if (wefaxImageHistory.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;
}
var img = wefaxImageHistory[0];
var ts = img._ts || "--";
var date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
var meta = [
img.ioc + " IOC",
img.lpm + " LPM",
img.line_count + " lines",
date + " " + ts
].join(" · ");
var imgSrc = img._dataUrl ? img._dataUrl : img.path ? "/images/" + escapeHtml(img.path.split("/").pop()) : null;
var 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() {
var items = wefaxImageHistory;
if (wefaxFilterText) {
items = items.filter(function(i) {
var haystack = [
String(i.ioc || ""),
String(i.lpm || ""),
String(i.line_count || "")
].join(" ").toUpperCase();
return haystack.indexOf(wefaxFilterText) >= 0;
}); });
} }
var sortVal = wefaxDom.sortSelect ? wefaxDom.sortSelect.value : "newest"; function escapeHtml(s) {
if (sortVal === "oldest") items = items.slice().reverse(); return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
return items;
}
function renderWefaxHistoryRow(img) {
var row = document.createElement("div");
row.className = "sat-history-row";
var ts = img._ts || "--";
var date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: "short", day: "numeric" }) : "";
var ioc = img.ioc || "--";
var lpm = img.lpm || "--";
var lines = img.line_count || 0;
var imgSrc = img._dataUrl ? img._dataUrl : img.path ? "/images/" + escapeHtml(img.path.split("/").pop()) : null;
var 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>" + lines + "</span>",
"<span>" + link + "</span>"
].join("");
return row;
}
function renderWefaxHistoryTable() {
if (!wefaxDom.historyList) return;
pruneWefaxHistory();
var items = getWefaxFilteredHistory();
var fragment = document.createDocumentFragment();
for (var i = 0; i < items.length; i++) {
fragment.appendChild(renderWefaxHistoryRow(items[i]));
} }
wefaxDom.historyList.replaceChildren(fragment); function scheduleWefaxUi(key, job) {
if (wefaxDom.historyCount) { if (typeof wefaxWindow.trxScheduleUiFrameJob === "function") {
var total = wefaxImageHistory.length; wefaxWindow.trxScheduleUiFrameJob(key, job);
var shown = items.length; return;
wefaxDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? total + " image" + (total === 1 ? "" : "s") : shown + " of " + total + " images"; }
job();
} }
} function switchWefaxView(view) {
function addWefaxImage(msg) { wefaxActiveView = view;
var tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now(); if (wefaxDom.liveView) wefaxDom.liveView.style.display = view === "live" ? "" : "none";
msg._tsMs = tsMs; if (wefaxDom.historyView) wefaxDom.historyView.style.display = view === "history" ? "" : "none";
msg._ts = new Date(tsMs).toLocaleTimeString([], { [wefaxDom.viewLiveBtn, wefaxDom.viewHistoryBtn].forEach(function(btn) {
hour: "2-digit", if (btn) btn.classList.remove("sat-view-active");
minute: "2-digit", });
second: "2-digit" 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 (wefaxLiveCtx && wefaxLiveLineCount > 0) { if (wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.addEventListener("click", function() {
var trimmed = wefaxLiveCtx.getImageData(0, 0, wefaxDom.liveCanvas.width, wefaxLiveLineCount); switchWefaxView("history");
wefaxDom.liveCanvas.height = wefaxLiveLineCount; });
wefaxLiveCtx.putImageData(trimmed, 0, 0); function resetLiveCanvas(pixelsPerLine) {
try { const canvas = wefaxDom.liveCanvas;
msg._dataUrl = wefaxDom.liveCanvas.toDataURL("image/png"); if (!canvas) return;
} catch (e) { 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 renderWefaxLatestCard() {
if (!wefaxDom.liveLatest) return;
if (wefaxImageHistory.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 = wefaxImageHistory[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 = wefaxImageHistory;
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 = wefaxImageHistory.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`;
} }
} }
wefaxImageHistory.unshift(msg); function addWefaxImage(msg) {
if (wefaxImageHistory.length > WEFAX_MAX_IMAGES) { const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
wefaxImageHistory = wefaxImageHistory.slice(0, WEFAX_MAX_IMAGES); msg._tsMs = tsMs;
} msg._ts = new Date(tsMs).toLocaleTimeString([], {
scheduleWefaxUi("wefax-latest", renderWefaxLatestCard);
if (wefaxActiveView === "history") {
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
}
}
window.onServerWefaxProgress = function(msg) {
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 <= 1 || !wefaxLiveCtx) {
resetLiveCanvas(msg.pixels_per_line || 1809);
}
if (msg.line_data) {
var binary = atob(msg.line_data);
var bytes = new Uint8Array(binary.length);
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
paintLine(bytes);
}
if (wefaxDom.liveInfo) {
wefaxDom.liveInfo.textContent = "Line " + msg.line_count + " · " + msg.ioc + " IOC · " + msg.lpm + " LPM";
}
if (wefaxDom.status) {
wefaxDom.status.textContent = "Receiving — line " + msg.line_count;
wefaxDom.status.style.color = "var(--text-accent)";
}
};
window.onServerWefax = function(msg) {
addWefaxImage(msg);
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "none";
if (wefaxDom.status) {
wefaxDom.status.textContent = "Complete — " + msg.line_count + " lines";
wefaxDom.status.style.color = "";
}
};
window.restoreWefaxHistory = function(messages) {
if (!messages || !messages.length) return;
for (var i = 0; i < messages.length; i++) {
var tsMs = Number.isFinite(messages[i].ts_ms) ? Number(messages[i].ts_ms) : Date.now();
messages[i]._tsMs = tsMs;
messages[i]._ts = new Date(tsMs).toLocaleTimeString([], {
hour: "2-digit", hour: "2-digit",
minute: "2-digit", minute: "2-digit",
second: "2-digit" second: "2-digit"
}); });
} const canvas = wefaxDom.liveCanvas;
wefaxImageHistory = messages.concat(wefaxImageHistory); if (wefaxLiveCtx && canvas && wefaxLiveLineCount > 0) {
pruneWefaxHistory(); const trimmed = wefaxLiveCtx.getImageData(0, 0, canvas.width, wefaxLiveLineCount);
scheduleWefaxUi("wefax-latest", renderWefaxLatestCard); canvas.height = wefaxLiveLineCount;
if (wefaxActiveView === "history") { wefaxLiveCtx = canvas.getContext("2d");
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable); if (!wefaxLiveCtx) return;
} wefaxLiveCtx.putImageData(trimmed, 0, 0);
}; try {
window.pruneWefaxHistoryView = function() { msg._dataUrl = canvas.toDataURL("image/png");
pruneWefaxHistory(); } catch {
renderWefaxHistoryTable();
renderWefaxLatestCard();
};
window.resetWefaxHistoryView = function() {
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) {
wefaxDom.filterInput.addEventListener("input", function() {
wefaxFilterText = wefaxDom.filterInput.value.trim().toUpperCase();
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
});
}
if (wefaxDom.sortSelect) {
wefaxDom.sortSelect.addEventListener("change", function() {
scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
});
}
window.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) {
wefaxDom.toggleBtn.addEventListener("click", async function() {
try {
if (window.takeSchedulerControlForDecoderDisable) {
await window.takeSchedulerControlForDecoderDisable(wefaxDom.toggleBtn);
} }
await postPath("/toggle_wefax_decode");
} catch (e) {
console.error("WEFAX toggle failed", e);
} }
}); wefaxImageHistory.unshift(msg);
} if (wefaxImageHistory.length > WEFAX_MAX_IMAGES) {
if (wefaxDom.clearBtn) { wefaxImageHistory = wefaxImageHistory.slice(0, WEFAX_MAX_IMAGES);
wefaxDom.clearBtn.addEventListener("click", async function() {
try {
await postPath("/clear_wefax_decode");
window.resetWefaxHistoryView();
} catch (e) {
console.error("WEFAX clear failed", e);
} }
}); scheduleWefaxUi("wefax-latest", renderWefaxLatestCard);
} if (wefaxActiveView === "history") {
renderWefaxLatestCard(); scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
}
}
wefaxWindow.onServerWefaxProgress = function(msg) {
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)";
}
};
wefaxWindow.onServerWefax = function(msg) {
addWefaxImage(msg);
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 = "";
}
};
wefaxWindow.restoreWefaxHistory = function(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);
}
};
wefaxWindow.pruneWefaxHistoryView = function() {
pruneWefaxHistory();
renderWefaxHistoryTable();
renderWefaxLatestCard();
};
wefaxWindow.resetWefaxHistoryView = function() {
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 wefaxWindow.postPath?.("/toggle_wefax_decode");
} catch (e) {
console.error("WEFAX toggle failed", e);
}
})();
});
}
if (wefaxDom.clearBtn) {
wefaxDom.clearBtn.addEventListener("click", () => {
void (async () => {
try {
await wefaxWindow.postPath?.("/clear_wefax_decode");
wefaxWindow.resetWefaxHistoryView?.();
} catch (e) {
console.error("WEFAX clear failed", e);
}
})();
});
}
renderWefaxLatestCard();
})();
@@ -31,7 +31,6 @@ await build({
"sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.js"), "sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.js"),
scheduler: path.join(sourceDir, "plugins", "scheduler.js"), scheduler: path.join(sourceDir, "plugins", "scheduler.js"),
vchan: path.join(sourceDir, "plugins", "vchan.js"), vchan: path.join(sourceDir, "plugins", "vchan.js"),
wefax: path.join(sourceDir, "plugins", "wefax.js"),
}, },
outdir: outputDir, outdir: outputDir,
bundle: false, bundle: false,
@@ -51,6 +50,7 @@ await build({
ft8: path.join(sourceDir, "plugins", "ft8.ts"), ft8: path.join(sourceDir, "plugins", "ft8.ts"),
"leaflet-ais-tracksymbol": path.join(sourceDir, "leaflet-ais-tracksymbol.ts"), "leaflet-ais-tracksymbol": path.join(sourceDir, "leaflet-ais-tracksymbol.ts"),
vdes: path.join(sourceDir, "plugins", "vdes.ts"), vdes: path.join(sourceDir, "plugins", "vdes.ts"),
wefax: path.join(sourceDir, "plugins", "wefax.ts"),
}, },
outdir: outputDir, outdir: outputDir,
bundle: true, bundle: true,
@@ -16,7 +16,7 @@ const pluginGroups: Readonly<Record<PluginGroup, readonly string[]>> = {
const loaded = new Set<string>(); const loaded = new Set<string>();
const loading = new Map<string, Promise<void>>(); const loading = new Map<string, Promise<void>>();
const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js"]); const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js"]);
function loadLegacyScript(path: string): Promise<void> { function loadLegacyScript(path: string): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -2,6 +2,43 @@
// //
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
export {};
interface WefaxImage {
ts_ms?: number;
ioc?: number;
lpm?: number;
line_count?: number;
path?: string;
_tsMs?: number;
_ts?: string;
_dataUrl?: string;
}
interface WefaxProgress {
state?: string;
line_data?: string;
line_count?: number;
pixels_per_line?: number;
ioc?: number;
lpm?: number;
}
interface WefaxBridge {
getDecodeHistoryRetentionMs?: () => number;
trxScheduleUiFrameJob?: (key: string, job: () => void) => void;
postPath?: (path: string) => Promise<unknown>;
takeSchedulerControlForDecoderDisable?: (button: HTMLElement) => Promise<unknown>;
onServerWefaxProgress?: (message: WefaxProgress) => void;
onServerWefax?: (message: WefaxImage) => void;
restoreWefaxHistory?: (messages: WefaxImage[]) => void;
pruneWefaxHistoryView?: () => void;
resetWefaxHistoryView?: () => void;
syncWefaxToggle?: (enabled: boolean) => void;
}
const wefaxWindow = window as unknown as WefaxBridge;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// wefax.js — WEFAX decoder plugin for trx-frontend-http // wefax.js — WEFAX decoder plugin for trx-frontend-http
// Live view: decoder state, live canvas, latest image card // Live view: decoder state, live canvas, latest image card
@@ -9,18 +46,18 @@
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// ── DOM references (cached once) ─────────────────────────────────── // ── DOM references (cached once) ───────────────────────────────────
var wefaxDom = { const wefaxDom = {
status: document.getElementById('wefax-status'), status: document.getElementById('wefax-status'),
liveView: document.getElementById('wefax-live-view'), liveView: document.getElementById('wefax-live-view'),
historyView: document.getElementById('wefax-history-view'), historyView: document.getElementById('wefax-history-view'),
liveContainer: document.getElementById('wefax-live-container'), liveContainer: document.getElementById('wefax-live-container'),
liveInfo: document.getElementById('wefax-live-info'), liveInfo: document.getElementById('wefax-live-info'),
liveCanvas: document.getElementById('wefax-live-canvas'), liveCanvas: document.getElementById('wefax-live-canvas') as HTMLCanvasElement | null,
liveLatest: document.getElementById('wefax-live-latest'), liveLatest: document.getElementById('wefax-live-latest'),
historyList: document.getElementById('wefax-history-list'), historyList: document.getElementById('wefax-history-list'),
historyCount: document.getElementById('wefax-history-count'), historyCount: document.getElementById('wefax-history-count'),
filterInput: document.getElementById('wefax-filter'), filterInput: document.getElementById('wefax-filter') as HTMLInputElement | null,
sortSelect: document.getElementById('wefax-sort'), sortSelect: document.getElementById('wefax-sort') as HTMLSelectElement | null,
toggleBtn: document.getElementById('wefax-decode-toggle-btn'), toggleBtn: document.getElementById('wefax-decode-toggle-btn'),
clearBtn: document.getElementById('wefax-clear-btn'), clearBtn: document.getElementById('wefax-clear-btn'),
viewLiveBtn: document.getElementById('wefax-view-live'), viewLiveBtn: document.getElementById('wefax-view-live'),
@@ -28,25 +65,25 @@ var wefaxDom = {
}; };
// ── State ─────────────────────────────────────────────────────────── // ── State ───────────────────────────────────────────────────────────
var wefaxImageHistory = []; let wefaxImageHistory: WefaxImage[] = [];
var WEFAX_MAX_IMAGES = 100; const WEFAX_MAX_IMAGES = 100;
var wefaxLiveCtx = null; let wefaxLiveCtx: CanvasRenderingContext2D | null = null;
var wefaxLiveLineCount = 0; let wefaxLiveLineCount = 0;
var wefaxLivePixelsPerLine = 1809; let wefaxLivePixelsPerLine = 1809;
var wefaxActiveView = 'live'; let wefaxActiveView: 'live' | 'history' = 'live';
var wefaxFilterText = ''; let wefaxFilterText = '';
// ── Helpers ───────────────────────────────────────────────────────── // ── Helpers ─────────────────────────────────────────────────────────
function currentWefaxHistoryRetentionMs() { function currentWefaxHistoryRetentionMs(): number {
return window.getDecodeHistoryRetentionMs ? window.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1000; return wefaxWindow.getDecodeHistoryRetentionMs?.() ?? 24 * 60 * 60 * 1000;
} }
function pruneWefaxHistory() { function pruneWefaxHistory() {
var cutoff = Date.now() - currentWefaxHistoryRetentionMs(); const cutoff = Date.now() - currentWefaxHistoryRetentionMs();
wefaxImageHistory = wefaxImageHistory.filter(function (m) { return (m._tsMs || 0) > cutoff; }); wefaxImageHistory = wefaxImageHistory.filter(function (m) { return (m._tsMs || 0) > cutoff; });
} }
function escapeHtml(s) { function escapeHtml(s: unknown): string {
return String(s) return String(s)
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
@@ -54,16 +91,16 @@ function escapeHtml(s) {
.replace(/"/g, '&quot;'); .replace(/"/g, '&quot;');
} }
function scheduleWefaxUi(key, job) { function scheduleWefaxUi(key: string, job: () => void): void {
if (typeof window.trxScheduleUiFrameJob === 'function') { if (typeof wefaxWindow.trxScheduleUiFrameJob === 'function') {
window.trxScheduleUiFrameJob(key, job); wefaxWindow.trxScheduleUiFrameJob(key, job);
return; return;
} }
job(); job();
} }
// ── View switching ────────────────────────────────────────────────── // ── View switching ──────────────────────────────────────────────────
function switchWefaxView(view) { function switchWefaxView(view: 'live' | 'history'): void {
wefaxActiveView = view; wefaxActiveView = view;
if (wefaxDom.liveView) wefaxDom.liveView.style.display = view === 'live' ? '' : 'none'; if (wefaxDom.liveView) wefaxDom.liveView.style.display = view === 'live' ? '' : 'none';
if (wefaxDom.historyView) wefaxDom.historyView.style.display = view === 'history' ? '' : 'none'; if (wefaxDom.historyView) wefaxDom.historyView.style.display = view === 'history' ? '' : 'none';
@@ -81,33 +118,39 @@ if (wefaxDom.viewLiveBtn) wefaxDom.viewLiveBtn.addEventListener('click', func
if (wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.addEventListener('click', function () { switchWefaxView('history'); }); if (wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.addEventListener('click', function () { switchWefaxView('history'); });
// ── Live canvas rendering ─────────────────────────────────────────── // ── Live canvas rendering ───────────────────────────────────────────
function resetLiveCanvas(pixelsPerLine) { function resetLiveCanvas(pixelsPerLine: number): void {
const canvas = wefaxDom.liveCanvas;
if (!canvas) return;
wefaxLivePixelsPerLine = pixelsPerLine; wefaxLivePixelsPerLine = pixelsPerLine;
wefaxLiveLineCount = 0; wefaxLiveLineCount = 0;
wefaxDom.liveCanvas.width = pixelsPerLine; canvas.width = pixelsPerLine;
wefaxDom.liveCanvas.height = 800; canvas.height = 800;
wefaxLiveCtx = wefaxDom.liveCanvas.getContext('2d'); wefaxLiveCtx = canvas.getContext('2d');
if (!wefaxLiveCtx) return;
wefaxLiveCtx.fillStyle = '#000'; wefaxLiveCtx.fillStyle = '#000';
wefaxLiveCtx.fillRect(0, 0, wefaxDom.liveCanvas.width, wefaxDom.liveCanvas.height); wefaxLiveCtx.fillRect(0, 0, canvas.width, canvas.height);
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = ''; if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = '';
} }
function paintLine(lineBytes) { function paintLine(lineBytes: Uint8Array): void {
if (!wefaxLiveCtx) return; const canvas = wefaxDom.liveCanvas;
var y = wefaxLiveLineCount; if (!wefaxLiveCtx || !canvas) return;
const y = wefaxLiveLineCount;
if (y >= wefaxDom.liveCanvas.height) { if (y >= canvas.height) {
var old = wefaxLiveCtx.getImageData(0, 0, wefaxDom.liveCanvas.width, wefaxDom.liveCanvas.height); const old = wefaxLiveCtx.getImageData(0, 0, canvas.width, canvas.height);
wefaxDom.liveCanvas.height *= 2; canvas.height *= 2;
wefaxLiveCtx = canvas.getContext('2d');
if (!wefaxLiveCtx) return;
wefaxLiveCtx.putImageData(old, 0, 0); wefaxLiveCtx.putImageData(old, 0, 0);
} }
var w = wefaxLivePixelsPerLine; const w = wefaxLivePixelsPerLine;
var imgData = wefaxLiveCtx.createImageData(w, 1); const imgData = wefaxLiveCtx.createImageData(w, 1);
var d = imgData.data; const d = imgData.data;
for (var x = 0; x < w; x++) { for (let x = 0; x < w; x++) {
var v = x < lineBytes.length ? lineBytes[x] : 0; const v = lineBytes[x] ?? 0;
var i = x * 4; const i = x * 4;
d[i] = v; d[i + 1] = v; d[i + 2] = v; d[i + 3] = 255; d[i] = v; d[i + 1] = v; d[i + 2] = v; d[i + 3] = 255;
} }
wefaxLiveCtx.putImageData(imgData, 0, y); wefaxLiveCtx.putImageData(imgData, 0, y);
@@ -123,23 +166,24 @@ function renderWefaxLatestCard() {
return; return;
} }
var img = wefaxImageHistory[0]; const img = wefaxImageHistory[0];
var ts = img._ts || '--'; if (!img) return;
var date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : ''; const ts = img._ts || '--';
var meta = [ const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : '';
img.ioc + ' IOC', const meta = [
img.lpm + ' LPM', `${String(img.ioc ?? '--')} IOC`,
img.line_count + ' lines', `${String(img.lpm ?? '--')} LPM`,
date + ' ' + ts, `${String(img.line_count ?? 0)} lines`,
`${date} ${ts}`,
].join(' \u00b7 '); ].join(' \u00b7 ');
var imgSrc = img._dataUrl const imgSrc = img._dataUrl
? img._dataUrl ? img._dataUrl
: img.path : img.path
? '/images/' + escapeHtml(img.path.split('/').pop()) ? '/images/' + escapeHtml(img.path.split('/').pop())
: null; : null;
var html = '<div class="sat-latest-card">'; let html = '<div class="sat-latest-card">';
html += '<div class="sat-latest-title">Latest decoded image</div>'; html += '<div class="sat-latest-title">Latest decoded image</div>';
html += '<div class="sat-latest-meta">' + escapeHtml(meta) + '</div>'; html += '<div class="sat-latest-meta">' + escapeHtml(meta) + '</div>';
if (imgSrc) { if (imgSrc) {
@@ -151,11 +195,11 @@ function renderWefaxLatestCard() {
// ── History view: table ───────────────────────────────────────────── // ── History view: table ─────────────────────────────────────────────
function getWefaxFilteredHistory() { function getWefaxFilteredHistory() {
var items = wefaxImageHistory; let items = wefaxImageHistory;
if (wefaxFilterText) { if (wefaxFilterText) {
items = items.filter(function (i) { items = items.filter(function (i) {
var haystack = [ const haystack = [
String(i.ioc || ''), String(i.ioc || ''),
String(i.lpm || ''), String(i.lpm || ''),
String(i.line_count || ''), String(i.line_count || ''),
@@ -164,28 +208,28 @@ function getWefaxFilteredHistory() {
}); });
} }
var sortVal = wefaxDom.sortSelect ? wefaxDom.sortSelect.value : 'newest'; const sortVal = wefaxDom.sortSelect ? wefaxDom.sortSelect.value : 'newest';
if (sortVal === 'oldest') items = items.slice().reverse(); if (sortVal === 'oldest') items = items.slice().reverse();
return items; return items;
} }
function renderWefaxHistoryRow(img) { function renderWefaxHistoryRow(img: WefaxImage): HTMLElement {
var row = document.createElement('div'); const row = document.createElement('div');
row.className = 'sat-history-row'; row.className = 'sat-history-row';
var ts = img._ts || '--'; const ts = img._ts || '--';
var date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: 'short', day: 'numeric' }) : ''; const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: 'short', day: 'numeric' }) : '';
var ioc = img.ioc || '--'; const ioc = img.ioc || '--';
var lpm = img.lpm || '--'; const lpm = img.lpm || '--';
var lines = img.line_count || 0; const lines = img.line_count || 0;
var imgSrc = img._dataUrl const imgSrc = img._dataUrl
? img._dataUrl ? img._dataUrl
: img.path : img.path
? '/images/' + escapeHtml(img.path.split('/').pop()) ? '/images/' + escapeHtml(img.path.split('/').pop())
: null; : null;
var link = imgSrc const link = imgSrc
? '<a href="' + imgSrc + '" target="_blank" style="color:var(--accent);">View</a>' ? '<a href="' + imgSrc + '" target="_blank" style="color:var(--accent);">View</a>'
: '--'; : '--';
@@ -193,7 +237,7 @@ function renderWefaxHistoryRow(img) {
'<span>' + escapeHtml(date + ' ' + ts) + '</span>', '<span>' + escapeHtml(date + ' ' + ts) + '</span>',
'<span>' + escapeHtml(String(ioc)) + '</span>', '<span>' + escapeHtml(String(ioc)) + '</span>',
'<span>' + escapeHtml(String(lpm)) + '</span>', '<span>' + escapeHtml(String(lpm)) + '</span>',
'<span>' + lines + '</span>', `<span>${String(lines)}</span>`,
'<span>' + link + '</span>', '<span>' + link + '</span>',
].join(''); ].join('');
@@ -203,28 +247,28 @@ function renderWefaxHistoryRow(img) {
function renderWefaxHistoryTable() { function renderWefaxHistoryTable() {
if (!wefaxDom.historyList) return; if (!wefaxDom.historyList) return;
pruneWefaxHistory(); pruneWefaxHistory();
var items = getWefaxFilteredHistory(); const items = getWefaxFilteredHistory();
var fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
for (var i = 0; i < items.length; i++) { for (const item of items) {
fragment.appendChild(renderWefaxHistoryRow(items[i])); fragment.appendChild(renderWefaxHistoryRow(item));
} }
wefaxDom.historyList.replaceChildren(fragment); wefaxDom.historyList.replaceChildren(fragment);
if (wefaxDom.historyCount) { if (wefaxDom.historyCount) {
var total = wefaxImageHistory.length; const total = wefaxImageHistory.length;
var shown = items.length; const shown = items.length;
wefaxDom.historyCount.textContent = wefaxDom.historyCount.textContent =
total === 0 total === 0
? 'No images yet' ? 'No images yet'
: shown === total : shown === total
? total + ' image' + (total === 1 ? '' : 's') ? `${String(total)} image${total === 1 ? '' : 's'}`
: shown + ' of ' + total + ' images'; : `${String(shown)} of ${String(total)} images`;
} }
} }
// ── Add image to history ──────────────────────────────────────────── // ── Add image to history ────────────────────────────────────────────
function addWefaxImage(msg) { function addWefaxImage(msg: WefaxImage): void {
var tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now(); const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
msg._tsMs = tsMs; msg._tsMs = tsMs;
msg._ts = new Date(tsMs).toLocaleTimeString([], { msg._ts = new Date(tsMs).toLocaleTimeString([], {
hour: '2-digit', hour: '2-digit',
@@ -233,11 +277,14 @@ function addWefaxImage(msg) {
}); });
// Capture the live canvas as a data URI for thumbnails. // Capture the live canvas as a data URI for thumbnails.
if (wefaxLiveCtx && wefaxLiveLineCount > 0) { const canvas = wefaxDom.liveCanvas;
var trimmed = wefaxLiveCtx.getImageData(0, 0, wefaxDom.liveCanvas.width, wefaxLiveLineCount); if (wefaxLiveCtx && canvas && wefaxLiveLineCount > 0) {
wefaxDom.liveCanvas.height = wefaxLiveLineCount; const trimmed = wefaxLiveCtx.getImageData(0, 0, canvas.width, wefaxLiveLineCount);
canvas.height = wefaxLiveLineCount;
wefaxLiveCtx = canvas.getContext('2d');
if (!wefaxLiveCtx) return;
wefaxLiveCtx.putImageData(trimmed, 0, 0); wefaxLiveCtx.putImageData(trimmed, 0, 0);
try { msg._dataUrl = wefaxDom.liveCanvas.toDataURL('image/png'); } catch (e) {} try { msg._dataUrl = canvas.toDataURL('image/png'); } catch {}
} }
wefaxImageHistory.unshift(msg); wefaxImageHistory.unshift(msg);
@@ -252,7 +299,7 @@ function addWefaxImage(msg) {
} }
// ── SSE event handlers (public API) ───────────────────────────────── // ── SSE event handlers (public API) ─────────────────────────────────
window.onServerWefaxProgress = function (msg) { wefaxWindow.onServerWefaxProgress = function (msg: WefaxProgress) {
// State-only update (no image data): show decoder state in status. // State-only update (no image data): show decoder state in status.
if (msg.state && !msg.line_data) { if (msg.state && !msg.line_data) {
if (wefaxDom.status) { if (wefaxDom.status) {
@@ -263,43 +310,43 @@ window.onServerWefaxProgress = function (msg) {
return; return;
} }
if (msg.line_count <= 1 || !wefaxLiveCtx) { if ((msg.line_count ?? 0) <= 1 || !wefaxLiveCtx) {
resetLiveCanvas(msg.pixels_per_line || 1809); resetLiveCanvas(msg.pixels_per_line || 1809);
} }
if (msg.line_data) { if (msg.line_data) {
var binary = atob(msg.line_data); const binary = atob(msg.line_data);
var bytes = new Uint8Array(binary.length); const bytes = new Uint8Array(binary.length);
for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
paintLine(bytes); paintLine(bytes);
} }
if (wefaxDom.liveInfo) { if (wefaxDom.liveInfo) {
wefaxDom.liveInfo.textContent = wefaxDom.liveInfo.textContent =
'Line ' + msg.line_count + ' \u00b7 ' + msg.ioc + ' IOC \u00b7 ' + msg.lpm + ' LPM'; `Line ${String(msg.line_count ?? 0)} \u00b7 ${String(msg.ioc ?? '--')} IOC \u00b7 ${String(msg.lpm ?? '--')} LPM`;
} }
if (wefaxDom.status) { if (wefaxDom.status) {
wefaxDom.status.textContent = 'Receiving \u2014 line ' + msg.line_count; wefaxDom.status.textContent = `Receiving \u2014 line ${String(msg.line_count ?? 0)}`;
wefaxDom.status.style.color = 'var(--text-accent)'; wefaxDom.status.style.color = 'var(--text-accent)';
} }
}; };
window.onServerWefax = function (msg) { wefaxWindow.onServerWefax = function (msg: WefaxImage) {
addWefaxImage(msg); addWefaxImage(msg);
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none'; if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none';
if (wefaxDom.status) { if (wefaxDom.status) {
wefaxDom.status.textContent = 'Complete \u2014 ' + msg.line_count + ' lines'; wefaxDom.status.textContent = `Complete \u2014 ${String(msg.line_count ?? 0)} lines`;
wefaxDom.status.style.color = ''; wefaxDom.status.style.color = '';
} }
}; };
window.restoreWefaxHistory = function (messages) { wefaxWindow.restoreWefaxHistory = function (messages: WefaxImage[]) {
if (!messages || !messages.length) return; if (!messages.length) return;
for (var i = 0; i < messages.length; i++) { for (const message of messages) {
var tsMs = Number.isFinite(messages[i].ts_ms) ? Number(messages[i].ts_ms) : Date.now(); const tsMs = Number.isFinite(message.ts_ms) ? Number(message.ts_ms) : Date.now();
messages[i]._tsMs = tsMs; message._tsMs = tsMs;
messages[i]._ts = new Date(tsMs).toLocaleTimeString([], { message._ts = new Date(tsMs).toLocaleTimeString([], {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
second: '2-digit', second: '2-digit',
@@ -313,13 +360,13 @@ window.restoreWefaxHistory = function (messages) {
} }
}; };
window.pruneWefaxHistoryView = function () { wefaxWindow.pruneWefaxHistoryView = function () {
pruneWefaxHistory(); pruneWefaxHistory();
renderWefaxHistoryTable(); renderWefaxHistoryTable();
renderWefaxLatestCard(); renderWefaxLatestCard();
}; };
window.resetWefaxHistoryView = function () { wefaxWindow.resetWefaxHistoryView = function () {
wefaxImageHistory = []; wefaxImageHistory = [];
if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = ''; if (wefaxDom.historyList) wefaxDom.historyList.innerHTML = '';
if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none'; if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = 'none';
@@ -335,8 +382,9 @@ window.resetWefaxHistoryView = function () {
// ── Filter / sort handlers ────────────────────────────────────────── // ── Filter / sort handlers ──────────────────────────────────────────
if (wefaxDom.filterInput) { if (wefaxDom.filterInput) {
const filterInput = wefaxDom.filterInput;
wefaxDom.filterInput.addEventListener('input', function () { wefaxDom.filterInput.addEventListener('input', function () {
wefaxFilterText = wefaxDom.filterInput.value.trim().toUpperCase(); wefaxFilterText = filterInput.value.trim().toUpperCase();
scheduleWefaxUi('wefax-history', renderWefaxHistoryTable); scheduleWefaxUi('wefax-history', renderWefaxHistoryTable);
}); });
} }
@@ -350,7 +398,7 @@ if (wefaxDom.sortSelect) {
// Sync the Enable/Disable button from the SSE state update. This is // Sync the Enable/Disable button from the SSE state update. This is
// belt-and-suspenders alongside app.js _decoderToggles — guarantees the // belt-and-suspenders alongside app.js _decoderToggles — guarantees the
// WEFAX button always reflects the server state. // WEFAX button always reflects the server state.
window.syncWefaxToggle = function (enabled) { wefaxWindow.syncWefaxToggle = function (enabled: boolean) {
if (!wefaxDom.toggleBtn) return; if (!wefaxDom.toggleBtn) return;
wefaxDom.toggleBtn.dataset.enabled = enabled ? 'true' : 'false'; wefaxDom.toggleBtn.dataset.enabled = enabled ? 'true' : 'false';
wefaxDom.toggleBtn.textContent = enabled ? 'Disable WEFAX' : 'Enable WEFAX'; wefaxDom.toggleBtn.textContent = enabled ? 'Disable WEFAX' : 'Enable WEFAX';
@@ -360,26 +408,27 @@ window.syncWefaxToggle = function (enabled) {
// ── Button handlers ───────────────────────────────────────────────── // ── Button handlers ─────────────────────────────────────────────────
if (wefaxDom.toggleBtn) { if (wefaxDom.toggleBtn) {
wefaxDom.toggleBtn.addEventListener('click', async function () { const toggleButton = wefaxDom.toggleBtn;
wefaxDom.toggleBtn.addEventListener('click', () => { void (async () => {
try { try {
if (window.takeSchedulerControlForDecoderDisable) { if (wefaxWindow.takeSchedulerControlForDecoderDisable) {
await window.takeSchedulerControlForDecoderDisable(wefaxDom.toggleBtn); await wefaxWindow.takeSchedulerControlForDecoderDisable(toggleButton);
} }
await postPath('/toggle_wefax_decode'); await wefaxWindow.postPath?.('/toggle_wefax_decode');
} catch (e) { } catch (e) {
console.error('WEFAX toggle failed', e); console.error('WEFAX toggle failed', e);
} }
}); })(); });
} }
if (wefaxDom.clearBtn) { if (wefaxDom.clearBtn) {
wefaxDom.clearBtn.addEventListener('click', async function () { wefaxDom.clearBtn.addEventListener('click', () => { void (async () => {
try { try {
await postPath('/clear_wefax_decode'); await wefaxWindow.postPath?.('/clear_wefax_decode');
window.resetWefaxHistoryView(); wefaxWindow.resetWefaxHistoryView?.();
} catch (e) { } catch (e) {
console.error('WEFAX clear failed', e); console.error('WEFAX clear failed', e);
} }
}); })(); });
} }
// ── Initial render ────────────────────────────────────────────────── // ── Initial render ──────────────────────────────────────────────────
@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// 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("WEFAX entry exposes typed lifecycle handlers and renders decoder state", async () => {
const status = { textContent: "", style: { color: "" } };
const window = {};
const context = vm.createContext({
window,
document: { getElementById: (id) => id === "wefax-status" ? status : null },
Date,
Number,
String,
Uint8Array,
console,
});
const source = await readFile(new URL("../../assets/web/generated/wefax.js", import.meta.url), "utf8");
new vm.Script(source).runInContext(context);
assert.equal(typeof window.onServerWefaxProgress, "function");
assert.equal(typeof window.restoreWefaxHistory, "function");
window.onServerWefaxProgress({ state: "Scanning 12 MHz" });
assert.equal(status.textContent, "Scanning 12 MHz");
assert.equal(status.style.color, "var(--text-accent)");
});