diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js
index ea21ccf1..3a85696f 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ais.js
@@ -1,334 +1,331 @@
-"use strict";
-(() => {
- // src/plugins/ais.ts
- var aisWindow = window;
- var escapeAisHtml = (input) => aisWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
- var aisStatus = document.getElementById("ais-status");
- var aisMessagesEl = document.getElementById("ais-messages");
- var aisFilterInput = document.getElementById("ais-filter");
- var aisBarOverlay = document.getElementById("ais-bar-overlay");
- var aisChannelSummaryEl = document.getElementById("ais-channel-summary");
- var aisVesselCountEl = document.getElementById("ais-vessel-count");
- var aisLatestSeenEl = document.getElementById("ais-latest-seen");
- var AIS_BAR_WINDOW_MS = 15 * 60 * 1e3;
- var AIS_DEFAULT_A_HZ = 161975e3;
- var AIS_CHANNEL_SPACING_HZ = 5e4;
- var aisFilterText = "";
- var aisMessageHistory = [];
- function currentAisHistoryRetentionMs() {
- return typeof aisWindow.getDecodeHistoryRetentionMs === "function" ? aisWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
+// src/plugins/ais.ts
+var aisWindow = window;
+var escapeAisHtml = (input) => aisWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
+var aisStatus = document.getElementById("ais-status");
+var aisMessagesEl = document.getElementById("ais-messages");
+var aisFilterInput = document.getElementById("ais-filter");
+var aisBarOverlay = document.getElementById("ais-bar-overlay");
+var aisChannelSummaryEl = document.getElementById("ais-channel-summary");
+var aisVesselCountEl = document.getElementById("ais-vessel-count");
+var aisLatestSeenEl = document.getElementById("ais-latest-seen");
+var AIS_BAR_WINDOW_MS = 15 * 60 * 1e3;
+var AIS_DEFAULT_A_HZ = 161975e3;
+var AIS_CHANNEL_SPACING_HZ = 5e4;
+var aisFilterText = "";
+var aisMessageHistory = [];
+function currentAisHistoryRetentionMs() {
+ return typeof aisWindow.getDecodeHistoryRetentionMs === "function" ? aisWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
+}
+function pruneAisMessageHistory() {
+ const cutoffMs = Date.now() - currentAisHistoryRetentionMs();
+ aisMessageHistory = aisMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs);
+}
+function scheduleAisUi(key, job) {
+ if (typeof aisWindow.trxScheduleUiFrameJob === "function") {
+ aisWindow.trxScheduleUiFrameJob(key, job);
+ return;
}
- function pruneAisMessageHistory() {
- const cutoffMs = Date.now() - currentAisHistoryRetentionMs();
- aisMessageHistory = aisMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs);
- }
- function scheduleAisUi(key, job) {
- if (typeof aisWindow.trxScheduleUiFrameJob === "function") {
- aisWindow.trxScheduleUiFrameJob(key, job);
- return;
- }
- job();
- }
- function scheduleAisHistoryRender() {
- scheduleAisUi("ais-history", () => {
- renderAisHistory();
- });
- }
- function scheduleAisBarUpdate() {
- scheduleAisUi("ais-bar", () => {
- updateAisBar();
- });
- }
- function formatAisMhz(freqHz) {
- return `${(freqHz / 1e6).toFixed(3)} MHz`;
- }
- function currentAisChannelPlan() {
- const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, "");
- const aHz = raw ? Number(raw) : AIS_DEFAULT_A_HZ;
- const safeAHz = Number.isFinite(aHz) && aHz > 0 ? aHz : AIS_DEFAULT_A_HZ;
- return {
- aHz: safeAHz,
- bHz: safeAHz + AIS_CHANNEL_SPACING_HZ
- };
- }
- function aisChannelInfo(channel) {
- const plan = currentAisChannelPlan();
- const ch = (channel ?? "").trim().toUpperCase();
- if (ch === "B") {
- return {
- label: "AIS-B",
- badgeClass: "ais-badge ais-badge-channel-b",
- freqText: formatAisMhz(plan.bHz)
- };
- }
- return {
- label: "AIS-A",
- badgeClass: "ais-badge ais-badge-channel-a",
- freqText: formatAisMhz(plan.aHz)
- };
- }
- function aisDisplayName(msg) {
- return msg.vessel_name || msg.callsign || `MMSI ${msg.mmsi}`;
- }
- function aisDisplayNameHtml(msg) {
- const label = escapeAisHtml(aisDisplayName(msg));
- const url = aisWindow.buildAisVesselUrl?.(msg.mmsi) ?? null;
- if (!url) return label;
- return `${label}`;
- }
- function aisTypeLabel(type) {
- switch (Number(type)) {
- case 1:
- case 2:
- case 3:
- return "Class A Position";
- case 4:
- return "Base Station";
- case 5:
- return "Static/Voyage";
- case 18:
- return "Class B Position";
- case 19:
- return "Class B Extended";
- case 21:
- return "Aid to Nav";
- case 24:
- return "Class B Static";
- default:
- return `Type ${type ?? "--"}`;
- }
- }
- function aisAgeText(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 aisMotionText(msg) {
- const parts = [
- msg.sog_knots != null ? `${msg.sog_knots.toFixed(1)} kn` : null,
- msg.cog_deg != null ? `${msg.cog_deg.toFixed(1)}° COG` : null,
- msg.heading_deg != null ? `${msg.heading_deg.toFixed(0)}° HDG` : null
- ].filter(Boolean);
- return parts.join(" · ");
- }
- function aisRouteText(msg) {
- return [msg.callsign, msg.destination].filter(Boolean).join(" -> ");
- }
- function aisDistanceText(msg) {
- if (aisWindow.serverLat == null || aisWindow.serverLon == null || msg.lat == null || msg.lon == null || !aisWindow.haversineKm) {
- return "";
- }
- const distKm = aisWindow.haversineKm(aisWindow.serverLat, aisWindow.serverLon, msg.lat, msg.lon);
- if (!Number.isFinite(distKm)) return "";
- if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`;
- return `${distKm.toFixed(1)} km from TRX`;
- }
- function aisLatestByVessel(messages) {
- const byMmsi = /* @__PURE__ */ new Map();
- for (const msg of messages) {
- const key = Number.isFinite(msg.mmsi) ? String(msg.mmsi) : `${msg.channel || "?"}:${msg._tsMs || 0}`;
- if (!byMmsi.has(key)) byMmsi.set(key, msg);
- }
- return Array.from(byMmsi.values());
- }
- function updateAisSummary() {
- const plan = currentAisChannelPlan();
- if (aisChannelSummaryEl) {
- aisChannelSummaryEl.textContent = `A ${formatAisMhz(plan.aHz)} · B ${formatAisMhz(plan.bHz)}`;
- }
- const vessels = aisLatestByVessel(aisMessageHistory);
- if (aisVesselCountEl) {
- const count = vessels.length;
- aisVesselCountEl.textContent = `${count} vessel${count === 1 ? "" : "s"}`;
- }
- if (aisLatestSeenEl) {
- const latest = aisMessageHistory[0];
- if (!latest) {
- aisLatestSeenEl.textContent = "No traffic yet";
- } else {
- const channel = aisChannelInfo(latest.channel);
- aisLatestSeenEl.textContent = `${channel.label} ${aisAgeText(latest._tsMs)}`;
- }
- }
- }
- function renderAisRow(msg) {
- const row = document.createElement("div");
- row.className = "ais-message";
- const ts = msg._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit"
- });
- const name = aisDisplayName(msg);
- const nameHtml = aisDisplayNameHtml(msg);
- const channel = aisChannelInfo(msg.channel);
- const motion = aisMotionText(msg);
- const route = aisRouteText(msg);
- const distance = aisDistanceText(msg);
- const pos = msg.lat != null && msg.lon != null ? `${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}` : "";
- row.dataset.filterText = [
- name,
- msg.mmsi,
- msg.channel,
- channel.label,
- msg.vessel_name,
- msg.callsign,
- msg.destination,
- aisTypeLabel(msg.message_type)
- ].filter(Boolean).join(" ").toUpperCase();
- row.innerHTML = `
${ts}${nameHtml}${escapeAisHtml(channel.label)}${escapeAisHtml(aisTypeLabel(msg.message_type))}
MMSI ${escapeAisHtml(String(msg.mmsi))}` + (route ? `${escapeAisHtml(route)}` : "") + `${escapeAisHtml(channel.freqText)}
` + (motion ? `${escapeAisHtml(motion)}` : `No motion data`) + (distance ? `${escapeAisHtml(distance)}` : "") + (pos ? `${pos}` : "") + `${escapeAisHtml(aisAgeText(msg._tsMs))}
`;
- applyAisFilterToRow(row);
- return row;
- }
- function applyAisFilterToRow(row) {
- if (!aisFilterText) {
- row.style.display = "";
- return;
- }
- const message = row.dataset.filterText || "";
- row.style.display = message.includes(aisFilterText) ? "" : "none";
- }
- function updateAisBar() {
- if (!aisBarOverlay) return;
- updateAisSummary();
- const isAis = (document.getElementById("mode")?.value || "").toUpperCase() === "AIS";
- const cutoffMs = Date.now() - AIS_BAR_WINDOW_MS;
- const recent = aisMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs);
- const messages = aisLatestByVessel(recent).slice(0, 8);
- if (!isAis || messages.length === 0) {
- aisBarOverlay.style.display = "none";
- aisBarOverlay.innerHTML = "";
- return;
- }
- let html = ``;
- for (const msg of messages) {
- const ts = msg._ts ? `${msg._ts}` : "";
- const pin = msg.lat != null && msg.lon != null ? `` : "";
- const name = `${aisDisplayNameHtml(msg)}`;
- const channel = aisChannelInfo(msg.channel);
- const distance = aisDistanceText(msg);
- const details = [
- `MMSI ${escapeAisHtml(String(msg.mmsi))}`,
- escapeAisHtml(channel.label),
- msg.sog_knots != null ? `${msg.sog_knots.toFixed(1)} kn` : null,
- msg.cog_deg != null ? `${msg.cog_deg.toFixed(1)}°` : null,
- distance ? escapeAisHtml(distance) : null,
- escapeAisHtml(aisAgeText(msg._tsMs))
- ].filter(Boolean).join(" · ");
- html += `${ts}${pin}${name}: ${details}
`;
- }
- aisBarOverlay.innerHTML = html;
- aisBarOverlay.style.display = "flex";
- }
- aisWindow.updateAisBar = updateAisBar;
- aisWindow.clearAisBar = function() {
- resetAisHistoryView();
+ job();
+}
+function scheduleAisHistoryRender() {
+ scheduleAisUi("ais-history", () => {
+ renderAisHistory();
+ });
+}
+function scheduleAisBarUpdate() {
+ scheduleAisUi("ais-bar", () => {
+ updateAisBar();
+ });
+}
+function formatAisMhz(freqHz) {
+ return `${(freqHz / 1e6).toFixed(3)} MHz`;
+}
+function currentAisChannelPlan() {
+ const raw = (document.getElementById("freq")?.value || "").replace(/[^\d]/g, "");
+ const aHz = raw ? Number(raw) : AIS_DEFAULT_A_HZ;
+ const safeAHz = Number.isFinite(aHz) && aHz > 0 ? aHz : AIS_DEFAULT_A_HZ;
+ return {
+ aHz: safeAHz,
+ bHz: safeAHz + AIS_CHANNEL_SPACING_HZ
};
- function resetAisHistoryView() {
- if (aisMessagesEl) aisMessagesEl.innerHTML = "";
- aisMessageHistory = [];
- updateAisBar();
- renderAisHistory();
- aisWindow.clearMapMarkersByType?.("ais");
+}
+function aisChannelInfo(channel) {
+ const plan = currentAisChannelPlan();
+ const ch = (channel ?? "").trim().toUpperCase();
+ if (ch === "B") {
+ return {
+ label: "AIS-B",
+ badgeClass: "ais-badge ais-badge-channel-b",
+ freqText: formatAisMhz(plan.bHz)
+ };
}
- function renderAisHistory() {
- pruneAisMessageHistory();
- if (!aisMessagesEl) {
- updateAisSummary();
- return;
+ return {
+ label: "AIS-A",
+ badgeClass: "ais-badge ais-badge-channel-a",
+ freqText: formatAisMhz(plan.aHz)
+ };
+}
+function aisDisplayName(msg) {
+ return msg.vessel_name || msg.callsign || `MMSI ${msg.mmsi}`;
+}
+function aisDisplayNameHtml(msg) {
+ const label = escapeAisHtml(aisDisplayName(msg));
+ const url = aisWindow.buildAisVesselUrl?.(msg.mmsi) ?? null;
+ if (!url) return label;
+ return `${label}`;
+}
+function aisTypeLabel(type) {
+ switch (Number(type)) {
+ case 1:
+ case 2:
+ case 3:
+ return "Class A Position";
+ case 4:
+ return "Base Station";
+ case 5:
+ return "Static/Voyage";
+ case 18:
+ return "Class B Position";
+ case 19:
+ return "Class B Extended";
+ case 21:
+ return "Aid to Nav";
+ case 24:
+ return "Class B Static";
+ default:
+ return `Type ${type ?? "--"}`;
+ }
+}
+function aisAgeText(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 aisMotionText(msg) {
+ const parts = [
+ msg.sog_knots != null ? `${msg.sog_knots.toFixed(1)} kn` : null,
+ msg.cog_deg != null ? `${msg.cog_deg.toFixed(1)}° COG` : null,
+ msg.heading_deg != null ? `${msg.heading_deg.toFixed(0)}° HDG` : null
+ ].filter(Boolean);
+ return parts.join(" · ");
+}
+function aisRouteText(msg) {
+ return [msg.callsign, msg.destination].filter(Boolean).join(" -> ");
+}
+function aisDistanceText(msg) {
+ if (aisWindow.serverLat == null || aisWindow.serverLon == null || msg.lat == null || msg.lon == null || !aisWindow.haversineKm) {
+ return "";
+ }
+ const distKm = aisWindow.haversineKm(aisWindow.serverLat, aisWindow.serverLon, msg.lat, msg.lon);
+ if (!Number.isFinite(distKm)) return "";
+ if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`;
+ return `${distKm.toFixed(1)} km from TRX`;
+}
+function aisLatestByVessel(messages) {
+ const byMmsi = /* @__PURE__ */ new Map();
+ for (const msg of messages) {
+ const key = Number.isFinite(msg.mmsi) ? String(msg.mmsi) : `${msg.channel || "?"}:${msg._tsMs || 0}`;
+ if (!byMmsi.has(key)) byMmsi.set(key, msg);
+ }
+ return Array.from(byMmsi.values());
+}
+function updateAisSummary() {
+ const plan = currentAisChannelPlan();
+ if (aisChannelSummaryEl) {
+ aisChannelSummaryEl.textContent = `A ${formatAisMhz(plan.aHz)} · B ${formatAisMhz(plan.bHz)}`;
+ }
+ const vessels = aisLatestByVessel(aisMessageHistory);
+ if (aisVesselCountEl) {
+ const count = vessels.length;
+ aisVesselCountEl.textContent = `${count} vessel${count === 1 ? "" : "s"}`;
+ }
+ if (aisLatestSeenEl) {
+ const latest = aisMessageHistory[0];
+ if (!latest) {
+ aisLatestSeenEl.textContent = "No traffic yet";
+ } else {
+ const channel = aisChannelInfo(latest.channel);
+ aisLatestSeenEl.textContent = `${channel.label} ${aisAgeText(latest._tsMs)}`;
}
- const fragment = document.createDocumentFragment();
- for (const message of aisMessageHistory) {
- fragment.appendChild(renderAisRow(message));
- }
- aisMessagesEl.replaceChildren(fragment);
+ }
+}
+function renderAisRow(msg) {
+ const row = document.createElement("div");
+ row.className = "ais-message";
+ const ts = msg._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit"
+ });
+ const name = aisDisplayName(msg);
+ const nameHtml = aisDisplayNameHtml(msg);
+ const channel = aisChannelInfo(msg.channel);
+ const motion = aisMotionText(msg);
+ const route = aisRouteText(msg);
+ const distance = aisDistanceText(msg);
+ const pos = msg.lat != null && msg.lon != null ? `${msg.lat.toFixed(4)}, ${msg.lon.toFixed(4)}` : "";
+ row.dataset.filterText = [
+ name,
+ msg.mmsi,
+ msg.channel,
+ channel.label,
+ msg.vessel_name,
+ msg.callsign,
+ msg.destination,
+ aisTypeLabel(msg.message_type)
+ ].filter(Boolean).join(" ").toUpperCase();
+ row.innerHTML = `${ts}${nameHtml}${escapeAisHtml(channel.label)}${escapeAisHtml(aisTypeLabel(msg.message_type))}
MMSI ${escapeAisHtml(String(msg.mmsi))}` + (route ? `${escapeAisHtml(route)}` : "") + `${escapeAisHtml(channel.freqText)}
` + (motion ? `${escapeAisHtml(motion)}` : `No motion data`) + (distance ? `${escapeAisHtml(distance)}` : "") + (pos ? `${pos}` : "") + `${escapeAisHtml(aisAgeText(msg._tsMs))}
`;
+ applyAisFilterToRow(row);
+ return row;
+}
+function applyAisFilterToRow(row) {
+ if (!aisFilterText) {
+ row.style.display = "";
+ return;
+ }
+ const message = row.dataset.filterText || "";
+ row.style.display = message.includes(aisFilterText) ? "" : "none";
+}
+function updateAisBar() {
+ if (!aisBarOverlay) return;
+ updateAisSummary();
+ const isAis = (document.getElementById("mode")?.value || "").toUpperCase() === "AIS";
+ const cutoffMs = Date.now() - AIS_BAR_WINDOW_MS;
+ const recent = aisMessageHistory.filter((msg) => (msg._tsMs ?? 0) >= cutoffMs);
+ const messages = aisLatestByVessel(recent).slice(0, 8);
+ if (!isAis || messages.length === 0) {
+ aisBarOverlay.style.display = "none";
+ aisBarOverlay.innerHTML = "";
+ return;
+ }
+ let html = ``;
+ for (const msg of messages) {
+ const ts = msg._ts ? `${msg._ts}` : "";
+ const pin = msg.lat != null && msg.lon != null ? `` : "";
+ const name = `${aisDisplayNameHtml(msg)}`;
+ const channel = aisChannelInfo(msg.channel);
+ const distance = aisDistanceText(msg);
+ const details = [
+ `MMSI ${escapeAisHtml(String(msg.mmsi))}`,
+ escapeAisHtml(channel.label),
+ msg.sog_knots != null ? `${msg.sog_knots.toFixed(1)} kn` : null,
+ msg.cog_deg != null ? `${msg.cog_deg.toFixed(1)}°` : null,
+ distance ? escapeAisHtml(distance) : null,
+ escapeAisHtml(aisAgeText(msg._tsMs))
+ ].filter(Boolean).join(" · ");
+ html += `${ts}${pin}${name}: ${details}
`;
+ }
+ aisBarOverlay.innerHTML = html;
+ aisBarOverlay.style.display = "flex";
+}
+aisWindow.updateAisBar = updateAisBar;
+aisWindow.clearAisBar = function() {
+ resetAisHistoryView();
+};
+function resetAisHistoryView() {
+ if (aisMessagesEl) aisMessagesEl.innerHTML = "";
+ aisMessageHistory = [];
+ updateAisBar();
+ renderAisHistory();
+ aisWindow.clearMapMarkersByType?.("ais");
+}
+function renderAisHistory() {
+ pruneAisMessageHistory();
+ if (!aisMessagesEl) {
updateAisSummary();
+ return;
}
- function addAisMessage(msg) {
- const tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
- msg._tsMs = tsMs;
- msg._ts = new Date(tsMs).toLocaleTimeString([], {
+ const fragment = document.createDocumentFragment();
+ for (const message of aisMessageHistory) {
+ fragment.appendChild(renderAisRow(message));
+ }
+ aisMessagesEl.replaceChildren(fragment);
+ updateAisSummary();
+}
+function addAisMessage(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"
+ });
+ aisMessageHistory.unshift(msg);
+ pruneAisMessageHistory();
+ scheduleAisBarUpdate();
+ scheduleAisHistoryRender();
+ if (msg.lat != null && msg.lon != null && aisWindow.aisMapAddVessel) {
+ aisWindow.aisMapAddVessel(msg);
+ }
+}
+function normalizeServerAisMessage(msg) {
+ return {
+ ...msg,
+ rig_id: msg.rig_id || null
+ };
+}
+function onServerAisBatch(messages) {
+ if (!Array.isArray(messages) || messages.length === 0) return;
+ if (aisStatus) aisStatus.textContent = "Receiving";
+ const normalized = [];
+ for (const msg of messages) {
+ const next = normalizeServerAisMessage(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"
});
- aisMessageHistory.unshift(msg);
- pruneAisMessageHistory();
- scheduleAisBarUpdate();
- scheduleAisHistoryRender();
- if (msg.lat != null && msg.lon != null && aisWindow.aisMapAddVessel) {
- aisWindow.aisMapAddVessel(msg);
+ if (next.lat != null && next.lon != null && aisWindow.aisMapAddVessel) {
+ aisWindow.aisMapAddVessel(next);
}
+ normalized.push(next);
}
- function normalizeServerAisMessage(msg) {
- return {
- ...msg,
- rig_id: msg.rig_id || null
- };
- }
- function onServerAisBatch(messages) {
- if (!Array.isArray(messages) || messages.length === 0) return;
- if (aisStatus) aisStatus.textContent = "Receiving";
- const normalized = [];
- for (const msg of messages) {
- const next = normalizeServerAisMessage(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 && aisWindow.aisMapAddVessel) {
- aisWindow.aisMapAddVessel(next);
- }
- normalized.push(next);
+ normalized.reverse();
+ aisMessageHistory = normalized.concat(aisMessageHistory);
+ pruneAisMessageHistory();
+ scheduleAisBarUpdate();
+ scheduleAisHistoryRender();
+}
+function pruneAisHistoryView() {
+ pruneAisMessageHistory();
+ updateAisBar();
+ renderAisHistory();
+}
+document.getElementById("settings-clear-ais-history")?.addEventListener("click", () => {
+ void (async () => {
+ if (!await aisWindow.trxUi.confirm({ title: "Clear AIS history?", message: "All stored AIS messages will be permanently removed.", confirmLabel: "Clear history" })) return;
+ try {
+ await aisWindow.postPath?.("/clear_ais_decode");
+ resetAisHistoryView();
+ } catch (e) {
+ console.error("AIS history clear failed", e);
}
- normalized.reverse();
- aisMessageHistory = normalized.concat(aisMessageHistory);
- pruneAisMessageHistory();
- scheduleAisBarUpdate();
- scheduleAisHistoryRender();
- }
- function pruneAisHistoryView() {
- pruneAisMessageHistory();
- updateAisBar();
+ })();
+});
+if (aisFilterInput) {
+ aisFilterInput.addEventListener("input", () => {
+ aisFilterText = aisFilterInput.value.trim().toUpperCase();
renderAisHistory();
- }
- document.getElementById("settings-clear-ais-history")?.addEventListener("click", () => {
- void (async () => {
- if (!await aisWindow.trxUi.confirm({ title: "Clear AIS history?", message: "All stored AIS messages will be permanently removed.", confirmLabel: "Clear history" })) return;
- try {
- await aisWindow.postPath?.("/clear_ais_decode");
- resetAisHistoryView();
- } catch (e) {
- console.error("AIS history clear failed", e);
- }
- })();
});
- if (aisFilterInput) {
- aisFilterInput.addEventListener("input", () => {
- aisFilterText = aisFilterInput.value.trim().toUpperCase();
- renderAisHistory();
- });
- }
- function onServerAis(msg) {
- if (aisStatus) aisStatus.textContent = "Receiving";
- addAisMessage(normalizeServerAisMessage(msg));
- }
- updateAisSummary();
- window.trxPluginRuntime.registerDecoder({
- id: "ais",
- onMessage: onServerAis,
- onBatch: onServerAisBatch,
- restore: onServerAisBatch,
- reset: resetAisHistoryView,
- prune: pruneAisHistoryView
- });
-})();
+}
+function onServerAis(msg) {
+ if (aisStatus) aisStatus.textContent = "Receiving";
+ addAisMessage(normalizeServerAisMessage(msg));
+}
+updateAisSummary();
+window.trxPluginRuntime.registerDecoder({
+ id: "ais",
+ onMessage: onServerAis,
+ onBatch: onServerAisBatch,
+ restore: onServerAisBatch,
+ reset: resetAisHistoryView,
+ prune: pruneAisHistoryView
+});
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js
index dbb05b92..f56e02b4 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/aprs.js
@@ -1,408 +1,314 @@
-"use strict";
-(() => {
- // src/plugins/aprs-shared.ts
- function aprsPacketCategory(packet) {
- const type = (packet.type ?? "").toLowerCase();
- const info = (packet.info ?? "").toLowerCase();
- if (packet.lat != null && packet.lon != null || type.includes("position")) return "position";
- if (type.includes("message") || info.startsWith(":")) return "message";
- if (type.includes("weather") || info.startsWith("_")) return "weather";
- if (type.includes("telemetry") || info.startsWith("t#")) return "telemetry";
- return "other";
- }
- function aprsCategoryLabel(category) {
- switch (category) {
- case "position":
- return "Position";
- case "message":
- return "Message";
- case "weather":
- return "Weather";
- case "telemetry":
- return "Telemetry";
- default:
- return "Other";
- }
- }
- function aprsAgeText(timestampMs) {
- if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return "just now";
- const seconds = Math.round(Math.max(0, Date.now() - timestampMs) / 1e3);
- if (seconds < 5) return "just now";
- if (seconds < 60) return `${String(seconds)}s ago`;
- const minutes = Math.round(seconds / 60);
- if (minutes < 60) return `${String(minutes)}m ago`;
- return `${String(Math.round(minutes / 60))}h ago`;
- }
- function aprsPacketSignature(packet) {
- return [
- packet.srcCall ?? "",
- packet.destCall ?? "",
- packet.path ?? "",
- packet.info ?? "",
- packet.type ?? "",
- packet.lat?.toFixed(4) ?? "",
- packet.lon?.toFixed(4) ?? ""
- ].join("|");
- }
- function collapseAprsDuplicates(packets) {
- const seen = /* @__PURE__ */ new Set();
- return packets.filter((packet) => {
- const signature = aprsPacketSignature(packet);
- if (seen.has(signature)) return false;
- seen.add(signature);
- return true;
- });
- }
- function aprsHexBytes(bytes) {
- if (!bytes?.length) return "--";
- return bytes.map((byte) => byte.toString(16).toUpperCase().padStart(2, "0")).join(" ");
- }
- function renderAprsInfo(packet) {
- if (packet.info_bytes?.length) {
- return packet.info_bytes.map((byte) => renderAprsByte(byte)).join("");
- }
- return Array.from(packet.info ?? "", (character) => renderAprsCharacter(character)).join("");
- }
- function renderAprsByte(byte) {
- return byte >= 32 && byte <= 126 ? escapeAprsCharacter(String.fromCharCode(byte)) : `0x${byte.toString(16).toUpperCase().padStart(2, "0")}`;
- }
- function renderAprsCharacter(character) {
- const code = character.charCodeAt(0);
- return code >= 32 && code <= 126 ? escapeAprsCharacter(character) : `0x${code.toString(16).toUpperCase().padStart(2, "0")}`;
- }
- function escapeAprsCharacter(character) {
- if (character === "<") return "<";
- if (character === ">") return ">";
- if (character === "&") return "&";
- if (character === '"') return """;
- return character;
- }
- function renderLocalAprsSymbol(packet, escapeHtml) {
- if (!packet.symbolTable || !packet.symbolCode) return "";
- const symbol = escapeHtml(packet.symbolCode);
- const table = packet.symbolTable === "/" ? "Primary" : "Alternate";
- return `${symbol}`;
- }
- function normalizeAprsPacket(packet, receiver) {
- return {
- rig_id: packet.rig_id || null,
- receiver,
- srcCall: packet.src_call ?? "",
- destCall: packet.dest_call ?? "",
- path: packet.path ?? "",
- info: packet.info ?? "",
- info_bytes: packet.info_bytes ?? [],
- type: packet.packet_type ?? "",
- crcOk: packet.crc_ok ?? false,
- ts_ms: packet.ts_ms ?? null,
- lat: packet.lat ?? null,
- lon: packet.lon ?? null,
- symbolTable: packet.symbol_table ?? null,
- symbolCode: packet.symbol_code ?? null
- };
- }
+import {
+ aprsAgeText,
+ aprsCategoryLabel,
+ aprsHexBytes,
+ aprsPacketCategory,
+ collapseAprsDuplicates,
+ normalizeAprsPacket,
+ renderAprsInfo,
+ renderLocalAprsSymbol
+} from "./chunk-M2I6DH4X.js";
- // src/plugins/aprs.ts
- var aprsWindow = window;
- var escapeAprsHtml = (input) => aprsWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
- var showAprsHint = (message, durationMs) => {
- aprsWindow.showHint?.(message, durationMs);
- };
- var aprsStatus = document.getElementById("aprs-status");
- var aprsPacketsEl = document.getElementById("aprs-packets");
- var aprsFilterInput = document.getElementById("aprs-filter");
- var aprsBarOverlay = document.getElementById("aprs-bar-overlay");
- var aprsOnlyPosBtn = document.getElementById("aprs-only-pos-btn");
- var aprsHideCrcBtn = document.getElementById("aprs-hide-crc-btn");
- var aprsCollapseDupBtn = document.getElementById("aprs-collapse-dup-btn");
- var aprsTotalCountEl = document.getElementById("aprs-total-count");
- var aprsVisibleCountEl = document.getElementById("aprs-visible-count");
- var aprsLatestSeenEl = document.getElementById("aprs-latest-seen");
- var APRS_BAR_WINDOW_MS = 15 * 60 * 1e3;
- var aprsFilterText = "";
- var aprsPacketHistory = [];
- var aprsBarDismissedAtMs = 0;
- var aprsOnlyPos = false;
- var aprsHideCrc = false;
- var aprsCollapseDup = false;
- var aprsTypeFilter = "all";
- function currentAprsHistoryRetentionMs() {
- return typeof aprsWindow.getDecodeHistoryRetentionMs === "function" ? aprsWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
+// src/plugins/aprs.ts
+var aprsWindow = window;
+var escapeAprsHtml = (input) => aprsWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
+var showAprsHint = (message, durationMs) => {
+ aprsWindow.showHint?.(message, durationMs);
+};
+var aprsStatus = document.getElementById("aprs-status");
+var aprsPacketsEl = document.getElementById("aprs-packets");
+var aprsFilterInput = document.getElementById("aprs-filter");
+var aprsBarOverlay = document.getElementById("aprs-bar-overlay");
+var aprsOnlyPosBtn = document.getElementById("aprs-only-pos-btn");
+var aprsHideCrcBtn = document.getElementById("aprs-hide-crc-btn");
+var aprsCollapseDupBtn = document.getElementById("aprs-collapse-dup-btn");
+var aprsTotalCountEl = document.getElementById("aprs-total-count");
+var aprsVisibleCountEl = document.getElementById("aprs-visible-count");
+var aprsLatestSeenEl = document.getElementById("aprs-latest-seen");
+var APRS_BAR_WINDOW_MS = 15 * 60 * 1e3;
+var aprsFilterText = "";
+var aprsPacketHistory = [];
+var aprsBarDismissedAtMs = 0;
+var aprsOnlyPos = false;
+var aprsHideCrc = false;
+var aprsCollapseDup = false;
+var aprsTypeFilter = "all";
+function currentAprsHistoryRetentionMs() {
+ return typeof aprsWindow.getDecodeHistoryRetentionMs === "function" ? aprsWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
+}
+function pruneAprsPacketHistory() {
+ const cutoffMs = Date.now() - currentAprsHistoryRetentionMs();
+ aprsPacketHistory = aprsPacketHistory.filter((pkt) => (pkt._tsMs ?? 0) >= cutoffMs);
+}
+function scheduleAprsUi(key, job) {
+ if (typeof aprsWindow.trxScheduleUiFrameJob === "function") {
+ aprsWindow.trxScheduleUiFrameJob(key, job);
+ return;
}
- function pruneAprsPacketHistory() {
- const cutoffMs = Date.now() - currentAprsHistoryRetentionMs();
- aprsPacketHistory = aprsPacketHistory.filter((pkt) => (pkt._tsMs ?? 0) >= cutoffMs);
+ job();
+}
+function scheduleAprsHistoryRender() {
+ scheduleAprsUi("aprs-history", () => {
+ renderAprsHistory();
+ });
+}
+function scheduleAprsBarUpdate() {
+ scheduleAprsUi("aprs-bar", () => {
+ updateAprsBar();
+ });
+}
+function aprsDistanceText(pkt) {
+ if (aprsWindow.serverLat == null || aprsWindow.serverLon == null || pkt.lat == null || pkt.lon == null || !aprsWindow.haversineKm) return "";
+ const distKm = aprsWindow.haversineKm(aprsWindow.serverLat, aprsWindow.serverLon, pkt.lat, pkt.lon);
+ if (!Number.isFinite(distKm)) return "";
+ if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`;
+ return `${distKm.toFixed(1)} km from TRX`;
+}
+function aprsFilterMatch(pkt) {
+ if (aprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false;
+ if (aprsHideCrc && !pkt.crcOk) return false;
+ if (aprsTypeFilter !== "all" && aprsPacketCategory(pkt) !== aprsTypeFilter) return false;
+ if (!aprsFilterText) return true;
+ const haystack = [
+ pkt.srcCall,
+ pkt.destCall,
+ pkt.path,
+ pkt.info,
+ pkt.type,
+ pkt.lat != null ? pkt.lat.toFixed(4) : "",
+ pkt.lon != null ? pkt.lon.toFixed(4) : "",
+ aprsPacketCategory(pkt)
+ ].filter(Boolean).join(" ").toUpperCase();
+ return haystack.includes(aprsFilterText);
+}
+function aprsVisiblePackets() {
+ const packets = aprsCollapseDup ? collapseAprsDuplicates(aprsPacketHistory) : aprsPacketHistory;
+ return packets.filter(aprsFilterMatch);
+}
+function updateAprsSummary() {
+ const visible = aprsVisiblePackets();
+ if (aprsTotalCountEl) {
+ aprsTotalCountEl.textContent = `${aprsPacketHistory.length} total`;
}
- function scheduleAprsUi(key, job) {
- if (typeof aprsWindow.trxScheduleUiFrameJob === "function") {
- aprsWindow.trxScheduleUiFrameJob(key, job);
- return;
+ if (aprsVisibleCountEl) {
+ aprsVisibleCountEl.textContent = `${visible.length} shown`;
+ }
+ if (aprsLatestSeenEl) {
+ const latest = aprsPacketHistory[0];
+ if (!latest) {
+ aprsLatestSeenEl.textContent = "No packets yet";
+ } else {
+ aprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`;
}
- job();
}
- function scheduleAprsHistoryRender() {
- scheduleAprsUi("aprs-history", () => {
- renderAprsHistory();
- });
- }
- function scheduleAprsBarUpdate() {
- scheduleAprsUi("aprs-bar", () => {
- updateAprsBar();
- });
- }
- function aprsDistanceText(pkt) {
- if (aprsWindow.serverLat == null || aprsWindow.serverLon == null || pkt.lat == null || pkt.lon == null || !aprsWindow.haversineKm) return "";
- const distKm = aprsWindow.haversineKm(aprsWindow.serverLat, aprsWindow.serverLon, pkt.lat, pkt.lon);
- if (!Number.isFinite(distKm)) return "";
- if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`;
- return `${distKm.toFixed(1)} km from TRX`;
- }
- function aprsFilterMatch(pkt) {
- if (aprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false;
- if (aprsHideCrc && !pkt.crcOk) return false;
- if (aprsTypeFilter !== "all" && aprsPacketCategory(pkt) !== aprsTypeFilter) return false;
- if (!aprsFilterText) return true;
- const haystack = [
- pkt.srcCall,
- pkt.destCall,
- pkt.path,
- pkt.info,
- pkt.type,
- pkt.lat != null ? pkt.lat.toFixed(4) : "",
- pkt.lon != null ? pkt.lon.toFixed(4) : "",
- aprsPacketCategory(pkt)
- ].filter(Boolean).join(" ").toUpperCase();
- return haystack.includes(aprsFilterText);
- }
- function aprsVisiblePackets() {
- const packets = aprsCollapseDup ? collapseAprsDuplicates(aprsPacketHistory) : aprsPacketHistory;
- return packets.filter(aprsFilterMatch);
- }
- function updateAprsSummary() {
- const visible = aprsVisiblePackets();
- if (aprsTotalCountEl) {
- aprsTotalCountEl.textContent = `${aprsPacketHistory.length} total`;
- }
- if (aprsVisibleCountEl) {
- aprsVisibleCountEl.textContent = `${visible.length} shown`;
- }
- if (aprsLatestSeenEl) {
- const latest = aprsPacketHistory[0];
- if (!latest) {
- aprsLatestSeenEl.textContent = "No packets yet";
- } else {
- aprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`;
+}
+function updateAprsChipState() {
+ document.querySelectorAll("[id^='aprs-type-']").forEach((btn) => {
+ btn.classList.toggle("active", btn.id === `aprs-type-${aprsTypeFilter}`);
+ });
+ aprsOnlyPosBtn?.classList.toggle("active", aprsOnlyPos);
+ aprsHideCrcBtn?.classList.toggle("active", aprsHideCrc);
+ aprsCollapseDupBtn?.classList.toggle("active", aprsCollapseDup);
+}
+function renderAprsRow(pkt, isFresh) {
+ const row = document.createElement("div");
+ row.className = "aprs-packet";
+ if (!pkt.crcOk) row.classList.add("aprs-packet-crc");
+ if (isFresh) row.classList.add("aprs-packet-new");
+ const ts = pkt._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
+ const age = aprsAgeText(pkt._tsMs);
+ const category = aprsPacketCategory(pkt);
+ const categoryLabel = aprsCategoryLabel(category);
+ const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`;
+ const pathBadge = pkt.path ? `${escapeAprsHtml(pkt.path)}` : "";
+ const crcBadge = pkt.crcOk ? "" : 'CRC Fail';
+ const symbolHtml = renderLocalAprsSymbol(pkt, escapeAprsHtml);
+ const posLink = pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}` : "";
+ const distance = aprsDistanceText(pkt);
+ const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;
+ row.innerHTML = `${ts}` + symbolHtml + `${escapeAprsHtml(pkt.srcCall ?? "")}>${escapeAprsHtml(pkt.destCall || "")}${escapeAprsHtml(categoryLabel)}` + pathBadge + crcBadge + `
${escapeAprsHtml(age)}` + (distance ? `${escapeAprsHtml(distance)}` : "") + `${escapeAprsHtml(pkt.type || "--")}
${renderAprsInfo(pkt)}` + (posLink ? `${posLink}` : "") + `
` + (pkt.lat != null && pkt.lon != null ? `
` : "") + (pkt.lat != null && pkt.lon != null ? `
` : "") + `
QRZDetails
Source${escapeAprsHtml(pkt.srcCall || "--")}Destination${escapeAprsHtml(pkt.destCall || "--")}Type${escapeAprsHtml(pkt.type || "--")}Path${escapeAprsHtml(pkt.path || "--")}Age${escapeAprsHtml(age)}CRC${pkt.crcOk ? "OK" : "Failed"}Position${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}Info${escapeAprsHtml(pkt.info || "--")}Info Bytes${escapeAprsHtml(aprsHexBytes(pkt.info_bytes))}
`;
+ row.querySelectorAll("[data-aprs-map]").forEach((el) => {
+ el.addEventListener("click", (evt) => {
+ evt.preventDefault();
+ const raw = el.dataset.aprsMap ?? "";
+ const [lat, lon] = raw.split(",").map(Number);
+ if (aprsWindow.navigateToAprsMap && typeof lat === "number" && typeof lon === "number" && Number.isFinite(lat) && Number.isFinite(lon)) {
+ aprsWindow.navigateToAprsMap(lat, lon);
}
- }
- }
- function updateAprsChipState() {
- document.querySelectorAll("[id^='aprs-type-']").forEach((btn) => {
- btn.classList.toggle("active", btn.id === `aprs-type-${aprsTypeFilter}`);
});
- aprsOnlyPosBtn?.classList.toggle("active", aprsOnlyPos);
- aprsHideCrcBtn?.classList.toggle("active", aprsHideCrc);
- aprsCollapseDupBtn?.classList.toggle("active", aprsCollapseDup);
- }
- function renderAprsRow(pkt, isFresh) {
- const row = document.createElement("div");
- row.className = "aprs-packet";
- if (!pkt.crcOk) row.classList.add("aprs-packet-crc");
- if (isFresh) row.classList.add("aprs-packet-new");
- const ts = pkt._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
- const age = aprsAgeText(pkt._tsMs);
- const category = aprsPacketCategory(pkt);
- const categoryLabel = aprsCategoryLabel(category);
- const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`;
- const pathBadge = pkt.path ? `${escapeAprsHtml(pkt.path)}` : "";
- const crcBadge = pkt.crcOk ? "" : 'CRC Fail';
- const symbolHtml = renderLocalAprsSymbol(pkt, escapeAprsHtml);
- const posLink = pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}` : "";
- const distance = aprsDistanceText(pkt);
- const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;
- row.innerHTML = `${ts}` + symbolHtml + `${escapeAprsHtml(pkt.srcCall ?? "")}>${escapeAprsHtml(pkt.destCall || "")}${escapeAprsHtml(categoryLabel)}` + pathBadge + crcBadge + `
${escapeAprsHtml(age)}` + (distance ? `${escapeAprsHtml(distance)}` : "") + `${escapeAprsHtml(pkt.type || "--")}
${renderAprsInfo(pkt)}` + (posLink ? `${posLink}` : "") + `
` + (pkt.lat != null && pkt.lon != null ? `
` : "") + (pkt.lat != null && pkt.lon != null ? `
` : "") + `
QRZDetails
Source${escapeAprsHtml(pkt.srcCall || "--")}Destination${escapeAprsHtml(pkt.destCall || "--")}Type${escapeAprsHtml(pkt.type || "--")}Path${escapeAprsHtml(pkt.path || "--")}Age${escapeAprsHtml(age)}CRC${pkt.crcOk ? "OK" : "Failed"}Position${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}Info${escapeAprsHtml(pkt.info || "--")}Info Bytes${escapeAprsHtml(aprsHexBytes(pkt.info_bytes))}
`;
- row.querySelectorAll("[data-aprs-map]").forEach((el) => {
- el.addEventListener("click", (evt) => {
- evt.preventDefault();
- const raw = el.dataset.aprsMap ?? "";
- const [lat, lon] = raw.split(",").map(Number);
- if (aprsWindow.navigateToAprsMap && typeof lat === "number" && typeof lon === "number" && Number.isFinite(lat) && Number.isFinite(lon)) {
- aprsWindow.navigateToAprsMap(lat, lon);
- }
- });
- });
- const copyBtn = row.querySelector("[data-aprs-copy]");
- if (copyBtn) {
- copyBtn.addEventListener("click", () => {
- void (async () => {
- const raw = copyBtn.dataset.aprsCopy ?? "";
- try {
- const clipboard = Reflect.get(navigator, "clipboard");
- if (clipboard) {
- await clipboard.writeText(raw);
- showAprsHint("Coordinates copied", 1200);
- }
- } catch {
- showAprsHint("Copy failed", 1500);
+ });
+ const copyBtn = row.querySelector("[data-aprs-copy]");
+ if (copyBtn) {
+ copyBtn.addEventListener("click", () => {
+ void (async () => {
+ const raw = copyBtn.dataset.aprsCopy ?? "";
+ try {
+ const clipboard = Reflect.get(navigator, "clipboard");
+ if (clipboard) {
+ await clipboard.writeText(raw);
+ showAprsHint("Coordinates copied", 1200);
}
- })();
- });
- }
- return row;
+ } catch {
+ showAprsHint("Copy failed", 1500);
+ }
+ })();
+ });
}
- function renderAprsHistory() {
- pruneAprsPacketHistory();
- if (!aprsPacketsEl) {
- updateAprsSummary();
- updateAprsChipState();
- return;
- }
- const visible = aprsVisiblePackets();
- const fragment = document.createDocumentFragment();
- for (const [index, packet] of visible.entries()) {
- fragment.appendChild(renderAprsRow(packet, index === 0));
- }
- aprsPacketsEl.replaceChildren(fragment);
+ return row;
+}
+function renderAprsHistory() {
+ pruneAprsPacketHistory();
+ if (!aprsPacketsEl) {
updateAprsSummary();
updateAprsChipState();
+ return;
}
- function updateAprsBar() {
- if (!aprsBarOverlay) return;
- const isPkt = (document.getElementById("mode")?.value || "").toUpperCase() === "PKT";
- const cutoffMs = Date.now() - APRS_BAR_WINDOW_MS;
- const okFrames = aprsPacketHistory.filter((p) => p.crcOk && (p._tsMs ?? 0) >= cutoffMs);
- const frames = collapseAprsDuplicates(okFrames).slice(0, 8);
- const newestTsMs = frames.reduce((latest, pkt) => Math.max(latest, Number(pkt._tsMs) || 0), 0);
- if (!isPkt || frames.length === 0 || newestTsMs <= aprsBarDismissedAtMs) {
- aprsBarOverlay.style.display = "none";
- aprsBarOverlay.innerHTML = "";
- return;
- }
- let html = ``;
- for (const pkt of frames) {
- const ts = pkt._ts ? `${pkt._ts}` : "";
- const call = `${escapeAprsHtml(pkt.srcCall ?? "")}`;
- const dest = escapeAprsHtml(pkt.destCall || "");
- const info = escapeAprsHtml(pkt.info || "");
- const pin = pkt.lat != null && pkt.lon != null ? `` : "";
- html += `${ts}${pin}${call}>${dest}: ${info}
`;
- }
- aprsBarOverlay.innerHTML = html;
- aprsBarOverlay.style.display = "flex";
+ const visible = aprsVisiblePackets();
+ const fragment = document.createDocumentFragment();
+ for (const [index, packet] of visible.entries()) {
+ fragment.appendChild(renderAprsRow(packet, index === 0));
}
- aprsWindow.updateAprsBar = updateAprsBar;
- aprsWindow.clearAprsBar = function() {
- resetAprsHistoryView();
- };
- aprsWindow.closeAprsBar = function() {
- aprsBarDismissedAtMs = Date.now();
- if (aprsBarOverlay) {
- aprsBarOverlay.style.display = "none";
- aprsBarOverlay.innerHTML = "";
- }
- };
- function resetAprsHistoryView() {
- if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
- aprsPacketHistory = [];
- updateAprsBar();
- renderAprsHistory();
- aprsWindow.clearMapMarkersByType?.("aprs");
+ aprsPacketsEl.replaceChildren(fragment);
+ updateAprsSummary();
+ updateAprsChipState();
+}
+function updateAprsBar() {
+ if (!aprsBarOverlay) return;
+ const isPkt = (document.getElementById("mode")?.value || "").toUpperCase() === "PKT";
+ const cutoffMs = Date.now() - APRS_BAR_WINDOW_MS;
+ const okFrames = aprsPacketHistory.filter((p) => p.crcOk && (p._tsMs ?? 0) >= cutoffMs);
+ const frames = collapseAprsDuplicates(okFrames).slice(0, 8);
+ const newestTsMs = frames.reduce((latest, pkt) => Math.max(latest, Number(pkt._tsMs) || 0), 0);
+ if (!isPkt || frames.length === 0 || newestTsMs <= aprsBarDismissedAtMs) {
+ aprsBarOverlay.style.display = "none";
+ aprsBarOverlay.innerHTML = "";
+ return;
}
- function pruneAprsHistoryView() {
- pruneAprsPacketHistory();
- updateAprsBar();
- renderAprsHistory();
+ let html = ``;
+ for (const pkt of frames) {
+ const ts = pkt._ts ? `${pkt._ts}` : "";
+ const call = `${escapeAprsHtml(pkt.srcCall ?? "")}`;
+ const dest = escapeAprsHtml(pkt.destCall || "");
+ const info = escapeAprsHtml(pkt.info || "");
+ const pin = pkt.lat != null && pkt.lon != null ? `` : "";
+ html += `${ts}${pin}${call}>${dest}: ${info}
`;
}
- function addAprsPacket(pkt) {
- const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
- pkt._tsMs = tsMs;
- pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
- aprsPacketHistory.unshift(pkt);
- pruneAprsPacketHistory();
- if (pkt.lat != null && pkt.lon != null && aprsWindow.aprsMapAddStation) {
- aprsWindow.aprsMapAddStation(pkt.srcCall ?? "", pkt.lat, pkt.lon, pkt.info ?? "", pkt.symbolTable, pkt.symbolCode, pkt);
- }
- if (pkt.crcOk) scheduleAprsBarUpdate();
- scheduleAprsHistoryRender();
- }
- function normalizeServerAprsPacket(pkt) {
- return normalizeAprsPacket(pkt, aprsWindow.getDecodeRigMeta?.() ?? null);
- }
- function onServerAprsBatch(packets) {
- if (!Array.isArray(packets) || packets.length === 0) return;
- if (aprsStatus) aprsStatus.textContent = "Receiving";
- const normalized = [];
- let hasCrcOk = false;
- for (const pkt of packets) {
- const next = normalizeServerAprsPacket(pkt);
- 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 && aprsWindow.aprsMapAddStation) {
- aprsWindow.aprsMapAddStation(next.srcCall ?? "", next.lat, next.lon, next.info ?? "", next.symbolTable, next.symbolCode, next);
- }
- if (next.crcOk) hasCrcOk = true;
- normalized.push(next);
- }
- normalized.reverse();
- aprsPacketHistory = normalized.concat(aprsPacketHistory);
- pruneAprsPacketHistory();
- if (hasCrcOk) scheduleAprsBarUpdate();
- scheduleAprsHistoryRender();
- }
- document.getElementById("settings-clear-aprs-history")?.addEventListener("click", () => {
- void (async () => {
- if (!await aprsWindow.trxUi.confirm({ title: "Clear APRS history?", message: "All stored APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
- try {
- await aprsWindow.postPath?.("/clear_aprs_decode");
- resetAprsHistoryView();
- } catch (e) {
- console.error("APRS history clear failed", e);
- }
- })();
- });
- if (aprsOnlyPosBtn) {
- aprsOnlyPosBtn.addEventListener("click", () => {
- aprsOnlyPos = !aprsOnlyPos;
- renderAprsHistory();
- });
- }
- if (aprsHideCrcBtn) {
- aprsHideCrcBtn.addEventListener("click", () => {
- aprsHideCrc = !aprsHideCrc;
- renderAprsHistory();
- });
- }
- if (aprsCollapseDupBtn) {
- aprsCollapseDupBtn.addEventListener("click", () => {
- aprsCollapseDup = !aprsCollapseDup;
- renderAprsHistory();
- });
- }
- ["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => {
- const btn = document.getElementById(`aprs-type-${type}`);
- if (!btn) return;
- btn.addEventListener("click", () => {
- aprsTypeFilter = type;
- renderAprsHistory();
- });
- });
- if (aprsFilterInput) {
- aprsFilterInput.addEventListener("input", () => {
- aprsFilterText = aprsFilterInput.value.trim().toUpperCase();
- renderAprsHistory();
- });
- }
- function onServerAprs(pkt) {
- if (aprsStatus) aprsStatus.textContent = "Receiving";
- addAprsPacket(normalizeServerAprsPacket(pkt));
+ aprsBarOverlay.innerHTML = html;
+ aprsBarOverlay.style.display = "flex";
+}
+aprsWindow.updateAprsBar = updateAprsBar;
+aprsWindow.clearAprsBar = function() {
+ resetAprsHistoryView();
+};
+aprsWindow.closeAprsBar = function() {
+ aprsBarDismissedAtMs = Date.now();
+ if (aprsBarOverlay) {
+ aprsBarOverlay.style.display = "none";
+ aprsBarOverlay.innerHTML = "";
}
+};
+function resetAprsHistoryView() {
+ if (aprsPacketsEl) aprsPacketsEl.innerHTML = "";
+ aprsPacketHistory = [];
+ updateAprsBar();
renderAprsHistory();
- window.trxPluginRuntime.registerDecoder({
- id: "aprs",
- onMessage: onServerAprs,
- onBatch: onServerAprsBatch,
- restore: onServerAprsBatch,
- reset: resetAprsHistoryView,
- prune: pruneAprsHistoryView
+ aprsWindow.clearMapMarkersByType?.("aprs");
+}
+function pruneAprsHistoryView() {
+ pruneAprsPacketHistory();
+ updateAprsBar();
+ renderAprsHistory();
+}
+function addAprsPacket(pkt) {
+ const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
+ pkt._tsMs = tsMs;
+ pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
+ aprsPacketHistory.unshift(pkt);
+ pruneAprsPacketHistory();
+ if (pkt.lat != null && pkt.lon != null && aprsWindow.aprsMapAddStation) {
+ aprsWindow.aprsMapAddStation(pkt.srcCall ?? "", pkt.lat, pkt.lon, pkt.info ?? "", pkt.symbolTable, pkt.symbolCode, pkt);
+ }
+ if (pkt.crcOk) scheduleAprsBarUpdate();
+ scheduleAprsHistoryRender();
+}
+function normalizeServerAprsPacket(pkt) {
+ return normalizeAprsPacket(pkt, aprsWindow.getDecodeRigMeta?.() ?? null);
+}
+function onServerAprsBatch(packets) {
+ if (!Array.isArray(packets) || packets.length === 0) return;
+ if (aprsStatus) aprsStatus.textContent = "Receiving";
+ const normalized = [];
+ let hasCrcOk = false;
+ for (const pkt of packets) {
+ const next = normalizeServerAprsPacket(pkt);
+ 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 && aprsWindow.aprsMapAddStation) {
+ aprsWindow.aprsMapAddStation(next.srcCall ?? "", next.lat, next.lon, next.info ?? "", next.symbolTable, next.symbolCode, next);
+ }
+ if (next.crcOk) hasCrcOk = true;
+ normalized.push(next);
+ }
+ normalized.reverse();
+ aprsPacketHistory = normalized.concat(aprsPacketHistory);
+ pruneAprsPacketHistory();
+ if (hasCrcOk) scheduleAprsBarUpdate();
+ scheduleAprsHistoryRender();
+}
+document.getElementById("settings-clear-aprs-history")?.addEventListener("click", () => {
+ void (async () => {
+ if (!await aprsWindow.trxUi.confirm({ title: "Clear APRS history?", message: "All stored APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
+ try {
+ await aprsWindow.postPath?.("/clear_aprs_decode");
+ resetAprsHistoryView();
+ } catch (e) {
+ console.error("APRS history clear failed", e);
+ }
+ })();
+});
+if (aprsOnlyPosBtn) {
+ aprsOnlyPosBtn.addEventListener("click", () => {
+ aprsOnlyPos = !aprsOnlyPos;
+ renderAprsHistory();
});
-})();
+}
+if (aprsHideCrcBtn) {
+ aprsHideCrcBtn.addEventListener("click", () => {
+ aprsHideCrc = !aprsHideCrc;
+ renderAprsHistory();
+ });
+}
+if (aprsCollapseDupBtn) {
+ aprsCollapseDupBtn.addEventListener("click", () => {
+ aprsCollapseDup = !aprsCollapseDup;
+ renderAprsHistory();
+ });
+}
+["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => {
+ const btn = document.getElementById(`aprs-type-${type}`);
+ if (!btn) return;
+ btn.addEventListener("click", () => {
+ aprsTypeFilter = type;
+ renderAprsHistory();
+ });
+});
+if (aprsFilterInput) {
+ aprsFilterInput.addEventListener("input", () => {
+ aprsFilterText = aprsFilterInput.value.trim().toUpperCase();
+ renderAprsHistory();
+ });
+}
+function onServerAprs(pkt) {
+ if (aprsStatus) aprsStatus.textContent = "Receiving";
+ addAprsPacket(normalizeServerAprsPacket(pkt));
+}
+renderAprsHistory();
+window.trxPluginRuntime.registerDecoder({
+ id: "aprs",
+ onMessage: onServerAprs,
+ onBatch: onServerAprsBatch,
+ restore: onServerAprsBatch,
+ reset: resetAprsHistoryView,
+ prune: pruneAprsHistoryView
+});
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/background-decode.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/background-decode.js
index a425cb37..289fad86 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/background-decode.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/background-decode.js
@@ -1,365 +1,362 @@
-"use strict";
-(() => {
- // src/plugins/background-decode.ts
- var bgdWindow = window;
- (function() {
- "use strict";
- function bgdSupportedIds() {
- return (bgdWindow.decoderRegistry || []).filter(function(d) {
- return d.background_decode;
- }).map(function(d) {
- return d.id;
+// src/plugins/background-decode.ts
+var bgdWindow = window;
+(function() {
+ "use strict";
+ function bgdSupportedIds() {
+ return (bgdWindow.decoderRegistry || []).filter(function(d) {
+ return d.background_decode;
+ }).map(function(d) {
+ return d.id;
+ });
+ }
+ let backgroundDecodeRole = null;
+ let currentRigId = null;
+ let currentConfig = null;
+ let bookmarkList = [];
+ let statusInterval = null;
+ let bgdDirty = false;
+ function initBackgroundDecode(rigId, role) {
+ backgroundDecodeRole = role;
+ currentRigId = rigId || null;
+ if (currentRigId) loadBackgroundDecode();
+ startStatusPolling();
+ }
+ function setBackgroundDecodeRig(rigId) {
+ const nextRigId = rigId || null;
+ if (nextRigId === currentRigId) return;
+ currentRigId = nextRigId;
+ if (!currentRigId) return;
+ loadBackgroundDecode();
+ }
+ function apiGetConfig(rigId) {
+ return fetch("/background-decode/" + encodeURIComponent(rigId)).then(function(r) {
+ if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
+ return r.json();
+ });
+ }
+ function apiPutConfig(rigId, config) {
+ return fetch("/background-decode/" + encodeURIComponent(rigId), {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(config)
+ }).then(function(r) {
+ if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
+ return r.json();
+ });
+ }
+ function apiResetConfig(rigId) {
+ return fetch("/background-decode/" + encodeURIComponent(rigId), {
+ method: "DELETE"
+ }).then(function(r) {
+ if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
+ return r.json();
+ });
+ }
+ function apiGetStatus(rigId) {
+ return fetch("/background-decode/" + encodeURIComponent(rigId) + "/status").then(function(r) {
+ if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
+ return r.json();
+ });
+ }
+ function apiGetBookmarks() {
+ return fetch("/bookmarks").then(function(r) {
+ if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
+ return r.json();
+ });
+ }
+ function loadBackgroundDecode() {
+ const rigId = currentRigId;
+ if (!rigId) return;
+ Promise.all([apiGetConfig(rigId), apiGetBookmarks()]).then(function([config, bookmarks]) {
+ currentConfig = config;
+ bookmarkList = Array.isArray(bookmarks) ? bookmarks : [];
+ renderBackgroundDecode();
+ clearBgdDirty();
+ pollBackgroundDecodeStatus();
+ }).catch(function(err) {
+ console.error("background decode load failed", err);
+ });
+ }
+ function supportedBookmarks() {
+ return bookmarkList.filter(function(bookmark) {
+ return bookmarkDecoderKinds(bookmark).length > 0;
+ });
+ }
+ function bookmarkDecoderKinds(bookmark) {
+ const ids = bgdSupportedIds();
+ const decoders = bookmark.decoders ?? [];
+ const explicit = decoders.map(function(item) {
+ return item.trim().toLowerCase();
+ }).filter(function(item, index, arr) {
+ return ids.indexOf(item) >= 0 && arr.indexOf(item) === index;
+ });
+ if (explicit.length > 0) return explicit;
+ const mode = bookmark.mode.trim().toUpperCase();
+ return (bgdWindow.decoderRegistry || []).filter(function(d) {
+ return d.activation === "mode_bound" && d.background_decode && d.active_modes.indexOf(mode) >= 0;
+ }).map(function(d) {
+ return d.id;
+ });
+ }
+ function renderBackgroundDecode() {
+ if (!currentConfig) {
+ currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
+ }
+ setCheckbox("background-decode-enabled", currentConfig.enabled);
+ renderBookmarkChecklist();
+ const isControl = backgroundDecodeRole === "control" || bgdWindow.authEnabled === false;
+ const panel = document.getElementById("background-decode-panel");
+ if (panel) {
+ panel.querySelectorAll("input, select, button.sch-write").forEach(function(el) {
+ el.disabled = !isControl;
});
}
- let backgroundDecodeRole = null;
- let currentRigId = null;
- let currentConfig = null;
- let bookmarkList = [];
- let statusInterval = null;
- let bgdDirty = false;
- function initBackgroundDecode(rigId, role) {
- backgroundDecodeRole = role;
- currentRigId = rigId || null;
- if (currentRigId) loadBackgroundDecode();
- startStatusPolling();
+ const saveBtn = document.getElementById("background-decode-save-btn");
+ const resetBtn = document.getElementById("background-decode-reset-btn");
+ if (saveBtn) saveBtn.style.display = isControl ? "" : "none";
+ if (resetBtn) resetBtn.style.display = isControl ? "" : "none";
+ }
+ function renderBookmarkChecklist(filterText = "") {
+ const container = document.getElementById("bgd-bookmark-checklist");
+ if (!container) return;
+ container.innerHTML = "";
+ const selectedIds = new Set(
+ currentConfig && Array.isArray(currentConfig.bookmark_ids) ? currentConfig.bookmark_ids : []
+ );
+ const all = supportedBookmarks();
+ const filter = (filterText || "").trim().toLowerCase();
+ const filtered = filter ? all.filter(function(bm) {
+ const text = (bm.name + " " + formatFreq(bm.freq_hz) + " " + bm.mode).toLowerCase();
+ return text.indexOf(filter) >= 0;
+ }) : all;
+ if (filtered.length === 0) {
+ container.innerHTML = '' + (all.length === 0 ? "No supported bookmarks available." : "No bookmarks match filter.") + "
";
+ return;
}
- function setBackgroundDecodeRig(rigId) {
- const nextRigId = rigId || null;
- if (nextRigId === currentRigId) return;
- currentRigId = nextRigId;
- if (!currentRigId) return;
- loadBackgroundDecode();
+ filtered.forEach(function(bookmark) {
+ const row = document.createElement("label");
+ row.className = "bgd-checklist-row";
+ const decoders = bookmarkDecoderKinds(bookmark);
+ const checked = selectedIds.has(bookmark.id) ? " checked" : "";
+ row.innerHTML = '' + escHtml(bookmark.name) + '' + escHtml(formatFreq(bookmark.freq_hz) + " " + bookmark.mode + " · " + decoders.join("/").toUpperCase()) + "";
+ row.querySelector("input")?.addEventListener("change", function(e) {
+ onChecklistToggle(bookmark.id, e.currentTarget.checked);
+ });
+ container.appendChild(row);
+ });
+ }
+ function onChecklistToggle(bookmarkId, checked) {
+ if (!currentConfig) {
+ currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
}
- function apiGetConfig(rigId) {
- return fetch("/background-decode/" + encodeURIComponent(rigId)).then(function(r) {
- if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
- return r.json();
+ if (!Array.isArray(currentConfig.bookmark_ids)) currentConfig.bookmark_ids = [];
+ if (checked && !currentConfig.bookmark_ids.includes(bookmarkId)) {
+ currentConfig.bookmark_ids.push(bookmarkId);
+ } else if (!checked) {
+ currentConfig.bookmark_ids = currentConfig.bookmark_ids.filter(function(id) {
+ return id !== bookmarkId;
});
}
- function apiPutConfig(rigId, config) {
- return fetch("/background-decode/" + encodeURIComponent(rigId), {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(config)
- }).then(function(r) {
- if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
- return r.json();
+ markBgdDirty();
+ }
+ function saveBackgroundDecode() {
+ const rigId = currentRigId;
+ if (!rigId) return;
+ const payload = {
+ remote: rigId,
+ enabled: document.getElementById("background-decode-enabled")?.checked ?? false,
+ bookmark_ids: currentConfig?.bookmark_ids.slice() ?? []
+ };
+ const btn = document.getElementById("background-decode-save-btn");
+ if (btn) btn.disabled = true;
+ apiPutConfig(rigId, payload).then(function(saved) {
+ currentConfig = saved;
+ renderBackgroundDecode();
+ clearBgdDirty();
+ pollBackgroundDecodeStatus();
+ showToast("Background decode saved.", false);
+ }).catch(function(err) {
+ showToast(`Save failed: ${errorMessage(err)}`, true);
+ }).finally(function() {
+ if (btn) btn.disabled = false;
+ });
+ }
+ async function resetBackgroundDecode() {
+ const rigId = currentRigId;
+ if (!rigId) return;
+ if (!await bgdWindow.trxUi.confirm({ title: "Reset background decoding?", message: "The current background decode configuration will be permanently reset.", confirmLabel: "Reset" })) return;
+ apiResetConfig(rigId).then(function(saved) {
+ currentConfig = saved;
+ renderBackgroundDecode();
+ clearBgdDirty();
+ pollBackgroundDecodeStatus();
+ showToast("Background decode reset.", false);
+ }).catch(function(err) {
+ showToast(`Reset failed: ${errorMessage(err)}`, true);
+ });
+ }
+ function startStatusPolling() {
+ if (statusInterval) clearInterval(statusInterval);
+ statusInterval = setInterval(pollBackgroundDecodeStatus, 15e3);
+ }
+ function pollBackgroundDecodeStatus() {
+ const rigId = currentRigId;
+ if (!rigId) return;
+ apiGetStatus(rigId).then(renderStatus).catch(function() {
+ });
+ }
+ function renderStatus(status) {
+ const card = document.getElementById("background-decode-status-card");
+ if (!card) return;
+ const entries = status.entries ?? [];
+ if (!entries.length) {
+ card.textContent = "No background decode bookmarks configured.";
+ return;
+ }
+ const summary = [];
+ if (status.active_rig) {
+ if (typeof status.center_hz === "number" && Number.isFinite(status.center_hz)) summary.push("Center " + formatFreq(status.center_hz));
+ if (typeof status.sample_rate === "number" && Number.isFinite(status.sample_rate) && status.sample_rate > 0) summary.push("Span ±" + formatFreq(status.sample_rate / 2));
+ } else {
+ summary.push("This rig is not currently selected for audio.");
+ }
+ let html = summary.length ? '' + escHtml(summary.join(" · ")) + "
" : "";
+ html += '';
+ entries.forEach(function(entry) {
+ const name = entry.bookmark_name || entry.bookmark_id || "Unknown bookmark";
+ const parts = [];
+ if (typeof entry.freq_hz === "number" && Number.isFinite(entry.freq_hz)) parts.push(formatFreq(entry.freq_hz));
+ if (entry.mode) parts.push(entry.mode);
+ if (Array.isArray(entry.decoder_kinds) && entry.decoder_kinds.length) {
+ parts.push(entry.decoder_kinds.join("/").toUpperCase());
+ }
+ html += '
' + escHtml(name) + '
' + escHtml(parts.join(" · ")) + '
' + escHtml(prettyState(entry.state)) + "
";
+ });
+ html += "
";
+ card.innerHTML = html;
+ }
+ function prettyState(state) {
+ switch (state) {
+ case "active":
+ return "✓ Active";
+ case "out_of_span":
+ return "△ Out of span";
+ case "waiting_for_spectrum":
+ return "△ Waiting";
+ case "waiting_for_user":
+ return "△ No user";
+ case "missing_bookmark":
+ return "✗ Missing";
+ case "no_supported_decoders":
+ return "✗ Unsupported";
+ case "disabled":
+ return "△ Disabled";
+ case "handled_by_scheduler":
+ return "△ Scheduler";
+ case "scheduler_has_control":
+ return "△ Scheduler";
+ case "handled_by_virtual_channel":
+ return "△ VChan";
+ default:
+ return "△ Inactive";
+ }
+ }
+ function setCheckbox(id, value) {
+ const el = document.getElementById(id);
+ if (el) el.checked = value;
+ }
+ function formatFreq(hz) {
+ if (!Number.isFinite(hz) || hz <= 0) return "--";
+ if (hz >= 1e6) return (hz / 1e6).toFixed(3).replace(/\.?0+$/, "") + " MHz";
+ if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + " kHz";
+ return `${String(hz)} Hz`;
+ }
+ function escHtml(value) {
+ const text = typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : "";
+ return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+ }
+ function errorMessage(error) {
+ return error instanceof Error ? error.message : String(error);
+ }
+ function markBgdDirty() {
+ if (bgdDirty) return;
+ bgdDirty = true;
+ const btn = document.getElementById("background-decode-save-btn");
+ if (btn) btn.classList.add("sch-dirty");
+ }
+ function clearBgdDirty() {
+ bgdDirty = false;
+ const btn = document.getElementById("background-decode-save-btn");
+ if (btn) btn.classList.remove("sch-dirty");
+ }
+ function showToast(msg, isError) {
+ const el = document.getElementById("background-decode-toast");
+ if (!el) return;
+ el.textContent = msg;
+ el.style.background = isError ? "var(--color-error, #c00)" : "var(--accent-green)";
+ el.style.display = "block";
+ setTimeout(function() {
+ el.style.display = "none";
+ }, 3e3);
+ }
+ function selectAllBookmarks() {
+ if (!currentConfig) {
+ currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
+ }
+ const ids = supportedBookmarks().map(function(bm) {
+ return bm.id;
+ });
+ currentConfig.bookmark_ids = ids;
+ renderBookmarkChecklist(document.getElementById("bgd-bookmark-filter")?.value);
+ markBgdDirty();
+ }
+ function deselectAllBookmarks() {
+ if (!currentConfig) {
+ currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
+ }
+ currentConfig.bookmark_ids = [];
+ renderBookmarkChecklist(document.getElementById("bgd-bookmark-filter")?.value);
+ markBgdDirty();
+ }
+ function wireBackgroundDecodeEvents() {
+ const filterInput = document.getElementById("bgd-bookmark-filter");
+ if (filterInput && !filterInput._wired) {
+ filterInput._wired = true;
+ filterInput.addEventListener("input", function() {
+ renderBookmarkChecklist(filterInput.value);
});
}
- function apiResetConfig(rigId) {
- return fetch("/background-decode/" + encodeURIComponent(rigId), {
- method: "DELETE"
- }).then(function(r) {
- if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
- return r.json();
+ const enabledCb = document.getElementById("background-decode-enabled");
+ if (enabledCb && !enabledCb._wired) {
+ enabledCb._wired = true;
+ enabledCb.addEventListener("change", function() {
+ markBgdDirty();
});
}
- function apiGetStatus(rigId) {
- return fetch("/background-decode/" + encodeURIComponent(rigId) + "/status").then(function(r) {
- if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
- return r.json();
+ const selectAllBtn = document.getElementById("bgd-select-all-btn");
+ if (selectAllBtn && !selectAllBtn._wired) {
+ selectAllBtn._wired = true;
+ selectAllBtn.addEventListener("click", selectAllBookmarks);
+ }
+ const deselectAllBtn = document.getElementById("bgd-deselect-all-btn");
+ if (deselectAllBtn && !deselectAllBtn._wired) {
+ deselectAllBtn._wired = true;
+ deselectAllBtn.addEventListener("click", deselectAllBookmarks);
+ }
+ const saveBtn = document.getElementById("background-decode-save-btn");
+ if (saveBtn && !saveBtn._wired) {
+ saveBtn._wired = true;
+ saveBtn.addEventListener("click", saveBackgroundDecode);
+ }
+ const resetBtn = document.getElementById("background-decode-reset-btn");
+ if (resetBtn && !resetBtn._wired) {
+ resetBtn._wired = true;
+ resetBtn.addEventListener("click", () => {
+ void resetBackgroundDecode();
});
}
- function apiGetBookmarks() {
- return fetch("/bookmarks").then(function(r) {
- if (!r.ok) throw new Error(`HTTP ${String(r.status)}`);
- return r.json();
- });
- }
- function loadBackgroundDecode() {
- const rigId = currentRigId;
- if (!rigId) return;
- Promise.all([apiGetConfig(rigId), apiGetBookmarks()]).then(function([config, bookmarks]) {
- currentConfig = config;
- bookmarkList = Array.isArray(bookmarks) ? bookmarks : [];
- renderBackgroundDecode();
- clearBgdDirty();
- pollBackgroundDecodeStatus();
- }).catch(function(err) {
- console.error("background decode load failed", err);
- });
- }
- function supportedBookmarks() {
- return bookmarkList.filter(function(bookmark) {
- return bookmarkDecoderKinds(bookmark).length > 0;
- });
- }
- function bookmarkDecoderKinds(bookmark) {
- const ids = bgdSupportedIds();
- const decoders = bookmark.decoders ?? [];
- const explicit = decoders.map(function(item) {
- return item.trim().toLowerCase();
- }).filter(function(item, index, arr) {
- return ids.indexOf(item) >= 0 && arr.indexOf(item) === index;
- });
- if (explicit.length > 0) return explicit;
- const mode = bookmark.mode.trim().toUpperCase();
- return (bgdWindow.decoderRegistry || []).filter(function(d) {
- return d.activation === "mode_bound" && d.background_decode && d.active_modes.indexOf(mode) >= 0;
- }).map(function(d) {
- return d.id;
- });
- }
- function renderBackgroundDecode() {
- if (!currentConfig) {
- currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
- }
- setCheckbox("background-decode-enabled", currentConfig.enabled);
- renderBookmarkChecklist();
- const isControl = backgroundDecodeRole === "control" || bgdWindow.authEnabled === false;
- const panel = document.getElementById("background-decode-panel");
- if (panel) {
- panel.querySelectorAll("input, select, button.sch-write").forEach(function(el) {
- el.disabled = !isControl;
- });
- }
- const saveBtn = document.getElementById("background-decode-save-btn");
- const resetBtn = document.getElementById("background-decode-reset-btn");
- if (saveBtn) saveBtn.style.display = isControl ? "" : "none";
- if (resetBtn) resetBtn.style.display = isControl ? "" : "none";
- }
- function renderBookmarkChecklist(filterText = "") {
- const container = document.getElementById("bgd-bookmark-checklist");
- if (!container) return;
- container.innerHTML = "";
- const selectedIds = new Set(
- currentConfig && Array.isArray(currentConfig.bookmark_ids) ? currentConfig.bookmark_ids : []
- );
- const all = supportedBookmarks();
- const filter = (filterText || "").trim().toLowerCase();
- const filtered = filter ? all.filter(function(bm) {
- const text = (bm.name + " " + formatFreq(bm.freq_hz) + " " + bm.mode).toLowerCase();
- return text.indexOf(filter) >= 0;
- }) : all;
- if (filtered.length === 0) {
- container.innerHTML = '' + (all.length === 0 ? "No supported bookmarks available." : "No bookmarks match filter.") + "
";
- return;
- }
- filtered.forEach(function(bookmark) {
- const row = document.createElement("label");
- row.className = "bgd-checklist-row";
- const decoders = bookmarkDecoderKinds(bookmark);
- const checked = selectedIds.has(bookmark.id) ? " checked" : "";
- row.innerHTML = '' + escHtml(bookmark.name) + '' + escHtml(formatFreq(bookmark.freq_hz) + " " + bookmark.mode + " · " + decoders.join("/").toUpperCase()) + "";
- row.querySelector("input")?.addEventListener("change", function(e) {
- onChecklistToggle(bookmark.id, e.currentTarget.checked);
- });
- container.appendChild(row);
- });
- }
- function onChecklistToggle(bookmarkId, checked) {
- if (!currentConfig) {
- currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
- }
- if (!Array.isArray(currentConfig.bookmark_ids)) currentConfig.bookmark_ids = [];
- if (checked && !currentConfig.bookmark_ids.includes(bookmarkId)) {
- currentConfig.bookmark_ids.push(bookmarkId);
- } else if (!checked) {
- currentConfig.bookmark_ids = currentConfig.bookmark_ids.filter(function(id) {
- return id !== bookmarkId;
- });
- }
- markBgdDirty();
- }
- function saveBackgroundDecode() {
- const rigId = currentRigId;
- if (!rigId) return;
- const payload = {
- remote: rigId,
- enabled: document.getElementById("background-decode-enabled")?.checked ?? false,
- bookmark_ids: currentConfig?.bookmark_ids.slice() ?? []
- };
- const btn = document.getElementById("background-decode-save-btn");
- if (btn) btn.disabled = true;
- apiPutConfig(rigId, payload).then(function(saved) {
- currentConfig = saved;
- renderBackgroundDecode();
- clearBgdDirty();
- pollBackgroundDecodeStatus();
- showToast("Background decode saved.", false);
- }).catch(function(err) {
- showToast(`Save failed: ${errorMessage(err)}`, true);
- }).finally(function() {
- if (btn) btn.disabled = false;
- });
- }
- async function resetBackgroundDecode() {
- const rigId = currentRigId;
- if (!rigId) return;
- if (!await bgdWindow.trxUi.confirm({ title: "Reset background decoding?", message: "The current background decode configuration will be permanently reset.", confirmLabel: "Reset" })) return;
- apiResetConfig(rigId).then(function(saved) {
- currentConfig = saved;
- renderBackgroundDecode();
- clearBgdDirty();
- pollBackgroundDecodeStatus();
- showToast("Background decode reset.", false);
- }).catch(function(err) {
- showToast(`Reset failed: ${errorMessage(err)}`, true);
- });
- }
- function startStatusPolling() {
- if (statusInterval) clearInterval(statusInterval);
- statusInterval = setInterval(pollBackgroundDecodeStatus, 15e3);
- }
- function pollBackgroundDecodeStatus() {
- const rigId = currentRigId;
- if (!rigId) return;
- apiGetStatus(rigId).then(renderStatus).catch(function() {
- });
- }
- function renderStatus(status) {
- const card = document.getElementById("background-decode-status-card");
- if (!card) return;
- const entries = status.entries ?? [];
- if (!entries.length) {
- card.textContent = "No background decode bookmarks configured.";
- return;
- }
- const summary = [];
- if (status.active_rig) {
- if (typeof status.center_hz === "number" && Number.isFinite(status.center_hz)) summary.push("Center " + formatFreq(status.center_hz));
- if (typeof status.sample_rate === "number" && Number.isFinite(status.sample_rate) && status.sample_rate > 0) summary.push("Span ±" + formatFreq(status.sample_rate / 2));
- } else {
- summary.push("This rig is not currently selected for audio.");
- }
- let html = summary.length ? '' + escHtml(summary.join(" · ")) + "
" : "";
- html += '';
- entries.forEach(function(entry) {
- const name = entry.bookmark_name || entry.bookmark_id || "Unknown bookmark";
- const parts = [];
- if (typeof entry.freq_hz === "number" && Number.isFinite(entry.freq_hz)) parts.push(formatFreq(entry.freq_hz));
- if (entry.mode) parts.push(entry.mode);
- if (Array.isArray(entry.decoder_kinds) && entry.decoder_kinds.length) {
- parts.push(entry.decoder_kinds.join("/").toUpperCase());
- }
- html += '
' + escHtml(name) + '
' + escHtml(parts.join(" · ")) + '
' + escHtml(prettyState(entry.state)) + "
";
- });
- html += "
";
- card.innerHTML = html;
- }
- function prettyState(state) {
- switch (state) {
- case "active":
- return "✓ Active";
- case "out_of_span":
- return "△ Out of span";
- case "waiting_for_spectrum":
- return "△ Waiting";
- case "waiting_for_user":
- return "△ No user";
- case "missing_bookmark":
- return "✗ Missing";
- case "no_supported_decoders":
- return "✗ Unsupported";
- case "disabled":
- return "△ Disabled";
- case "handled_by_scheduler":
- return "△ Scheduler";
- case "scheduler_has_control":
- return "△ Scheduler";
- case "handled_by_virtual_channel":
- return "△ VChan";
- default:
- return "△ Inactive";
- }
- }
- function setCheckbox(id, value) {
- const el = document.getElementById(id);
- if (el) el.checked = value;
- }
- function formatFreq(hz) {
- if (!Number.isFinite(hz) || hz <= 0) return "--";
- if (hz >= 1e6) return (hz / 1e6).toFixed(3).replace(/\.?0+$/, "") + " MHz";
- if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + " kHz";
- return `${String(hz)} Hz`;
- }
- function escHtml(value) {
- const text = typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : "";
- return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
- }
- function errorMessage(error) {
- return error instanceof Error ? error.message : String(error);
- }
- function markBgdDirty() {
- if (bgdDirty) return;
- bgdDirty = true;
- const btn = document.getElementById("background-decode-save-btn");
- if (btn) btn.classList.add("sch-dirty");
- }
- function clearBgdDirty() {
- bgdDirty = false;
- const btn = document.getElementById("background-decode-save-btn");
- if (btn) btn.classList.remove("sch-dirty");
- }
- function showToast(msg, isError) {
- const el = document.getElementById("background-decode-toast");
- if (!el) return;
- el.textContent = msg;
- el.style.background = isError ? "var(--color-error, #c00)" : "var(--accent-green)";
- el.style.display = "block";
- setTimeout(function() {
- el.style.display = "none";
- }, 3e3);
- }
- function selectAllBookmarks() {
- if (!currentConfig) {
- currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
- }
- const ids = supportedBookmarks().map(function(bm) {
- return bm.id;
- });
- currentConfig.bookmark_ids = ids;
- renderBookmarkChecklist(document.getElementById("bgd-bookmark-filter")?.value);
- markBgdDirty();
- }
- function deselectAllBookmarks() {
- if (!currentConfig) {
- currentConfig = { remote: currentRigId, enabled: false, bookmark_ids: [] };
- }
- currentConfig.bookmark_ids = [];
- renderBookmarkChecklist(document.getElementById("bgd-bookmark-filter")?.value);
- markBgdDirty();
- }
- function wireBackgroundDecodeEvents() {
- const filterInput = document.getElementById("bgd-bookmark-filter");
- if (filterInput && !filterInput._wired) {
- filterInput._wired = true;
- filterInput.addEventListener("input", function() {
- renderBookmarkChecklist(filterInput.value);
- });
- }
- const enabledCb = document.getElementById("background-decode-enabled");
- if (enabledCb && !enabledCb._wired) {
- enabledCb._wired = true;
- enabledCb.addEventListener("change", function() {
- markBgdDirty();
- });
- }
- const selectAllBtn = document.getElementById("bgd-select-all-btn");
- if (selectAllBtn && !selectAllBtn._wired) {
- selectAllBtn._wired = true;
- selectAllBtn.addEventListener("click", selectAllBookmarks);
- }
- const deselectAllBtn = document.getElementById("bgd-deselect-all-btn");
- if (deselectAllBtn && !deselectAllBtn._wired) {
- deselectAllBtn._wired = true;
- deselectAllBtn.addEventListener("click", deselectAllBookmarks);
- }
- const saveBtn = document.getElementById("background-decode-save-btn");
- if (saveBtn && !saveBtn._wired) {
- saveBtn._wired = true;
- saveBtn.addEventListener("click", saveBackgroundDecode);
- }
- const resetBtn = document.getElementById("background-decode-reset-btn");
- if (resetBtn && !resetBtn._wired) {
- resetBtn._wired = true;
- resetBtn.addEventListener("click", () => {
- void resetBackgroundDecode();
- });
- }
- }
- bgdWindow.initBackgroundDecode = initBackgroundDecode;
- bgdWindow.wireBackgroundDecodeEvents = wireBackgroundDecodeEvents;
- bgdWindow.setBackgroundDecodeRig = setBackgroundDecodeRig;
- })();
+ }
+ bgdWindow.initBackgroundDecode = initBackgroundDecode;
+ bgdWindow.wireBackgroundDecodeEvents = wireBackgroundDecodeEvents;
+ bgdWindow.setBackgroundDecodeRig = setBackgroundDecodeRig;
})();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/bookmarks.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/bookmarks.js
index d33c6ad0..6bcd316f 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/bookmarks.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/bookmarks.js
@@ -1,679 +1,676 @@
-"use strict";
-(() => {
- // src/plugins/bookmarks.ts
- var bridge = window;
- function bmEl(id) {
- const element = document.getElementById(id);
- if (!element) throw new Error(`Missing bookmark element #${id}`);
- return element;
+// src/plugins/bookmarks.ts
+var bridge = window;
+function bmEl(id) {
+ const element = document.getElementById(id);
+ if (!element) throw new Error(`Missing bookmark element #${id}`);
+ return element;
+}
+function errorMessage(error) {
+ return error instanceof Error ? error.message : String(error);
+}
+var bmScope = "general";
+function bmScopeParam(prefix, scope) {
+ const sep = prefix ? "&" : "?";
+ return sep + "scope=" + encodeURIComponent(scope != null ? scope : bmScope);
+}
+var bmList = [];
+var bmOverlayList = [];
+var bmOverlayRevision = 0;
+var bmFilteredList = [];
+var bmEditScope = null;
+var bmCurrentPage = 1;
+var BM_PAGE_SIZE = 25;
+var bmSelected = /* @__PURE__ */ new Set();
+function bmFmtFreq(hz) {
+ if (!Number.isFinite(hz) || hz <= 0) return "--";
+ if (hz >= 1e9) return (hz / 1e9).toFixed(6).replace(/\.?0+$/, "") + " GHz";
+ if (hz >= 1e6) return (hz / 1e6).toFixed(6).replace(/\.?0+$/, "") + " MHz";
+ if (hz >= 1e3) return (hz / 1e3).toFixed(3).replace(/\.?0+$/, "") + " kHz";
+ return `${hz} Hz`;
+}
+function bmEsc(str) {
+ const d = document.createElement("div");
+ d.appendChild(document.createTextNode(String(str)));
+ return d.innerHTML;
+}
+function bmCanControl() {
+ return typeof bridge.authEnabled !== "undefined" && !bridge.authEnabled || typeof bridge.authRole !== "undefined" && bridge.authRole === "control";
+}
+function bmSyncAccess() {
+ const canCtrl = bmCanControl();
+ const addBtn = bmEl("bm-add-btn");
+ const selectAllBtn = bmEl("bm-select-all-btn");
+ if (addBtn) addBtn.style.display = canCtrl ? "" : "none";
+ if (selectAllBtn) selectAllBtn.style.display = canCtrl ? "" : "none";
+}
+function bmListScope() {
+ const rig = typeof bridge.lastActiveRigId !== "undefined" ? bridge.lastActiveRigId : null;
+ return rig || "general";
+}
+async function bmFetchOverlay() {
+ const overlayScope = bmListScope();
+ try {
+ const resp = await fetch("/bookmarks" + bmScopeParam(false, overlayScope));
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ bmOverlayList = await resp.json();
+ } catch (e) {
+ console.error("Failed to fetch overlay bookmarks:", e);
+ bmOverlayList = [];
}
- function errorMessage(error) {
- return error instanceof Error ? error.message : String(error);
+ bmOverlayRevision++;
+ if (typeof bridge.syncBookmarkMapLocators === "function") {
+ bridge.syncBookmarkMapLocators(bmOverlayList);
}
- var bmScope = "general";
- function bmScopeParam(prefix, scope) {
- const sep = prefix ? "&" : "?";
- return sep + "scope=" + encodeURIComponent(scope != null ? scope : bmScope);
+ if (typeof bridge.scheduleSpectrumDraw === "function") bridge.scheduleSpectrumDraw();
+}
+async function bmFetch(categoryFilter) {
+ let url = "/bookmarks";
+ let hasQuery = false;
+ if (categoryFilter && categoryFilter !== "") {
+ url += "?category=" + encodeURIComponent(categoryFilter);
+ hasQuery = true;
}
- var bmList = [];
- var bmOverlayList = [];
- var bmOverlayRevision = 0;
- var bmFilteredList = [];
- var bmEditScope = null;
- var bmCurrentPage = 1;
- var BM_PAGE_SIZE = 25;
- var bmSelected = /* @__PURE__ */ new Set();
- function bmFmtFreq(hz) {
- if (!Number.isFinite(hz) || hz <= 0) return "--";
- if (hz >= 1e9) return (hz / 1e9).toFixed(6).replace(/\.?0+$/, "") + " GHz";
- if (hz >= 1e6) return (hz / 1e6).toFixed(6).replace(/\.?0+$/, "") + " MHz";
- if (hz >= 1e3) return (hz / 1e3).toFixed(3).replace(/\.?0+$/, "") + " kHz";
- return `${hz} Hz`;
+ url += bmScopeParam(hasQuery);
+ const overlayPromise = bmFetchOverlay();
+ try {
+ const resp = await fetch(url);
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ bmList = await resp.json();
+ } catch (e) {
+ console.error("Failed to fetch bookmarks:", e);
+ bmList = [];
}
- function bmEsc(str) {
- const d = document.createElement("div");
- d.appendChild(document.createTextNode(String(str)));
- return d.innerHTML;
- }
- function bmCanControl() {
- return typeof bridge.authEnabled !== "undefined" && !bridge.authEnabled || typeof bridge.authRole !== "undefined" && bridge.authRole === "control";
- }
- function bmSyncAccess() {
- const canCtrl = bmCanControl();
- const addBtn = bmEl("bm-add-btn");
- const selectAllBtn = bmEl("bm-select-all-btn");
- if (addBtn) addBtn.style.display = canCtrl ? "" : "none";
- if (selectAllBtn) selectAllBtn.style.display = canCtrl ? "" : "none";
- }
- function bmListScope() {
- const rig = typeof bridge.lastActiveRigId !== "undefined" ? bridge.lastActiveRigId : null;
- return rig || "general";
- }
- async function bmFetchOverlay() {
- const overlayScope = bmListScope();
- try {
- const resp = await fetch("/bookmarks" + bmScopeParam(false, overlayScope));
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
- bmOverlayList = await resp.json();
- } catch (e) {
- console.error("Failed to fetch overlay bookmarks:", e);
- bmOverlayList = [];
- }
- bmOverlayRevision++;
- if (typeof bridge.syncBookmarkMapLocators === "function") {
- bridge.syncBookmarkMapLocators(bmOverlayList);
- }
- if (typeof bridge.scheduleSpectrumDraw === "function") bridge.scheduleSpectrumDraw();
- }
- async function bmFetch(categoryFilter) {
- let url = "/bookmarks";
- let hasQuery = false;
- if (categoryFilter && categoryFilter !== "") {
- url += "?category=" + encodeURIComponent(categoryFilter);
- hasQuery = true;
- }
- url += bmScopeParam(hasQuery);
- const overlayPromise = bmFetchOverlay();
- try {
- const resp = await fetch(url);
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
- bmList = await resp.json();
- } catch (e) {
- console.error("Failed to fetch bookmarks:", e);
- bmList = [];
- }
- bmSelected.clear();
- bmUpdateSelectionUi();
- bmSyncAccess();
- bmApplyFilters();
- void bmRefreshCategoryFilter(categoryFilter);
- await overlayPromise;
- }
- function bmApplyFilters() {
- const text = (bmEl("bm-text-filter")?.value || "").trim().toLowerCase();
- const modeFilter = (bmEl("bm-mode-filter")?.value || "").trim().toUpperCase();
- let filtered = modeFilter ? bmList.filter((bm) => (bm.mode || "").toUpperCase() === modeFilter) : bmList;
- filtered = text ? filtered.filter(
- (bm) => (bm.name || "").toLowerCase().includes(text) || (bm.locator || "").toLowerCase().includes(text) || (bm.category || "").toLowerCase().includes(text) || (bm.comment || "").toLowerCase().includes(text)
- ) : filtered;
- bmFilteredList = filtered;
- bmCurrentPage = 1;
- bmRender(filtered);
- }
- async function bmRefreshCategoryFilter(keepValue) {
- const sel = bmEl("bm-category-filter");
- const modeSel = bmEl("bm-mode-filter");
- if (!sel && !modeSel) return;
- try {
- const resp = await fetch("/bookmarks" + bmScopeParam(false));
- if (!resp.ok) return;
- const all = await resp.json();
- if (sel) {
- const cats = [...new Set(all.map((b) => b.category || "").filter(Boolean))].sort();
- while (sel.options.length > 1) sel.remove(1);
- cats.forEach((cat) => {
- const opt = document.createElement("option");
- opt.value = cat;
- opt.textContent = cat;
- sel.add(opt);
- });
- if (keepValue && cats.includes(keepValue)) sel.value = keepValue;
- }
- if (modeSel) {
- const keepMode = modeSel.value;
- const modes = [...new Set(all.map((b) => (b.mode || "").trim().toUpperCase()).filter(Boolean))].sort();
- while (modeSel.options.length > 1) modeSel.remove(1);
- modes.forEach((mode) => {
- const opt = document.createElement("option");
- opt.value = mode;
- opt.textContent = mode;
- modeSel.add(opt);
- });
- if (keepMode && modes.includes(keepMode)) modeSel.value = keepMode;
- }
- } catch {
- }
- }
- function bmRender(list) {
- const tbody = bmEl("bm-tbody");
- const emptyEl = bmEl("bm-empty");
- const paginatorEl = bmEl("bm-paginator");
- const pageSummaryEl = bmEl("bm-page-summary");
- const pageIndicatorEl = bmEl("bm-page-indicator");
- const prevBtn = bmEl("bm-page-prev");
- const nextBtn = bmEl("bm-page-next");
- if (!tbody) return;
- tbody.innerHTML = "";
- if (list.length === 0) {
- if (emptyEl) emptyEl.style.display = "";
- if (paginatorEl) paginatorEl.style.display = "none";
- return;
- }
- if (emptyEl) emptyEl.style.display = "none";
- const canControl = bmCanControl();
- const totalPages = Math.max(1, Math.ceil(list.length / BM_PAGE_SIZE));
- const page = Math.min(Math.max(bmCurrentPage, 1), totalPages);
- bmCurrentPage = page;
- const startIndex = (page - 1) * BM_PAGE_SIZE;
- const endIndex = Math.min(startIndex + BM_PAGE_SIZE, list.length);
- const pageItems = list.slice(startIndex, endIndex);
- const showScope = bmScope !== "general";
- pageItems.forEach((bm) => {
- const tr = document.createElement("tr");
- tr.dataset.bmId = bm.id;
- const bwCell = bm.bandwidth_hz ? bmFmtFreq(bm.bandwidth_hz) : "--";
- const locatorCell = bm.locator || "--";
- const catCell = bm.category || "Uncategorised";
- const decoderCell = (bm.decoders || []).join(", ").toUpperCase() || "--";
- const commentCell = bm.comment || "";
- const checked = bmSelected.has(bm.id) ? " checked" : "";
- const scopeBadge = showScope && bm.scope === "general" ? ' G' : "";
- tr.innerHTML = ` | ${bmEsc(bm.name)}${scopeBadge} | ${bmFmtFreq(bm.freq_hz)} | ${bmEsc(bm.mode)} | ${bwCell} | ${bmEsc(locatorCell)} | ${bmEsc(catCell)} | ${bmEsc(decoderCell)} | ${bmEsc(commentCell)} | ` + (canControl ? `` : "") + ` | `;
- tbody.appendChild(tr);
- });
- bmSyncSelectAllCheckbox();
- if (paginatorEl) paginatorEl.style.display = totalPages > 1 ? "flex" : "";
- if (pageSummaryEl) pageSummaryEl.textContent = `Showing ${startIndex + 1}-${endIndex} of ${list.length}`;
- if (pageIndicatorEl) pageIndicatorEl.textContent = `Page ${page} of ${totalPages}`;
- if (prevBtn) prevBtn.disabled = page <= 1;
- if (nextBtn) nextBtn.disabled = page >= totalPages;
- }
- function bmChangePage(delta) {
- const totalPages = Math.max(1, Math.ceil(bmFilteredList.length / BM_PAGE_SIZE));
- const nextPage = Math.min(Math.max(bmCurrentPage + delta, 1), totalPages);
- if (nextPage === bmCurrentPage) return;
- bmCurrentPage = nextPage;
- bmRender(bmFilteredList);
- }
- function bmReadDecoders() {
- return (bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable).filter((d) => bmEl("bm-dec-" + d.id)?.checked).map((d) => d.id);
- }
- function bmWriteDecoders(decoders) {
- const set = new Set(decoders || []);
- (bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable).forEach((d) => {
- const el = bmEl("bm-dec-" + d.id);
- if (el) el.checked = set.has(d.id);
- });
- }
- function bmBuildDecoderCheckboxes() {
- const container = bmEl("bm-decoder-checkboxes");
- if (!container) return;
- container.innerHTML = "";
- (bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable).forEach((d) => {
- const label = document.createElement("label");
- label.className = "bm-decoder-check";
- label.innerHTML = ' ' + d.label;
- container.appendChild(label);
- });
- }
- function bmOpenForm(bm) {
- const wrap = bmEl("bm-form-wrap");
- if (!wrap) return;
- bmEditScope = bm ? bm.scope || bmScope : null;
- bmBuildDecoderCheckboxes();
- bmEl("bm-id").value = bm ? bm.id : "";
- bmEl("bm-name").value = bm ? bm.name : "";
- bmEl("bm-freq").value = bm ? String(bm.freq_hz) : "";
- bmEl("bm-mode").value = bm ? bm.mode : "";
- bmEl("bm-bw").value = bm?.bandwidth_hz ? String(bm.bandwidth_hz) : "";
- bmEl("bm-locator").value = bm ? bm.locator || "" : "";
- bmEl("bm-category-input").value = bm ? bm.category || "" : "";
- bmEl("bm-comment").value = bm ? bm.comment || "" : "";
- bmWriteDecoders(bm?.decoders ?? []);
- bmEl("bm-form-title").textContent = bm ? "Edit Bookmark" : "Add Bookmark";
- wrap.style.display = "flex";
- bmEl("bm-name").focus();
- }
- function bmCloseForm() {
- const wrap = bmEl("bm-form-wrap");
- if (wrap) wrap.style.display = "none";
- }
- function bmPrefillFromStatus() {
- if (typeof bridge.lastFreqHz === "number" && Number.isFinite(bridge.lastFreqHz)) {
- bmEl("bm-freq").value = String(Math.round(bridge.lastFreqHz));
- }
- if (typeof bridge.lastModeName === "string" && bridge.lastModeName) {
- bmEl("bm-mode").value = bridge.lastModeName;
- }
- if (typeof bridge.currentBandwidthHz === "number" && bridge.currentBandwidthHz > 0) {
- bmEl("bm-bw").value = String(Math.round(bridge.currentBandwidthHz));
- }
- const activeDecoders = (bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable && d.activation === "toggle").filter((d) => {
- const btn = bmEl(d.id + "-decode-toggle-btn");
- return btn && btn.dataset.enabled === "true";
- }).map((d) => d.id);
- bmWriteDecoders(activeDecoders);
- }
- async function bmSave(e) {
- e.preventDefault();
- const id = bmEl("bm-id").value;
- const name = bmEl("bm-name").value.trim();
- const freqStr = bmEl("bm-freq").value;
- const freq_hz = parseInt(freqStr, 10);
- const mode = bmEl("bm-mode").value.trim();
- const bwStr = bmEl("bm-bw").value;
- const bandwidth_hz = bwStr ? parseInt(bwStr, 10) : null;
- const locator = bmEl("bm-locator").value.trim().toUpperCase();
- const category = bmEl("bm-category-input").value.trim();
- const comment = bmEl("bm-comment").value.trim();
- const decoders = bmReadDecoders();
- const formError = bmEl("bm-form-error");
- if (formError) formError.textContent = "";
- if (!name || !Number.isFinite(freq_hz) || !mode) {
- if (formError) formError.textContent = "Enter a name, a valid frequency, and a mode.";
- const invalid = !name ? bmEl("bm-name") : !Number.isFinite(freq_hz) ? bmEl("bm-freq") : bmEl("bm-mode");
- invalid?.focus();
- return;
- }
- const body = {
- name,
- freq_hz,
- mode,
- bandwidth_hz,
- locator: locator || null,
- category,
- comment,
- decoders
- };
- try {
- let resp;
- if (id) {
- resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false, bmEditScope), {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(body)
- });
- } else {
- resp = await fetch("/bookmarks" + bmScopeParam(false), {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(body)
- });
- }
- if (!resp.ok) {
- const text = await resp.text();
- if (resp.status === 409) {
- throw new Error("A bookmark for that frequency already exists.");
- }
- throw new Error(text || `HTTP ${resp.status}`);
- }
- bmCloseForm();
- await bmFetch(bmEl("bm-category-filter").value);
- } catch (err) {
- console.error("Failed to save bookmark:", err);
- if (formError) formError.textContent = "Failed to save bookmark: " + errorMessage(err);
- bridge.trxUi.notify?.("Bookmark could not be saved", { kind: "error" });
- }
- }
- async function bmDelete(id) {
- if (!await bridge.trxUi.confirm({ title: "Delete bookmark?", message: "This bookmark will be permanently removed.", confirmLabel: "Delete" })) return;
- const bm = bmList.find((b) => b.id === id);
- const scope = bm ? bm.scope : void 0;
- try {
- const resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false, scope), {
- method: "DELETE"
+ bmSelected.clear();
+ bmUpdateSelectionUi();
+ bmSyncAccess();
+ bmApplyFilters();
+ void bmRefreshCategoryFilter(categoryFilter);
+ await overlayPromise;
+}
+function bmApplyFilters() {
+ const text = (bmEl("bm-text-filter")?.value || "").trim().toLowerCase();
+ const modeFilter = (bmEl("bm-mode-filter")?.value || "").trim().toUpperCase();
+ let filtered = modeFilter ? bmList.filter((bm) => (bm.mode || "").toUpperCase() === modeFilter) : bmList;
+ filtered = text ? filtered.filter(
+ (bm) => (bm.name || "").toLowerCase().includes(text) || (bm.locator || "").toLowerCase().includes(text) || (bm.category || "").toLowerCase().includes(text) || (bm.comment || "").toLowerCase().includes(text)
+ ) : filtered;
+ bmFilteredList = filtered;
+ bmCurrentPage = 1;
+ bmRender(filtered);
+}
+async function bmRefreshCategoryFilter(keepValue) {
+ const sel = bmEl("bm-category-filter");
+ const modeSel = bmEl("bm-mode-filter");
+ if (!sel && !modeSel) return;
+ try {
+ const resp = await fetch("/bookmarks" + bmScopeParam(false));
+ if (!resp.ok) return;
+ const all = await resp.json();
+ if (sel) {
+ const cats = [...new Set(all.map((b) => b.category || "").filter(Boolean))].sort();
+ while (sel.options.length > 1) sel.remove(1);
+ cats.forEach((cat) => {
+ const opt = document.createElement("option");
+ opt.value = cat;
+ opt.textContent = cat;
+ sel.add(opt);
});
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
- await bmFetch(bmEl("bm-category-filter").value);
- } catch (err) {
- console.error("Failed to delete bookmark:", err);
- bridge.trxUi.notify?.("Failed to delete bookmark: " + errorMessage(err), { kind: "error" });
+ if (keepValue && cats.includes(keepValue)) sel.value = keepValue;
}
+ if (modeSel) {
+ const keepMode = modeSel.value;
+ const modes = [...new Set(all.map((b) => (b.mode || "").trim().toUpperCase()).filter(Boolean))].sort();
+ while (modeSel.options.length > 1) modeSel.remove(1);
+ modes.forEach((mode) => {
+ const opt = document.createElement("option");
+ opt.value = mode;
+ opt.textContent = mode;
+ modeSel.add(opt);
+ });
+ if (keepMode && modes.includes(keepMode)) modeSel.value = keepMode;
+ }
+ } catch {
}
- function bmApply(bm) {
- try {
- if (typeof bridge.modeEl !== "undefined" && bridge.modeEl) {
- bridge.modeEl.value = (bm.mode || "").toUpperCase();
+}
+function bmRender(list) {
+ const tbody = bmEl("bm-tbody");
+ const emptyEl = bmEl("bm-empty");
+ const paginatorEl = bmEl("bm-paginator");
+ const pageSummaryEl = bmEl("bm-page-summary");
+ const pageIndicatorEl = bmEl("bm-page-indicator");
+ const prevBtn = bmEl("bm-page-prev");
+ const nextBtn = bmEl("bm-page-next");
+ if (!tbody) return;
+ tbody.innerHTML = "";
+ if (list.length === 0) {
+ if (emptyEl) emptyEl.style.display = "";
+ if (paginatorEl) paginatorEl.style.display = "none";
+ return;
+ }
+ if (emptyEl) emptyEl.style.display = "none";
+ const canControl = bmCanControl();
+ const totalPages = Math.max(1, Math.ceil(list.length / BM_PAGE_SIZE));
+ const page = Math.min(Math.max(bmCurrentPage, 1), totalPages);
+ bmCurrentPage = page;
+ const startIndex = (page - 1) * BM_PAGE_SIZE;
+ const endIndex = Math.min(startIndex + BM_PAGE_SIZE, list.length);
+ const pageItems = list.slice(startIndex, endIndex);
+ const showScope = bmScope !== "general";
+ pageItems.forEach((bm) => {
+ const tr = document.createElement("tr");
+ tr.dataset.bmId = bm.id;
+ const bwCell = bm.bandwidth_hz ? bmFmtFreq(bm.bandwidth_hz) : "--";
+ const locatorCell = bm.locator || "--";
+ const catCell = bm.category || "Uncategorised";
+ const decoderCell = (bm.decoders || []).join(", ").toUpperCase() || "--";
+ const commentCell = bm.comment || "";
+ const checked = bmSelected.has(bm.id) ? " checked" : "";
+ const scopeBadge = showScope && bm.scope === "general" ? ' G' : "";
+ tr.innerHTML = ` | ${bmEsc(bm.name)}${scopeBadge} | ${bmFmtFreq(bm.freq_hz)} | ${bmEsc(bm.mode)} | ${bwCell} | ${bmEsc(locatorCell)} | ${bmEsc(catCell)} | ${bmEsc(decoderCell)} | ${bmEsc(commentCell)} | ` + (canControl ? `` : "") + ` | `;
+ tbody.appendChild(tr);
+ });
+ bmSyncSelectAllCheckbox();
+ if (paginatorEl) paginatorEl.style.display = totalPages > 1 ? "flex" : "";
+ if (pageSummaryEl) pageSummaryEl.textContent = `Showing ${startIndex + 1}-${endIndex} of ${list.length}`;
+ if (pageIndicatorEl) pageIndicatorEl.textContent = `Page ${page} of ${totalPages}`;
+ if (prevBtn) prevBtn.disabled = page <= 1;
+ if (nextBtn) nextBtn.disabled = page >= totalPages;
+}
+function bmChangePage(delta) {
+ const totalPages = Math.max(1, Math.ceil(bmFilteredList.length / BM_PAGE_SIZE));
+ const nextPage = Math.min(Math.max(bmCurrentPage + delta, 1), totalPages);
+ if (nextPage === bmCurrentPage) return;
+ bmCurrentPage = nextPage;
+ bmRender(bmFilteredList);
+}
+function bmReadDecoders() {
+ return (bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable).filter((d) => bmEl("bm-dec-" + d.id)?.checked).map((d) => d.id);
+}
+function bmWriteDecoders(decoders) {
+ const set = new Set(decoders || []);
+ (bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable).forEach((d) => {
+ const el = bmEl("bm-dec-" + d.id);
+ if (el) el.checked = set.has(d.id);
+ });
+}
+function bmBuildDecoderCheckboxes() {
+ const container = bmEl("bm-decoder-checkboxes");
+ if (!container) return;
+ container.innerHTML = "";
+ (bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable).forEach((d) => {
+ const label = document.createElement("label");
+ label.className = "bm-decoder-check";
+ label.innerHTML = ' ' + d.label;
+ container.appendChild(label);
+ });
+}
+function bmOpenForm(bm) {
+ const wrap = bmEl("bm-form-wrap");
+ if (!wrap) return;
+ bmEditScope = bm ? bm.scope || bmScope : null;
+ bmBuildDecoderCheckboxes();
+ bmEl("bm-id").value = bm ? bm.id : "";
+ bmEl("bm-name").value = bm ? bm.name : "";
+ bmEl("bm-freq").value = bm ? String(bm.freq_hz) : "";
+ bmEl("bm-mode").value = bm ? bm.mode : "";
+ bmEl("bm-bw").value = bm?.bandwidth_hz ? String(bm.bandwidth_hz) : "";
+ bmEl("bm-locator").value = bm ? bm.locator || "" : "";
+ bmEl("bm-category-input").value = bm ? bm.category || "" : "";
+ bmEl("bm-comment").value = bm ? bm.comment || "" : "";
+ bmWriteDecoders(bm?.decoders ?? []);
+ bmEl("bm-form-title").textContent = bm ? "Edit Bookmark" : "Add Bookmark";
+ wrap.style.display = "flex";
+ bmEl("bm-name").focus();
+}
+function bmCloseForm() {
+ const wrap = bmEl("bm-form-wrap");
+ if (wrap) wrap.style.display = "none";
+}
+function bmPrefillFromStatus() {
+ if (typeof bridge.lastFreqHz === "number" && Number.isFinite(bridge.lastFreqHz)) {
+ bmEl("bm-freq").value = String(Math.round(bridge.lastFreqHz));
+ }
+ if (typeof bridge.lastModeName === "string" && bridge.lastModeName) {
+ bmEl("bm-mode").value = bridge.lastModeName;
+ }
+ if (typeof bridge.currentBandwidthHz === "number" && bridge.currentBandwidthHz > 0) {
+ bmEl("bm-bw").value = String(Math.round(bridge.currentBandwidthHz));
+ }
+ const activeDecoders = (bridge.decoderRegistry || []).filter((d) => d.bookmark_selectable && d.activation === "toggle").filter((d) => {
+ const btn = bmEl(d.id + "-decode-toggle-btn");
+ return btn && btn.dataset.enabled === "true";
+ }).map((d) => d.id);
+ bmWriteDecoders(activeDecoders);
+}
+async function bmSave(e) {
+ e.preventDefault();
+ const id = bmEl("bm-id").value;
+ const name = bmEl("bm-name").value.trim();
+ const freqStr = bmEl("bm-freq").value;
+ const freq_hz = parseInt(freqStr, 10);
+ const mode = bmEl("bm-mode").value.trim();
+ const bwStr = bmEl("bm-bw").value;
+ const bandwidth_hz = bwStr ? parseInt(bwStr, 10) : null;
+ const locator = bmEl("bm-locator").value.trim().toUpperCase();
+ const category = bmEl("bm-category-input").value.trim();
+ const comment = bmEl("bm-comment").value.trim();
+ const decoders = bmReadDecoders();
+ const formError = bmEl("bm-form-error");
+ if (formError) formError.textContent = "";
+ if (!name || !Number.isFinite(freq_hz) || !mode) {
+ if (formError) formError.textContent = "Enter a name, a valid frequency, and a mode.";
+ const invalid = !name ? bmEl("bm-name") : !Number.isFinite(freq_hz) ? bmEl("bm-freq") : bmEl("bm-mode");
+ invalid?.focus();
+ return;
+ }
+ const body = {
+ name,
+ freq_hz,
+ mode,
+ bandwidth_hz,
+ locator: locator || null,
+ category,
+ comment,
+ decoders
+ };
+ try {
+ let resp;
+ if (id) {
+ resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false, bmEditScope), {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body)
+ });
+ } else {
+ resp = await fetch("/bookmarks" + bmScopeParam(false), {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body)
+ });
+ }
+ if (!resp.ok) {
+ const text = await resp.text();
+ if (resp.status === 409) {
+ throw new Error("A bookmark for that frequency already exists.");
+ }
+ throw new Error(text || `HTTP ${resp.status}`);
+ }
+ bmCloseForm();
+ await bmFetch(bmEl("bm-category-filter").value);
+ } catch (err) {
+ console.error("Failed to save bookmark:", err);
+ if (formError) formError.textContent = "Failed to save bookmark: " + errorMessage(err);
+ bridge.trxUi.notify?.("Bookmark could not be saved", { kind: "error" });
+ }
+}
+async function bmDelete(id) {
+ if (!await bridge.trxUi.confirm({ title: "Delete bookmark?", message: "This bookmark will be permanently removed.", confirmLabel: "Delete" })) return;
+ const bm = bmList.find((b) => b.id === id);
+ const scope = bm ? bm.scope : void 0;
+ try {
+ const resp = await fetch("/bookmarks/" + encodeURIComponent(id) + bmScopeParam(false, scope), {
+ method: "DELETE"
+ });
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ await bmFetch(bmEl("bm-category-filter").value);
+ } catch (err) {
+ console.error("Failed to delete bookmark:", err);
+ bridge.trxUi.notify?.("Failed to delete bookmark: " + errorMessage(err), { kind: "error" });
+ }
+}
+function bmApply(bm) {
+ try {
+ if (typeof bridge.modeEl !== "undefined" && bridge.modeEl) {
+ bridge.modeEl.value = (bm.mode || "").toUpperCase();
+ }
+ if (bm.bandwidth_hz) {
+ if (typeof bridge.currentBandwidthHz !== "undefined") {
+ bridge.currentBandwidthHz = bm.bandwidth_hz;
+ }
+ bridge.currentBandwidthHz = bm.bandwidth_hz;
+ if (typeof bridge.syncBandwidthInput === "function") {
+ bridge.syncBandwidthInput(bm.bandwidth_hz);
+ }
+ }
+ if (typeof bridge.applyLocalTunedFrequency === "function") {
+ if (typeof bridge._freqOptimisticSeq !== "undefined") {
+ ++bridge._freqOptimisticSeq;
+ bridge._freqOptimisticHz = bm.freq_hz;
+ }
+ bridge.applyLocalTunedFrequency(bm.freq_hz, true);
+ }
+ if (typeof bridge.scheduleSpectrumDraw === "function" && typeof bridge.lastSpectrumData !== "undefined" && bridge.lastSpectrumData) {
+ bridge.scheduleSpectrumDraw();
+ }
+ const tunePromise = (async () => {
+ if (typeof bridge.vchanTakeSchedulerControl === "function") {
+ await bridge.vchanTakeSchedulerControl();
+ }
+ const onVirtual = typeof bridge.vchanInterceptMode === "function" && await bridge.vchanInterceptMode(bm.mode);
+ if (!onVirtual) {
+ await bridge.postPath("/set_mode?mode=" + encodeURIComponent(bm.mode));
}
if (bm.bandwidth_hz) {
- if (typeof bridge.currentBandwidthHz !== "undefined") {
- bridge.currentBandwidthHz = bm.bandwidth_hz;
- }
- bridge.currentBandwidthHz = bm.bandwidth_hz;
- if (typeof bridge.syncBandwidthInput === "function") {
- bridge.syncBandwidthInput(bm.bandwidth_hz);
+ const bwHandledByVchan = typeof bridge.vchanInterceptBandwidth === "function" && await bridge.vchanInterceptBandwidth(bm.bandwidth_hz);
+ if (!bwHandledByVchan) {
+ await bridge.postPath(`/set_bandwidth?hz=${bm.bandwidth_hz}`);
}
}
- if (typeof bridge.applyLocalTunedFrequency === "function") {
- if (typeof bridge._freqOptimisticSeq !== "undefined") {
- ++bridge._freqOptimisticSeq;
- bridge._freqOptimisticHz = bm.freq_hz;
- }
- bridge.applyLocalTunedFrequency(bm.freq_hz, true);
- }
- if (typeof bridge.scheduleSpectrumDraw === "function" && typeof bridge.lastSpectrumData !== "undefined" && bridge.lastSpectrumData) {
- bridge.scheduleSpectrumDraw();
- }
- const tunePromise = (async () => {
- if (typeof bridge.vchanTakeSchedulerControl === "function") {
- await bridge.vchanTakeSchedulerControl();
- }
- const onVirtual = typeof bridge.vchanInterceptMode === "function" && await bridge.vchanInterceptMode(bm.mode);
- if (!onVirtual) {
- await bridge.postPath("/set_mode?mode=" + encodeURIComponent(bm.mode));
- }
- if (bm.bandwidth_hz) {
- const bwHandledByVchan = typeof bridge.vchanInterceptBandwidth === "function" && await bridge.vchanInterceptBandwidth(bm.bandwidth_hz);
- if (!bwHandledByVchan) {
- await bridge.postPath(`/set_bandwidth?hz=${bm.bandwidth_hz}`);
- }
- }
- if (typeof bridge.setRigFrequency === "function") {
- await bridge.setRigFrequency(bm.freq_hz);
- } else {
- await bridge.postPath(`/set_freq?hz=${bm.freq_hz}`);
- }
- })();
- const hasDecoders = Array.isArray(bm.decoders) && bm.decoders.length > 0;
- const modeUp = (bm.mode || "").toUpperCase();
- const allToggleDecoders = (bridge.decoderRegistry || []).filter(
- (d) => d.activation === "toggle"
- );
- const decoderPromise = allToggleDecoders.length ? (async () => {
- let statusUrl = "/status";
- if (typeof bridge.lastActiveRigId !== "undefined" && bridge.lastActiveRigId) {
- statusUrl += "?remote=" + encodeURIComponent(bridge.lastActiveRigId);
- }
- const statusResp = await fetch(statusUrl);
- if (!statusResp.ok) return;
- const st = await statusResp.json();
- const toggles = [];
- for (const d of allToggleDecoders) {
- const statusKey = d.id.replace(/-/g, "_") + "_decode_enabled";
- const currentlyOn = !!st[statusKey];
- const compatible = Array.isArray(d.active_modes) && d.active_modes.includes(modeUp);
- let wanted;
- if (!compatible) {
- wanted = false;
- } else if (hasDecoders) {
- wanted = bm.decoders?.includes(d.id) ?? false;
- } else {
- wanted = currentlyOn;
- }
- if (wanted !== currentlyOn) {
- toggles.push(bridge.postPath("/toggle_" + d.id.replace(/-/g, "_") + "_decode"));
- }
- }
- if (toggles.length) await Promise.all(toggles);
- })() : Promise.resolve();
- void Promise.all([tunePromise, decoderPromise]).catch((error) => {
- console.error("Bookmark apply background error:", error);
- });
- } catch (err) {
- console.error("Failed to apply bookmark:", err);
- }
- }
- bridge.trx ??= {};
- bridge.trx.modules ??= {};
- bridge.trx.modules.bookmarks = {
- get overlayList() {
- return bmOverlayList;
- },
- get overlayRevision() {
- return bmOverlayRevision;
- },
- refreshOverlay: bmFetchOverlay,
- invalidateColors() {
- bmOverlayRevision += 1;
- },
- apply: bmApply
- };
- function bmUpdateSelectionUi() {
- const count = bmSelected.size;
- const canCtrl = bmCanControl();
- const visible = count > 0 && canCtrl;
- const btn = bmEl("bm-del-selected-btn");
- const countEl = bmEl("bm-del-selected-count");
- if (btn) btn.style.display = visible ? "" : "none";
- if (countEl) countEl.textContent = String(count);
- const moveWrap = bmEl("bm-move-selected-wrap");
- const moveCountEl = bmEl("bm-move-selected-count");
- if (moveWrap) moveWrap.style.display = visible ? "" : "none";
- if (moveCountEl) moveCountEl.textContent = String(count);
- if (visible) bmPopulateMoveTarget();
- const selectAllBtn = bmEl("bm-select-all-btn");
- if (selectAllBtn && bmCanControl()) {
- const allSelected = bmFilteredList.length > 0 && bmFilteredList.every((bm) => bmSelected.has(bm.id));
- selectAllBtn.textContent = allSelected ? "Deselect All" : "Select All";
- }
- }
- function bmPopulateMoveTarget() {
- const sel = bmEl("bm-move-target");
- if (!sel) return;
- const rigIds = typeof bridge.lastRigIds !== "undefined" && Array.isArray(bridge.lastRigIds) ? bridge.lastRigIds : [];
- const displayNames = typeof bridge.lastRigDisplayNames !== "undefined" ? bridge.lastRigDisplayNames : {};
- const prev = sel.value;
- sel.innerHTML = "";
- if (bmScope !== "general") {
- const opt = document.createElement("option");
- opt.value = "general";
- opt.textContent = "General";
- sel.appendChild(opt);
- }
- rigIds.forEach((id) => {
- if (id === bmScope) return;
- const opt = document.createElement("option");
- opt.value = id;
- opt.textContent = displayNames[id] || id;
- sel.appendChild(opt);
- });
- if (prev && sel.querySelector(`option[value="${CSS.escape(prev)}"]`)) {
- sel.value = prev;
- }
- }
- async function bmMoveSelected() {
- const ids = Array.from(bmSelected);
- if (ids.length === 0) return;
- const target = bmEl("bm-move-target")?.value;
- if (!target) return;
- const targetLabel = bmEl("bm-move-target")?.selectedOptions[0]?.textContent || target;
- if (!await bridge.trxUi.confirm({
- title: "Move selected bookmarks?",
- message: `${ids.length} bookmark${ids.length > 1 ? "s" : ""} will move to “${targetLabel}”.`,
- confirmLabel: "Move",
- danger: false
- })) return;
- try {
- const byScope = {};
- for (const id of ids) {
- const bm = bmList.find((b) => b.id === id);
- const scope = bm?.scope || bmScope;
- if (scope === target) continue;
- (byScope[scope] ||= []).push(id);
- }
- await Promise.all(Object.entries(byScope).map(
- ([scope, scopeIds]) => fetch("/bookmarks/batch_move" + bmScopeParam(false, scope), {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ ids: scopeIds, to: target })
- }).then((r) => {
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
- })
- ));
- bmSelected.clear();
- bmUpdateSelectionUi();
- await bmFetch(bmEl("bm-category-filter").value);
- } catch (err) {
- console.error("Failed to move bookmarks:", err);
- bridge.trxUi.notify?.("Failed to move bookmarks: " + errorMessage(err), { kind: "error" });
- }
- }
- function bmSyncSelectAllCheckbox() {
- const selectAll = bmEl("bm-select-all");
- if (!selectAll) return;
- const checkboxes = document.querySelectorAll(".bm-row-sel");
- if (checkboxes.length === 0) {
- selectAll.checked = false;
- selectAll.indeterminate = false;
- return;
- }
- const checkedCount = Array.from(checkboxes).filter((cb) => cb.checked).length;
- selectAll.checked = checkedCount === checkboxes.length;
- selectAll.indeterminate = checkedCount > 0 && checkedCount < checkboxes.length;
- }
- async function bmDeleteSelected() {
- const ids = Array.from(bmSelected);
- if (ids.length === 0) return;
- if (!await bridge.trxUi.confirm({
- title: "Delete selected bookmarks?",
- message: `${ids.length} bookmark${ids.length > 1 ? "s" : ""} will be permanently removed.`,
- confirmLabel: "Delete"
- })) return;
- try {
- const byScope = {};
- for (const id of ids) {
- const bm = bmList.find((b) => b.id === id);
- const scope = bm?.scope || bmScope;
- (byScope[scope] ||= []).push(id);
- }
- await Promise.all(Object.entries(byScope).map(
- ([scope, scopeIds]) => fetch("/bookmarks/batch_delete" + bmScopeParam(false, scope), {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ ids: scopeIds })
- }).then((r) => {
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
- })
- ));
- bmSelected.clear();
- bmUpdateSelectionUi();
- await bmFetch(bmEl("bm-category-filter").value);
- } catch (err) {
- console.error("Failed to delete bookmarks:", err);
- bridge.trxUi.notify?.("Failed to delete bookmarks: " + errorMessage(err), { kind: "error" });
- }
- }
- function bmPopulateScopePicker() {
- const picker = bmEl("bm-scope-picker");
- if (!picker) return;
- const rigIds = typeof bridge.lastRigIds !== "undefined" && Array.isArray(bridge.lastRigIds) ? bridge.lastRigIds : [];
- const displayNames = typeof bridge.lastRigDisplayNames !== "undefined" ? bridge.lastRigDisplayNames : {};
- const prev = picker.value;
- while (picker.options.length > 1) picker.remove(1);
- rigIds.forEach((id) => {
- const opt = document.createElement("option");
- opt.value = id;
- opt.textContent = displayNames[id] || id;
- picker.appendChild(opt);
- });
- if (prev && (prev === "general" || rigIds.includes(prev))) {
- picker.value = prev;
- } else {
- picker.value = "general";
- }
- bmScope = picker.value;
- }
- (function initBookmarks() {
- bmSyncAccess();
- bmBuildDecoderCheckboxes();
- if (typeof bridge.onDecoderRegistryReady === "function") {
- bridge.onDecoderRegistryReady(bmBuildDecoderCheckboxes);
- }
- bmPopulateScopePicker();
- const scopePicker = bmEl("bm-scope-picker");
- if (scopePicker) {
- scopePicker.addEventListener("change", (e) => {
- bmScope = e.currentTarget.value;
- void bmFetch(bmEl("bm-category-filter").value || "");
- });
- }
- document.querySelector(".tab-bar")?.addEventListener("click", (e) => {
- const btn = e.target instanceof Element ? e.target.closest('.tab[data-tab="bookmarks"]') : null;
- if (!btn) return;
- void bmFetch(bmEl("bm-category-filter").value);
- });
- bmEl("bm-add-btn").addEventListener("click", () => {
- bmOpenForm(null);
- bmPrefillFromStatus();
- });
- bmEl("bm-category-filter").addEventListener("change", (e) => {
- void bmFetch(e.currentTarget.value);
- });
- bmEl("bm-mode-filter").addEventListener("change", () => {
- bmApplyFilters();
- });
- bmEl("bm-text-filter").addEventListener("input", () => {
- bmApplyFilters();
- });
- bmEl("bm-page-prev").addEventListener("click", () => {
- bmChangePage(-1);
- });
- bmEl("bm-page-next").addEventListener("click", () => {
- bmChangePage(1);
- });
- bmEl("bm-form").addEventListener("submit", (event) => {
- void bmSave(event);
- });
- bmEl("bm-form-cancel").addEventListener("click", bmCloseForm);
- const formWrap = bmEl("bm-form-wrap");
- if (formWrap) {
- formWrap.addEventListener("click", (e) => {
- if (e.target === formWrap) bmCloseForm();
- });
- }
- document.addEventListener("keydown", (e) => {
- if (e.key === "Escape" && bmEl("bm-form-wrap")?.style.display === "flex") {
- bmCloseForm();
- }
- });
- bmEl("bm-select-all").addEventListener("change", (e) => {
- const checked = e.currentTarget.checked;
- document.querySelectorAll(".bm-row-sel").forEach((cb) => {
- cb.checked = checked;
- const id = cb.dataset.bmId;
- if (!id) return;
- if (checked) bmSelected.add(id);
- else bmSelected.delete(id);
- });
- bmUpdateSelectionUi();
- });
- bmEl("bm-select-all-btn").addEventListener("click", () => {
- const allSelected = bmFilteredList.length > 0 && bmFilteredList.every((bm) => bmSelected.has(bm.id));
- if (allSelected) {
- bmSelected.clear();
+ if (typeof bridge.setRigFrequency === "function") {
+ await bridge.setRigFrequency(bm.freq_hz);
} else {
- bmFilteredList.forEach((bm) => bmSelected.add(bm.id));
+ await bridge.postPath(`/set_freq?hz=${bm.freq_hz}`);
}
- document.querySelectorAll(".bm-row-sel").forEach((cb) => {
- cb.checked = !!cb.dataset.bmId && bmSelected.has(cb.dataset.bmId);
- });
- bmSyncSelectAllCheckbox();
- bmUpdateSelectionUi();
- });
- bmEl("bm-del-selected-btn").addEventListener("click", () => {
- void bmDeleteSelected();
- });
- bmEl("bm-move-selected-btn").addEventListener("click", () => {
- void bmMoveSelected();
- });
- bmEl("bm-tbody").addEventListener("click", (e) => {
- void (async () => {
- if (!(e.target instanceof Element)) return;
- const checkbox = e.target.closest(".bm-row-sel");
- if (checkbox) {
- const id = checkbox.dataset.bmId;
- if (!id) return;
- if (checkbox.checked) bmSelected.add(id);
- else bmSelected.delete(id);
- bmSyncSelectAllCheckbox();
- bmUpdateSelectionUi();
- return;
+ })();
+ const hasDecoders = Array.isArray(bm.decoders) && bm.decoders.length > 0;
+ const modeUp = (bm.mode || "").toUpperCase();
+ const allToggleDecoders = (bridge.decoderRegistry || []).filter(
+ (d) => d.activation === "toggle"
+ );
+ const decoderPromise = allToggleDecoders.length ? (async () => {
+ let statusUrl = "/status";
+ if (typeof bridge.lastActiveRigId !== "undefined" && bridge.lastActiveRigId) {
+ statusUrl += "?remote=" + encodeURIComponent(bridge.lastActiveRigId);
+ }
+ const statusResp = await fetch(statusUrl);
+ if (!statusResp.ok) return;
+ const st = await statusResp.json();
+ const toggles = [];
+ for (const d of allToggleDecoders) {
+ const statusKey = d.id.replace(/-/g, "_") + "_decode_enabled";
+ const currentlyOn = !!st[statusKey];
+ const compatible = Array.isArray(d.active_modes) && d.active_modes.includes(modeUp);
+ let wanted;
+ if (!compatible) {
+ wanted = false;
+ } else if (hasDecoders) {
+ wanted = bm.decoders?.includes(d.id) ?? false;
+ } else {
+ wanted = currentlyOn;
}
- const tuneBtn = e.target.closest(".bm-tune-btn");
- const editBtn = e.target.closest(".bm-edit-btn");
- const delBtn = e.target.closest(".bm-del-btn");
- if (tuneBtn) {
- const bm = bmList.find((b) => b.id === tuneBtn.dataset.bmId);
- if (bm) bmApply(bm);
- } else if (editBtn) {
- const bm = bmList.find((b) => b.id === editBtn.dataset.bmId);
- if (bm) bmOpenForm(bm);
- } else if (delBtn) {
- const id = delBtn.dataset.bmId;
- if (id) await bmDelete(id);
+ if (wanted !== currentlyOn) {
+ toggles.push(bridge.postPath("/toggle_" + d.id.replace(/-/g, "_") + "_decode"));
}
- })();
+ }
+ if (toggles.length) await Promise.all(toggles);
+ })() : Promise.resolve();
+ void Promise.all([tunePromise, decoderPromise]).catch((error) => {
+ console.error("Bookmark apply background error:", error);
});
- void bmFetch("");
- })();
+ } catch (err) {
+ console.error("Failed to apply bookmark:", err);
+ }
+}
+bridge.trx ??= {};
+bridge.trx.modules ??= {};
+bridge.trx.modules.bookmarks = {
+ get overlayList() {
+ return bmOverlayList;
+ },
+ get overlayRevision() {
+ return bmOverlayRevision;
+ },
+ refreshOverlay: bmFetchOverlay,
+ invalidateColors() {
+ bmOverlayRevision += 1;
+ },
+ apply: bmApply
+};
+function bmUpdateSelectionUi() {
+ const count = bmSelected.size;
+ const canCtrl = bmCanControl();
+ const visible = count > 0 && canCtrl;
+ const btn = bmEl("bm-del-selected-btn");
+ const countEl = bmEl("bm-del-selected-count");
+ if (btn) btn.style.display = visible ? "" : "none";
+ if (countEl) countEl.textContent = String(count);
+ const moveWrap = bmEl("bm-move-selected-wrap");
+ const moveCountEl = bmEl("bm-move-selected-count");
+ if (moveWrap) moveWrap.style.display = visible ? "" : "none";
+ if (moveCountEl) moveCountEl.textContent = String(count);
+ if (visible) bmPopulateMoveTarget();
+ const selectAllBtn = bmEl("bm-select-all-btn");
+ if (selectAllBtn && bmCanControl()) {
+ const allSelected = bmFilteredList.length > 0 && bmFilteredList.every((bm) => bmSelected.has(bm.id));
+ selectAllBtn.textContent = allSelected ? "Deselect All" : "Select All";
+ }
+}
+function bmPopulateMoveTarget() {
+ const sel = bmEl("bm-move-target");
+ if (!sel) return;
+ const rigIds = typeof bridge.lastRigIds !== "undefined" && Array.isArray(bridge.lastRigIds) ? bridge.lastRigIds : [];
+ const displayNames = typeof bridge.lastRigDisplayNames !== "undefined" ? bridge.lastRigDisplayNames : {};
+ const prev = sel.value;
+ sel.innerHTML = "";
+ if (bmScope !== "general") {
+ const opt = document.createElement("option");
+ opt.value = "general";
+ opt.textContent = "General";
+ sel.appendChild(opt);
+ }
+ rigIds.forEach((id) => {
+ if (id === bmScope) return;
+ const opt = document.createElement("option");
+ opt.value = id;
+ opt.textContent = displayNames[id] || id;
+ sel.appendChild(opt);
+ });
+ if (prev && sel.querySelector(`option[value="${CSS.escape(prev)}"]`)) {
+ sel.value = prev;
+ }
+}
+async function bmMoveSelected() {
+ const ids = Array.from(bmSelected);
+ if (ids.length === 0) return;
+ const target = bmEl("bm-move-target")?.value;
+ if (!target) return;
+ const targetLabel = bmEl("bm-move-target")?.selectedOptions[0]?.textContent || target;
+ if (!await bridge.trxUi.confirm({
+ title: "Move selected bookmarks?",
+ message: `${ids.length} bookmark${ids.length > 1 ? "s" : ""} will move to “${targetLabel}”.`,
+ confirmLabel: "Move",
+ danger: false
+ })) return;
+ try {
+ const byScope = {};
+ for (const id of ids) {
+ const bm = bmList.find((b) => b.id === id);
+ const scope = bm?.scope || bmScope;
+ if (scope === target) continue;
+ (byScope[scope] ||= []).push(id);
+ }
+ await Promise.all(Object.entries(byScope).map(
+ ([scope, scopeIds]) => fetch("/bookmarks/batch_move" + bmScopeParam(false, scope), {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ ids: scopeIds, to: target })
+ }).then((r) => {
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
+ })
+ ));
+ bmSelected.clear();
+ bmUpdateSelectionUi();
+ await bmFetch(bmEl("bm-category-filter").value);
+ } catch (err) {
+ console.error("Failed to move bookmarks:", err);
+ bridge.trxUi.notify?.("Failed to move bookmarks: " + errorMessage(err), { kind: "error" });
+ }
+}
+function bmSyncSelectAllCheckbox() {
+ const selectAll = bmEl("bm-select-all");
+ if (!selectAll) return;
+ const checkboxes = document.querySelectorAll(".bm-row-sel");
+ if (checkboxes.length === 0) {
+ selectAll.checked = false;
+ selectAll.indeterminate = false;
+ return;
+ }
+ const checkedCount = Array.from(checkboxes).filter((cb) => cb.checked).length;
+ selectAll.checked = checkedCount === checkboxes.length;
+ selectAll.indeterminate = checkedCount > 0 && checkedCount < checkboxes.length;
+}
+async function bmDeleteSelected() {
+ const ids = Array.from(bmSelected);
+ if (ids.length === 0) return;
+ if (!await bridge.trxUi.confirm({
+ title: "Delete selected bookmarks?",
+ message: `${ids.length} bookmark${ids.length > 1 ? "s" : ""} will be permanently removed.`,
+ confirmLabel: "Delete"
+ })) return;
+ try {
+ const byScope = {};
+ for (const id of ids) {
+ const bm = bmList.find((b) => b.id === id);
+ const scope = bm?.scope || bmScope;
+ (byScope[scope] ||= []).push(id);
+ }
+ await Promise.all(Object.entries(byScope).map(
+ ([scope, scopeIds]) => fetch("/bookmarks/batch_delete" + bmScopeParam(false, scope), {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ ids: scopeIds })
+ }).then((r) => {
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
+ })
+ ));
+ bmSelected.clear();
+ bmUpdateSelectionUi();
+ await bmFetch(bmEl("bm-category-filter").value);
+ } catch (err) {
+ console.error("Failed to delete bookmarks:", err);
+ bridge.trxUi.notify?.("Failed to delete bookmarks: " + errorMessage(err), { kind: "error" });
+ }
+}
+function bmPopulateScopePicker() {
+ const picker = bmEl("bm-scope-picker");
+ if (!picker) return;
+ const rigIds = typeof bridge.lastRigIds !== "undefined" && Array.isArray(bridge.lastRigIds) ? bridge.lastRigIds : [];
+ const displayNames = typeof bridge.lastRigDisplayNames !== "undefined" ? bridge.lastRigDisplayNames : {};
+ const prev = picker.value;
+ while (picker.options.length > 1) picker.remove(1);
+ rigIds.forEach((id) => {
+ const opt = document.createElement("option");
+ opt.value = id;
+ opt.textContent = displayNames[id] || id;
+ picker.appendChild(opt);
+ });
+ if (prev && (prev === "general" || rigIds.includes(prev))) {
+ picker.value = prev;
+ } else {
+ picker.value = "general";
+ }
+ bmScope = picker.value;
+}
+(function initBookmarks() {
+ bmSyncAccess();
+ bmBuildDecoderCheckboxes();
+ if (typeof bridge.onDecoderRegistryReady === "function") {
+ bridge.onDecoderRegistryReady(bmBuildDecoderCheckboxes);
+ }
+ bmPopulateScopePicker();
+ const scopePicker = bmEl("bm-scope-picker");
+ if (scopePicker) {
+ scopePicker.addEventListener("change", (e) => {
+ bmScope = e.currentTarget.value;
+ void bmFetch(bmEl("bm-category-filter").value || "");
+ });
+ }
+ document.querySelector(".tab-bar")?.addEventListener("click", (e) => {
+ const btn = e.target instanceof Element ? e.target.closest('.tab[data-tab="bookmarks"]') : null;
+ if (!btn) return;
+ void bmFetch(bmEl("bm-category-filter").value);
+ });
+ bmEl("bm-add-btn").addEventListener("click", () => {
+ bmOpenForm(null);
+ bmPrefillFromStatus();
+ });
+ bmEl("bm-category-filter").addEventListener("change", (e) => {
+ void bmFetch(e.currentTarget.value);
+ });
+ bmEl("bm-mode-filter").addEventListener("change", () => {
+ bmApplyFilters();
+ });
+ bmEl("bm-text-filter").addEventListener("input", () => {
+ bmApplyFilters();
+ });
+ bmEl("bm-page-prev").addEventListener("click", () => {
+ bmChangePage(-1);
+ });
+ bmEl("bm-page-next").addEventListener("click", () => {
+ bmChangePage(1);
+ });
+ bmEl("bm-form").addEventListener("submit", (event) => {
+ void bmSave(event);
+ });
+ bmEl("bm-form-cancel").addEventListener("click", bmCloseForm);
+ const formWrap = bmEl("bm-form-wrap");
+ if (formWrap) {
+ formWrap.addEventListener("click", (e) => {
+ if (e.target === formWrap) bmCloseForm();
+ });
+ }
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape" && bmEl("bm-form-wrap")?.style.display === "flex") {
+ bmCloseForm();
+ }
+ });
+ bmEl("bm-select-all").addEventListener("change", (e) => {
+ const checked = e.currentTarget.checked;
+ document.querySelectorAll(".bm-row-sel").forEach((cb) => {
+ cb.checked = checked;
+ const id = cb.dataset.bmId;
+ if (!id) return;
+ if (checked) bmSelected.add(id);
+ else bmSelected.delete(id);
+ });
+ bmUpdateSelectionUi();
+ });
+ bmEl("bm-select-all-btn").addEventListener("click", () => {
+ const allSelected = bmFilteredList.length > 0 && bmFilteredList.every((bm) => bmSelected.has(bm.id));
+ if (allSelected) {
+ bmSelected.clear();
+ } else {
+ bmFilteredList.forEach((bm) => bmSelected.add(bm.id));
+ }
+ document.querySelectorAll(".bm-row-sel").forEach((cb) => {
+ cb.checked = !!cb.dataset.bmId && bmSelected.has(cb.dataset.bmId);
+ });
+ bmSyncSelectAllCheckbox();
+ bmUpdateSelectionUi();
+ });
+ bmEl("bm-del-selected-btn").addEventListener("click", () => {
+ void bmDeleteSelected();
+ });
+ bmEl("bm-move-selected-btn").addEventListener("click", () => {
+ void bmMoveSelected();
+ });
+ bmEl("bm-tbody").addEventListener("click", (e) => {
+ void (async () => {
+ if (!(e.target instanceof Element)) return;
+ const checkbox = e.target.closest(".bm-row-sel");
+ if (checkbox) {
+ const id = checkbox.dataset.bmId;
+ if (!id) return;
+ if (checkbox.checked) bmSelected.add(id);
+ else bmSelected.delete(id);
+ bmSyncSelectAllCheckbox();
+ bmUpdateSelectionUi();
+ return;
+ }
+ const tuneBtn = e.target.closest(".bm-tune-btn");
+ const editBtn = e.target.closest(".bm-edit-btn");
+ const delBtn = e.target.closest(".bm-del-btn");
+ if (tuneBtn) {
+ const bm = bmList.find((b) => b.id === tuneBtn.dataset.bmId);
+ if (bm) bmApply(bm);
+ } else if (editBtn) {
+ const bm = bmList.find((b) => b.id === editBtn.dataset.bmId);
+ if (bm) bmOpenForm(bm);
+ } else if (delBtn) {
+ const id = delBtn.dataset.bmId;
+ if (id) await bmDelete(id);
+ }
+ })();
+ });
+ void bmFetch("");
})();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-M2I6DH4X.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-M2I6DH4X.js
new file mode 100644
index 00000000..1d386a5f
--- /dev/null
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-M2I6DH4X.js
@@ -0,0 +1,112 @@
+// src/plugins/aprs-shared.ts
+function aprsPacketCategory(packet) {
+ const type = (packet.type ?? "").toLowerCase();
+ const info = (packet.info ?? "").toLowerCase();
+ if (packet.lat != null && packet.lon != null || type.includes("position")) return "position";
+ if (type.includes("message") || info.startsWith(":")) return "message";
+ if (type.includes("weather") || info.startsWith("_")) return "weather";
+ if (type.includes("telemetry") || info.startsWith("t#")) return "telemetry";
+ return "other";
+}
+function aprsCategoryLabel(category) {
+ switch (category) {
+ case "position":
+ return "Position";
+ case "message":
+ return "Message";
+ case "weather":
+ return "Weather";
+ case "telemetry":
+ return "Telemetry";
+ default:
+ return "Other";
+ }
+}
+function aprsAgeText(timestampMs) {
+ if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return "just now";
+ const seconds = Math.round(Math.max(0, Date.now() - timestampMs) / 1e3);
+ if (seconds < 5) return "just now";
+ if (seconds < 60) return `${String(seconds)}s ago`;
+ const minutes = Math.round(seconds / 60);
+ if (minutes < 60) return `${String(minutes)}m ago`;
+ return `${String(Math.round(minutes / 60))}h ago`;
+}
+function aprsPacketSignature(packet) {
+ return [
+ packet.srcCall ?? "",
+ packet.destCall ?? "",
+ packet.path ?? "",
+ packet.info ?? "",
+ packet.type ?? "",
+ packet.lat?.toFixed(4) ?? "",
+ packet.lon?.toFixed(4) ?? ""
+ ].join("|");
+}
+function collapseAprsDuplicates(packets) {
+ const seen = /* @__PURE__ */ new Set();
+ return packets.filter((packet) => {
+ const signature = aprsPacketSignature(packet);
+ if (seen.has(signature)) return false;
+ seen.add(signature);
+ return true;
+ });
+}
+function aprsHexBytes(bytes) {
+ if (!bytes?.length) return "--";
+ return bytes.map((byte) => byte.toString(16).toUpperCase().padStart(2, "0")).join(" ");
+}
+function renderAprsInfo(packet) {
+ if (packet.info_bytes?.length) {
+ return packet.info_bytes.map((byte) => renderAprsByte(byte)).join("");
+ }
+ return Array.from(packet.info ?? "", (character) => renderAprsCharacter(character)).join("");
+}
+function renderAprsByte(byte) {
+ return byte >= 32 && byte <= 126 ? escapeAprsCharacter(String.fromCharCode(byte)) : `0x${byte.toString(16).toUpperCase().padStart(2, "0")}`;
+}
+function renderAprsCharacter(character) {
+ const code = character.charCodeAt(0);
+ return code >= 32 && code <= 126 ? escapeAprsCharacter(character) : `0x${code.toString(16).toUpperCase().padStart(2, "0")}`;
+}
+function escapeAprsCharacter(character) {
+ if (character === "<") return "<";
+ if (character === ">") return ">";
+ if (character === "&") return "&";
+ if (character === '"') return """;
+ return character;
+}
+function renderLocalAprsSymbol(packet, escapeHtml) {
+ if (!packet.symbolTable || !packet.symbolCode) return "";
+ const symbol = escapeHtml(packet.symbolCode);
+ const table = packet.symbolTable === "/" ? "Primary" : "Alternate";
+ return `${symbol}`;
+}
+function normalizeAprsPacket(packet, receiver) {
+ return {
+ rig_id: packet.rig_id || null,
+ receiver,
+ srcCall: packet.src_call ?? "",
+ destCall: packet.dest_call ?? "",
+ path: packet.path ?? "",
+ info: packet.info ?? "",
+ info_bytes: packet.info_bytes ?? [],
+ type: packet.packet_type ?? "",
+ crcOk: packet.crc_ok ?? false,
+ ts_ms: packet.ts_ms ?? null,
+ lat: packet.lat ?? null,
+ lon: packet.lon ?? null,
+ symbolTable: packet.symbol_table ?? null,
+ symbolCode: packet.symbol_code ?? null
+ };
+}
+
+export {
+ aprsPacketCategory,
+ aprsCategoryLabel,
+ aprsAgeText,
+ collapseAprsDuplicates,
+ aprsHexBytes,
+ renderAprsInfo,
+ renderLocalAprsSymbol,
+ normalizeAprsPacket
+};
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-SGMG5LG2.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-SGMG5LG2.js
new file mode 100644
index 00000000..1fa6534f
--- /dev/null
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/chunk-SGMG5LG2.js
@@ -0,0 +1,269 @@
+// src/plugins/ftx-family.ts
+var bridge = window;
+function finiteNumber(value) {
+ const number = typeof value === "number" ? value : Number(value);
+ return Number.isFinite(number) ? number : null;
+}
+function isAlphaNumeric(value) {
+ return value !== void 0 && /[A-Za-z0-9]/.test(value);
+}
+function isGrid(value) {
+ const normalized = value.trim().toUpperCase();
+ return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && normalized !== "RR73" && normalized !== "73" && normalized !== "RR";
+}
+function escapeFtxHtml(input) {
+ return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
+}
+function extractFtxGrids(message) {
+ return [...new Set(message.toUpperCase().split(/[^A-Z0-9]+/).filter(isGrid))];
+}
+function tokenize(message) {
+ return message.toUpperCase().split(/[^A-Z0-9/]+/).filter(Boolean);
+}
+function isCallsign(token) {
+ return token !== void 0 && token.length >= 3 && token.length <= 12 && !["CQ", "DE", "QRZ", "DX"].includes(token) && !isGrid(token) && /^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token);
+}
+function extractFtxLocatorDetails(message) {
+ const tokens = tokenize(message);
+ const grids = extractFtxGrids(message);
+ const gridIndex = tokens.findIndex(isGrid);
+ const callsigns = tokens.slice(0, gridIndex < 0 ? tokens.length : gridIndex).filter(isCallsign);
+ const directed = callsigns.length >= 2 && !["CQ", "DE", "QRZ"].includes(tokens[0] ?? "");
+ const source = directed ? callsigns[1] ?? null : callsigns[0] ?? null;
+ const target = directed ? callsigns[0] ?? null : null;
+ return grids.map((grid) => ({ grid, station: source, source, target }));
+}
+function extractFtxCallsign(message) {
+ return extractFtxLocatorDetails(message)[0]?.station ?? tokenize(message).find(isCallsign) ?? null;
+}
+function renderFtxMessage(message) {
+ let html = "";
+ let index = 0;
+ while (index < message.length) {
+ if (!isAlphaNumeric(message[index])) {
+ html += escapeFtxHtml(message[index] ?? "");
+ index += 1;
+ continue;
+ }
+ let end = index + 1;
+ while (end < message.length && isAlphaNumeric(message[end])) end += 1;
+ const token = message.slice(index, end);
+ const grid = token.toUpperCase();
+ html += isGrid(grid) ? `${grid}` : escapeFtxHtml(token);
+ index = end;
+ }
+ return html;
+}
+function installFtxCompatibilityHelpers() {
+ bridge.renderFt8Message = renderFtxMessage;
+ bridge.ft8EscapeHtml = escapeFtxHtml;
+ bridge.ft8ExtractLocatorDetails = extractFtxLocatorDetails;
+ bridge.ft8ExtractAllGrids = extractFtxGrids;
+ bridge.ft8ExtractLikelyCallsign = extractFtxCallsign;
+}
+function initializeFt8FamilyBar() {
+ const labels = { ft8: "FT8", ft4: "FT4", ft2: "FT2" };
+ const builders = {};
+ const dismissed = { ft8: 0, ft4: 0, ft2: 0 };
+ const overlay = document.getElementById("ft8-bar-overlay");
+ let active = "ft8";
+ const update = () => {
+ if (!overlay) return;
+ const mode = (document.getElementById("mode")?.value ?? "").toUpperCase();
+ const result = builders[active]?.();
+ if (mode !== "DIG" && mode !== "USB" || !result || result.count === 0 || result.newestTsMs <= dismissed[active]) {
+ overlay.style.display = "none";
+ overlay.innerHTML = "";
+ return;
+ }
+ const label = labels[active];
+ overlay.innerHTML = `${result.html}`;
+ overlay.style.display = "flex";
+ };
+ bridge.registerFt8FamilyBarRenderer = (decoder, builder) => {
+ builders[decoder] = builder;
+ };
+ bridge.setFt8FamilyBarDecoder = (decoder) => {
+ active = decoder;
+ update();
+ };
+ bridge.updateFt8Bar = update;
+ bridge.clearFt8Bar = () => {
+ bridge.trxPluginRuntime.reset(active);
+ };
+ bridge.closeFt8Bar = () => {
+ dismissed[active] = Date.now();
+ update();
+ };
+}
+function initializeFtxDecoder(config) {
+ const { id, label, periodMs, periodDigits = 1 } = config;
+ const status = document.getElementById(`${id}-status`);
+ const period = document.getElementById(`${id}-period`);
+ const messagesElement = document.getElementById(`${id}-messages`);
+ const filterInput = document.getElementById(`${id}-filter`);
+ let filterText = "";
+ let history = [];
+ const renderMessage = (message) => {
+ return bridge.renderFt8Message?.(message) ?? renderFtxMessage(message);
+ };
+ const retentionMs = () => bridge.getDecodeHistoryRetentionMs?.() ?? 864e5;
+ const prune = () => {
+ const cutoff = Date.now() - retentionMs();
+ history = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= cutoff);
+ };
+ const schedule = (job) => {
+ if (bridge.trxScheduleUiFrameJob) bridge.trxScheduleUiFrameJob(`${id}-history`, job);
+ else job();
+ };
+ const displayFrequency = (value) => {
+ const raw = finiteNumber(value);
+ if (raw === null) return null;
+ const base = finiteNumber(bridge.ft8BaseHz);
+ return base !== null && base > 0 && raw >= 0 && raw < 1e5 ? base + raw : raw;
+ };
+ const renderRow = (message) => {
+ const row = document.createElement("div");
+ row.className = "ft8-row";
+ const raw = message.message ?? "";
+ row.dataset.message = raw.toUpperCase();
+ row.dataset.decoder = id;
+ const storedFrequency = finiteNumber(message.freq_hz);
+ row.dataset.storedFreqHz = storedFrequency === null ? "" : String(storedFrequency);
+ const snr = finiteNumber(message.snr_db);
+ const delta = finiteNumber(message.dt_s);
+ const frequency = displayFrequency(message.freq_hz);
+ const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
+ const time = timestamp === null ? "--:--:--" : new Date(timestamp).toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit"
+ });
+ row.innerHTML = `${time}${snr?.toFixed(1) ?? "--"}${delta?.toFixed(2) ?? "--"}${frequency?.toFixed(0) ?? "--"}${renderMessage(raw)}`;
+ return row;
+ };
+ const render = () => {
+ prune();
+ if (!messagesElement) return;
+ const fragment = document.createDocumentFragment();
+ let count = 0;
+ for (const message of history) {
+ if (count >= 200) break;
+ if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue;
+ fragment.appendChild(renderRow(message));
+ count += 1;
+ }
+ messagesElement.replaceChildren(fragment);
+ };
+ const normalize = (message) => {
+ const raw = message.message ?? "";
+ const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw);
+ const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw);
+ const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw);
+ const frequency = displayFrequency(message.freq_hz);
+ if (grids.length > 0) {
+ bridge.mapAddLocator?.(raw, grids, id, station, {
+ ...message,
+ freq_hz: frequency ?? message.freq_hz,
+ locator_details: locatorDetails
+ });
+ }
+ return {
+ receiver: bridge.getDecodeRigMeta?.() ?? null,
+ ts_ms: message.ts_ms,
+ snr_db: message.snr_db,
+ dt_s: message.dt_s,
+ freq_hz: frequency ?? message.freq_hz,
+ message: message.message,
+ _tsMs: finiteNumber(message.ts_ms) ?? Date.now()
+ };
+ };
+ const receiveBatch = (messages) => {
+ if (messages.length === 0) return;
+ if (status) status.textContent = "Receiving";
+ history = messages.map(normalize).reverse().concat(history);
+ prune();
+ bridge.setFt8FamilyBarDecoder?.(id);
+ bridge.updateFt8Bar?.();
+ schedule(render);
+ };
+ const reset = () => {
+ history = [];
+ bridge.updateFt8Bar?.();
+ render();
+ bridge.clearMapMarkersByType?.(id);
+ };
+ const barFrames = () => {
+ const recent = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= Date.now() - 9e5).slice(0, 8);
+ let html = "";
+ for (const message of recent) {
+ const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
+ const time = timestamp === null ? "" : `${bridge.fmtTime?.(timestamp) ?? ""}`;
+ const snr = finiteNumber(message.snr_db);
+ const delta = finiteNumber(message.dt_s);
+ const frequency = displayFrequency(message.freq_hz);
+ const detail = [snr === null ? "-- dB" : `${snr.toFixed(1)} dB`, delta === null ? null : `dt ${delta.toFixed(2)}`, frequency === null ? null : `${frequency.toFixed(0)} Hz`].filter((part) => part !== null).join(" · ");
+ html += `${time}${renderMessage(message.message ?? "")}${detail ? ` · ${detail}` : ""}
`;
+ }
+ return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
+ };
+ bridge.trxPluginRuntime.registerDecoder({
+ id,
+ onMessage: (message) => {
+ receiveBatch([message]);
+ },
+ onBatch: receiveBatch,
+ restore: receiveBatch,
+ prune: () => {
+ prune();
+ render();
+ },
+ reset
+ });
+ bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
+ const updatePeriod = () => {
+ if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`;
+ };
+ updatePeriod();
+ window.setInterval(updatePeriod, 250);
+ filterInput?.addEventListener("input", () => {
+ filterText = filterInput.value.trim().toUpperCase();
+ render();
+ });
+ messagesElement?.addEventListener("click", (event) => {
+ if (!(event.target instanceof Element)) return;
+ const grid = event.target.closest(".ft8-locator[data-locator-grid]")?.dataset.locatorGrid;
+ if (grid) {
+ bridge.navigateToMapLocator?.(grid.toUpperCase(), id);
+ event.preventDefault();
+ }
+ });
+ const toggle = document.getElementById(`${id}-decode-toggle-btn`);
+ toggle?.addEventListener("click", () => {
+ void (async () => {
+ try {
+ await bridge.takeSchedulerControlForDecoderDisable?.(toggle);
+ await bridge.postPath?.(`/toggle_${id}_decode`);
+ } catch (error) {
+ console.error(`${label} toggle failed`, error);
+ }
+ })();
+ });
+ document.getElementById(`settings-clear-${id}-history`)?.addEventListener("click", () => {
+ void (async () => {
+ if (!await bridge.trxUi.confirm({ title: `Clear ${label} history?`, message: `All stored ${label} decodes will be permanently removed.`, confirmLabel: "Clear history" })) return;
+ try {
+ await bridge.postPath?.(`/clear_${id}_decode`);
+ reset();
+ } catch (error) {
+ console.error(`${label} history clear failed`, error);
+ }
+ })();
+ });
+}
+
+export {
+ installFtxCompatibilityHelpers,
+ initializeFt8FamilyBar,
+ initializeFtxDecoder
+};
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/cw.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/cw.js
index 562ae85d..514dd896 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/cw.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/cw.js
@@ -1,403 +1,400 @@
-"use strict";
-(() => {
- // src/plugins/cw.ts
- var cwWindow = window;
- var cwStatusEl = document.getElementById("cw-status");
- var cwOutputEl = document.getElementById("cw-output");
- var cwAutoInput = document.getElementById("cw-auto");
- var cwWpmInput = document.getElementById("cw-wpm");
- var cwToneInput = document.getElementById("cw-tone");
- var cwSignalIndicator = document.getElementById("cw-signal-indicator");
- var cwToneCanvas = document.getElementById("cw-tone-waterfall");
- var cwToneGl = cwToneCanvas && cwWindow.createTrxWebGlRenderer ? cwWindow.createTrxWebGlRenderer(cwToneCanvas, { alpha: true }) : null;
- var cwTonePickerEl = document.querySelector(".cw-tone-picker");
- var cwToneRangeEl = document.getElementById("cw-tone-range");
- var cwBarOverlay = document.getElementById("cw-bar-overlay");
- var CW_MAX_LINES = 200;
- var CW_TONE_MIN_HZ = 100;
- var CW_TONE_MAX_HZ = 1e4;
- var CW_WPM_MIN = 5;
- var CW_WPM_MAX = 40;
- var CW_BAR_WINDOW_MS = 15 * 60 * 1e3;
- var CW_BAR_LINE_GAP_MS = 5e3;
- var cwLastAppendTime = 0;
- var cwTonePickerRaf = null;
- var cwBarHistory = [];
- var cwBarCurrentLine = null;
- var cwBarDismissedAtMs = 0;
- var cwAutoLocalOverride = null;
- function escapeCwHtml(input) {
- return cwWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
- }
- function applyCwAutoUi(enabled) {
- if (cwAutoInput) cwAutoInput.checked = enabled;
- if (cwWpmInput) {
- cwWpmInput.disabled = enabled;
- cwWpmInput.readOnly = enabled;
- }
- if (cwToneInput) {
- cwToneInput.disabled = enabled;
- cwToneInput.readOnly = enabled;
- }
- if (cwTonePickerEl) {
- cwTonePickerEl.classList.toggle("is-auto", enabled);
- }
- }
- cwWindow.applyCwAutoUi = applyCwAutoUi;
- cwWindow.applyCwAutoUiFromServer = function(enabled) {
- if (cwAutoLocalOverride !== null) return;
- applyCwAutoUi(enabled);
- };
- function cwBarFlushCurrentLine() {
- if (cwBarCurrentLine && cwBarCurrentLine.text.trim()) {
- cwBarHistory.unshift(cwBarCurrentLine);
- if (cwBarHistory.length > 50) cwBarHistory.length = 50;
- }
- cwBarCurrentLine = null;
- }
- function updateCwBar() {
- if (!cwBarOverlay) return;
- const mode = (document.getElementById("mode")?.value || "").toUpperCase();
- const isCw = mode === "CW" || mode === "CWR";
- const cutoffMs = Date.now() - CW_BAR_WINDOW_MS;
- const recent = cwBarHistory.filter((l) => l.tsMs >= cutoffMs);
- const liveLines = cwBarCurrentLine && cwBarCurrentLine.text ? [cwBarCurrentLine, ...recent] : recent;
- const newestTsMs = liveLines.reduce((latest, line) => Math.max(latest, line.tsMs || 0), 0);
- if (!isCw || liveLines.length === 0 || newestTsMs <= cwBarDismissedAtMs) {
- cwBarOverlay.style.display = "none";
- cwBarOverlay.innerHTML = "";
- return;
- }
- let html = ``;
- for (const line of liveLines.slice(0, 8)) {
- const ts = line.ts ? `${line.ts}` : "";
- const meta = [
- line.wpm ? `${String(line.wpm)} WPM` : null,
- line.tone_hz ? `${String(line.tone_hz)} Hz` : null
- ].filter(Boolean).join(" · ");
- html += `${ts}${escapeCwHtml(line.text)}` + (meta ? ` ${escapeCwHtml(meta)}` : "") + `
`;
- }
- cwBarOverlay.innerHTML = html;
- cwBarOverlay.style.display = "flex";
- }
- cwWindow.updateCwBar = updateCwBar;
- cwWindow.clearCwBar = function() {
- resetCwHistoryView();
- };
- cwWindow.closeCwBar = function() {
- cwBarDismissedAtMs = Date.now();
- if (cwBarOverlay) {
- cwBarOverlay.style.display = "none";
- cwBarOverlay.innerHTML = "";
- }
- };
- function clampCwWpm(wpm) {
- const numeric = Number(wpm);
- if (!Number.isFinite(numeric)) return 15;
- return Math.round(Math.max(CW_WPM_MIN, Math.min(CW_WPM_MAX, numeric)));
- }
- function clampCwTone(tone) {
- const numeric = Number(tone);
- if (!Number.isFinite(numeric)) return 700;
- return Math.round(Math.max(CW_TONE_MIN_HZ, Math.min(CW_TONE_MAX_HZ, numeric)));
- }
- function currentCwToneRange() {
- const tunedHz = Number.isFinite(cwWindow.lastFreqHz) ? Number(cwWindow.lastFreqHz) : NaN;
- const bandwidthHz = Number.isFinite(cwWindow.currentBandwidthHz) ? Number(cwWindow.currentBandwidthHz) : NaN;
- if (!Number.isFinite(tunedHz) || !Number.isFinite(bandwidthHz) || bandwidthHz <= 0) {
- return null;
- }
- const mode = (document.getElementById("mode")?.value || "").toUpperCase();
- const lowerSideband = mode === "CWR";
- const upperSideband = mode === "CW";
- if (!lowerSideband && !upperSideband) return null;
- const toneMinHz = CW_TONE_MIN_HZ;
- const toneMaxHz = CW_TONE_MAX_HZ;
- return {
- tunedHz,
- bandwidthHz,
- toneMinHz,
- toneMaxHz,
- toneSpanHz: Math.max(1, toneMaxHz - toneMinHz),
- lowerSideband,
- mode
- };
- }
- function cwToneToRfHz(range, toneHz) {
- if (!range) return NaN;
- return range.lowerSideband ? range.tunedHz - toneHz : range.tunedHz + toneHz;
- }
- function toneClampForRange(tone, range) {
- const clamped = clampCwTone(tone);
- if (!range) return clamped;
- return Math.max(range.toneMinHz, Math.min(range.toneMaxHz, clamped));
- }
- function ensureCwToneCanvasResolution() {
- if (!cwToneCanvas || !cwToneGl || !cwToneGl.ready) return false;
- const rect = cwToneCanvas.getBoundingClientRect();
- const cssWidth = Math.round(rect.width);
- const cssHeight = Math.round(rect.height);
- if (cssWidth < 8 || cssHeight < 8) {
- return false;
- }
- const dpr = window.devicePixelRatio || 1;
- return cwToneGl.ensureSize(cssWidth, cssHeight, dpr);
- }
- function drawCwTonePicker() {
- if (!cwToneCanvas || !cwToneGl || !cwToneGl.ready) return;
- ensureCwToneCanvasResolution();
- if (cwToneCanvas.width < 8 || cwToneCanvas.height < 8) return;
- const width = cwToneCanvas.width;
- const height = cwToneCanvas.height;
- cwToneGl.clear([0, 0, 0, 0]);
- const range = currentCwToneRange();
- if (!cwWindow.lastSpectrumData || !Array.isArray(cwWindow.lastSpectrumData.bins) || !cwWindow.lastSpectrumData.bins.length || !range) {
- if (cwToneRangeEl) {
- const mode = (document.getElementById("mode")?.value || "").toUpperCase();
- if (mode !== "CW" && mode !== "CWR") {
- cwToneRangeEl.textContent = "CW/CWR mode required";
- } else if (!cwWindow.lastSpectrumData || !Array.isArray(cwWindow.lastSpectrumData.bins) || !cwWindow.lastSpectrumData.bins.length) {
- cwToneRangeEl.textContent = "Waiting for spectrum";
- }
- }
- cwToneGl.fillRect(0, 0, width, height, [130 / 255, 150 / 255, 165 / 255, 0.22]);
- return;
- }
- if (cwToneRangeEl) {
- const side = range.lowerSideband ? "Lower side" : "Upper side";
- cwToneRangeEl.textContent = `Audio ${String(range.toneMinHz)}-${String(range.toneMaxHz)} Hz · ${side}`;
- }
- const bins = cwWindow.lastSpectrumData.bins;
- const sampleRate = cwWindow.lastSpectrumData.sample_rate;
- const centerHz = cwWindow.lastSpectrumData.center_hz;
- const maxIdx = Math.max(1, bins.length - 1);
- const fullLoHz = centerHz - sampleRate / 2;
- const tones = new Array(width).fill(-140);
- for (let x = 0; x < width; x += 1) {
- const frac = width <= 1 ? 0 : x / (width - 1);
- const toneHz = range.toneMinHz + frac * range.toneSpanHz;
- const rfHz = cwToneToRfHz(range, toneHz);
- const idx = Math.max(0, Math.min(maxIdx, Math.round((rfHz - fullLoHz) / sampleRate * maxIdx)));
- const power = Number.isFinite(Number(bins[idx])) ? Number(bins[idx]) : -140;
- tones[x] = power;
- }
- const smoothed = new Array(width).fill(-140);
- const smoothRadius = Math.max(1, Math.round(width / 180));
- for (let x = 0; x < width; x += 1) {
- let sum = 0;
- let count = 0;
- for (let i = x - smoothRadius; i <= x + smoothRadius; i += 1) {
- if (i < 0 || i >= width) continue;
- sum += tones[i] ?? -140;
- count += 1;
- }
- smoothed[x] = count > 0 ? sum / count : tones[x] ?? -140;
- }
- const sorted = smoothed.slice().sort((a, b) => a - b);
- const q20 = sorted[Math.floor((sorted.length - 1) * 0.2)] ?? -120;
- const q95 = sorted[Math.floor((sorted.length - 1) * 0.95)] ?? -70;
- const floorDb = Math.min(q20 - 2, q95 - 10);
- const ceilDb = Math.max(floorDb + 18, q95 + 2);
- const dbSpan = Math.max(1, ceilDb - floorDb);
- const yForDb = (db) => {
- const n = Math.max(0, Math.min(1, (db - floorDb) / dbSpan));
- return Math.round((1 - n) * (height - 1));
- };
- const rootStyle = getComputedStyle(document.documentElement);
- const accent = (rootStyle.getPropertyValue("--accent-green") || "").trim() || "#00d17f";
- const parseColor = typeof cwWindow.trxParseCssColor === "function" ? cwWindow.trxParseCssColor : null;
- const accentRgba = parseColor ? parseColor(accent) : [0, 0.82, 0.5, 1];
- const axisColor = [230 / 255, 235 / 255, 245 / 255, 0.15];
- cwToneGl.fillRect(0, 0, width, height, [7 / 255, 12 / 255, 18 / 255, 0.94]);
- const hGridCount = 4;
- const gridSegments = [];
- for (let i = 1; i <= hGridCount; i += 1) {
- const y = Math.round(i / (hGridCount + 1) * (height - 1));
- gridSegments.push(0, y, width, y);
- }
- cwToneGl.drawSegments(gridSegments, axisColor, 1);
- const toneStep = range.toneSpanHz <= 500 ? 50 : range.toneSpanHz <= 1e3 ? 100 : 200;
- const firstTick = Math.ceil(range.toneMinHz / toneStep) * toneStep;
- const tickSegments = [];
- for (let tone = firstTick; tone <= range.toneMaxHz; tone += toneStep) {
- const frac = (tone - range.toneMinHz) / range.toneSpanHz;
- const x = Math.max(0, Math.min(width - 1, Math.round(frac * (width - 1))));
- tickSegments.push(x, 0, x, height);
- }
- cwToneGl.drawSegments(tickSegments, axisColor, 1);
- const linePoints = [];
- for (let x = 0; x < width; x += 1) {
- linePoints.push(x, yForDb(smoothed[x] ?? -140));
- }
- cwToneGl.drawFilledArea(linePoints, height, [accentRgba[0], accentRgba[1], accentRgba[2], 0.24]);
- cwToneGl.drawPolyline(linePoints, accentRgba, Math.max(1.2, (window.devicePixelRatio || 1) * 1.2));
- const currentTone = toneClampForRange(cwToneInput ? cwToneInput.value : 700, range);
- const markerFrac = (currentTone - range.toneMinHz) / range.toneSpanHz;
- const markerX = Math.max(0, Math.min(width - 1, Math.round(markerFrac * (width - 1))));
- const markerY = yForDb(smoothed[Math.max(0, Math.min(width - 1, markerX))] ?? -140);
- cwToneGl.drawSegments([markerX, 0, markerX, height], [1, 1, 1, 0.9], 1.5);
- cwToneGl.drawPoints([markerX, markerY], Math.max(2, Math.round(height * 0.055)), [1, 1, 1, 0.9]);
- if (cwAutoInput?.checked) {
- cwToneGl.fillRect(0, 0, width, height, [0, 0, 0, 0.22]);
- }
- }
- async function setCwTone(tone, { syncInput = true } = {}) {
- const range = currentCwToneRange();
- const clamped = toneClampForRange(tone, range);
- if (cwToneInput && syncInput) {
- cwToneInput.value = String(clamped);
- }
- try {
- await cwWindow.postPath?.(`/set_cw_tone?tone_hz=${encodeURIComponent(clamped)}`);
- } catch (e) {
- console.error("CW tone set failed", e);
- }
- drawCwTonePicker();
- }
- if (cwAutoInput) {
- cwAutoInput.addEventListener("change", () => {
- void (async () => {
- const enabled = cwAutoInput.checked;
- cwAutoLocalOverride = enabled;
- applyCwAutoUi(enabled);
- try {
- await cwWindow.postPath?.(`/set_cw_auto?enabled=${enabled ? "true" : "false"}`);
- drawCwTonePicker();
- } catch (error) {
- console.error("CW auto toggle failed", error);
- } finally {
- cwAutoLocalOverride = null;
- }
- })();
- });
- }
+// src/plugins/cw.ts
+var cwWindow = window;
+var cwStatusEl = document.getElementById("cw-status");
+var cwOutputEl = document.getElementById("cw-output");
+var cwAutoInput = document.getElementById("cw-auto");
+var cwWpmInput = document.getElementById("cw-wpm");
+var cwToneInput = document.getElementById("cw-tone");
+var cwSignalIndicator = document.getElementById("cw-signal-indicator");
+var cwToneCanvas = document.getElementById("cw-tone-waterfall");
+var cwToneGl = cwToneCanvas && cwWindow.createTrxWebGlRenderer ? cwWindow.createTrxWebGlRenderer(cwToneCanvas, { alpha: true }) : null;
+var cwTonePickerEl = document.querySelector(".cw-tone-picker");
+var cwToneRangeEl = document.getElementById("cw-tone-range");
+var cwBarOverlay = document.getElementById("cw-bar-overlay");
+var CW_MAX_LINES = 200;
+var CW_TONE_MIN_HZ = 100;
+var CW_TONE_MAX_HZ = 1e4;
+var CW_WPM_MIN = 5;
+var CW_WPM_MAX = 40;
+var CW_BAR_WINDOW_MS = 15 * 60 * 1e3;
+var CW_BAR_LINE_GAP_MS = 5e3;
+var cwLastAppendTime = 0;
+var cwTonePickerRaf = null;
+var cwBarHistory = [];
+var cwBarCurrentLine = null;
+var cwBarDismissedAtMs = 0;
+var cwAutoLocalOverride = null;
+function escapeCwHtml(input) {
+ return cwWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
+}
+function applyCwAutoUi(enabled) {
+ if (cwAutoInput) cwAutoInput.checked = enabled;
if (cwWpmInput) {
- cwWpmInput.addEventListener("change", () => {
- void (async () => {
- if (cwAutoInput?.checked) return;
- const wpm = clampCwWpm(cwWpmInput.value);
- cwWpmInput.value = String(wpm);
- try {
- await cwWindow.postPath?.(`/set_cw_wpm?wpm=${encodeURIComponent(wpm)}`);
- } catch (error) {
- console.error("CW WPM set failed", error);
- }
- })();
- });
+ cwWpmInput.disabled = enabled;
+ cwWpmInput.readOnly = enabled;
}
if (cwToneInput) {
- cwToneInput.addEventListener("change", () => {
- if (!cwAutoInput?.checked) void setCwTone(cwToneInput.value);
- });
+ cwToneInput.disabled = enabled;
+ cwToneInput.readOnly = enabled;
}
- if (cwToneCanvas) {
- cwToneCanvas.addEventListener("click", (event) => {
- if (cwAutoInput?.checked) return;
- const rect = cwToneCanvas.getBoundingClientRect();
- if (rect.width <= 0) return;
- const range = currentCwToneRange();
- if (!range) return;
- const frac = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width));
- const tone = range.toneMinHz + frac * range.toneSpanHz;
- void setCwTone(tone);
- });
+ if (cwTonePickerEl) {
+ cwTonePickerEl.classList.toggle("is-auto", enabled);
}
- function resetCwHistoryView() {
- if (cwOutputEl) cwOutputEl.innerHTML = "";
- cwLastAppendTime = 0;
- cwBarHistory = [];
- cwBarCurrentLine = null;
- updateCwBar();
- drawCwTonePicker();
+}
+cwWindow.applyCwAutoUi = applyCwAutoUi;
+cwWindow.applyCwAutoUiFromServer = function(enabled) {
+ if (cwAutoLocalOverride !== null) return;
+ applyCwAutoUi(enabled);
+};
+function cwBarFlushCurrentLine() {
+ if (cwBarCurrentLine && cwBarCurrentLine.text.trim()) {
+ cwBarHistory.unshift(cwBarCurrentLine);
+ if (cwBarHistory.length > 50) cwBarHistory.length = 50;
}
- document.getElementById("settings-clear-cw-history")?.addEventListener("click", () => {
+ cwBarCurrentLine = null;
+}
+function updateCwBar() {
+ if (!cwBarOverlay) return;
+ const mode = (document.getElementById("mode")?.value || "").toUpperCase();
+ const isCw = mode === "CW" || mode === "CWR";
+ const cutoffMs = Date.now() - CW_BAR_WINDOW_MS;
+ const recent = cwBarHistory.filter((l) => l.tsMs >= cutoffMs);
+ const liveLines = cwBarCurrentLine && cwBarCurrentLine.text ? [cwBarCurrentLine, ...recent] : recent;
+ const newestTsMs = liveLines.reduce((latest, line) => Math.max(latest, line.tsMs || 0), 0);
+ if (!isCw || liveLines.length === 0 || newestTsMs <= cwBarDismissedAtMs) {
+ cwBarOverlay.style.display = "none";
+ cwBarOverlay.innerHTML = "";
+ return;
+ }
+ let html = ``;
+ for (const line of liveLines.slice(0, 8)) {
+ const ts = line.ts ? `${line.ts}` : "";
+ const meta = [
+ line.wpm ? `${String(line.wpm)} WPM` : null,
+ line.tone_hz ? `${String(line.tone_hz)} Hz` : null
+ ].filter(Boolean).join(" · ");
+ html += `${ts}${escapeCwHtml(line.text)}` + (meta ? ` ${escapeCwHtml(meta)}` : "") + `
`;
+ }
+ cwBarOverlay.innerHTML = html;
+ cwBarOverlay.style.display = "flex";
+}
+cwWindow.updateCwBar = updateCwBar;
+cwWindow.clearCwBar = function() {
+ resetCwHistoryView();
+};
+cwWindow.closeCwBar = function() {
+ cwBarDismissedAtMs = Date.now();
+ if (cwBarOverlay) {
+ cwBarOverlay.style.display = "none";
+ cwBarOverlay.innerHTML = "";
+ }
+};
+function clampCwWpm(wpm) {
+ const numeric = Number(wpm);
+ if (!Number.isFinite(numeric)) return 15;
+ return Math.round(Math.max(CW_WPM_MIN, Math.min(CW_WPM_MAX, numeric)));
+}
+function clampCwTone(tone) {
+ const numeric = Number(tone);
+ if (!Number.isFinite(numeric)) return 700;
+ return Math.round(Math.max(CW_TONE_MIN_HZ, Math.min(CW_TONE_MAX_HZ, numeric)));
+}
+function currentCwToneRange() {
+ const tunedHz = Number.isFinite(cwWindow.lastFreqHz) ? Number(cwWindow.lastFreqHz) : NaN;
+ const bandwidthHz = Number.isFinite(cwWindow.currentBandwidthHz) ? Number(cwWindow.currentBandwidthHz) : NaN;
+ if (!Number.isFinite(tunedHz) || !Number.isFinite(bandwidthHz) || bandwidthHz <= 0) {
+ return null;
+ }
+ const mode = (document.getElementById("mode")?.value || "").toUpperCase();
+ const lowerSideband = mode === "CWR";
+ const upperSideband = mode === "CW";
+ if (!lowerSideband && !upperSideband) return null;
+ const toneMinHz = CW_TONE_MIN_HZ;
+ const toneMaxHz = CW_TONE_MAX_HZ;
+ return {
+ tunedHz,
+ bandwidthHz,
+ toneMinHz,
+ toneMaxHz,
+ toneSpanHz: Math.max(1, toneMaxHz - toneMinHz),
+ lowerSideband,
+ mode
+ };
+}
+function cwToneToRfHz(range, toneHz) {
+ if (!range) return NaN;
+ return range.lowerSideband ? range.tunedHz - toneHz : range.tunedHz + toneHz;
+}
+function toneClampForRange(tone, range) {
+ const clamped = clampCwTone(tone);
+ if (!range) return clamped;
+ return Math.max(range.toneMinHz, Math.min(range.toneMaxHz, clamped));
+}
+function ensureCwToneCanvasResolution() {
+ if (!cwToneCanvas || !cwToneGl || !cwToneGl.ready) return false;
+ const rect = cwToneCanvas.getBoundingClientRect();
+ const cssWidth = Math.round(rect.width);
+ const cssHeight = Math.round(rect.height);
+ if (cssWidth < 8 || cssHeight < 8) {
+ return false;
+ }
+ const dpr = window.devicePixelRatio || 1;
+ return cwToneGl.ensureSize(cssWidth, cssHeight, dpr);
+}
+function drawCwTonePicker() {
+ if (!cwToneCanvas || !cwToneGl || !cwToneGl.ready) return;
+ ensureCwToneCanvasResolution();
+ if (cwToneCanvas.width < 8 || cwToneCanvas.height < 8) return;
+ const width = cwToneCanvas.width;
+ const height = cwToneCanvas.height;
+ cwToneGl.clear([0, 0, 0, 0]);
+ const range = currentCwToneRange();
+ if (!cwWindow.lastSpectrumData || !Array.isArray(cwWindow.lastSpectrumData.bins) || !cwWindow.lastSpectrumData.bins.length || !range) {
+ if (cwToneRangeEl) {
+ const mode = (document.getElementById("mode")?.value || "").toUpperCase();
+ if (mode !== "CW" && mode !== "CWR") {
+ cwToneRangeEl.textContent = "CW/CWR mode required";
+ } else if (!cwWindow.lastSpectrumData || !Array.isArray(cwWindow.lastSpectrumData.bins) || !cwWindow.lastSpectrumData.bins.length) {
+ cwToneRangeEl.textContent = "Waiting for spectrum";
+ }
+ }
+ cwToneGl.fillRect(0, 0, width, height, [130 / 255, 150 / 255, 165 / 255, 0.22]);
+ return;
+ }
+ if (cwToneRangeEl) {
+ const side = range.lowerSideband ? "Lower side" : "Upper side";
+ cwToneRangeEl.textContent = `Audio ${String(range.toneMinHz)}-${String(range.toneMaxHz)} Hz · ${side}`;
+ }
+ const bins = cwWindow.lastSpectrumData.bins;
+ const sampleRate = cwWindow.lastSpectrumData.sample_rate;
+ const centerHz = cwWindow.lastSpectrumData.center_hz;
+ const maxIdx = Math.max(1, bins.length - 1);
+ const fullLoHz = centerHz - sampleRate / 2;
+ const tones = new Array(width).fill(-140);
+ for (let x = 0; x < width; x += 1) {
+ const frac = width <= 1 ? 0 : x / (width - 1);
+ const toneHz = range.toneMinHz + frac * range.toneSpanHz;
+ const rfHz = cwToneToRfHz(range, toneHz);
+ const idx = Math.max(0, Math.min(maxIdx, Math.round((rfHz - fullLoHz) / sampleRate * maxIdx)));
+ const power = Number.isFinite(Number(bins[idx])) ? Number(bins[idx]) : -140;
+ tones[x] = power;
+ }
+ const smoothed = new Array(width).fill(-140);
+ const smoothRadius = Math.max(1, Math.round(width / 180));
+ for (let x = 0; x < width; x += 1) {
+ let sum = 0;
+ let count = 0;
+ for (let i = x - smoothRadius; i <= x + smoothRadius; i += 1) {
+ if (i < 0 || i >= width) continue;
+ sum += tones[i] ?? -140;
+ count += 1;
+ }
+ smoothed[x] = count > 0 ? sum / count : tones[x] ?? -140;
+ }
+ const sorted = smoothed.slice().sort((a, b) => a - b);
+ const q20 = sorted[Math.floor((sorted.length - 1) * 0.2)] ?? -120;
+ const q95 = sorted[Math.floor((sorted.length - 1) * 0.95)] ?? -70;
+ const floorDb = Math.min(q20 - 2, q95 - 10);
+ const ceilDb = Math.max(floorDb + 18, q95 + 2);
+ const dbSpan = Math.max(1, ceilDb - floorDb);
+ const yForDb = (db) => {
+ const n = Math.max(0, Math.min(1, (db - floorDb) / dbSpan));
+ return Math.round((1 - n) * (height - 1));
+ };
+ const rootStyle = getComputedStyle(document.documentElement);
+ const accent = (rootStyle.getPropertyValue("--accent-green") || "").trim() || "#00d17f";
+ const parseColor = typeof cwWindow.trxParseCssColor === "function" ? cwWindow.trxParseCssColor : null;
+ const accentRgba = parseColor ? parseColor(accent) : [0, 0.82, 0.5, 1];
+ const axisColor = [230 / 255, 235 / 255, 245 / 255, 0.15];
+ cwToneGl.fillRect(0, 0, width, height, [7 / 255, 12 / 255, 18 / 255, 0.94]);
+ const hGridCount = 4;
+ const gridSegments = [];
+ for (let i = 1; i <= hGridCount; i += 1) {
+ const y = Math.round(i / (hGridCount + 1) * (height - 1));
+ gridSegments.push(0, y, width, y);
+ }
+ cwToneGl.drawSegments(gridSegments, axisColor, 1);
+ const toneStep = range.toneSpanHz <= 500 ? 50 : range.toneSpanHz <= 1e3 ? 100 : 200;
+ const firstTick = Math.ceil(range.toneMinHz / toneStep) * toneStep;
+ const tickSegments = [];
+ for (let tone = firstTick; tone <= range.toneMaxHz; tone += toneStep) {
+ const frac = (tone - range.toneMinHz) / range.toneSpanHz;
+ const x = Math.max(0, Math.min(width - 1, Math.round(frac * (width - 1))));
+ tickSegments.push(x, 0, x, height);
+ }
+ cwToneGl.drawSegments(tickSegments, axisColor, 1);
+ const linePoints = [];
+ for (let x = 0; x < width; x += 1) {
+ linePoints.push(x, yForDb(smoothed[x] ?? -140));
+ }
+ cwToneGl.drawFilledArea(linePoints, height, [accentRgba[0], accentRgba[1], accentRgba[2], 0.24]);
+ cwToneGl.drawPolyline(linePoints, accentRgba, Math.max(1.2, (window.devicePixelRatio || 1) * 1.2));
+ const currentTone = toneClampForRange(cwToneInput ? cwToneInput.value : 700, range);
+ const markerFrac = (currentTone - range.toneMinHz) / range.toneSpanHz;
+ const markerX = Math.max(0, Math.min(width - 1, Math.round(markerFrac * (width - 1))));
+ const markerY = yForDb(smoothed[Math.max(0, Math.min(width - 1, markerX))] ?? -140);
+ cwToneGl.drawSegments([markerX, 0, markerX, height], [1, 1, 1, 0.9], 1.5);
+ cwToneGl.drawPoints([markerX, markerY], Math.max(2, Math.round(height * 0.055)), [1, 1, 1, 0.9]);
+ if (cwAutoInput?.checked) {
+ cwToneGl.fillRect(0, 0, width, height, [0, 0, 0, 0.22]);
+ }
+}
+async function setCwTone(tone, { syncInput = true } = {}) {
+ const range = currentCwToneRange();
+ const clamped = toneClampForRange(tone, range);
+ if (cwToneInput && syncInput) {
+ cwToneInput.value = String(clamped);
+ }
+ try {
+ await cwWindow.postPath?.(`/set_cw_tone?tone_hz=${encodeURIComponent(clamped)}`);
+ } catch (e) {
+ console.error("CW tone set failed", e);
+ }
+ drawCwTonePicker();
+}
+if (cwAutoInput) {
+ cwAutoInput.addEventListener("change", () => {
void (async () => {
- if (!await cwWindow.trxUi.confirm({ title: "Clear CW history?", message: "All stored CW decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
+ const enabled = cwAutoInput.checked;
+ cwAutoLocalOverride = enabled;
+ applyCwAutoUi(enabled);
try {
- await cwWindow.postPath?.("/clear_cw_decode");
- resetCwHistoryView();
+ await cwWindow.postPath?.(`/set_cw_auto?enabled=${enabled ? "true" : "false"}`);
+ drawCwTonePicker();
} catch (error) {
- console.error("CW history clear failed", error);
+ console.error("CW auto toggle failed", error);
+ } finally {
+ cwAutoLocalOverride = null;
}
})();
});
- function onServerCw(evt) {
- if (cwStatusEl) cwStatusEl.textContent = "Receiving";
- if (evt.text && cwOutputEl) {
- const now = Date.now();
- if (!cwOutputEl.lastElementChild || now - cwLastAppendTime > 1e4 || evt.text === "\n") {
- const line = document.createElement("div");
- line.className = "cw-line";
- cwOutputEl.appendChild(line);
+}
+if (cwWpmInput) {
+ cwWpmInput.addEventListener("change", () => {
+ void (async () => {
+ if (cwAutoInput?.checked) return;
+ const wpm = clampCwWpm(cwWpmInput.value);
+ cwWpmInput.value = String(wpm);
+ try {
+ await cwWindow.postPath?.(`/set_cw_wpm?wpm=${encodeURIComponent(wpm)}`);
+ } catch (error) {
+ console.error("CW WPM set failed", error);
}
- cwLastAppendTime = now;
- const lastLine = cwOutputEl.lastElementChild;
- if (lastLine) {
- lastLine.textContent += evt.text;
- }
- while (cwOutputEl.children.length > CW_MAX_LINES) {
- const firstChild = cwOutputEl.firstChild;
- if (!firstChild) break;
- cwOutputEl.removeChild(firstChild);
- }
- cwOutputEl.scrollTop = cwOutputEl.scrollHeight;
- }
- if (evt.text) {
- const now = Date.now();
- if (evt.text === "\n") {
- cwBarFlushCurrentLine();
- } else {
- if (!cwBarCurrentLine || now - cwBarCurrentLine.lastMs > CW_BAR_LINE_GAP_MS) {
- cwBarFlushCurrentLine();
- const ts = new Date(now).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
- cwBarCurrentLine = { tsMs: now, ts, text: "", wpm: null, tone_hz: null, lastMs: now };
- }
- cwBarCurrentLine.text += evt.text;
- cwBarCurrentLine.lastMs = now;
- if (Number.isFinite(Number(evt.wpm))) cwBarCurrentLine.wpm = clampCwWpm(evt.wpm);
- if (Number.isFinite(Number(evt.tone_hz))) cwBarCurrentLine.tone_hz = Math.round(Number(evt.tone_hz));
- }
- updateCwBar();
- }
- if (cwSignalIndicator) {
- cwSignalIndicator.className = evt.signal_on ? "cw-signal-on" : "cw-signal-off";
- }
- if (!cwAutoInput || cwAutoInput.checked) {
- if (cwWpmInput && Number.isFinite(Number(evt.wpm))) {
- cwWpmInput.value = String(clampCwWpm(evt.wpm));
- }
- if (cwToneInput && Number.isFinite(Number(evt.tone_hz))) {
- cwToneInput.value = String(toneClampForRange(evt.tone_hz, currentCwToneRange()));
- }
- }
- if (cwTonePickerRaf != null) return;
- cwTonePickerRaf = requestAnimationFrame(() => {
- cwTonePickerRaf = null;
- drawCwTonePicker();
- });
- }
- function restoreCwHistory(events) {
- if (!Array.isArray(events) || events.length === 0) return;
- if (cwStatusEl) cwStatusEl.textContent = "Receiving";
- for (const evt of events) {
- onServerCw(evt);
- }
- }
- cwWindow.trxPluginRuntime.registerDecoder({
- id: "cw",
- onMessage: onServerCw,
- restore: restoreCwHistory,
- reset: resetCwHistoryView
+ })();
});
- cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
- ensureCwToneCanvasResolution();
- drawCwTonePicker();
- };
- window.addEventListener("resize", () => {
- if (ensureCwToneCanvasResolution()) drawCwTonePicker();
+}
+if (cwToneInput) {
+ cwToneInput.addEventListener("change", () => {
+ if (!cwAutoInput?.checked) void setCwTone(cwToneInput.value);
});
- applyCwAutoUi(!!cwAutoInput?.checked);
+}
+if (cwToneCanvas) {
+ cwToneCanvas.addEventListener("click", (event) => {
+ if (cwAutoInput?.checked) return;
+ const rect = cwToneCanvas.getBoundingClientRect();
+ if (rect.width <= 0) return;
+ const range = currentCwToneRange();
+ if (!range) return;
+ const frac = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width));
+ const tone = range.toneMinHz + frac * range.toneSpanHz;
+ void setCwTone(tone);
+ });
+}
+function resetCwHistoryView() {
+ if (cwOutputEl) cwOutputEl.innerHTML = "";
+ cwLastAppendTime = 0;
+ cwBarHistory = [];
+ cwBarCurrentLine = null;
updateCwBar();
+ drawCwTonePicker();
+}
+document.getElementById("settings-clear-cw-history")?.addEventListener("click", () => {
+ void (async () => {
+ if (!await cwWindow.trxUi.confirm({ title: "Clear CW history?", message: "All stored CW decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
+ try {
+ await cwWindow.postPath?.("/clear_cw_decode");
+ resetCwHistoryView();
+ } catch (error) {
+ console.error("CW history clear failed", error);
+ }
+ })();
+});
+function onServerCw(evt) {
+ if (cwStatusEl) cwStatusEl.textContent = "Receiving";
+ if (evt.text && cwOutputEl) {
+ const now = Date.now();
+ if (!cwOutputEl.lastElementChild || now - cwLastAppendTime > 1e4 || evt.text === "\n") {
+ const line = document.createElement("div");
+ line.className = "cw-line";
+ cwOutputEl.appendChild(line);
+ }
+ cwLastAppendTime = now;
+ const lastLine = cwOutputEl.lastElementChild;
+ if (lastLine) {
+ lastLine.textContent += evt.text;
+ }
+ while (cwOutputEl.children.length > CW_MAX_LINES) {
+ const firstChild = cwOutputEl.firstChild;
+ if (!firstChild) break;
+ cwOutputEl.removeChild(firstChild);
+ }
+ cwOutputEl.scrollTop = cwOutputEl.scrollHeight;
+ }
+ if (evt.text) {
+ const now = Date.now();
+ if (evt.text === "\n") {
+ cwBarFlushCurrentLine();
+ } else {
+ if (!cwBarCurrentLine || now - cwBarCurrentLine.lastMs > CW_BAR_LINE_GAP_MS) {
+ cwBarFlushCurrentLine();
+ const ts = new Date(now).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
+ cwBarCurrentLine = { tsMs: now, ts, text: "", wpm: null, tone_hz: null, lastMs: now };
+ }
+ cwBarCurrentLine.text += evt.text;
+ cwBarCurrentLine.lastMs = now;
+ if (Number.isFinite(Number(evt.wpm))) cwBarCurrentLine.wpm = clampCwWpm(evt.wpm);
+ if (Number.isFinite(Number(evt.tone_hz))) cwBarCurrentLine.tone_hz = Math.round(Number(evt.tone_hz));
+ }
+ updateCwBar();
+ }
+ if (cwSignalIndicator) {
+ cwSignalIndicator.className = evt.signal_on ? "cw-signal-on" : "cw-signal-off";
+ }
+ if (!cwAutoInput || cwAutoInput.checked) {
+ if (cwWpmInput && Number.isFinite(Number(evt.wpm))) {
+ cwWpmInput.value = String(clampCwWpm(evt.wpm));
+ }
+ if (cwToneInput && Number.isFinite(Number(evt.tone_hz))) {
+ cwToneInput.value = String(toneClampForRange(evt.tone_hz, currentCwToneRange()));
+ }
+ }
+ if (cwTonePickerRaf != null) return;
+ cwTonePickerRaf = requestAnimationFrame(() => {
+ cwTonePickerRaf = null;
+ drawCwTonePicker();
+ });
+}
+function restoreCwHistory(events) {
+ if (!Array.isArray(events) || events.length === 0) return;
+ if (cwStatusEl) cwStatusEl.textContent = "Receiving";
+ for (const evt of events) {
+ onServerCw(evt);
+ }
+}
+cwWindow.trxPluginRuntime.registerDecoder({
+ id: "cw",
+ onMessage: onServerCw,
+ restore: restoreCwHistory,
+ reset: resetCwHistoryView
+});
+cwWindow.refreshCwTonePicker = function refreshCwTonePicker() {
ensureCwToneCanvasResolution();
drawCwTonePicker();
-})();
+};
+window.addEventListener("resize", () => {
+ if (ensureCwToneCanvasResolution()) drawCwTonePicker();
+});
+applyCwAutoUi(!!cwAutoInput?.checked);
+updateCwBar();
+ensureCwToneCanvasResolution();
+drawCwTonePicker();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft2.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft2.js
index f9f1872b..bb0f2236 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft2.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft2.js
@@ -1,227 +1,6 @@
-"use strict";
-(() => {
- // src/plugins/ftx-family.ts
- var bridge = window;
- function finiteNumber(value) {
- const number = typeof value === "number" ? value : Number(value);
- return Number.isFinite(number) ? number : null;
- }
- function isAlphaNumeric(value) {
- return value !== void 0 && /[A-Za-z0-9]/.test(value);
- }
- function isGrid(value) {
- const normalized = value.trim().toUpperCase();
- return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && normalized !== "RR73" && normalized !== "73" && normalized !== "RR";
- }
- function escapeFtxHtml(input) {
- return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
- }
- function extractFtxGrids(message) {
- return [...new Set(message.toUpperCase().split(/[^A-Z0-9]+/).filter(isGrid))];
- }
- function tokenize(message) {
- return message.toUpperCase().split(/[^A-Z0-9/]+/).filter(Boolean);
- }
- function isCallsign(token) {
- return token !== void 0 && token.length >= 3 && token.length <= 12 && !["CQ", "DE", "QRZ", "DX"].includes(token) && !isGrid(token) && /^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token);
- }
- function extractFtxLocatorDetails(message) {
- const tokens = tokenize(message);
- const grids = extractFtxGrids(message);
- const gridIndex = tokens.findIndex(isGrid);
- const callsigns = tokens.slice(0, gridIndex < 0 ? tokens.length : gridIndex).filter(isCallsign);
- const directed = callsigns.length >= 2 && !["CQ", "DE", "QRZ"].includes(tokens[0] ?? "");
- const source = directed ? callsigns[1] ?? null : callsigns[0] ?? null;
- const target = directed ? callsigns[0] ?? null : null;
- return grids.map((grid) => ({ grid, station: source, source, target }));
- }
- function extractFtxCallsign(message) {
- return extractFtxLocatorDetails(message)[0]?.station ?? tokenize(message).find(isCallsign) ?? null;
- }
- function renderFtxMessage(message) {
- let html = "";
- let index = 0;
- while (index < message.length) {
- if (!isAlphaNumeric(message[index])) {
- html += escapeFtxHtml(message[index] ?? "");
- index += 1;
- continue;
- }
- let end = index + 1;
- while (end < message.length && isAlphaNumeric(message[end])) end += 1;
- const token = message.slice(index, end);
- const grid = token.toUpperCase();
- html += isGrid(grid) ? `${grid}` : escapeFtxHtml(token);
- index = end;
- }
- return html;
- }
- function initializeFtxDecoder(config) {
- const { id, label, periodMs, periodDigits = 1 } = config;
- const status = document.getElementById(`${id}-status`);
- const period = document.getElementById(`${id}-period`);
- const messagesElement = document.getElementById(`${id}-messages`);
- const filterInput = document.getElementById(`${id}-filter`);
- let filterText = "";
- let history = [];
- const renderMessage = (message) => {
- return bridge.renderFt8Message?.(message) ?? renderFtxMessage(message);
- };
- const retentionMs = () => bridge.getDecodeHistoryRetentionMs?.() ?? 864e5;
- const prune = () => {
- const cutoff = Date.now() - retentionMs();
- history = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= cutoff);
- };
- const schedule = (job) => {
- if (bridge.trxScheduleUiFrameJob) bridge.trxScheduleUiFrameJob(`${id}-history`, job);
- else job();
- };
- const displayFrequency = (value) => {
- const raw = finiteNumber(value);
- if (raw === null) return null;
- const base = finiteNumber(bridge.ft8BaseHz);
- return base !== null && base > 0 && raw >= 0 && raw < 1e5 ? base + raw : raw;
- };
- const renderRow = (message) => {
- const row = document.createElement("div");
- row.className = "ft8-row";
- const raw = message.message ?? "";
- row.dataset.message = raw.toUpperCase();
- row.dataset.decoder = id;
- const storedFrequency = finiteNumber(message.freq_hz);
- row.dataset.storedFreqHz = storedFrequency === null ? "" : String(storedFrequency);
- const snr = finiteNumber(message.snr_db);
- const delta = finiteNumber(message.dt_s);
- const frequency = displayFrequency(message.freq_hz);
- const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
- const time = timestamp === null ? "--:--:--" : new Date(timestamp).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit"
- });
- row.innerHTML = `${time}${snr?.toFixed(1) ?? "--"}${delta?.toFixed(2) ?? "--"}${frequency?.toFixed(0) ?? "--"}${renderMessage(raw)}`;
- return row;
- };
- const render = () => {
- prune();
- if (!messagesElement) return;
- const fragment = document.createDocumentFragment();
- let count = 0;
- for (const message of history) {
- if (count >= 200) break;
- if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue;
- fragment.appendChild(renderRow(message));
- count += 1;
- }
- messagesElement.replaceChildren(fragment);
- };
- const normalize = (message) => {
- const raw = message.message ?? "";
- const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw);
- const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw);
- const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw);
- const frequency = displayFrequency(message.freq_hz);
- if (grids.length > 0) {
- bridge.mapAddLocator?.(raw, grids, id, station, {
- ...message,
- freq_hz: frequency ?? message.freq_hz,
- locator_details: locatorDetails
- });
- }
- return {
- receiver: bridge.getDecodeRigMeta?.() ?? null,
- ts_ms: message.ts_ms,
- snr_db: message.snr_db,
- dt_s: message.dt_s,
- freq_hz: frequency ?? message.freq_hz,
- message: message.message,
- _tsMs: finiteNumber(message.ts_ms) ?? Date.now()
- };
- };
- const receiveBatch = (messages) => {
- if (messages.length === 0) return;
- if (status) status.textContent = "Receiving";
- history = messages.map(normalize).reverse().concat(history);
- prune();
- bridge.setFt8FamilyBarDecoder?.(id);
- bridge.updateFt8Bar?.();
- schedule(render);
- };
- const reset = () => {
- history = [];
- bridge.updateFt8Bar?.();
- render();
- bridge.clearMapMarkersByType?.(id);
- };
- const barFrames = () => {
- const recent = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= Date.now() - 9e5).slice(0, 8);
- let html = "";
- for (const message of recent) {
- const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
- const time = timestamp === null ? "" : `${bridge.fmtTime?.(timestamp) ?? ""}`;
- const snr = finiteNumber(message.snr_db);
- const delta = finiteNumber(message.dt_s);
- const frequency = displayFrequency(message.freq_hz);
- const detail = [snr === null ? "-- dB" : `${snr.toFixed(1)} dB`, delta === null ? null : `dt ${delta.toFixed(2)}`, frequency === null ? null : `${frequency.toFixed(0)} Hz`].filter((part) => part !== null).join(" · ");
- html += `${time}${renderMessage(message.message ?? "")}${detail ? ` · ${detail}` : ""}
`;
- }
- return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
- };
- bridge.trxPluginRuntime.registerDecoder({
- id,
- onMessage: (message) => {
- receiveBatch([message]);
- },
- onBatch: receiveBatch,
- restore: receiveBatch,
- prune: () => {
- prune();
- render();
- },
- reset
- });
- bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
- const updatePeriod = () => {
- if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`;
- };
- updatePeriod();
- window.setInterval(updatePeriod, 250);
- filterInput?.addEventListener("input", () => {
- filterText = filterInput.value.trim().toUpperCase();
- render();
- });
- messagesElement?.addEventListener("click", (event) => {
- if (!(event.target instanceof Element)) return;
- const grid = event.target.closest(".ft8-locator[data-locator-grid]")?.dataset.locatorGrid;
- if (grid) {
- bridge.navigateToMapLocator?.(grid.toUpperCase(), id);
- event.preventDefault();
- }
- });
- const toggle = document.getElementById(`${id}-decode-toggle-btn`);
- toggle?.addEventListener("click", () => {
- void (async () => {
- try {
- await bridge.takeSchedulerControlForDecoderDisable?.(toggle);
- await bridge.postPath?.(`/toggle_${id}_decode`);
- } catch (error) {
- console.error(`${label} toggle failed`, error);
- }
- })();
- });
- document.getElementById(`settings-clear-${id}-history`)?.addEventListener("click", () => {
- void (async () => {
- if (!await bridge.trxUi.confirm({ title: `Clear ${label} history?`, message: `All stored ${label} decodes will be permanently removed.`, confirmLabel: "Clear history" })) return;
- try {
- await bridge.postPath?.(`/clear_${id}_decode`);
- reset();
- } catch (error) {
- console.error(`${label} history clear failed`, error);
- }
- })();
- });
- }
+import {
+ initializeFtxDecoder
+} from "./chunk-SGMG5LG2.js";
- // src/plugins/ft2.ts
- initializeFtxDecoder({ id: "ft2", label: "FT2", periodMs: 3750 });
-})();
+// src/plugins/ft2.ts
+initializeFtxDecoder({ id: "ft2", label: "FT2", periodMs: 3750 });
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft4.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft4.js
index 0076c658..bafde67f 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft4.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft4.js
@@ -1,227 +1,6 @@
-"use strict";
-(() => {
- // src/plugins/ftx-family.ts
- var bridge = window;
- function finiteNumber(value) {
- const number = typeof value === "number" ? value : Number(value);
- return Number.isFinite(number) ? number : null;
- }
- function isAlphaNumeric(value) {
- return value !== void 0 && /[A-Za-z0-9]/.test(value);
- }
- function isGrid(value) {
- const normalized = value.trim().toUpperCase();
- return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && normalized !== "RR73" && normalized !== "73" && normalized !== "RR";
- }
- function escapeFtxHtml(input) {
- return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
- }
- function extractFtxGrids(message) {
- return [...new Set(message.toUpperCase().split(/[^A-Z0-9]+/).filter(isGrid))];
- }
- function tokenize(message) {
- return message.toUpperCase().split(/[^A-Z0-9/]+/).filter(Boolean);
- }
- function isCallsign(token) {
- return token !== void 0 && token.length >= 3 && token.length <= 12 && !["CQ", "DE", "QRZ", "DX"].includes(token) && !isGrid(token) && /^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token);
- }
- function extractFtxLocatorDetails(message) {
- const tokens = tokenize(message);
- const grids = extractFtxGrids(message);
- const gridIndex = tokens.findIndex(isGrid);
- const callsigns = tokens.slice(0, gridIndex < 0 ? tokens.length : gridIndex).filter(isCallsign);
- const directed = callsigns.length >= 2 && !["CQ", "DE", "QRZ"].includes(tokens[0] ?? "");
- const source = directed ? callsigns[1] ?? null : callsigns[0] ?? null;
- const target = directed ? callsigns[0] ?? null : null;
- return grids.map((grid) => ({ grid, station: source, source, target }));
- }
- function extractFtxCallsign(message) {
- return extractFtxLocatorDetails(message)[0]?.station ?? tokenize(message).find(isCallsign) ?? null;
- }
- function renderFtxMessage(message) {
- let html = "";
- let index = 0;
- while (index < message.length) {
- if (!isAlphaNumeric(message[index])) {
- html += escapeFtxHtml(message[index] ?? "");
- index += 1;
- continue;
- }
- let end = index + 1;
- while (end < message.length && isAlphaNumeric(message[end])) end += 1;
- const token = message.slice(index, end);
- const grid = token.toUpperCase();
- html += isGrid(grid) ? `${grid}` : escapeFtxHtml(token);
- index = end;
- }
- return html;
- }
- function initializeFtxDecoder(config) {
- const { id, label, periodMs, periodDigits = 1 } = config;
- const status = document.getElementById(`${id}-status`);
- const period = document.getElementById(`${id}-period`);
- const messagesElement = document.getElementById(`${id}-messages`);
- const filterInput = document.getElementById(`${id}-filter`);
- let filterText = "";
- let history = [];
- const renderMessage = (message) => {
- return bridge.renderFt8Message?.(message) ?? renderFtxMessage(message);
- };
- const retentionMs = () => bridge.getDecodeHistoryRetentionMs?.() ?? 864e5;
- const prune = () => {
- const cutoff = Date.now() - retentionMs();
- history = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= cutoff);
- };
- const schedule = (job) => {
- if (bridge.trxScheduleUiFrameJob) bridge.trxScheduleUiFrameJob(`${id}-history`, job);
- else job();
- };
- const displayFrequency = (value) => {
- const raw = finiteNumber(value);
- if (raw === null) return null;
- const base = finiteNumber(bridge.ft8BaseHz);
- return base !== null && base > 0 && raw >= 0 && raw < 1e5 ? base + raw : raw;
- };
- const renderRow = (message) => {
- const row = document.createElement("div");
- row.className = "ft8-row";
- const raw = message.message ?? "";
- row.dataset.message = raw.toUpperCase();
- row.dataset.decoder = id;
- const storedFrequency = finiteNumber(message.freq_hz);
- row.dataset.storedFreqHz = storedFrequency === null ? "" : String(storedFrequency);
- const snr = finiteNumber(message.snr_db);
- const delta = finiteNumber(message.dt_s);
- const frequency = displayFrequency(message.freq_hz);
- const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
- const time = timestamp === null ? "--:--:--" : new Date(timestamp).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit"
- });
- row.innerHTML = `${time}${snr?.toFixed(1) ?? "--"}${delta?.toFixed(2) ?? "--"}${frequency?.toFixed(0) ?? "--"}${renderMessage(raw)}`;
- return row;
- };
- const render = () => {
- prune();
- if (!messagesElement) return;
- const fragment = document.createDocumentFragment();
- let count = 0;
- for (const message of history) {
- if (count >= 200) break;
- if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue;
- fragment.appendChild(renderRow(message));
- count += 1;
- }
- messagesElement.replaceChildren(fragment);
- };
- const normalize = (message) => {
- const raw = message.message ?? "";
- const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw);
- const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw);
- const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw);
- const frequency = displayFrequency(message.freq_hz);
- if (grids.length > 0) {
- bridge.mapAddLocator?.(raw, grids, id, station, {
- ...message,
- freq_hz: frequency ?? message.freq_hz,
- locator_details: locatorDetails
- });
- }
- return {
- receiver: bridge.getDecodeRigMeta?.() ?? null,
- ts_ms: message.ts_ms,
- snr_db: message.snr_db,
- dt_s: message.dt_s,
- freq_hz: frequency ?? message.freq_hz,
- message: message.message,
- _tsMs: finiteNumber(message.ts_ms) ?? Date.now()
- };
- };
- const receiveBatch = (messages) => {
- if (messages.length === 0) return;
- if (status) status.textContent = "Receiving";
- history = messages.map(normalize).reverse().concat(history);
- prune();
- bridge.setFt8FamilyBarDecoder?.(id);
- bridge.updateFt8Bar?.();
- schedule(render);
- };
- const reset = () => {
- history = [];
- bridge.updateFt8Bar?.();
- render();
- bridge.clearMapMarkersByType?.(id);
- };
- const barFrames = () => {
- const recent = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= Date.now() - 9e5).slice(0, 8);
- let html = "";
- for (const message of recent) {
- const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
- const time = timestamp === null ? "" : `${bridge.fmtTime?.(timestamp) ?? ""}`;
- const snr = finiteNumber(message.snr_db);
- const delta = finiteNumber(message.dt_s);
- const frequency = displayFrequency(message.freq_hz);
- const detail = [snr === null ? "-- dB" : `${snr.toFixed(1)} dB`, delta === null ? null : `dt ${delta.toFixed(2)}`, frequency === null ? null : `${frequency.toFixed(0)} Hz`].filter((part) => part !== null).join(" · ");
- html += `${time}${renderMessage(message.message ?? "")}${detail ? ` · ${detail}` : ""}
`;
- }
- return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
- };
- bridge.trxPluginRuntime.registerDecoder({
- id,
- onMessage: (message) => {
- receiveBatch([message]);
- },
- onBatch: receiveBatch,
- restore: receiveBatch,
- prune: () => {
- prune();
- render();
- },
- reset
- });
- bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
- const updatePeriod = () => {
- if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`;
- };
- updatePeriod();
- window.setInterval(updatePeriod, 250);
- filterInput?.addEventListener("input", () => {
- filterText = filterInput.value.trim().toUpperCase();
- render();
- });
- messagesElement?.addEventListener("click", (event) => {
- if (!(event.target instanceof Element)) return;
- const grid = event.target.closest(".ft8-locator[data-locator-grid]")?.dataset.locatorGrid;
- if (grid) {
- bridge.navigateToMapLocator?.(grid.toUpperCase(), id);
- event.preventDefault();
- }
- });
- const toggle = document.getElementById(`${id}-decode-toggle-btn`);
- toggle?.addEventListener("click", () => {
- void (async () => {
- try {
- await bridge.takeSchedulerControlForDecoderDisable?.(toggle);
- await bridge.postPath?.(`/toggle_${id}_decode`);
- } catch (error) {
- console.error(`${label} toggle failed`, error);
- }
- })();
- });
- document.getElementById(`settings-clear-${id}-history`)?.addEventListener("click", () => {
- void (async () => {
- if (!await bridge.trxUi.confirm({ title: `Clear ${label} history?`, message: `All stored ${label} decodes will be permanently removed.`, confirmLabel: "Clear history" })) return;
- try {
- await bridge.postPath?.(`/clear_${id}_decode`);
- reset();
- } catch (error) {
- console.error(`${label} history clear failed`, error);
- }
- })();
- });
- }
+import {
+ initializeFtxDecoder
+} from "./chunk-SGMG5LG2.js";
- // src/plugins/ft4.ts
- initializeFtxDecoder({ id: "ft4", label: "FT4", periodMs: 7500 });
-})();
+// src/plugins/ft4.ts
+initializeFtxDecoder({ id: "ft4", label: "FT4", periodMs: 7500 });
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft8.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft8.js
index 8c782734..d9d961ce 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft8.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/ft8.js
@@ -1,271 +1,10 @@
-"use strict";
-(() => {
- // src/plugins/ftx-family.ts
- var bridge = window;
- function finiteNumber(value) {
- const number = typeof value === "number" ? value : Number(value);
- return Number.isFinite(number) ? number : null;
- }
- function isAlphaNumeric(value) {
- return value !== void 0 && /[A-Za-z0-9]/.test(value);
- }
- function isGrid(value) {
- const normalized = value.trim().toUpperCase();
- return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && normalized !== "RR73" && normalized !== "73" && normalized !== "RR";
- }
- function escapeFtxHtml(input) {
- return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
- }
- function extractFtxGrids(message) {
- return [...new Set(message.toUpperCase().split(/[^A-Z0-9]+/).filter(isGrid))];
- }
- function tokenize(message) {
- return message.toUpperCase().split(/[^A-Z0-9/]+/).filter(Boolean);
- }
- function isCallsign(token) {
- return token !== void 0 && token.length >= 3 && token.length <= 12 && !["CQ", "DE", "QRZ", "DX"].includes(token) && !isGrid(token) && /^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token);
- }
- function extractFtxLocatorDetails(message) {
- const tokens = tokenize(message);
- const grids = extractFtxGrids(message);
- const gridIndex = tokens.findIndex(isGrid);
- const callsigns = tokens.slice(0, gridIndex < 0 ? tokens.length : gridIndex).filter(isCallsign);
- const directed = callsigns.length >= 2 && !["CQ", "DE", "QRZ"].includes(tokens[0] ?? "");
- const source = directed ? callsigns[1] ?? null : callsigns[0] ?? null;
- const target = directed ? callsigns[0] ?? null : null;
- return grids.map((grid) => ({ grid, station: source, source, target }));
- }
- function extractFtxCallsign(message) {
- return extractFtxLocatorDetails(message)[0]?.station ?? tokenize(message).find(isCallsign) ?? null;
- }
- function renderFtxMessage(message) {
- let html = "";
- let index = 0;
- while (index < message.length) {
- if (!isAlphaNumeric(message[index])) {
- html += escapeFtxHtml(message[index] ?? "");
- index += 1;
- continue;
- }
- let end = index + 1;
- while (end < message.length && isAlphaNumeric(message[end])) end += 1;
- const token = message.slice(index, end);
- const grid = token.toUpperCase();
- html += isGrid(grid) ? `${grid}` : escapeFtxHtml(token);
- index = end;
- }
- return html;
- }
- function installFtxCompatibilityHelpers() {
- bridge.renderFt8Message = renderFtxMessage;
- bridge.ft8EscapeHtml = escapeFtxHtml;
- bridge.ft8ExtractLocatorDetails = extractFtxLocatorDetails;
- bridge.ft8ExtractAllGrids = extractFtxGrids;
- bridge.ft8ExtractLikelyCallsign = extractFtxCallsign;
- }
- function initializeFt8FamilyBar() {
- const labels = { ft8: "FT8", ft4: "FT4", ft2: "FT2" };
- const builders = {};
- const dismissed = { ft8: 0, ft4: 0, ft2: 0 };
- const overlay = document.getElementById("ft8-bar-overlay");
- let active = "ft8";
- const update = () => {
- if (!overlay) return;
- const mode = (document.getElementById("mode")?.value ?? "").toUpperCase();
- const result = builders[active]?.();
- if (mode !== "DIG" && mode !== "USB" || !result || result.count === 0 || result.newestTsMs <= dismissed[active]) {
- overlay.style.display = "none";
- overlay.innerHTML = "";
- return;
- }
- const label = labels[active];
- overlay.innerHTML = `${result.html}`;
- overlay.style.display = "flex";
- };
- bridge.registerFt8FamilyBarRenderer = (decoder, builder) => {
- builders[decoder] = builder;
- };
- bridge.setFt8FamilyBarDecoder = (decoder) => {
- active = decoder;
- update();
- };
- bridge.updateFt8Bar = update;
- bridge.clearFt8Bar = () => {
- bridge.trxPluginRuntime.reset(active);
- };
- bridge.closeFt8Bar = () => {
- dismissed[active] = Date.now();
- update();
- };
- }
- function initializeFtxDecoder(config) {
- const { id, label, periodMs, periodDigits = 1 } = config;
- const status = document.getElementById(`${id}-status`);
- const period = document.getElementById(`${id}-period`);
- const messagesElement = document.getElementById(`${id}-messages`);
- const filterInput = document.getElementById(`${id}-filter`);
- let filterText = "";
- let history = [];
- const renderMessage = (message) => {
- return bridge.renderFt8Message?.(message) ?? renderFtxMessage(message);
- };
- const retentionMs = () => bridge.getDecodeHistoryRetentionMs?.() ?? 864e5;
- const prune = () => {
- const cutoff = Date.now() - retentionMs();
- history = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= cutoff);
- };
- const schedule = (job) => {
- if (bridge.trxScheduleUiFrameJob) bridge.trxScheduleUiFrameJob(`${id}-history`, job);
- else job();
- };
- const displayFrequency = (value) => {
- const raw = finiteNumber(value);
- if (raw === null) return null;
- const base = finiteNumber(bridge.ft8BaseHz);
- return base !== null && base > 0 && raw >= 0 && raw < 1e5 ? base + raw : raw;
- };
- const renderRow = (message) => {
- const row = document.createElement("div");
- row.className = "ft8-row";
- const raw = message.message ?? "";
- row.dataset.message = raw.toUpperCase();
- row.dataset.decoder = id;
- const storedFrequency = finiteNumber(message.freq_hz);
- row.dataset.storedFreqHz = storedFrequency === null ? "" : String(storedFrequency);
- const snr = finiteNumber(message.snr_db);
- const delta = finiteNumber(message.dt_s);
- const frequency = displayFrequency(message.freq_hz);
- const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
- const time = timestamp === null ? "--:--:--" : new Date(timestamp).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit"
- });
- row.innerHTML = `${time}${snr?.toFixed(1) ?? "--"}${delta?.toFixed(2) ?? "--"}${frequency?.toFixed(0) ?? "--"}${renderMessage(raw)}`;
- return row;
- };
- const render = () => {
- prune();
- if (!messagesElement) return;
- const fragment = document.createDocumentFragment();
- let count = 0;
- for (const message of history) {
- if (count >= 200) break;
- if (filterText && !(message.message ?? "").toUpperCase().includes(filterText)) continue;
- fragment.appendChild(renderRow(message));
- count += 1;
- }
- messagesElement.replaceChildren(fragment);
- };
- const normalize = (message) => {
- const raw = message.message ?? "";
- const locatorDetails = bridge.ft8ExtractLocatorDetails?.(raw) ?? extractFtxLocatorDetails(raw);
- const grids = locatorDetails.length > 0 ? locatorDetails.map(({ grid }) => grid) : bridge.ft8ExtractAllGrids?.(raw) ?? extractFtxGrids(raw);
- const station = bridge.ft8ExtractLikelyCallsign?.(raw) ?? extractFtxCallsign(raw);
- const frequency = displayFrequency(message.freq_hz);
- if (grids.length > 0) {
- bridge.mapAddLocator?.(raw, grids, id, station, {
- ...message,
- freq_hz: frequency ?? message.freq_hz,
- locator_details: locatorDetails
- });
- }
- return {
- receiver: bridge.getDecodeRigMeta?.() ?? null,
- ts_ms: message.ts_ms,
- snr_db: message.snr_db,
- dt_s: message.dt_s,
- freq_hz: frequency ?? message.freq_hz,
- message: message.message,
- _tsMs: finiteNumber(message.ts_ms) ?? Date.now()
- };
- };
- const receiveBatch = (messages) => {
- if (messages.length === 0) return;
- if (status) status.textContent = "Receiving";
- history = messages.map(normalize).reverse().concat(history);
- prune();
- bridge.setFt8FamilyBarDecoder?.(id);
- bridge.updateFt8Bar?.();
- schedule(render);
- };
- const reset = () => {
- history = [];
- bridge.updateFt8Bar?.();
- render();
- bridge.clearMapMarkersByType?.(id);
- };
- const barFrames = () => {
- const recent = history.filter((message) => (finiteNumber(message._tsMs ?? message.ts_ms) ?? 0) >= Date.now() - 9e5).slice(0, 8);
- let html = "";
- for (const message of recent) {
- const timestamp = finiteNumber(message._tsMs ?? message.ts_ms);
- const time = timestamp === null ? "" : `${bridge.fmtTime?.(timestamp) ?? ""}`;
- const snr = finiteNumber(message.snr_db);
- const delta = finiteNumber(message.dt_s);
- const frequency = displayFrequency(message.freq_hz);
- const detail = [snr === null ? "-- dB" : `${snr.toFixed(1)} dB`, delta === null ? null : `dt ${delta.toFixed(2)}`, frequency === null ? null : `${frequency.toFixed(0)} Hz`].filter((part) => part !== null).join(" · ");
- html += `${time}${renderMessage(message.message ?? "")}${detail ? ` · ${detail}` : ""}
`;
- }
- return { count: recent.length, newestTsMs: recent.reduce((latest, message) => Math.max(latest, finiteNumber(message._tsMs ?? message.ts_ms) ?? 0), 0), html };
- };
- bridge.trxPluginRuntime.registerDecoder({
- id,
- onMessage: (message) => {
- receiveBatch([message]);
- },
- onBatch: receiveBatch,
- restore: receiveBatch,
- prune: () => {
- prune();
- render();
- },
- reset
- });
- bridge.registerFt8FamilyBarRenderer?.(id, barFrames);
- const updatePeriod = () => {
- if (period) period.textContent = `Next slot ${((periodMs - Date.now() % periodMs) / 1e3).toFixed(periodDigits)}s`;
- };
- updatePeriod();
- window.setInterval(updatePeriod, 250);
- filterInput?.addEventListener("input", () => {
- filterText = filterInput.value.trim().toUpperCase();
- render();
- });
- messagesElement?.addEventListener("click", (event) => {
- if (!(event.target instanceof Element)) return;
- const grid = event.target.closest(".ft8-locator[data-locator-grid]")?.dataset.locatorGrid;
- if (grid) {
- bridge.navigateToMapLocator?.(grid.toUpperCase(), id);
- event.preventDefault();
- }
- });
- const toggle = document.getElementById(`${id}-decode-toggle-btn`);
- toggle?.addEventListener("click", () => {
- void (async () => {
- try {
- await bridge.takeSchedulerControlForDecoderDisable?.(toggle);
- await bridge.postPath?.(`/toggle_${id}_decode`);
- } catch (error) {
- console.error(`${label} toggle failed`, error);
- }
- })();
- });
- document.getElementById(`settings-clear-${id}-history`)?.addEventListener("click", () => {
- void (async () => {
- if (!await bridge.trxUi.confirm({ title: `Clear ${label} history?`, message: `All stored ${label} decodes will be permanently removed.`, confirmLabel: "Clear history" })) return;
- try {
- await bridge.postPath?.(`/clear_${id}_decode`);
- reset();
- } catch (error) {
- console.error(`${label} history clear failed`, error);
- }
- })();
- });
- }
+import {
+ initializeFt8FamilyBar,
+ initializeFtxDecoder,
+ installFtxCompatibilityHelpers
+} from "./chunk-SGMG5LG2.js";
- // src/plugins/ft8.ts
- installFtxCompatibilityHelpers();
- initializeFt8FamilyBar();
- initializeFtxDecoder({ id: "ft8", label: "FT8", periodMs: 15e3, periodDigits: 0 });
-})();
+// src/plugins/ft8.ts
+installFtxCompatibilityHelpers();
+initializeFt8FamilyBar();
+initializeFtxDecoder({ id: "ft8", label: "FT8", periodMs: 15e3, periodDigits: 0 });
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js
index 7b9cead2..aa270dfe 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/hf-aprs.js
@@ -1,359 +1,265 @@
-"use strict";
-(() => {
- // src/plugins/aprs-shared.ts
- function aprsPacketCategory(packet) {
- const type = (packet.type ?? "").toLowerCase();
- const info = (packet.info ?? "").toLowerCase();
- if (packet.lat != null && packet.lon != null || type.includes("position")) return "position";
- if (type.includes("message") || info.startsWith(":")) return "message";
- if (type.includes("weather") || info.startsWith("_")) return "weather";
- if (type.includes("telemetry") || info.startsWith("t#")) return "telemetry";
- return "other";
- }
- function aprsCategoryLabel(category) {
- switch (category) {
- case "position":
- return "Position";
- case "message":
- return "Message";
- case "weather":
- return "Weather";
- case "telemetry":
- return "Telemetry";
- default:
- return "Other";
- }
- }
- function aprsAgeText(timestampMs) {
- if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return "just now";
- const seconds = Math.round(Math.max(0, Date.now() - timestampMs) / 1e3);
- if (seconds < 5) return "just now";
- if (seconds < 60) return `${String(seconds)}s ago`;
- const minutes = Math.round(seconds / 60);
- if (minutes < 60) return `${String(minutes)}m ago`;
- return `${String(Math.round(minutes / 60))}h ago`;
- }
- function aprsPacketSignature(packet) {
- return [
- packet.srcCall ?? "",
- packet.destCall ?? "",
- packet.path ?? "",
- packet.info ?? "",
- packet.type ?? "",
- packet.lat?.toFixed(4) ?? "",
- packet.lon?.toFixed(4) ?? ""
- ].join("|");
- }
- function collapseAprsDuplicates(packets) {
- const seen = /* @__PURE__ */ new Set();
- return packets.filter((packet) => {
- const signature = aprsPacketSignature(packet);
- if (seen.has(signature)) return false;
- seen.add(signature);
- return true;
- });
- }
- function aprsHexBytes(bytes) {
- if (!bytes?.length) return "--";
- return bytes.map((byte) => byte.toString(16).toUpperCase().padStart(2, "0")).join(" ");
- }
- function renderAprsInfo(packet) {
- if (packet.info_bytes?.length) {
- return packet.info_bytes.map((byte) => renderAprsByte(byte)).join("");
- }
- return Array.from(packet.info ?? "", (character) => renderAprsCharacter(character)).join("");
- }
- function renderAprsByte(byte) {
- return byte >= 32 && byte <= 126 ? escapeAprsCharacter(String.fromCharCode(byte)) : `0x${byte.toString(16).toUpperCase().padStart(2, "0")}`;
- }
- function renderAprsCharacter(character) {
- const code = character.charCodeAt(0);
- return code >= 32 && code <= 126 ? escapeAprsCharacter(character) : `0x${code.toString(16).toUpperCase().padStart(2, "0")}`;
- }
- function escapeAprsCharacter(character) {
- if (character === "<") return "<";
- if (character === ">") return ">";
- if (character === "&") return "&";
- if (character === '"') return """;
- return character;
- }
- function renderLocalAprsSymbol(packet, escapeHtml) {
- if (!packet.symbolTable || !packet.symbolCode) return "";
- const symbol = escapeHtml(packet.symbolCode);
- const table = packet.symbolTable === "/" ? "Primary" : "Alternate";
- return `${symbol}`;
- }
- function normalizeAprsPacket(packet, receiver) {
- return {
- rig_id: packet.rig_id || null,
- receiver,
- srcCall: packet.src_call ?? "",
- destCall: packet.dest_call ?? "",
- path: packet.path ?? "",
- info: packet.info ?? "",
- info_bytes: packet.info_bytes ?? [],
- type: packet.packet_type ?? "",
- crcOk: packet.crc_ok ?? false,
- ts_ms: packet.ts_ms ?? null,
- lat: packet.lat ?? null,
- lon: packet.lon ?? null,
- symbolTable: packet.symbol_table ?? null,
- symbolCode: packet.symbol_code ?? null
- };
- }
+import {
+ aprsAgeText,
+ aprsCategoryLabel,
+ aprsHexBytes,
+ aprsPacketCategory,
+ collapseAprsDuplicates,
+ normalizeAprsPacket,
+ renderAprsInfo,
+ renderLocalAprsSymbol
+} from "./chunk-M2I6DH4X.js";
- // src/plugins/hf-aprs.ts
- var hfAprsWindow = window;
- var escapeHfAprsHtml = (input) => hfAprsWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
- var hfAprsStatus = document.getElementById("hf-aprs-status");
- var hfAprsPacketsEl = document.getElementById("hf-aprs-packets");
- var hfAprsFilterInput = document.getElementById("hf-aprs-filter");
- var hfAprsOnlyPosBtn = document.getElementById("hf-aprs-only-pos-btn");
- var hfAprsHideCrcBtn = document.getElementById("hf-aprs-hide-crc-btn");
- var hfAprsCollapseDupBtn = document.getElementById("hf-aprs-collapse-dup-btn");
- var hfAprsTotalCountEl = document.getElementById("hf-aprs-total-count");
- var hfAprsVisibleCountEl = document.getElementById("hf-aprs-visible-count");
- var hfAprsLatestSeenEl = document.getElementById("hf-aprs-latest-seen");
- var hfAprsFilterText = "";
- var hfAprsPacketHistory = [];
- var hfAprsOnlyPos = false;
- var hfAprsHideCrc = false;
- var hfAprsCollapseDup = false;
- var hfAprsTypeFilter = "all";
- function currentHfAprsHistoryRetentionMs() {
- return typeof hfAprsWindow.getDecodeHistoryRetentionMs === "function" ? hfAprsWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
- }
- function pruneHfAprsPacketHistory() {
- const cutoffMs = Date.now() - currentHfAprsHistoryRetentionMs();
- hfAprsPacketHistory = hfAprsPacketHistory.filter((pkt) => (pkt._tsMs ?? 0) >= cutoffMs);
- }
- function scheduleHfAprsHistoryRender() {
- if (typeof hfAprsWindow.trxScheduleUiFrameJob === "function") {
- hfAprsWindow.trxScheduleUiFrameJob("hf-aprs-history", () => {
- renderHfAprsHistory();
- });
- return;
- }
- renderHfAprsHistory();
- }
- function hfAprsDistanceText(pkt) {
- if (hfAprsWindow.serverLat == null || hfAprsWindow.serverLon == null || pkt.lat == null || pkt.lon == null || !hfAprsWindow.haversineKm) return "";
- const distKm = hfAprsWindow.haversineKm(hfAprsWindow.serverLat, hfAprsWindow.serverLon, pkt.lat, pkt.lon);
- if (!Number.isFinite(distKm)) return "";
- if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`;
- return `${distKm.toFixed(1)} km from TRX`;
- }
- function hfAprsFilterMatch(pkt) {
- if (hfAprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false;
- if (hfAprsHideCrc && !pkt.crcOk) return false;
- if (hfAprsTypeFilter !== "all" && aprsPacketCategory(pkt) !== hfAprsTypeFilter) return false;
- if (!hfAprsFilterText) return true;
- const haystack = [
- pkt.srcCall,
- pkt.destCall,
- pkt.path,
- pkt.info,
- pkt.type,
- pkt.lat != null ? pkt.lat.toFixed(4) : "",
- pkt.lon != null ? pkt.lon.toFixed(4) : "",
- aprsPacketCategory(pkt)
- ].filter(Boolean).join(" ").toUpperCase();
- return haystack.includes(hfAprsFilterText);
- }
- function hfAprsVisiblePackets() {
- const packets = hfAprsCollapseDup ? collapseHfAprsDuplicates(hfAprsPacketHistory) : hfAprsPacketHistory;
- return packets.filter(hfAprsFilterMatch);
- }
- var collapseHfAprsDuplicates = collapseAprsDuplicates;
- function updateHfAprsSummary() {
- const visible = hfAprsVisiblePackets();
- if (hfAprsTotalCountEl) {
- hfAprsTotalCountEl.textContent = `${hfAprsPacketHistory.length} total`;
- }
- if (hfAprsVisibleCountEl) {
- hfAprsVisibleCountEl.textContent = `${visible.length} shown`;
- }
- if (hfAprsLatestSeenEl) {
- const latest = hfAprsPacketHistory[0];
- if (!latest) {
- hfAprsLatestSeenEl.textContent = "No packets yet";
- } else {
- hfAprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`;
- }
- }
- }
- function updateHfAprsChipState() {
- document.querySelectorAll("[id^='hf-aprs-type-']").forEach((btn) => {
- btn.classList.toggle("active", btn.id === `hf-aprs-type-${hfAprsTypeFilter}`);
- });
- hfAprsOnlyPosBtn?.classList.toggle("active", hfAprsOnlyPos);
- hfAprsHideCrcBtn?.classList.toggle("active", hfAprsHideCrc);
- hfAprsCollapseDupBtn?.classList.toggle("active", hfAprsCollapseDup);
- }
- function renderHfAprsRow(pkt, isFresh) {
- const row = document.createElement("div");
- row.className = "aprs-packet";
- if (!pkt.crcOk) row.classList.add("aprs-packet-crc");
- if (isFresh) row.classList.add("aprs-packet-new");
- const ts = pkt._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
- const age = aprsAgeText(pkt._tsMs);
- const category = aprsPacketCategory(pkt);
- const categoryLabel = aprsCategoryLabel(category);
- const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`;
- const pathBadge = pkt.path ? `${escapeHfAprsHtml(pkt.path)}` : "";
- const crcBadge = pkt.crcOk ? "" : 'CRC Fail';
- const hfBadge = 'HF';
- const symbolHtml = renderLocalAprsSymbol(pkt, escapeHfAprsHtml);
- const posLink = pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}` : "";
- const distance = hfAprsDistanceText(pkt);
- const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;
- row.innerHTML = `${ts}` + hfBadge + symbolHtml + `${escapeHfAprsHtml(pkt.srcCall ?? "")}>${escapeHfAprsHtml(pkt.destCall || "")}${escapeHfAprsHtml(categoryLabel)}` + pathBadge + crcBadge + `
${escapeHfAprsHtml(age)}` + (distance ? `${escapeHfAprsHtml(distance)}` : "") + `${escapeHfAprsHtml(pkt.type || "--")}
${renderAprsInfo(pkt)}` + (posLink ? `${posLink}` : "") + `
` + (pkt.lat != null && pkt.lon != null ? `
` : "") + (pkt.lat != null && pkt.lon != null ? `
` : "") + `
QRZDetails
Source${escapeHfAprsHtml(pkt.srcCall || "--")}Destination${escapeHfAprsHtml(pkt.destCall || "--")}Type${escapeHfAprsHtml(pkt.type || "--")}Path${escapeHfAprsHtml(pkt.path || "--")}Age${escapeHfAprsHtml(age)}CRC${pkt.crcOk ? "OK" : "Failed"}Position${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}Info${escapeHfAprsHtml(pkt.info || "--")}Info Bytes${escapeHfAprsHtml(aprsHexBytes(pkt.info_bytes))}
`;
- row.querySelectorAll("[data-aprs-map]").forEach((el) => {
- el.addEventListener("click", (evt) => {
- evt.preventDefault();
- const raw = el.dataset.aprsMap ?? "";
- const [lat, lon] = raw.split(",").map(Number);
- if (hfAprsWindow.navigateToAprsMap && typeof lat === "number" && typeof lon === "number" && Number.isFinite(lat) && Number.isFinite(lon)) {
- hfAprsWindow.navigateToAprsMap(lat, lon);
- }
- });
- });
- const copyBtn = row.querySelector("[data-aprs-copy]");
- if (copyBtn) {
- copyBtn.addEventListener("click", () => {
- void (async () => {
- const raw = copyBtn.dataset.aprsCopy ?? "";
- try {
- const clipboard = Reflect.get(navigator, "clipboard");
- if (clipboard) {
- await clipboard.writeText(raw);
- hfAprsWindow.showHint?.("Coordinates copied", 1200);
- }
- } catch {
- hfAprsWindow.showHint?.("Copy failed", 1500);
- }
- })();
- });
- }
- return row;
- }
- function renderHfAprsHistory() {
- pruneHfAprsPacketHistory();
- if (!hfAprsPacketsEl) {
- updateHfAprsSummary();
- updateHfAprsChipState();
- return;
- }
- const visible = hfAprsVisiblePackets();
- const fragment = document.createDocumentFragment();
- for (const [index, packet] of visible.entries()) {
- fragment.appendChild(renderHfAprsRow(packet, index === 0));
- }
- hfAprsPacketsEl.replaceChildren(fragment);
- updateHfAprsSummary();
- updateHfAprsChipState();
- }
- function resetHfAprsHistoryView() {
- if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
- hfAprsPacketHistory = [];
- renderHfAprsHistory();
- }
- function pruneHfAprsHistoryView() {
- pruneHfAprsPacketHistory();
- renderHfAprsHistory();
- }
- function addHfAprsPacket(pkt) {
- const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
- pkt._tsMs = tsMs;
- pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
- hfAprsPacketHistory.unshift(pkt);
- pruneHfAprsPacketHistory();
- scheduleHfAprsHistoryRender();
- }
- function normalizeServerHfAprsPacket(pkt) {
- return normalizeAprsPacket(pkt, hfAprsWindow.getDecodeRigMeta?.() ?? null);
- }
- function onServerHfAprsBatch(packets) {
- if (!Array.isArray(packets) || packets.length === 0) return;
- if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
- const normalized = [];
- for (const pkt of packets) {
- const next = normalizeServerHfAprsPacket(pkt);
- 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" });
- normalized.push(next);
- }
- normalized.reverse();
- hfAprsPacketHistory = normalized.concat(hfAprsPacketHistory);
- pruneHfAprsPacketHistory();
- scheduleHfAprsHistoryRender();
- }
- var hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn");
- hfAprsDecodeToggleBtn?.addEventListener("click", () => {
- void (async () => {
- try {
- await hfAprsWindow.takeSchedulerControlForDecoderDisable?.(hfAprsDecodeToggleBtn);
- await hfAprsWindow.postPath?.("/toggle_hf_aprs_decode");
- } catch (e) {
- console.error("HF APRS toggle failed", e);
- }
- })();
- });
- document.getElementById("settings-clear-hf-aprs-history")?.addEventListener("click", () => {
- void (async () => {
- if (!await hfAprsWindow.trxUi.confirm({ title: "Clear HF APRS history?", message: "All stored HF APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
- try {
- await hfAprsWindow.postPath?.("/clear_hf_aprs_decode");
- resetHfAprsHistoryView();
- } catch (e) {
- console.error("HF APRS history clear failed", e);
- }
- })();
- });
- if (hfAprsOnlyPosBtn) {
- hfAprsOnlyPosBtn.addEventListener("click", () => {
- hfAprsOnlyPos = !hfAprsOnlyPos;
+// src/plugins/hf-aprs.ts
+var hfAprsWindow = window;
+var escapeHfAprsHtml = (input) => hfAprsWindow.escapeMapHtml?.(input) ?? input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
+var hfAprsStatus = document.getElementById("hf-aprs-status");
+var hfAprsPacketsEl = document.getElementById("hf-aprs-packets");
+var hfAprsFilterInput = document.getElementById("hf-aprs-filter");
+var hfAprsOnlyPosBtn = document.getElementById("hf-aprs-only-pos-btn");
+var hfAprsHideCrcBtn = document.getElementById("hf-aprs-hide-crc-btn");
+var hfAprsCollapseDupBtn = document.getElementById("hf-aprs-collapse-dup-btn");
+var hfAprsTotalCountEl = document.getElementById("hf-aprs-total-count");
+var hfAprsVisibleCountEl = document.getElementById("hf-aprs-visible-count");
+var hfAprsLatestSeenEl = document.getElementById("hf-aprs-latest-seen");
+var hfAprsFilterText = "";
+var hfAprsPacketHistory = [];
+var hfAprsOnlyPos = false;
+var hfAprsHideCrc = false;
+var hfAprsCollapseDup = false;
+var hfAprsTypeFilter = "all";
+function currentHfAprsHistoryRetentionMs() {
+ return typeof hfAprsWindow.getDecodeHistoryRetentionMs === "function" ? hfAprsWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
+}
+function pruneHfAprsPacketHistory() {
+ const cutoffMs = Date.now() - currentHfAprsHistoryRetentionMs();
+ hfAprsPacketHistory = hfAprsPacketHistory.filter((pkt) => (pkt._tsMs ?? 0) >= cutoffMs);
+}
+function scheduleHfAprsHistoryRender() {
+ if (typeof hfAprsWindow.trxScheduleUiFrameJob === "function") {
+ hfAprsWindow.trxScheduleUiFrameJob("hf-aprs-history", () => {
renderHfAprsHistory();
});
- }
- if (hfAprsHideCrcBtn) {
- hfAprsHideCrcBtn.addEventListener("click", () => {
- hfAprsHideCrc = !hfAprsHideCrc;
- renderHfAprsHistory();
- });
- }
- if (hfAprsCollapseDupBtn) {
- hfAprsCollapseDupBtn.addEventListener("click", () => {
- hfAprsCollapseDup = !hfAprsCollapseDup;
- renderHfAprsHistory();
- });
- }
- ["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => {
- const btn = document.getElementById(`hf-aprs-type-${type}`);
- if (!btn) return;
- btn.addEventListener("click", () => {
- hfAprsTypeFilter = type;
- renderHfAprsHistory();
- });
- });
- if (hfAprsFilterInput) {
- hfAprsFilterInput.addEventListener("input", () => {
- hfAprsFilterText = hfAprsFilterInput.value.trim().toUpperCase();
- renderHfAprsHistory();
- });
- }
- function onServerHfAprs(pkt) {
- if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
- addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
+ return;
}
renderHfAprsHistory();
- window.trxPluginRuntime.registerDecoder({
- id: "hf_aprs",
- onMessage: onServerHfAprs,
- onBatch: onServerHfAprsBatch,
- restore: onServerHfAprsBatch,
- reset: resetHfAprsHistoryView,
- prune: pruneHfAprsHistoryView
+}
+function hfAprsDistanceText(pkt) {
+ if (hfAprsWindow.serverLat == null || hfAprsWindow.serverLon == null || pkt.lat == null || pkt.lon == null || !hfAprsWindow.haversineKm) return "";
+ const distKm = hfAprsWindow.haversineKm(hfAprsWindow.serverLat, hfAprsWindow.serverLon, pkt.lat, pkt.lon);
+ if (!Number.isFinite(distKm)) return "";
+ if (distKm < 1) return `${Math.round(distKm * 1e3)} m from TRX`;
+ return `${distKm.toFixed(1)} km from TRX`;
+}
+function hfAprsFilterMatch(pkt) {
+ if (hfAprsOnlyPos && (pkt.lat == null || pkt.lon == null)) return false;
+ if (hfAprsHideCrc && !pkt.crcOk) return false;
+ if (hfAprsTypeFilter !== "all" && aprsPacketCategory(pkt) !== hfAprsTypeFilter) return false;
+ if (!hfAprsFilterText) return true;
+ const haystack = [
+ pkt.srcCall,
+ pkt.destCall,
+ pkt.path,
+ pkt.info,
+ pkt.type,
+ pkt.lat != null ? pkt.lat.toFixed(4) : "",
+ pkt.lon != null ? pkt.lon.toFixed(4) : "",
+ aprsPacketCategory(pkt)
+ ].filter(Boolean).join(" ").toUpperCase();
+ return haystack.includes(hfAprsFilterText);
+}
+function hfAprsVisiblePackets() {
+ const packets = hfAprsCollapseDup ? collapseHfAprsDuplicates(hfAprsPacketHistory) : hfAprsPacketHistory;
+ return packets.filter(hfAprsFilterMatch);
+}
+var collapseHfAprsDuplicates = collapseAprsDuplicates;
+function updateHfAprsSummary() {
+ const visible = hfAprsVisiblePackets();
+ if (hfAprsTotalCountEl) {
+ hfAprsTotalCountEl.textContent = `${hfAprsPacketHistory.length} total`;
+ }
+ if (hfAprsVisibleCountEl) {
+ hfAprsVisibleCountEl.textContent = `${visible.length} shown`;
+ }
+ if (hfAprsLatestSeenEl) {
+ const latest = hfAprsPacketHistory[0];
+ if (!latest) {
+ hfAprsLatestSeenEl.textContent = "No packets yet";
+ } else {
+ hfAprsLatestSeenEl.textContent = `${latest.srcCall} ${aprsAgeText(latest._tsMs)}`;
+ }
+ }
+}
+function updateHfAprsChipState() {
+ document.querySelectorAll("[id^='hf-aprs-type-']").forEach((btn) => {
+ btn.classList.toggle("active", btn.id === `hf-aprs-type-${hfAprsTypeFilter}`);
});
-})();
+ hfAprsOnlyPosBtn?.classList.toggle("active", hfAprsOnlyPos);
+ hfAprsHideCrcBtn?.classList.toggle("active", hfAprsHideCrc);
+ hfAprsCollapseDupBtn?.classList.toggle("active", hfAprsCollapseDup);
+}
+function renderHfAprsRow(pkt, isFresh) {
+ const row = document.createElement("div");
+ row.className = "aprs-packet";
+ if (!pkt.crcOk) row.classList.add("aprs-packet-crc");
+ if (isFresh) row.classList.add("aprs-packet-new");
+ const ts = pkt._ts || (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
+ const age = aprsAgeText(pkt._tsMs);
+ const category = aprsPacketCategory(pkt);
+ const categoryLabel = aprsCategoryLabel(category);
+ const categoryClass = `aprs-badge aprs-badge-type aprs-badge-type-${category}`;
+ const pathBadge = pkt.path ? `${escapeHfAprsHtml(pkt.path)}` : "";
+ const crcBadge = pkt.crcOk ? "" : 'CRC Fail';
+ const hfBadge = 'HF';
+ const symbolHtml = renderLocalAprsSymbol(pkt, escapeHfAprsHtml);
+ const posLink = pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(4)}, ${pkt.lon.toFixed(4)}` : "";
+ const distance = hfAprsDistanceText(pkt);
+ const qrzHref = `https://qrzcq.com/call/${encodeURIComponent(pkt.srcCall || "")}`;
+ row.innerHTML = `${ts}` + hfBadge + symbolHtml + `${escapeHfAprsHtml(pkt.srcCall ?? "")}>${escapeHfAprsHtml(pkt.destCall || "")}${escapeHfAprsHtml(categoryLabel)}` + pathBadge + crcBadge + `
${escapeHfAprsHtml(age)}` + (distance ? `${escapeHfAprsHtml(distance)}` : "") + `${escapeHfAprsHtml(pkt.type || "--")}
${renderAprsInfo(pkt)}` + (posLink ? `${posLink}` : "") + `
` + (pkt.lat != null && pkt.lon != null ? `
` : "") + (pkt.lat != null && pkt.lon != null ? `
` : "") + `
QRZDetails
Source${escapeHfAprsHtml(pkt.srcCall || "--")}Destination${escapeHfAprsHtml(pkt.destCall || "--")}Type${escapeHfAprsHtml(pkt.type || "--")}Path${escapeHfAprsHtml(pkt.path || "--")}Age${escapeHfAprsHtml(age)}CRC${pkt.crcOk ? "OK" : "Failed"}Position${pkt.lat != null && pkt.lon != null ? `${pkt.lat.toFixed(5)}, ${pkt.lon.toFixed(5)}` : "--"}Info${escapeHfAprsHtml(pkt.info || "--")}Info Bytes${escapeHfAprsHtml(aprsHexBytes(pkt.info_bytes))}
`;
+ row.querySelectorAll("[data-aprs-map]").forEach((el) => {
+ el.addEventListener("click", (evt) => {
+ evt.preventDefault();
+ const raw = el.dataset.aprsMap ?? "";
+ const [lat, lon] = raw.split(",").map(Number);
+ if (hfAprsWindow.navigateToAprsMap && typeof lat === "number" && typeof lon === "number" && Number.isFinite(lat) && Number.isFinite(lon)) {
+ hfAprsWindow.navigateToAprsMap(lat, lon);
+ }
+ });
+ });
+ const copyBtn = row.querySelector("[data-aprs-copy]");
+ if (copyBtn) {
+ copyBtn.addEventListener("click", () => {
+ void (async () => {
+ const raw = copyBtn.dataset.aprsCopy ?? "";
+ try {
+ const clipboard = Reflect.get(navigator, "clipboard");
+ if (clipboard) {
+ await clipboard.writeText(raw);
+ hfAprsWindow.showHint?.("Coordinates copied", 1200);
+ }
+ } catch {
+ hfAprsWindow.showHint?.("Copy failed", 1500);
+ }
+ })();
+ });
+ }
+ return row;
+}
+function renderHfAprsHistory() {
+ pruneHfAprsPacketHistory();
+ if (!hfAprsPacketsEl) {
+ updateHfAprsSummary();
+ updateHfAprsChipState();
+ return;
+ }
+ const visible = hfAprsVisiblePackets();
+ const fragment = document.createDocumentFragment();
+ for (const [index, packet] of visible.entries()) {
+ fragment.appendChild(renderHfAprsRow(packet, index === 0));
+ }
+ hfAprsPacketsEl.replaceChildren(fragment);
+ updateHfAprsSummary();
+ updateHfAprsChipState();
+}
+function resetHfAprsHistoryView() {
+ if (hfAprsPacketsEl) hfAprsPacketsEl.innerHTML = "";
+ hfAprsPacketHistory = [];
+ renderHfAprsHistory();
+}
+function pruneHfAprsHistoryView() {
+ pruneHfAprsPacketHistory();
+ renderHfAprsHistory();
+}
+function addHfAprsPacket(pkt) {
+ const tsMs = Number.isFinite(pkt.ts_ms) ? Number(pkt.ts_ms) : Date.now();
+ pkt._tsMs = tsMs;
+ pkt._ts = new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
+ hfAprsPacketHistory.unshift(pkt);
+ pruneHfAprsPacketHistory();
+ scheduleHfAprsHistoryRender();
+}
+function normalizeServerHfAprsPacket(pkt) {
+ return normalizeAprsPacket(pkt, hfAprsWindow.getDecodeRigMeta?.() ?? null);
+}
+function onServerHfAprsBatch(packets) {
+ if (!Array.isArray(packets) || packets.length === 0) return;
+ if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
+ const normalized = [];
+ for (const pkt of packets) {
+ const next = normalizeServerHfAprsPacket(pkt);
+ 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" });
+ normalized.push(next);
+ }
+ normalized.reverse();
+ hfAprsPacketHistory = normalized.concat(hfAprsPacketHistory);
+ pruneHfAprsPacketHistory();
+ scheduleHfAprsHistoryRender();
+}
+var hfAprsDecodeToggleBtn = document.getElementById("hf-aprs-decode-toggle-btn");
+hfAprsDecodeToggleBtn?.addEventListener("click", () => {
+ void (async () => {
+ try {
+ await hfAprsWindow.takeSchedulerControlForDecoderDisable?.(hfAprsDecodeToggleBtn);
+ await hfAprsWindow.postPath?.("/toggle_hf_aprs_decode");
+ } catch (e) {
+ console.error("HF APRS toggle failed", e);
+ }
+ })();
+});
+document.getElementById("settings-clear-hf-aprs-history")?.addEventListener("click", () => {
+ void (async () => {
+ if (!await hfAprsWindow.trxUi.confirm({ title: "Clear HF APRS history?", message: "All stored HF APRS packets will be permanently removed.", confirmLabel: "Clear history" })) return;
+ try {
+ await hfAprsWindow.postPath?.("/clear_hf_aprs_decode");
+ resetHfAprsHistoryView();
+ } catch (e) {
+ console.error("HF APRS history clear failed", e);
+ }
+ })();
+});
+if (hfAprsOnlyPosBtn) {
+ hfAprsOnlyPosBtn.addEventListener("click", () => {
+ hfAprsOnlyPos = !hfAprsOnlyPos;
+ renderHfAprsHistory();
+ });
+}
+if (hfAprsHideCrcBtn) {
+ hfAprsHideCrcBtn.addEventListener("click", () => {
+ hfAprsHideCrc = !hfAprsHideCrc;
+ renderHfAprsHistory();
+ });
+}
+if (hfAprsCollapseDupBtn) {
+ hfAprsCollapseDupBtn.addEventListener("click", () => {
+ hfAprsCollapseDup = !hfAprsCollapseDup;
+ renderHfAprsHistory();
+ });
+}
+["all", "position", "message", "weather", "telemetry", "other"].forEach((type) => {
+ const btn = document.getElementById(`hf-aprs-type-${type}`);
+ if (!btn) return;
+ btn.addEventListener("click", () => {
+ hfAprsTypeFilter = type;
+ renderHfAprsHistory();
+ });
+});
+if (hfAprsFilterInput) {
+ hfAprsFilterInput.addEventListener("input", () => {
+ hfAprsFilterText = hfAprsFilterInput.value.trim().toUpperCase();
+ renderHfAprsHistory();
+ });
+}
+function onServerHfAprs(pkt) {
+ if (hfAprsStatus) hfAprsStatus.textContent = "Receiving";
+ addHfAprsPacket(normalizeServerHfAprsPacket(pkt));
+}
+renderHfAprsHistory();
+window.trxPluginRuntime.registerDecoder({
+ id: "hf_aprs",
+ onMessage: onServerHfAprs,
+ onBatch: onServerHfAprsBatch,
+ restore: onServerHfAprsBatch,
+ reset: resetHfAprsHistoryView,
+ prune: pruneHfAprsHistoryView
+});
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/leaflet-ais-tracksymbol.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/leaflet-ais-tracksymbol.js
index 32f73063..8ee66d38 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/leaflet-ais-tracksymbol.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/leaflet-ais-tracksymbol.js
@@ -1,98 +1,95 @@
-"use strict";
-(() => {
- // src/leaflet-ais-tracksymbol.ts
- (function() {
- const leaflet = globalThis.L;
- if (!leaflet) return;
- function clamp(value, min, max) {
- return Math.max(min, Math.min(max, value));
- }
- function finiteAngle(value) {
- if (value === null || !Number.isFinite(value)) return null;
- const normalized = (value % 360 + 360) % 360;
- return normalized;
- }
- function svgColor(value, fallback) {
- const text = value || fallback || "";
- return text.replace(/"/g, """);
- }
- function buildSymbolHtml(options, zoom) {
- const heading = finiteAngle(options.heading);
- const course = finiteAngle(options.course);
- const angle = heading != null ? heading : course;
- const speed = Number.isFinite(options.speed) ? Math.max(0, Number(options.speed)) : 0;
- const sizeBase = Number.isFinite(options.size) ? options.size : 22;
+// src/leaflet-ais-tracksymbol.ts
+(function() {
+ const leaflet = globalThis.L;
+ if (!leaflet) return;
+ function clamp(value, min, max) {
+ return Math.max(min, Math.min(max, value));
+ }
+ function finiteAngle(value) {
+ if (value === null || !Number.isFinite(value)) return null;
+ const normalized = (value % 360 + 360) % 360;
+ return normalized;
+ }
+ function svgColor(value, fallback) {
+ const text = value || fallback || "";
+ return text.replace(/"/g, """);
+ }
+ function buildSymbolHtml(options, zoom) {
+ const heading = finiteAngle(options.heading);
+ const course = finiteAngle(options.course);
+ const angle = heading != null ? heading : course;
+ const speed = Number.isFinite(options.speed) ? Math.max(0, Number(options.speed)) : 0;
+ const sizeBase = Number.isFinite(options.size) ? options.size : 22;
+ const zoomBoost = zoom >= 12 ? 4 : zoom >= 9 ? 2 : 0;
+ const size = clamp(sizeBase + zoomBoost, 16, 32);
+ const courseLen = course != null ? clamp(size * (0.55 + Math.min(speed, 30) / 30), size * 0.55, size * 1.2) : 0;
+ const color = svgColor(options.color, "#ff7559");
+ const outline = svgColor(options.outline, "#6b2118");
+ const body = angle != null ? `` : ``;
+ const courseLine = course != null ? `` : "";
+ return ``;
+ }
+ leaflet.TrxAisTrackSymbol = leaflet.Marker.extend({
+ options: {
+ heading: null,
+ course: null,
+ speed: null,
+ color: "#ff7559",
+ outline: "#6b2118",
+ size: 22,
+ interactive: true,
+ keyboard: true,
+ riseOnHover: true
+ },
+ initialize: function(latlng, options) {
+ const merged = leaflet.Util.extend({}, this.options, options || {});
+ merged.icon = leaflet.divIcon({
+ className: "trx-ais-track-symbol-icon",
+ html: "",
+ iconSize: [merged.size, merged.size],
+ iconAnchor: [merged.size / 2, merged.size / 2]
+ });
+ leaflet.Marker.prototype.initialize.call(this, latlng, merged);
+ },
+ onAdd: function(map) {
+ leaflet.Marker.prototype.onAdd.call(this, map);
+ this._refreshIcon();
+ this._boundZoomRefresh = this._refreshIcon.bind(this);
+ map.on("zoomend", this._boundZoomRefresh);
+ },
+ onRemove: function(map) {
+ if (this._boundZoomRefresh) {
+ map.off("zoomend", this._boundZoomRefresh);
+ this._boundZoomRefresh = null;
+ }
+ leaflet.Marker.prototype.onRemove.call(this, map);
+ },
+ setAisState: function(next) {
+ if ("heading" in next) this.options.heading = next.heading;
+ if ("course" in next) this.options.course = next.course;
+ if ("speed" in next) this.options.speed = next.speed;
+ if ("color" in next) this.options.color = next.color;
+ if ("outline" in next) this.options.outline = next.outline;
+ this._refreshIcon();
+ return this;
+ },
+ _refreshIcon: function() {
+ if (!this._icon) return;
+ const zoom = this._map && typeof this._map.getZoom === "function" ? this._map.getZoom() : 0;
+ const html = buildSymbolHtml(this.options, zoom);
+ this._icon.innerHTML = html;
+ const sizeBase = Number.isFinite(this.options.size) ? this.options.size : 22;
const zoomBoost = zoom >= 12 ? 4 : zoom >= 9 ? 2 : 0;
const size = clamp(sizeBase + zoomBoost, 16, 32);
- const courseLen = course != null ? clamp(size * (0.55 + Math.min(speed, 30) / 30), size * 0.55, size * 1.2) : 0;
- const color = svgColor(options.color, "#ff7559");
- const outline = svgColor(options.outline, "#6b2118");
- const body = angle != null ? `` : ``;
- const courseLine = course != null ? `` : "";
- return ``;
+ this._icon.style.width = `${size}px`;
+ this._icon.style.height = `${size}px`;
+ this._icon.style.marginLeft = `${-size / 2}px`;
+ this._icon.style.marginTop = `${-size / 2}px`;
}
- leaflet.TrxAisTrackSymbol = leaflet.Marker.extend({
- options: {
- heading: null,
- course: null,
- speed: null,
- color: "#ff7559",
- outline: "#6b2118",
- size: 22,
- interactive: true,
- keyboard: true,
- riseOnHover: true
- },
- initialize: function(latlng, options) {
- const merged = leaflet.Util.extend({}, this.options, options || {});
- merged.icon = leaflet.divIcon({
- className: "trx-ais-track-symbol-icon",
- html: "",
- iconSize: [merged.size, merged.size],
- iconAnchor: [merged.size / 2, merged.size / 2]
- });
- leaflet.Marker.prototype.initialize.call(this, latlng, merged);
- },
- onAdd: function(map) {
- leaflet.Marker.prototype.onAdd.call(this, map);
- this._refreshIcon();
- this._boundZoomRefresh = this._refreshIcon.bind(this);
- map.on("zoomend", this._boundZoomRefresh);
- },
- onRemove: function(map) {
- if (this._boundZoomRefresh) {
- map.off("zoomend", this._boundZoomRefresh);
- this._boundZoomRefresh = null;
- }
- leaflet.Marker.prototype.onRemove.call(this, map);
- },
- setAisState: function(next) {
- if ("heading" in next) this.options.heading = next.heading;
- if ("course" in next) this.options.course = next.course;
- if ("speed" in next) this.options.speed = next.speed;
- if ("color" in next) this.options.color = next.color;
- if ("outline" in next) this.options.outline = next.outline;
- this._refreshIcon();
- return this;
- },
- _refreshIcon: function() {
- if (!this._icon) return;
- const zoom = this._map && typeof this._map.getZoom === "function" ? this._map.getZoom() : 0;
- const html = buildSymbolHtml(this.options, zoom);
- this._icon.innerHTML = html;
- const sizeBase = Number.isFinite(this.options.size) ? this.options.size : 22;
- const zoomBoost = zoom >= 12 ? 4 : zoom >= 9 ? 2 : 0;
- const size = clamp(sizeBase + zoomBoost, 16, 32);
- this._icon.style.width = `${size}px`;
- this._icon.style.height = `${size}px`;
- this._icon.style.marginLeft = `${-size / 2}px`;
- this._icon.style.marginTop = `${-size / 2}px`;
- }
- });
- leaflet.trxAisTrackSymbol = function(latlng, options) {
- const Constructor = leaflet.TrxAisTrackSymbol;
- if (!Constructor) throw new Error("AIS track symbol constructor is unavailable");
- return new Constructor(latlng, options);
- };
- })();
+ });
+ leaflet.trxAisTrackSymbol = function(latlng, options) {
+ const Constructor = leaflet.TrxAisTrackSymbol;
+ if (!Constructor) throw new Error("AIS track symbol constructor is unavailable");
+ return new Constructor(latlng, options);
+ };
})();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js
index 6c00d230..0f033c1f 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/map-core.js
@@ -1,3114 +1,3111 @@
-"use strict";
-(() => {
- // src/map-core.ts
- function mapEl(id) {
- const element = document.querySelector(`#${CSS.escape(id)}`);
- if (!element) throw new Error(`Missing map element #${id}`);
- return element;
+// src/map-core.ts
+function mapEl(id) {
+ const element = document.querySelector(`#${CSS.escape(id)}`);
+ if (!element) throw new Error(`Missing map element #${id}`);
+ return element;
+}
+var mapWindow = window;
+(function() {
+ "use strict";
+ const { state: T, core: C, modules } = mapWindow.trx;
+ const {
+ saveSetting,
+ loadSetting,
+ escapeMapHtml,
+ formatFreqForHumans,
+ scheduleUiFrameJob,
+ latLonToMaidenhead,
+ locatorToLatLon,
+ haversineKm,
+ formatDistanceKm,
+ formatTimeAgo
+ } = C;
+ function updateMapRigFilter() {
+ const el = mapEl("map-rig-filter");
+ if (!el) return;
+ const prev = el.value;
+ while (el.options.length > 1) el.remove(1);
+ for (const id of T.lastRigIds) {
+ const opt = document.createElement("option");
+ opt.value = id;
+ opt.textContent = T.lastRigDisplayNames[id] || id;
+ el.appendChild(opt);
+ }
+ if (prev && T.lastRigIds.includes(prev)) {
+ el.value = prev;
+ } else {
+ el.value = "";
+ mapRigFilter = "";
+ }
+ updateStatsRigFilter();
}
- var mapWindow = window;
- (function() {
- "use strict";
- const { state: T, core: C, modules } = mapWindow.trx;
- const {
- saveSetting,
- loadSetting,
- escapeMapHtml,
- formatFreqForHumans,
- scheduleUiFrameJob,
- latLonToMaidenhead,
- locatorToLatLon,
- haversineKm,
- formatDistanceKm,
- formatTimeAgo
- } = C;
- function updateMapRigFilter() {
- const el = mapEl("map-rig-filter");
- if (!el) return;
- const prev = el.value;
- while (el.options.length > 1) el.remove(1);
- for (const id of T.lastRigIds) {
- const opt = document.createElement("option");
- opt.value = id;
- opt.textContent = T.lastRigDisplayNames[id] || id;
- el.appendChild(opt);
- }
- if (prev && T.lastRigIds.includes(prev)) {
- el.value = prev;
- } else {
- el.value = "";
- mapRigFilter = "";
- }
- updateStatsRigFilter();
+ let aprsMap = null;
+ let aprsMapBaseLayer = null;
+ const aprsMapReceiverMarkers = {};
+ let aprsRadioPaths = [];
+ let selectedLocatorMarker = null;
+ let selectedLocatorPulseRaf = null;
+ let mapFullscreenListenerBound = false;
+ let mapP2pRadioPathsEnabled = loadSetting("mapP2pRadioPathsEnabled", true) !== false;
+ let mapDecodeContactPathsEnabled = loadSetting("mapDecodeContactPathsEnabled", true) !== false;
+ let mapOverlayPanelVisible = loadSetting("mapOverlayPanelVisible", true) !== false;
+ const MAP_HISTORY_LIMIT_OPTIONS = [15, 30, 60, 180, 360, 720, 1440];
+ const MAP_QSO_SUMMARY_LIMIT = 5;
+ const stationMarkers = /* @__PURE__ */ new Map();
+ const locatorMarkers = /* @__PURE__ */ new Map();
+ const decodeContactPaths = /* @__PURE__ */ new Map();
+ let selectedMapQsoKey = null;
+ const mapMarkers = /* @__PURE__ */ new Set();
+ const DEFAULT_MAP_SOURCE_FILTER = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, ft4: true, ft2: true, wspr: true, sat: false };
+ const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER };
+ const mapLocatorFilter = { phase: "band", bands: /* @__PURE__ */ new Set() };
+ let mapSearchFilter = "";
+ let mapRigFilter = "";
+ let mapHistoryPruneTimer = null;
+ let mapHistoryLimitMinutes = normalizeMapHistoryLimitMinutes(
+ Number(loadSetting("mapHistoryLimitMinutes", 1440))
+ );
+ const APRS_TRACK_MAX_POINTS = 64;
+ const AIS_TRACK_MAX_POINTS = 64;
+ const aisMarkers = /* @__PURE__ */ new Map();
+ const vdesMarkers = /* @__PURE__ */ new Map();
+ let selectedAprsTrackCall = null;
+ let selectedAisTrackMmsi = null;
+ const HAM_BANDS = [
+ { label: "2200m", meters: 2200 },
+ { label: "630m", meters: 630 },
+ { label: "160m", meters: 160 },
+ { label: "80m", meters: 80 },
+ { label: "60m", meters: 60 },
+ { label: "40m", meters: 40 },
+ { label: "30m", meters: 30 },
+ { label: "20m", meters: 20 },
+ { label: "17m", meters: 17 },
+ { label: "15m", meters: 15 },
+ { label: "12m", meters: 12 },
+ { label: "10m", meters: 10 },
+ { label: "6m", meters: 6 },
+ { label: "4m", meters: 4 },
+ { label: "3m", meters: 3 },
+ { label: "2m", meters: 2 },
+ { label: "1m", meters: 1 },
+ { label: "70cm", meters: 0.7 },
+ { label: "23cm", meters: 0.23 },
+ { label: "13cm", meters: 0.13 },
+ { label: "9cm", meters: 0.09 },
+ { label: "6cm", meters: 0.06 },
+ { label: "3cm", meters: 0.03 }
+ ].map((band) => ({
+ ...band,
+ nominalHz: 299792458 / band.meters
+ }));
+ function normalizeLocatorFreqHz(hz) {
+ if (!Number.isFinite(hz) || hz <= 0) return null;
+ if (hz >= 1e5) return hz;
+ const baseHz = Number(mapWindow.ft8BaseHz);
+ if (Number.isFinite(baseHz) && baseHz > 0) {
+ return baseHz + hz;
}
- let aprsMap = null;
- let aprsMapBaseLayer = null;
- const aprsMapReceiverMarkers = {};
- let aprsRadioPaths = [];
- let selectedLocatorMarker = null;
- let selectedLocatorPulseRaf = null;
- let mapFullscreenListenerBound = false;
- let mapP2pRadioPathsEnabled = loadSetting("mapP2pRadioPathsEnabled", true) !== false;
- let mapDecodeContactPathsEnabled = loadSetting("mapDecodeContactPathsEnabled", true) !== false;
- let mapOverlayPanelVisible = loadSetting("mapOverlayPanelVisible", true) !== false;
- const MAP_HISTORY_LIMIT_OPTIONS = [15, 30, 60, 180, 360, 720, 1440];
- const MAP_QSO_SUMMARY_LIMIT = 5;
- const stationMarkers = /* @__PURE__ */ new Map();
- const locatorMarkers = /* @__PURE__ */ new Map();
- const decodeContactPaths = /* @__PURE__ */ new Map();
- let selectedMapQsoKey = null;
- const mapMarkers = /* @__PURE__ */ new Set();
- const DEFAULT_MAP_SOURCE_FILTER = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, ft4: true, ft2: true, wspr: true, sat: false };
- const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER };
- const mapLocatorFilter = { phase: "band", bands: /* @__PURE__ */ new Set() };
- let mapSearchFilter = "";
- let mapRigFilter = "";
- let mapHistoryPruneTimer = null;
- let mapHistoryLimitMinutes = normalizeMapHistoryLimitMinutes(
- Number(loadSetting("mapHistoryLimitMinutes", 1440))
+ return hz;
+ }
+ function normalizeMapHistoryLimitMinutes(value) {
+ const minutes = Math.round(Number(value));
+ return MAP_HISTORY_LIMIT_OPTIONS.includes(minutes) ? minutes : 1440;
+ }
+ function mapHistoryCutoffMs() {
+ return Date.now() - mapHistoryLimitMinutes * 60 * 1e3;
+ }
+ function trimTrackHistory(history, cutoffMs, maxPoints) {
+ const list = Array.isArray(history) ? history : [];
+ const trimmed = list.filter((point) => Number(point?.tsMs) >= cutoffMs);
+ if (trimmed.length > maxPoints) {
+ trimmed.splice(0, trimmed.length - maxPoints);
+ }
+ return trimmed;
+ }
+ function refreshAprsTrack(call, entry) {
+ if (!entry) return;
+ if (!Array.isArray(entry.trackPoints) || entry.trackPoints.length < 2) {
+ if (entry.track) {
+ entry.track.remove();
+ entry.track = null;
+ }
+ return;
+ }
+ if (entry.track) {
+ entry.track.setLatLngs(entry.trackPoints);
+ return;
+ }
+ const track = L.polyline(entry.trackPoints, {
+ color: "#f0be4d",
+ weight: 2,
+ opacity: 0.72,
+ lineCap: "round",
+ lineJoin: "round",
+ interactive: false
+ });
+ track.__trxType = "aprs";
+ track._aprsCall = call;
+ entry.track = track;
+ }
+ function refreshAisTrack(mmsi, entry) {
+ if (!entry) return;
+ if (!Array.isArray(entry.trackPoints) || entry.trackPoints.length < 2) {
+ if (entry.track) {
+ entry.track.remove();
+ entry.track = null;
+ }
+ return;
+ }
+ if (entry.track) {
+ entry.track.setLatLngs(entry.trackPoints);
+ return;
+ }
+ const track = L.polyline(entry.trackPoints, {
+ color: getAisAccentColor(),
+ weight: 2,
+ opacity: 0.68,
+ lineCap: "round",
+ lineJoin: "round",
+ interactive: false,
+ dashArray: "5 4"
+ });
+ track.__trxType = "ais";
+ track._aisMmsi = mmsi;
+ entry.track = track;
+ }
+ function setRetainedMapMarkerVisible(marker, visible) {
+ if (!marker) return;
+ marker.__trxHistoryVisible = visible;
+ if (!visible) {
+ if (marker === selectedLocatorMarker) {
+ setSelectedLocatorMarker(null);
+ clearMapRadioPath();
+ }
+ if (aprsMap && aprsMap.hasLayer(marker)) marker.removeFrom(aprsMap);
+ }
+ }
+ function ensureAprsMarker(call, entry) {
+ if (!aprsMap || !entry || entry.marker || entry.lat == null || entry.lon == null) return;
+ _aprsAddMarkerToMap(call, entry);
+ }
+ function ensureAisMarker(key, entry) {
+ if (!aprsMap || !entry || entry.marker || entry?.msg?.lat == null || entry?.msg?.lon == null) return;
+ const marker = createAisMarker(entry.msg.lat, entry.msg.lon, entry.msg).addTo(aprsMap).bindPopup(buildAisPopupHtml(entry.msg));
+ marker.__trxType = "ais";
+ marker.__trxRigIds = entry.rigIds || /* @__PURE__ */ new Set();
+ marker._aisMmsi = String(key);
+ entry.marker = marker;
+ mapMarkers.add(marker);
+ }
+ function ensureVdesMarker(key, entry) {
+ if (!aprsMap || !entry || entry.marker || entry?.msg?.lat == null || entry?.msg?.lon == null) return;
+ const marker = L.circleMarker([entry.msg.lat, entry.msg.lon], {
+ radius: 5,
+ color: "#5c394f",
+ fillColor: "#c46392",
+ fillOpacity: 0.82
+ }).addTo(aprsMap).bindPopup(buildVdesPopupHtml(entry.msg));
+ marker.__trxType = "vdes";
+ marker.__trxRigIds = entry.rigIds || /* @__PURE__ */ new Set();
+ marker._vdesKey = String(key);
+ entry.marker = marker;
+ mapMarkers.add(marker);
+ }
+ function ensureDecodeLocatorMarker(entry) {
+ if (!aprsMap || !entry || entry.marker || !entry.grid || entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "ft2" && entry.sourceType !== "wspr") return;
+ const bounds = maidenheadToBounds(entry.grid);
+ if (!bounds) return;
+ const count = Math.max(entry.stationDetails?.size || 0, entry.stations?.size || 0, 1);
+ const tooltipHtml = buildDecodeLocatorTooltipHtml(entry.grid, entry, entry.sourceType);
+ const marker = L.rectangle(bounds, locatorStyleForEntry(entry, count)).addTo(aprsMap).bindPopup(tooltipHtml);
+ marker.__trxType = entry.sourceType;
+ marker.__trxRigIds = entry.rigIds || /* @__PURE__ */ new Set();
+ sendLocatorOverlayToBack(marker);
+ assignLocatorMarkerMeta(marker, entry.sourceType, entry.bandMeta);
+ entry.marker = marker;
+ mapMarkers.add(marker);
+ }
+ function pruneAprsEntry(call, entry, cutoffMs) {
+ const canRenderMap = !!aprsMap && !T.decodeHistoryReplayActive;
+ const pktTsMs = Number(entry?.pkt?._tsMs);
+ const visible = Number.isFinite(pktTsMs) && pktTsMs >= cutoffMs;
+ entry.visibleInHistoryWindow = visible;
+ entry.trackPoints = trimTrackHistory(entry.trackHistory, cutoffMs, APRS_TRACK_MAX_POINTS).map((point) => [point.lat, point.lon]);
+ if (canRenderMap) {
+ refreshAprsTrack(call, entry);
+ } else {
+ C.markDecodeMapSyncPending();
+ }
+ if (!visible) {
+ if (canRenderMap && selectedAprsTrackCall && String(selectedAprsTrackCall) === String(call)) {
+ selectedAprsTrackCall = null;
+ }
+ if (canRenderMap && entry?.track) {
+ entry.track.remove();
+ entry.track = null;
+ }
+ if (canRenderMap) setRetainedMapMarkerVisible(entry?.marker, false);
+ return false;
+ }
+ if (!canRenderMap) return true;
+ ensureAprsMarker(call, entry);
+ setRetainedMapMarkerVisible(entry?.marker, true);
+ if (entry?.marker && entry.lat != null && entry.lon != null) {
+ entry.marker.setLatLng([entry.lat, entry.lon]);
+ entry.marker.setPopupContent(buildAprsPopupHtml(call, entry.lat, entry.lon, entry.info || "", entry.pkt));
+ }
+ return true;
+ }
+ function pruneAisEntry(key, entry, cutoffMs) {
+ const canRenderMap = !!aprsMap && !T.decodeHistoryReplayActive;
+ const msgTsMs = Number(entry?.msg?._tsMs);
+ const visible = Number.isFinite(msgTsMs) && msgTsMs >= cutoffMs;
+ entry.visibleInHistoryWindow = visible;
+ entry.trackPoints = trimTrackHistory(entry.trackHistory, cutoffMs, AIS_TRACK_MAX_POINTS).map((point) => [point.lat, point.lon]);
+ if (canRenderMap) {
+ refreshAisTrack(key, entry);
+ } else {
+ C.markDecodeMapSyncPending();
+ }
+ if (!visible) {
+ if (canRenderMap && selectedAisTrackMmsi && String(selectedAisTrackMmsi) === String(key)) {
+ selectedAisTrackMmsi = null;
+ }
+ if (canRenderMap && entry?.track) {
+ entry.track.remove();
+ entry.track = null;
+ }
+ if (canRenderMap) setRetainedMapMarkerVisible(entry?.marker, false);
+ return false;
+ }
+ if (!canRenderMap) return true;
+ ensureAisMarker(key, entry);
+ setRetainedMapMarkerVisible(entry?.marker, true);
+ if (entry?.marker && entry.msg) {
+ updateAisMarker(entry.marker, entry.msg, buildAisPopupHtml(entry.msg));
+ }
+ return true;
+ }
+ function pruneLocatorEntry(key, entry, cutoffMs) {
+ const canRenderMap = !!aprsMap && !T.decodeHistoryReplayActive;
+ if (!entry || entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "ft2" && entry.sourceType !== "wspr") return true;
+ if (!(entry.allStationDetails instanceof Map)) {
+ entry.allStationDetails = entry.stationDetails instanceof Map ? new Map(entry.stationDetails) : /* @__PURE__ */ new Map();
+ }
+ const nextDetails = /* @__PURE__ */ new Map();
+ for (const [detailKey, detail] of entry.allStationDetails.entries()) {
+ const tsMs = Number(detail?.ts_ms);
+ if (Number.isFinite(tsMs) && tsMs >= cutoffMs) {
+ nextDetails.set(detailKey, detail);
+ }
+ }
+ entry.visibleInHistoryWindow = nextDetails.size > 0;
+ if (nextDetails.size === 0) {
+ entry.stationDetails = /* @__PURE__ */ new Map();
+ entry.stations = /* @__PURE__ */ new Set();
+ entry.bandMeta = /* @__PURE__ */ new Map();
+ if (canRenderMap) setRetainedMapMarkerVisible(entry.marker, false);
+ else C.markDecodeMapSyncPending();
+ return false;
+ }
+ const nextStations = /* @__PURE__ */ new Set();
+ for (const detail of nextDetails.values()) {
+ const source = String(detail?.source || detail?.station || "").trim().toUpperCase();
+ if (source) nextStations.add(source);
+ }
+ entry.stationDetails = nextDetails;
+ entry.stations = nextStations;
+ entry.bandMeta = collectBandMeta(
+ Array.from(nextDetails.values()).map((detail) => Number(detail?.freq_hz))
);
- const APRS_TRACK_MAX_POINTS = 64;
- const AIS_TRACK_MAX_POINTS = 64;
- const aisMarkers = /* @__PURE__ */ new Map();
- const vdesMarkers = /* @__PURE__ */ new Map();
- let selectedAprsTrackCall = null;
- let selectedAisTrackMmsi = null;
- const HAM_BANDS = [
- { label: "2200m", meters: 2200 },
- { label: "630m", meters: 630 },
- { label: "160m", meters: 160 },
- { label: "80m", meters: 80 },
- { label: "60m", meters: 60 },
- { label: "40m", meters: 40 },
- { label: "30m", meters: 30 },
- { label: "20m", meters: 20 },
- { label: "17m", meters: 17 },
- { label: "15m", meters: 15 },
- { label: "12m", meters: 12 },
- { label: "10m", meters: 10 },
- { label: "6m", meters: 6 },
- { label: "4m", meters: 4 },
- { label: "3m", meters: 3 },
- { label: "2m", meters: 2 },
- { label: "1m", meters: 1 },
- { label: "70cm", meters: 0.7 },
- { label: "23cm", meters: 0.23 },
- { label: "13cm", meters: 0.13 },
- { label: "9cm", meters: 0.09 },
- { label: "6cm", meters: 0.06 },
- { label: "3cm", meters: 0.03 }
- ].map((band) => ({
- ...band,
- nominalHz: 299792458 / band.meters
- }));
- function normalizeLocatorFreqHz(hz) {
- if (!Number.isFinite(hz) || hz <= 0) return null;
- if (hz >= 1e5) return hz;
- const baseHz = Number(mapWindow.ft8BaseHz);
- if (Number.isFinite(baseHz) && baseHz > 0) {
- return baseHz + hz;
+ const count = Math.max(nextDetails.size, nextStations.size || 0, 1);
+ if (!canRenderMap) {
+ C.markDecodeMapSyncPending();
+ return true;
+ }
+ ensureDecodeLocatorMarker(entry);
+ setRetainedMapMarkerVisible(entry.marker, true);
+ if (entry.marker && entry.grid && entry.sourceType) {
+ entry.marker.setStyle(locatorStyleForEntry(entry, count));
+ entry.marker.setPopupContent(buildDecodeLocatorTooltipHtml(entry.grid, entry, entry.sourceType));
+ assignLocatorMarkerMeta(entry.marker, entry.sourceType, entry.bandMeta);
+ }
+ return true;
+ }
+ function pruneMapHistory() {
+ const cutoffMs = mapHistoryCutoffMs();
+ for (const [call, entry] of stationMarkers.entries()) {
+ pruneAprsEntry(call, entry, cutoffMs);
+ }
+ for (const [key, entry] of aisMarkers.entries()) {
+ pruneAisEntry(key, entry, cutoffMs);
+ }
+ for (const [key, entry] of vdesMarkers.entries()) {
+ const tsMs = Number(entry?.msg?._tsMs);
+ const visible = Number.isFinite(tsMs) && tsMs >= cutoffMs;
+ entry.visibleInHistoryWindow = visible;
+ if (!visible) {
+ setRetainedMapMarkerVisible(entry?.marker, false);
+ continue;
}
- return hz;
- }
- function normalizeMapHistoryLimitMinutes(value) {
- const minutes = Math.round(Number(value));
- return MAP_HISTORY_LIMIT_OPTIONS.includes(minutes) ? minutes : 1440;
- }
- function mapHistoryCutoffMs() {
- return Date.now() - mapHistoryLimitMinutes * 60 * 1e3;
- }
- function trimTrackHistory(history, cutoffMs, maxPoints) {
- const list = Array.isArray(history) ? history : [];
- const trimmed = list.filter((point) => Number(point?.tsMs) >= cutoffMs);
- if (trimmed.length > maxPoints) {
- trimmed.splice(0, trimmed.length - maxPoints);
+ ensureVdesMarker(key, entry);
+ setRetainedMapMarkerVisible(entry?.marker, true);
+ if (entry?.marker && entry.msg?.lat != null && entry.msg.lon != null) {
+ entry.marker.setLatLng([entry.msg.lat, entry.msg.lon]);
+ entry.marker.setPopupContent(buildVdesPopupHtml(entry.msg));
}
- return trimmed;
}
- function refreshAprsTrack(call, entry) {
+ for (const [key, entry] of locatorMarkers.entries()) {
+ pruneLocatorEntry(key, entry, cutoffMs);
+ }
+ if (!aprsMap || T.decodeHistoryReplayActive) {
+ C.markDecodeMapSyncPending();
+ return;
+ }
+ rebuildDecodeContactPaths();
+ rebuildMapLocatorFilters();
+ applyMapFilter();
+ }
+ function locatorSourceLabel(type) {
+ if (type === "bookmark") return "Bookmarks";
+ if (type === "wspr") return "WSPR";
+ if (type === "ft4") return "FT4";
+ if (type === "ft2") return "FT2";
+ return "FT8";
+ }
+ function mapSourceLabel(type) {
+ if (type === "bookmark") return "Bookmarks";
+ return String(type || "").toUpperCase();
+ }
+ function locatorFilterColor(type) {
+ const hues = locatorThemeHues();
+ const lightTheme = C.currentTheme() === "light";
+ const sat = lightTheme ? 66 : 76;
+ const light = lightTheme ? 42 : 56;
+ const hue = type === "bookmark" ? hues.bookmark : type === "wspr" ? hues.wspr : type === "ft4" ? hues.ft4 : type === "ft2" ? hues.ft2 : hues.ft8;
+ return `hsl(${hue.toFixed(1)} ${sat}% ${light}%)`;
+ }
+ function mapSourceColor(type) {
+ if (type === "ais") return "#38bdf8";
+ if (type === "vdes") return "#a78bfa";
+ if (type === "sat") return "#f59e0b";
+ if (type === "aprs") return "#00d17f";
+ return locatorFilterColor(type);
+ }
+ function bandForHz(hz) {
+ const rfHz = normalizeLocatorFreqHz(hz);
+ if (rfHz == null || !Number.isFinite(rfHz) || rfHz <= 0) return null;
+ let bestBand = null;
+ let bestDistance = Infinity;
+ for (const band of HAM_BANDS) {
+ const distance = Math.abs(Math.log(rfHz / band.nominalHz));
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestBand = band;
+ }
+ }
+ return bestBand;
+ }
+ function collectBandMeta(freqs) {
+ const out = /* @__PURE__ */ new Map();
+ if (!Array.isArray(freqs)) return out;
+ for (const hz of freqs) {
+ const band = bandForHz(hz);
+ if (band && !out.has(band.label)) out.set(band.label, band.nominalHz);
+ }
+ return out;
+ }
+ function assignLocatorMarkerMeta(marker, sourceType, bandMeta) {
+ if (!marker) return;
+ const safeMeta = bandMeta ?? /* @__PURE__ */ new Map();
+ marker._locatorFilterMeta = {
+ sourceType,
+ bands: new Set(safeMeta.keys()),
+ bandMeta: new Map(safeMeta)
+ };
+ }
+ function parseMapColor(input) {
+ const value = String(input || "").trim();
+ if (!value) return null;
+ const hex = value.match(/^#([0-9a-f]{3,8})$/i);
+ if (hex) {
+ const raw = hex[1];
+ if (!raw) return null;
+ if (raw.length === 3 || raw.length === 4) {
+ return {
+ r: parseInt(raw.charAt(0).repeat(2), 16),
+ g: parseInt(raw.charAt(1).repeat(2), 16),
+ b: parseInt(raw.charAt(2).repeat(2), 16)
+ };
+ }
+ if (raw.length === 6 || raw.length === 8) {
+ return {
+ r: parseInt(raw.slice(0, 2), 16),
+ g: parseInt(raw.slice(2, 4), 16),
+ b: parseInt(raw.slice(4, 6), 16)
+ };
+ }
+ }
+ const rgb = value.match(/^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)/i);
+ if (rgb) {
+ return {
+ r: Math.max(0, Math.min(255, Number(rgb[1]))),
+ g: Math.max(0, Math.min(255, Number(rgb[2]))),
+ b: Math.max(0, Math.min(255, Number(rgb[3])))
+ };
+ }
+ return null;
+ }
+ function rgbToHsl(rgb) {
+ if (!rgb) return null;
+ const r = rgb.r / 255;
+ const g = rgb.g / 255;
+ const b = rgb.b / 255;
+ const max = Math.max(r, g, b);
+ const min = Math.min(r, g, b);
+ const l = (max + min) / 2;
+ if (max === min) {
+ return { h: 0, s: 0, l: l * 100 };
+ }
+ const d = max - min;
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ let h;
+ switch (max) {
+ case r:
+ h = (g - b) / d + (g < b ? 6 : 0);
+ break;
+ case g:
+ h = (b - r) / d + 2;
+ break;
+ default:
+ h = (r - g) / d + 4;
+ break;
+ }
+ return { h: h * 60 % 360, s: s * 100, l: l * 100 };
+ }
+ function wrapHue(hue) {
+ const value = Number(hue) || 0;
+ return (value % 360 + 360) % 360;
+ }
+ function paletteHue(input, fallback) {
+ const hsl = rgbToHsl(parseMapColor(input));
+ return hsl && Number.isFinite(hsl.h) ? hsl.h : fallback;
+ }
+ function locatorThemeHues() {
+ const pal = C.canvasPalette();
+ const baseHue = paletteHue(pal?.spectrumLine, 145);
+ const waveHue = paletteHue(pal?.waveformLine, baseHue + 34);
+ const peakHue = paletteHue(pal?.waveformPeak, baseHue - 42);
+ return {
+ bookmark: wrapHue(baseHue),
+ ft8: wrapHue(peakHue),
+ ft4: wrapHue(peakHue + 30),
+ ft2: wrapHue(peakHue + 60),
+ wspr: wrapHue((waveHue + baseHue) / 2),
+ bandBase: wrapHue(baseHue * 0.65 + peakHue * 0.35)
+ };
+ }
+ function locatorBandIndex(label) {
+ const idx = HAM_BANDS.findIndex((band) => band.label === label);
+ return idx >= 0 ? idx : 0;
+ }
+ function locatorBandChipColor(label) {
+ const hues = locatorThemeHues();
+ const lightTheme = C.currentTheme() === "light";
+ const hue = wrapHue(hues.bandBase + locatorBandIndex(label) * 137.508);
+ const sat = lightTheme ? 68 : 78;
+ const light = lightTheme ? 44 : 58;
+ return `hsl(${hue.toFixed(1)} ${sat}% ${light}%)`;
+ }
+ function locatorBandLabelForEntry(entry) {
+ const meta = entry?.bandMeta ?? /* @__PURE__ */ new Map();
+ if (meta.size === 0) return null;
+ if (mapLocatorFilter.phase === "band" && mapLocatorFilter.bands.size > 0) {
+ for (const label of mapLocatorFilter.bands) {
+ if (meta.has(label)) return label;
+ }
+ }
+ let bestLabel = null;
+ let bestHz = -Infinity;
+ for (const [label, hz] of meta.entries()) {
+ const value = Number.isFinite(hz) ? Number(hz) : 0;
+ if (value > bestHz) {
+ bestHz = value;
+ bestLabel = label;
+ }
+ }
+ return bestLabel;
+ }
+ function locatorHueForEntry(entry) {
+ const hues = locatorThemeHues();
+ if (mapLocatorFilter.phase === "band") {
+ const label = locatorBandLabelForEntry(entry);
+ if (label) {
+ return wrapHue(hues.bandBase + locatorBandIndex(label) * 137.508);
+ }
+ }
+ if (entry?.sourceType === "bookmark") return hues.bookmark;
+ if (entry?.sourceType === "wspr") return hues.wspr;
+ if (entry?.sourceType === "ft4") return hues.ft4;
+ if (entry?.sourceType === "ft2") return hues.ft2;
+ return hues.ft8;
+ }
+ function locatorStyleForEntry(entry, count) {
+ const safeCount = Math.max(1, Number.isFinite(count) ? count : 1);
+ const intensity = Math.min(1, Math.log2(safeCount + 1) / 5);
+ const hue = locatorHueForEntry(entry);
+ const lightTheme = C.currentTheme() === "light";
+ const strokeSat = lightTheme ? 62 : 74;
+ const fillSat = lightTheme ? 68 : 78;
+ const strokeLight = lightTheme ? 40 : 56;
+ const fillLight = lightTheme ? 60 : 42;
+ return {
+ color: `hsl(${hue.toFixed(1)} ${Math.min(92, strokeSat + intensity * 10).toFixed(1)}% ${Math.max(24, strokeLight - intensity * 4).toFixed(1)}%)`,
+ opacity: 0.42 + intensity * 0.5,
+ weight: 1 + intensity * 1.2,
+ fillColor: `hsl(${hue.toFixed(1)} ${Math.min(96, fillSat + intensity * 8).toFixed(1)}% ${Math.max(20, fillLight - intensity * 5).toFixed(1)}%)`,
+ fillOpacity: 0.16 + intensity * 0.34
+ };
+ }
+ function locatorEntryCount(entry) {
+ if (Array.isArray(entry?.bookmarks)) return Math.max(entry.bookmarks.length, 1);
+ if (entry?.stationDetails instanceof Map) return Math.max(entry.stationDetails.size, 1);
+ if (entry?.stations instanceof Set) return Math.max(entry.stations.size, 1);
+ return 1;
+ }
+ function locatorEntryForMarker(marker) {
+ if (!marker) return null;
+ for (const entry of locatorMarkers.values()) {
+ if (entry?.marker === marker) return entry;
+ }
+ return null;
+ }
+ function syncLocatorMarkerStyles() {
+ for (const entry of locatorMarkers.values()) {
+ if (!entry?.marker) continue;
+ entry.marker.setStyle(locatorStyleForEntry(entry, locatorEntryCount(entry)));
+ }
+ for (const entry of decodeContactPaths.values()) {
+ if (!entry?.line) continue;
+ const color = decodeContactPathColor(entry);
+ entry.line.setStyle({ color, opacity: 0.78 });
+ }
+ }
+ function stopSelectedLocatorPulse() {
+ if (selectedLocatorPulseRaf != null) {
+ cancelAnimationFrame(selectedLocatorPulseRaf);
+ selectedLocatorPulseRaf = null;
+ }
+ }
+ function startSelectedLocatorPulse(marker) {
+ stopSelectedLocatorPulse();
+ if (!marker || !aprsMap || !aprsMap.hasLayer(marker)) return;
+ const tick = (ts) => {
+ if (!selectedLocatorMarker || selectedLocatorMarker !== marker || !aprsMap || !aprsMap.hasLayer(marker)) {
+ return;
+ }
+ const entry = locatorEntryForMarker(marker);
if (!entry) return;
- if (!Array.isArray(entry.trackPoints) || entry.trackPoints.length < 2) {
- if (entry.track) {
- entry.track.remove();
- entry.track = null;
- }
- return;
- }
- if (entry.track) {
- entry.track.setLatLngs(entry.trackPoints);
- return;
- }
- const track = L.polyline(entry.trackPoints, {
- color: "#f0be4d",
- weight: 2,
- opacity: 0.72,
- lineCap: "round",
- lineJoin: "round",
+ const base = locatorStyleForEntry(entry, locatorEntryCount(entry));
+ const phase = ts % 1600 / 1600;
+ const wave = (Math.sin(phase * Math.PI * 2 - Math.PI / 2) + 1) / 2;
+ marker.setStyle({
+ ...base,
+ opacity: Math.min(1, (base.opacity || 0.8) + 0.12 * wave),
+ weight: (base.weight || 1.8) + 1.8 * wave
+ });
+ selectedLocatorPulseRaf = requestAnimationFrame(tick);
+ };
+ selectedLocatorPulseRaf = requestAnimationFrame(tick);
+ }
+ function clearMapRadioPath() {
+ for (const p of aprsRadioPaths) p.remove();
+ aprsRadioPaths = [];
+ }
+ function clearDecodeContactPathRender(entry) {
+ if (!entry) return;
+ if (entry.line) {
+ entry.line.remove();
+ entry.line = null;
+ }
+ if (entry.labelMarker) {
+ entry.labelMarker.remove();
+ entry.labelMarker = null;
+ }
+ }
+ function clearDecodeContactPaths() {
+ for (const entry of decodeContactPaths.values()) {
+ clearDecodeContactPathRender(entry);
+ }
+ decodeContactPaths.clear();
+ updateMapPathsAnimationClass();
+ }
+ const MAP_PATHS_STATIC_THRESHOLD = 20;
+ function updateMapPathsAnimationClass() {
+ const mapContainer = mapEl("aprs-map");
+ mapContainer.classList.toggle("map-paths-static", decodeContactPaths.size > MAP_PATHS_STATIC_THRESHOLD);
+ }
+ function formatDecodeContactDistance(distanceKm) {
+ const text = formatDistanceKm(distanceKm);
+ return text || "--";
+ }
+ function decodeLocatorPathVisibility(grid) {
+ const normalizedGrid = String(grid || "").trim().toUpperCase();
+ if (!normalizedGrid || !aprsMap) return false;
+ for (const entry of locatorMarkers.values()) {
+ if (!entry || entry.grid !== normalizedGrid) continue;
+ if (entry.sourceType !== "ft8" && entry.sourceType !== "wspr") continue;
+ if (entry.marker && aprsMap.hasLayer(entry.marker)) return true;
+ }
+ return false;
+ }
+ function midpointLatLon(a, b) {
+ if (!a || !b) return null;
+ if (!Number.isFinite(a.lat) || !Number.isFinite(a.lon) || !Number.isFinite(b.lat) || !Number.isFinite(b.lon)) {
+ return null;
+ }
+ return {
+ lat: (a.lat + b.lat) / 2,
+ lon: (a.lon + b.lon) / 2
+ };
+ }
+ function decodeContactPathColor(entry) {
+ if (entry?.bandLabel) return locatorBandChipColor(entry.bandLabel);
+ const srcEntry = entry.sourceGrid ? locatorMarkers.get(entry.sourceGrid) : void 0;
+ if (srcEntry) {
+ const label = locatorBandLabelForEntry(srcEntry);
+ if (label) return locatorBandChipColor(label);
+ return locatorStyleForEntry(srcEntry, locatorEntryCount(srcEntry)).color ?? locatorFilterColor("ft8");
+ }
+ return locatorFilterColor("ft8");
+ }
+ function ensureDecodeContactPathRendered(entry) {
+ if (!aprsMap || !entry.from || !entry.to) return;
+ const linePoints = [
+ [entry.from.lat, entry.from.lon],
+ [entry.to.lat, entry.to.lon]
+ ];
+ const color = decodeContactPathColor(entry);
+ if (!entry.line) {
+ entry.line = L.polyline(linePoints, {
+ color,
+ opacity: 0.78,
+ className: "decode-contact-path",
+ weight: 2.8,
interactive: false
- });
- track.__trxType = "aprs";
- track._aprsCall = call;
- entry.track = track;
+ }).addTo(aprsMap);
+ } else {
+ entry.line.setLatLngs(linePoints);
+ entry.line.setStyle({ color, opacity: 0.78 });
+ if (!aprsMap.hasLayer(entry.line)) entry.line.addTo(aprsMap);
}
- function refreshAisTrack(mmsi, entry) {
- if (!entry) return;
- if (!Array.isArray(entry.trackPoints) || entry.trackPoints.length < 2) {
- if (entry.track) {
- entry.track.remove();
- entry.track = null;
- }
- return;
- }
- if (entry.track) {
- entry.track.setLatLngs(entry.trackPoints);
- return;
- }
- const track = L.polyline(entry.trackPoints, {
- color: getAisAccentColor(),
- weight: 2,
- opacity: 0.68,
- lineCap: "round",
- lineJoin: "round",
+ const mid = midpointLatLon(entry.from, entry.to);
+ if (!mid) return;
+ const title = `${entry.source} ↔ ${entry.target} · ${entry.distanceText}`;
+ const icon = L.divIcon({
+ className: "decode-contact-distance-label",
+ html: `${escapeMapHtml(entry.distanceText)}`
+ });
+ if (!entry.labelMarker) {
+ entry.labelMarker = L.marker([mid.lat, mid.lon], {
+ icon,
interactive: false,
- dashArray: "5 4"
- });
- track.__trxType = "ais";
- track._aisMmsi = mmsi;
- entry.track = track;
+ keyboard: false,
+ zIndexOffset: 900
+ }).addTo(aprsMap);
+ } else {
+ entry.labelMarker.setLatLng([mid.lat, mid.lon]);
+ entry.labelMarker.setIcon(icon);
+ if (!aprsMap.hasLayer(entry.labelMarker)) entry.labelMarker.addTo(aprsMap);
}
- function setRetainedMapMarkerVisible(marker, visible) {
- if (!marker) return;
- marker.__trxHistoryVisible = visible;
+ if (entry.line && typeof entry.line.bringToBack === "function") entry.line.bringToBack();
+ }
+ function decodeContactPathMatchesCurrentMap(entry) {
+ return !!entry.sourceGrid && !!entry.targetGrid && decodeLocatorPathVisibility(entry.sourceGrid) && decodeLocatorPathVisibility(entry.targetGrid);
+ }
+ function decodeContactPathRenderVisible(entry) {
+ return mapDecodeContactPathsEnabled && decodeContactPathMatchesCurrentMap(entry);
+ }
+ function syncDecodeContactPathVisibility() {
+ if (selectedMapQsoKey) {
+ const selectedEntry = decodeContactPaths.get(selectedMapQsoKey);
+ if (!selectedEntry || !decodeContactPathMatchesCurrentMap(selectedEntry)) {
+ selectedMapQsoKey = null;
+ }
+ }
+ for (const entry of decodeContactPaths.values()) {
+ const visible = decodeContactPathRenderVisible(entry) && (!selectedMapQsoKey || entry.pathKey === selectedMapQsoKey);
if (!visible) {
- if (marker === selectedLocatorMarker) {
- setSelectedLocatorMarker(null);
- clearMapRadioPath();
- }
- if (aprsMap && aprsMap.hasLayer(marker)) marker.removeFrom(aprsMap);
+ clearDecodeContactPathRender(entry);
+ continue;
}
+ ensureDecodeContactPathRendered(entry);
}
- function ensureAprsMarker(call, entry) {
- if (!aprsMap || !entry || entry.marker || entry.lat == null || entry.lon == null) return;
- _aprsAddMarkerToMap(call, entry);
- }
- function ensureAisMarker(key, entry) {
- if (!aprsMap || !entry || entry.marker || entry?.msg?.lat == null || entry?.msg?.lon == null) return;
- const marker = createAisMarker(entry.msg.lat, entry.msg.lon, entry.msg).addTo(aprsMap).bindPopup(buildAisPopupHtml(entry.msg));
- marker.__trxType = "ais";
- marker.__trxRigIds = entry.rigIds || /* @__PURE__ */ new Set();
- marker._aisMmsi = String(key);
- entry.marker = marker;
- mapMarkers.add(marker);
- }
- function ensureVdesMarker(key, entry) {
- if (!aprsMap || !entry || entry.marker || entry?.msg?.lat == null || entry?.msg?.lon == null) return;
- const marker = L.circleMarker([entry.msg.lat, entry.msg.lon], {
- radius: 5,
- color: "#5c394f",
- fillColor: "#c46392",
- fillOpacity: 0.82
- }).addTo(aprsMap).bindPopup(buildVdesPopupHtml(entry.msg));
- marker.__trxType = "vdes";
- marker.__trxRigIds = entry.rigIds || /* @__PURE__ */ new Set();
- marker._vdesKey = String(key);
- entry.marker = marker;
- mapMarkers.add(marker);
- }
- function ensureDecodeLocatorMarker(entry) {
- if (!aprsMap || !entry || entry.marker || !entry.grid || entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "ft2" && entry.sourceType !== "wspr") return;
- const bounds = maidenheadToBounds(entry.grid);
- if (!bounds) return;
- const count = Math.max(entry.stationDetails?.size || 0, entry.stations?.size || 0, 1);
- const tooltipHtml = buildDecodeLocatorTooltipHtml(entry.grid, entry, entry.sourceType);
- const marker = L.rectangle(bounds, locatorStyleForEntry(entry, count)).addTo(aprsMap).bindPopup(tooltipHtml);
- marker.__trxType = entry.sourceType;
- marker.__trxRigIds = entry.rigIds || /* @__PURE__ */ new Set();
- sendLocatorOverlayToBack(marker);
- assignLocatorMarkerMeta(marker, entry.sourceType, entry.bandMeta);
- entry.marker = marker;
- mapMarkers.add(marker);
- }
- function pruneAprsEntry(call, entry, cutoffMs) {
- const canRenderMap = !!aprsMap && !T.decodeHistoryReplayActive;
- const pktTsMs = Number(entry?.pkt?._tsMs);
- const visible = Number.isFinite(pktTsMs) && pktTsMs >= cutoffMs;
- entry.visibleInHistoryWindow = visible;
- entry.trackPoints = trimTrackHistory(entry.trackHistory, cutoffMs, APRS_TRACK_MAX_POINTS).map((point) => [point.lat, point.lon]);
- if (canRenderMap) {
- refreshAprsTrack(call, entry);
- } else {
- C.markDecodeMapSyncPending();
- }
- if (!visible) {
- if (canRenderMap && selectedAprsTrackCall && String(selectedAprsTrackCall) === String(call)) {
- selectedAprsTrackCall = null;
- }
- if (canRenderMap && entry?.track) {
- entry.track.remove();
- entry.track = null;
- }
- if (canRenderMap) setRetainedMapMarkerVisible(entry?.marker, false);
- return false;
- }
- if (!canRenderMap) return true;
- ensureAprsMarker(call, entry);
- setRetainedMapMarkerVisible(entry?.marker, true);
- if (entry?.marker && entry.lat != null && entry.lon != null) {
- entry.marker.setLatLng([entry.lat, entry.lon]);
- entry.marker.setPopupContent(buildAprsPopupHtml(call, entry.lat, entry.lon, entry.info || "", entry.pkt));
- }
- return true;
- }
- function pruneAisEntry(key, entry, cutoffMs) {
- const canRenderMap = !!aprsMap && !T.decodeHistoryReplayActive;
- const msgTsMs = Number(entry?.msg?._tsMs);
- const visible = Number.isFinite(msgTsMs) && msgTsMs >= cutoffMs;
- entry.visibleInHistoryWindow = visible;
- entry.trackPoints = trimTrackHistory(entry.trackHistory, cutoffMs, AIS_TRACK_MAX_POINTS).map((point) => [point.lat, point.lon]);
- if (canRenderMap) {
- refreshAisTrack(key, entry);
- } else {
- C.markDecodeMapSyncPending();
- }
- if (!visible) {
- if (canRenderMap && selectedAisTrackMmsi && String(selectedAisTrackMmsi) === String(key)) {
- selectedAisTrackMmsi = null;
- }
- if (canRenderMap && entry?.track) {
- entry.track.remove();
- entry.track = null;
- }
- if (canRenderMap) setRetainedMapMarkerVisible(entry?.marker, false);
- return false;
- }
- if (!canRenderMap) return true;
- ensureAisMarker(key, entry);
- setRetainedMapMarkerVisible(entry?.marker, true);
- if (entry?.marker && entry.msg) {
- updateAisMarker(entry.marker, entry.msg, buildAisPopupHtml(entry.msg));
- }
- return true;
- }
- function pruneLocatorEntry(key, entry, cutoffMs) {
- const canRenderMap = !!aprsMap && !T.decodeHistoryReplayActive;
- if (!entry || entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "ft2" && entry.sourceType !== "wspr") return true;
- if (!(entry.allStationDetails instanceof Map)) {
- entry.allStationDetails = entry.stationDetails instanceof Map ? new Map(entry.stationDetails) : /* @__PURE__ */ new Map();
- }
- const nextDetails = /* @__PURE__ */ new Map();
- for (const [detailKey, detail] of entry.allStationDetails.entries()) {
- const tsMs = Number(detail?.ts_ms);
- if (Number.isFinite(tsMs) && tsMs >= cutoffMs) {
- nextDetails.set(detailKey, detail);
+ scheduleStatsRender();
+ updateMapPathsAnimationClass();
+ }
+ function _resolveReceiverLocations(rigIds) {
+ const seen = /* @__PURE__ */ new Set();
+ const locations = [];
+ if (rigIds && rigIds.size) {
+ for (const rid of rigIds) {
+ const rig = T.serverRigs.find((r) => r.remote === rid);
+ if (rig && rig.latitude != null && rig.longitude != null) {
+ const key = _receiverLocationKey(rig.latitude, rig.longitude);
+ if (!seen.has(key)) {
+ seen.add(key);
+ locations.push([rig.latitude, rig.longitude]);
+ }
}
}
- entry.visibleInHistoryWindow = nextDetails.size > 0;
- if (nextDetails.size === 0) {
- entry.stationDetails = /* @__PURE__ */ new Map();
- entry.stations = /* @__PURE__ */ new Set();
- entry.bandMeta = /* @__PURE__ */ new Map();
- if (canRenderMap) setRetainedMapMarkerVisible(entry.marker, false);
- else C.markDecodeMapSyncPending();
- return false;
- }
- const nextStations = /* @__PURE__ */ new Set();
- for (const detail of nextDetails.values()) {
- const source = String(detail?.source || detail?.station || "").trim().toUpperCase();
- if (source) nextStations.add(source);
- }
- entry.stationDetails = nextDetails;
- entry.stations = nextStations;
- entry.bandMeta = collectBandMeta(
- Array.from(nextDetails.values()).map((detail) => Number(detail?.freq_hz))
+ }
+ if (locations.length === 0 && T.serverLat != null && T.serverLon != null) {
+ locations.push([T.serverLat, T.serverLon]);
+ }
+ return locations;
+ }
+ function setMapRadioPathTo(lat, lon, color, className = "aprs-radio-path", rigIds) {
+ clearMapRadioPath();
+ if (!mapP2pRadioPathsEnabled || !Number.isFinite(lat) || !Number.isFinite(lon) || !aprsMap) {
+ return;
+ }
+ const sources = _resolveReceiverLocations(rigIds);
+ for (const src of sources) {
+ aprsRadioPaths.push(
+ L.polyline(
+ [src, [lat, lon]],
+ { color, opacity: 0.85, weight: 2, interactive: false, className }
+ ).addTo(aprsMap)
);
- const count = Math.max(nextDetails.size, nextStations.size || 0, 1);
- if (!canRenderMap) {
- C.markDecodeMapSyncPending();
- return true;
- }
- ensureDecodeLocatorMarker(entry);
- setRetainedMapMarkerVisible(entry.marker, true);
- if (entry.marker && entry.grid && entry.sourceType) {
- entry.marker.setStyle(locatorStyleForEntry(entry, count));
- entry.marker.setPopupContent(buildDecodeLocatorTooltipHtml(entry.grid, entry, entry.sourceType));
- assignLocatorMarkerMeta(entry.marker, entry.sourceType, entry.bandMeta);
- }
- return true;
}
- function pruneMapHistory() {
- const cutoffMs = mapHistoryCutoffMs();
- for (const [call, entry] of stationMarkers.entries()) {
- pruneAprsEntry(call, entry, cutoffMs);
- }
- for (const [key, entry] of aisMarkers.entries()) {
- pruneAisEntry(key, entry, cutoffMs);
- }
- for (const [key, entry] of vdesMarkers.entries()) {
- const tsMs = Number(entry?.msg?._tsMs);
- const visible = Number.isFinite(tsMs) && tsMs >= cutoffMs;
- entry.visibleInHistoryWindow = visible;
- if (!visible) {
- setRetainedMapMarkerVisible(entry?.marker, false);
- continue;
- }
- ensureVdesMarker(key, entry);
- setRetainedMapMarkerVisible(entry?.marker, true);
- if (entry?.marker && entry.msg?.lat != null && entry.msg.lon != null) {
- entry.marker.setLatLng([entry.msg.lat, entry.msg.lon]);
- entry.marker.setPopupContent(buildVdesPopupHtml(entry.msg));
+ }
+ function locatorMarkerCenter(marker) {
+ if (!marker) return null;
+ if (typeof marker.getBounds === "function") {
+ const bounds = marker.getBounds();
+ if (bounds && typeof bounds.getCenter === "function") {
+ const center = bounds.getCenter();
+ if (Number.isFinite(center?.lat) && Number.isFinite(center?.lng)) {
+ return { lat: center.lat, lon: center.lng };
}
}
- for (const [key, entry] of locatorMarkers.entries()) {
- pruneLocatorEntry(key, entry, cutoffMs);
+ }
+ if (typeof marker.getLatLng === "function") {
+ const ll = marker.getLatLng();
+ if (Number.isFinite(ll?.lat) && Number.isFinite(ll?.lng)) {
+ return { lat: ll.lat, lon: ll.lng };
}
- if (!aprsMap || T.decodeHistoryReplayActive) {
- C.markDecodeMapSyncPending();
- return;
+ }
+ return null;
+ }
+ function setLocatorMarkerHighlight(marker, enabled) {
+ const element = typeof marker?.getElement === "function" ? marker.getElement() : marker?._path;
+ if (!element) return;
+ element.classList.toggle("trx-locator-selected", !!enabled);
+ }
+ function setSelectedLocatorMarker(marker) {
+ if (selectedLocatorMarker && selectedLocatorMarker !== marker) {
+ setLocatorMarkerHighlight(selectedLocatorMarker, false);
+ const prevEntry = locatorEntryForMarker(selectedLocatorMarker);
+ if (prevEntry?.marker) {
+ prevEntry.marker.setStyle(locatorStyleForEntry(prevEntry, locatorEntryCount(prevEntry)));
}
- rebuildDecodeContactPaths();
+ }
+ stopSelectedLocatorPulse();
+ selectedLocatorMarker = marker || null;
+ if (selectedLocatorMarker) {
+ setLocatorMarkerHighlight(selectedLocatorMarker, true);
+ startSelectedLocatorPulse(selectedLocatorMarker);
+ }
+ }
+ function isLocatorOverlay(marker) {
+ const type = marker?.__trxType;
+ return type === "bookmark" || type === "ft8" || type === "ft4" || type === "ft2" || type === "wspr";
+ }
+ function sendLocatorOverlayToBack(marker) {
+ if (!isLocatorOverlay(marker) || typeof marker?.bringToBack !== "function") return;
+ marker.bringToBack();
+ }
+ function renderMapLocatorChipRow(container, items, selectedSet, kind) {
+ if (!container) return;
+ container.replaceChildren();
+ if (!Array.isArray(items) || items.length === 0) {
+ container.innerHTML = `No ${kind === "band" ? "bands" : "sources"} available`;
+ return;
+ }
+ let helperText = "";
+ const sourceKeys = kind === "source" ? Object.keys(DEFAULT_MAP_SOURCE_FILTER) : [];
+ const noneSelected = kind === "source" && sourceKeys.every((k) => !mapFilter[k]);
+ if (kind === "source") {
+ if (noneSelected) {
+ helperText = "All sources visible — click to filter";
+ }
+ } else if (!(selectedSet instanceof Set) || selectedSet.size === 0) {
+ helperText = `All ${kind === "band" ? "bands" : "sources"} visible by default`;
+ }
+ for (const item of items) {
+ const btn = document.createElement("button");
+ btn.type = "button";
+ btn.className = "map-locator-chip";
+ const isActive = kind === "source" ? !!mapFilter[item.key] : !!selectedSet?.has(item.key);
+ if (kind === "source" && noneSelected) {
+ btn.classList.add("is-default");
+ } else if (!isActive) {
+ btn.classList.add("is-inactive");
+ }
+ btn.dataset.filterKind = kind;
+ btn.dataset.filterKey = item.key;
+ btn.style.setProperty("--chip-color", item.color);
+ btn.innerHTML = `${escapeMapHtml(item.label)}`;
+ container.appendChild(btn);
+ }
+ if (helperText) {
+ const hint = document.createElement("span");
+ hint.className = "map-locator-empty";
+ hint.textContent = helperText;
+ container.appendChild(hint);
+ }
+ }
+ function renderMapLocatorPhaseRow(container, phase) {
+ if (!container) return;
+ container.replaceChildren();
+ const phases = [
+ { key: "type", label: "Source" },
+ { key: "band", label: "Band" }
+ ];
+ for (const item of phases) {
+ const btn = document.createElement("button");
+ btn.type = "button";
+ btn.className = "map-locator-phase-btn";
+ if (phase === item.key) btn.classList.add("is-active");
+ btn.dataset.phase = item.key;
+ btn.textContent = item.label;
+ container.appendChild(btn);
+ }
+ }
+ function renderMapLocatorLegend(phase, sourceItems, bandItems) {
+ const legendEl = mapEl("map-band-legend");
+ if (!legendEl) return;
+ const isSourcePhase = phase === "type";
+ const items = Array.isArray(isSourcePhase ? sourceItems : bandItems) ? isSourcePhase ? sourceItems : bandItems : [];
+ if (items.length === 0) {
+ legendEl.classList.add("is-empty");
+ legendEl.replaceChildren();
+ return;
+ }
+ legendEl.classList.remove("is-empty");
+ const rows = items.map((item) => {
+ const label = escapeMapHtml(item.label);
+ const color = escapeMapHtml(item.color);
+ return `${label}`;
+ }).join("");
+ const title = isSourcePhase ? "Source Colors" : "Band Colors";
+ legendEl.innerHTML = `${title}
${rows}
`;
+ }
+ mapWindow.enableMapSourceFilter = function(key) {
+ if (Object.prototype.hasOwnProperty.call(mapFilter, key) && !mapFilter[key]) {
+ mapFilter[key] = true;
rebuildMapLocatorFilters();
applyMapFilter();
}
- function locatorSourceLabel(type) {
- if (type === "bookmark") return "Bookmarks";
- if (type === "wspr") return "WSPR";
- if (type === "ft4") return "FT4";
- if (type === "ft2") return "FT2";
- return "FT8";
- }
- function mapSourceLabel(type) {
- if (type === "bookmark") return "Bookmarks";
- return String(type || "").toUpperCase();
- }
- function locatorFilterColor(type) {
- const hues = locatorThemeHues();
- const lightTheme = C.currentTheme() === "light";
- const sat = lightTheme ? 66 : 76;
- const light = lightTheme ? 42 : 56;
- const hue = type === "bookmark" ? hues.bookmark : type === "wspr" ? hues.wspr : type === "ft4" ? hues.ft4 : type === "ft2" ? hues.ft2 : hues.ft8;
- return `hsl(${hue.toFixed(1)} ${sat}% ${light}%)`;
- }
- function mapSourceColor(type) {
- if (type === "ais") return "#38bdf8";
- if (type === "vdes") return "#a78bfa";
- if (type === "sat") return "#f59e0b";
- if (type === "aprs") return "#00d17f";
- return locatorFilterColor(type);
- }
- function bandForHz(hz) {
- const rfHz = normalizeLocatorFreqHz(hz);
- if (rfHz == null || !Number.isFinite(rfHz) || rfHz <= 0) return null;
- let bestBand = null;
- let bestDistance = Infinity;
- for (const band of HAM_BANDS) {
- const distance = Math.abs(Math.log(rfHz / band.nominalHz));
- if (distance < bestDistance) {
- bestDistance = distance;
- bestBand = band;
- }
+ };
+ function rebuildMapLocatorFilters() {
+ const phaseEl = mapEl("map-locator-phase");
+ const choiceEl = mapEl("map-locator-choice-filter");
+ const choiceLabelEl = mapEl("map-locator-choice-label");
+ const availableSources = /* @__PURE__ */ new Set();
+ for (const entry of aisMarkers.values()) {
+ if (entry?.visibleInHistoryWindow) {
+ availableSources.add("ais");
+ break;
}
- return bestBand;
}
- function collectBandMeta(freqs) {
- const out = /* @__PURE__ */ new Map();
- if (!Array.isArray(freqs)) return out;
- for (const hz of freqs) {
- const band = bandForHz(hz);
- if (band && !out.has(band.label)) out.set(band.label, band.nominalHz);
+ for (const entry of vdesMarkers.values()) {
+ if (entry?.visibleInHistoryWindow) {
+ availableSources.add("vdes");
+ break;
}
- return out;
}
- function assignLocatorMarkerMeta(marker, sourceType, bandMeta) {
- if (!marker) return;
- const safeMeta = bandMeta ?? /* @__PURE__ */ new Map();
- marker._locatorFilterMeta = {
- sourceType,
- bands: new Set(safeMeta.keys()),
- bandMeta: new Map(safeMeta)
- };
- }
- function parseMapColor(input) {
- const value = String(input || "").trim();
- if (!value) return null;
- const hex = value.match(/^#([0-9a-f]{3,8})$/i);
- if (hex) {
- const raw = hex[1];
- if (!raw) return null;
- if (raw.length === 3 || raw.length === 4) {
- return {
- r: parseInt(raw.charAt(0).repeat(2), 16),
- g: parseInt(raw.charAt(1).repeat(2), 16),
- b: parseInt(raw.charAt(2).repeat(2), 16)
- };
- }
- if (raw.length === 6 || raw.length === 8) {
- return {
- r: parseInt(raw.slice(0, 2), 16),
- g: parseInt(raw.slice(2, 4), 16),
- b: parseInt(raw.slice(4, 6), 16)
- };
- }
+ for (const entry of stationMarkers.values()) {
+ if (entry?.type === "aprs" && entry?.visibleInHistoryWindow) {
+ availableSources.add("aprs");
+ break;
}
- const rgb = value.match(/^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)/i);
- if (rgb) {
- return {
- r: Math.max(0, Math.min(255, Number(rgb[1]))),
- g: Math.max(0, Math.min(255, Number(rgb[2]))),
- b: Math.max(0, Math.min(255, Number(rgb[3])))
- };
- }
- return null;
}
- function rgbToHsl(rgb) {
- if (!rgb) return null;
- const r = rgb.r / 255;
- const g = rgb.g / 255;
- const b = rgb.b / 255;
- const max = Math.max(r, g, b);
- const min = Math.min(r, g, b);
- const l = (max + min) / 2;
- if (max === min) {
- return { h: 0, s: 0, l: l * 100 };
- }
- const d = max - min;
- const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
- let h;
- switch (max) {
- case r:
- h = (g - b) / d + (g < b ? 6 : 0);
- break;
- case g:
- h = (b - r) / d + 2;
- break;
- default:
- h = (r - g) / d + 4;
- break;
- }
- return { h: h * 60 % 360, s: s * 100, l: l * 100 };
- }
- function wrapHue(hue) {
- const value = Number(hue) || 0;
- return (value % 360 + 360) % 360;
- }
- function paletteHue(input, fallback) {
- const hsl = rgbToHsl(parseMapColor(input));
- return hsl && Number.isFinite(hsl.h) ? hsl.h : fallback;
- }
- function locatorThemeHues() {
- const pal = C.canvasPalette();
- const baseHue = paletteHue(pal?.spectrumLine, 145);
- const waveHue = paletteHue(pal?.waveformLine, baseHue + 34);
- const peakHue = paletteHue(pal?.waveformPeak, baseHue - 42);
- return {
- bookmark: wrapHue(baseHue),
- ft8: wrapHue(peakHue),
- ft4: wrapHue(peakHue + 30),
- ft2: wrapHue(peakHue + 60),
- wspr: wrapHue((waveHue + baseHue) / 2),
- bandBase: wrapHue(baseHue * 0.65 + peakHue * 0.35)
- };
- }
- function locatorBandIndex(label) {
- const idx = HAM_BANDS.findIndex((band) => band.label === label);
- return idx >= 0 ? idx : 0;
- }
- function locatorBandChipColor(label) {
- const hues = locatorThemeHues();
- const lightTheme = C.currentTheme() === "light";
- const hue = wrapHue(hues.bandBase + locatorBandIndex(label) * 137.508);
- const sat = lightTheme ? 68 : 78;
- const light = lightTheme ? 44 : 58;
- return `hsl(${hue.toFixed(1)} ${sat}% ${light}%)`;
- }
- function locatorBandLabelForEntry(entry) {
+ const bandMap = /* @__PURE__ */ new Map();
+ for (const entry of locatorMarkers.values()) {
+ const sourceType = entry?.sourceType;
+ if (!sourceType) continue;
+ if ((sourceType === "ft8" || sourceType === "ft4" || sourceType === "ft2" || sourceType === "wspr") && !entry?.visibleInHistoryWindow) continue;
+ availableSources.add(sourceType);
const meta = entry?.bandMeta ?? /* @__PURE__ */ new Map();
- if (meta.size === 0) return null;
- if (mapLocatorFilter.phase === "band" && mapLocatorFilter.bands.size > 0) {
- for (const label of mapLocatorFilter.bands) {
- if (meta.has(label)) return label;
- }
- }
- let bestLabel = null;
- let bestHz = -Infinity;
for (const [label, hz] of meta.entries()) {
- const value = Number.isFinite(hz) ? Number(hz) : 0;
- if (value > bestHz) {
- bestHz = value;
- bestLabel = label;
+ if (!bandMap.has(label)) {
+ bandMap.set(label, {
+ key: label,
+ label,
+ color: locatorBandChipColor(label),
+ kind: "band",
+ sortHz: Number.isFinite(hz) ? hz : 0
+ });
+ continue;
+ }
+ const existing = bandMap.get(label);
+ if (existing && Number.isFinite(hz) && (existing.sortHz == null || !Number.isFinite(existing.sortHz) || hz > existing.sortHz)) {
+ existing.sortHz = hz;
+ }
+ if (existing && !existing.color) {
+ existing.color = locatorBandChipColor(label);
}
}
- return bestLabel;
}
- function locatorHueForEntry(entry) {
- const hues = locatorThemeHues();
- if (mapLocatorFilter.phase === "band") {
- const label = locatorBandLabelForEntry(entry);
- if (label) {
- return wrapHue(hues.bandBase + locatorBandIndex(label) * 137.508);
- }
- }
- if (entry?.sourceType === "bookmark") return hues.bookmark;
- if (entry?.sourceType === "wspr") return hues.wspr;
- if (entry?.sourceType === "ft4") return hues.ft4;
- if (entry?.sourceType === "ft2") return hues.ft2;
- return hues.ft8;
+ for (const key of Array.from(mapLocatorFilter.bands)) {
+ if (!bandMap.has(key)) mapLocatorFilter.bands.delete(key);
}
- function locatorStyleForEntry(entry, count) {
- const safeCount = Math.max(1, Number.isFinite(count) ? count : 1);
- const intensity = Math.min(1, Math.log2(safeCount + 1) / 5);
- const hue = locatorHueForEntry(entry);
- const lightTheme = C.currentTheme() === "light";
- const strokeSat = lightTheme ? 62 : 74;
- const fillSat = lightTheme ? 68 : 78;
- const strokeLight = lightTheme ? 40 : 56;
- const fillLight = lightTheme ? 60 : 42;
- return {
- color: `hsl(${hue.toFixed(1)} ${Math.min(92, strokeSat + intensity * 10).toFixed(1)}% ${Math.max(24, strokeLight - intensity * 4).toFixed(1)}%)`,
- opacity: 0.42 + intensity * 0.5,
- weight: 1 + intensity * 1.2,
- fillColor: `hsl(${hue.toFixed(1)} ${Math.min(96, fillSat + intensity * 8).toFixed(1)}% ${Math.max(20, fillLight - intensity * 5).toFixed(1)}%)`,
- fillOpacity: 0.16 + intensity * 0.34
- };
+ const sourceItems = ["ais", "vdes", "aprs", "bookmark", "ft8", "ft4", "ft2", "wspr"].filter((key) => availableSources.has(key)).map((key) => ({
+ key,
+ label: mapSourceLabel(key),
+ color: mapSourceColor(key),
+ kind: "source"
+ }));
+ const bandItems = Array.from(bandMap.values()).sort((a, b) => (b.sortHz ?? 0) - (a.sortHz ?? 0) || a.label.localeCompare(b.label));
+ renderMapLocatorLegend(mapLocatorFilter.phase, sourceItems, bandItems);
+ if (!phaseEl || !choiceEl || !choiceLabelEl) return;
+ renderMapLocatorPhaseRow(phaseEl, mapLocatorFilter.phase);
+ if (mapLocatorFilter.phase === "band") {
+ choiceLabelEl.textContent = "Visible Bands";
+ renderMapLocatorChipRow(choiceEl, bandItems, mapLocatorFilter.bands, "band");
+ } else {
+ choiceLabelEl.textContent = "Visible Sources";
+ renderMapLocatorChipRow(choiceEl, sourceItems, null, "source");
}
- function locatorEntryCount(entry) {
- if (Array.isArray(entry?.bookmarks)) return Math.max(entry.bookmarks.length, 1);
- if (entry?.stationDetails instanceof Map) return Math.max(entry.stationDetails.size, 1);
- if (entry?.stations instanceof Set) return Math.max(entry.stations.size, 1);
- return 1;
- }
- function locatorEntryForMarker(marker) {
- if (!marker) return null;
- for (const entry of locatorMarkers.values()) {
- if (entry?.marker === marker) return entry;
- }
- return null;
- }
- function syncLocatorMarkerStyles() {
- for (const entry of locatorMarkers.values()) {
- if (!entry?.marker) continue;
- entry.marker.setStyle(locatorStyleForEntry(entry, locatorEntryCount(entry)));
- }
- for (const entry of decodeContactPaths.values()) {
- if (!entry?.line) continue;
- const color = decodeContactPathColor(entry);
- entry.line.setStyle({ color, opacity: 0.78 });
- }
- }
- function stopSelectedLocatorPulse() {
- if (selectedLocatorPulseRaf != null) {
- cancelAnimationFrame(selectedLocatorPulseRaf);
- selectedLocatorPulseRaf = null;
- }
- }
- function startSelectedLocatorPulse(marker) {
- stopSelectedLocatorPulse();
- if (!marker || !aprsMap || !aprsMap.hasLayer(marker)) return;
- const tick = (ts) => {
- if (!selectedLocatorMarker || selectedLocatorMarker !== marker || !aprsMap || !aprsMap.hasLayer(marker)) {
- return;
- }
- const entry = locatorEntryForMarker(marker);
- if (!entry) return;
- const base = locatorStyleForEntry(entry, locatorEntryCount(entry));
- const phase = ts % 1600 / 1600;
- const wave = (Math.sin(phase * Math.PI * 2 - Math.PI / 2) + 1) / 2;
- marker.setStyle({
- ...base,
- opacity: Math.min(1, (base.opacity || 0.8) + 0.12 * wave),
- weight: (base.weight || 1.8) + 1.8 * wave
- });
- selectedLocatorPulseRaf = requestAnimationFrame(tick);
- };
- selectedLocatorPulseRaf = requestAnimationFrame(tick);
- }
- function clearMapRadioPath() {
- for (const p of aprsRadioPaths) p.remove();
- aprsRadioPaths = [];
- }
- function clearDecodeContactPathRender(entry) {
- if (!entry) return;
- if (entry.line) {
- entry.line.remove();
- entry.line = null;
- }
- if (entry.labelMarker) {
- entry.labelMarker.remove();
- entry.labelMarker = null;
- }
- }
- function clearDecodeContactPaths() {
- for (const entry of decodeContactPaths.values()) {
- clearDecodeContactPathRender(entry);
- }
- decodeContactPaths.clear();
- updateMapPathsAnimationClass();
- }
- const MAP_PATHS_STATIC_THRESHOLD = 20;
- function updateMapPathsAnimationClass() {
- const mapContainer = mapEl("aprs-map");
- mapContainer.classList.toggle("map-paths-static", decodeContactPaths.size > MAP_PATHS_STATIC_THRESHOLD);
- }
- function formatDecodeContactDistance(distanceKm) {
- const text = formatDistanceKm(distanceKm);
- return text || "--";
- }
- function decodeLocatorPathVisibility(grid) {
- const normalizedGrid = String(grid || "").trim().toUpperCase();
- if (!normalizedGrid || !aprsMap) return false;
- for (const entry of locatorMarkers.values()) {
- if (!entry || entry.grid !== normalizedGrid) continue;
- if (entry.sourceType !== "ft8" && entry.sourceType !== "wspr") continue;
- if (entry.marker && aprsMap.hasLayer(entry.marker)) return true;
+ syncLocatorMarkerStyles();
+ syncDecodeContactPathVisibility();
+ }
+ function markerPassesLocatorFilters(marker) {
+ const meta = marker?._locatorFilterMeta;
+ if (!meta) return true;
+ if (mapLocatorFilter.phase === "band") {
+ if (mapLocatorFilter.bands.size === 0) return true;
+ if (!(meta.bands instanceof Set)) return false;
+ for (const label of mapLocatorFilter.bands) {
+ if (meta.bands.has(label)) return true;
}
return false;
}
- function midpointLatLon(a, b) {
- if (!a || !b) return null;
- if (!Number.isFinite(a.lat) || !Number.isFinite(a.lon) || !Number.isFinite(b.lat) || !Number.isFinite(b.lon)) {
- return null;
- }
- return {
- lat: (a.lat + b.lat) / 2,
- lon: (a.lon + b.lon) / 2
- };
- }
- function decodeContactPathColor(entry) {
- if (entry?.bandLabel) return locatorBandChipColor(entry.bandLabel);
- const srcEntry = entry.sourceGrid ? locatorMarkers.get(entry.sourceGrid) : void 0;
- if (srcEntry) {
- const label = locatorBandLabelForEntry(srcEntry);
- if (label) return locatorBandChipColor(label);
- return locatorStyleForEntry(srcEntry, locatorEntryCount(srcEntry)).color ?? locatorFilterColor("ft8");
- }
- return locatorFilterColor("ft8");
- }
- function ensureDecodeContactPathRendered(entry) {
- if (!aprsMap || !entry.from || !entry.to) return;
- const linePoints = [
- [entry.from.lat, entry.from.lon],
- [entry.to.lat, entry.to.lon]
- ];
- const color = decodeContactPathColor(entry);
- if (!entry.line) {
- entry.line = L.polyline(linePoints, {
- color,
- opacity: 0.78,
- className: "decode-contact-path",
- weight: 2.8,
- interactive: false
- }).addTo(aprsMap);
- } else {
- entry.line.setLatLngs(linePoints);
- entry.line.setStyle({ color, opacity: 0.78 });
- if (!aprsMap.hasLayer(entry.line)) entry.line.addTo(aprsMap);
- }
- const mid = midpointLatLon(entry.from, entry.to);
- if (!mid) return;
- const title = `${entry.source} ↔ ${entry.target} · ${entry.distanceText}`;
- const icon = L.divIcon({
- className: "decode-contact-distance-label",
- html: `${escapeMapHtml(entry.distanceText)}`
- });
- if (!entry.labelMarker) {
- entry.labelMarker = L.marker([mid.lat, mid.lon], {
- icon,
- interactive: false,
- keyboard: false,
- zIndexOffset: 900
- }).addTo(aprsMap);
- } else {
- entry.labelMarker.setLatLng([mid.lat, mid.lon]);
- entry.labelMarker.setIcon(icon);
- if (!aprsMap.hasLayer(entry.labelMarker)) entry.labelMarker.addTo(aprsMap);
- }
- if (entry.line && typeof entry.line.bringToBack === "function") entry.line.bringToBack();
- }
- function decodeContactPathMatchesCurrentMap(entry) {
- return !!entry.sourceGrid && !!entry.targetGrid && decodeLocatorPathVisibility(entry.sourceGrid) && decodeLocatorPathVisibility(entry.targetGrid);
- }
- function decodeContactPathRenderVisible(entry) {
- return mapDecodeContactPathsEnabled && decodeContactPathMatchesCurrentMap(entry);
- }
- function syncDecodeContactPathVisibility() {
- if (selectedMapQsoKey) {
- const selectedEntry = decodeContactPaths.get(selectedMapQsoKey);
- if (!selectedEntry || !decodeContactPathMatchesCurrentMap(selectedEntry)) {
- selectedMapQsoKey = null;
+ return true;
+ }
+ function markerSearchText(marker) {
+ const type = marker?.__trxType;
+ if (type === "bookmark" || type === "ft8" || type === "ft4" || type === "ft2" || type === "wspr") {
+ const entry = locatorEntryForMarker(marker);
+ const parts = [];
+ if (entry?.grid) parts.push(entry.grid);
+ if (entry?.sourceType) parts.push(locatorSourceLabel(entry.sourceType));
+ if (entry?.bandMeta instanceof Map) parts.push(...Array.from(entry.bandMeta.keys()));
+ if (Array.isArray(entry?.bookmarks)) {
+ for (const bm of entry.bookmarks) {
+ if (bm?.name) parts.push(String(bm.name));
+ if (bm?.locator) parts.push(String(bm.locator));
+ if (bm?.mode) parts.push(String(bm.mode));
+ if (bm?.category) parts.push(String(bm.category));
+ if (bm?.comment) parts.push(String(bm.comment));
+ if (Number.isFinite(bm?.freq_hz)) parts.push(String(Math.round(Number(bm.freq_hz))));
}
}
- for (const entry of decodeContactPaths.values()) {
- const visible = decodeContactPathRenderVisible(entry) && (!selectedMapQsoKey || entry.pathKey === selectedMapQsoKey);
- if (!visible) {
- clearDecodeContactPathRender(entry);
- continue;
- }
- ensureDecodeContactPathRendered(entry);
+ if (entry?.stations instanceof Set) {
+ parts.push(...Array.from(entry.stations.values()).map((v) => String(v)));
}
- scheduleStatsRender();
- updateMapPathsAnimationClass();
- }
- function _resolveReceiverLocations(rigIds) {
- const seen = /* @__PURE__ */ new Set();
- const locations = [];
- if (rigIds && rigIds.size) {
- for (const rid of rigIds) {
- const rig = T.serverRigs.find((r) => r.remote === rid);
- if (rig && rig.latitude != null && rig.longitude != null) {
- const key = _receiverLocationKey(rig.latitude, rig.longitude);
- if (!seen.has(key)) {
- seen.add(key);
- locations.push([rig.latitude, rig.longitude]);
- }
- }
+ if (entry?.stationDetails instanceof Map) {
+ for (const detail of entry.stationDetails.values()) {
+ if (detail?.station) parts.push(String(detail.station));
+ if (detail?.message) parts.push(String(detail.message));
+ if (Number.isFinite(detail?.freq_hz)) parts.push(String(Math.round(Number(detail.freq_hz))));
}
}
- if (locations.length === 0 && T.serverLat != null && T.serverLon != null) {
- locations.push([T.serverLat, T.serverLon]);
- }
- return locations;
+ return parts.join(" ").toLowerCase();
}
- function setMapRadioPathTo(lat, lon, color, className = "aprs-radio-path", rigIds) {
- clearMapRadioPath();
- if (!mapP2pRadioPathsEnabled || !Number.isFinite(lat) || !Number.isFinite(lon) || !aprsMap) {
- return;
- }
- const sources = _resolveReceiverLocations(rigIds);
- for (const src of sources) {
- aprsRadioPaths.push(
- L.polyline(
- [src, [lat, lon]],
- { color, opacity: 0.85, weight: 2, interactive: false, className }
- ).addTo(aprsMap)
- );
- }
+ if (type === "aprs") {
+ const call = marker?._aprsCall ? String(marker._aprsCall) : "";
+ const entry = stationMarkers.get(call);
+ const info = entry?.info ? String(entry.info) : "";
+ const pktRaw = entry?.pkt?.raw ? String(entry.pkt.raw) : "";
+ return `${call} ${info} ${pktRaw}`.toLowerCase();
}
- function locatorMarkerCenter(marker) {
- if (!marker) return null;
- if (typeof marker.getBounds === "function") {
- const bounds = marker.getBounds();
- if (bounds && typeof bounds.getCenter === "function") {
- const center = bounds.getCenter();
- if (Number.isFinite(center?.lat) && Number.isFinite(center?.lng)) {
- return { lat: center.lat, lon: center.lng };
- }
- }
- }
- if (typeof marker.getLatLng === "function") {
- const ll = marker.getLatLng();
- if (Number.isFinite(ll?.lat) && Number.isFinite(ll?.lng)) {
- return { lat: ll.lat, lon: ll.lng };
- }
- }
- return null;
- }
- function setLocatorMarkerHighlight(marker, enabled) {
- const element = typeof marker?.getElement === "function" ? marker.getElement() : marker?._path;
- if (!element) return;
- element.classList.toggle("trx-locator-selected", !!enabled);
- }
- function setSelectedLocatorMarker(marker) {
- if (selectedLocatorMarker && selectedLocatorMarker !== marker) {
- setLocatorMarkerHighlight(selectedLocatorMarker, false);
- const prevEntry = locatorEntryForMarker(selectedLocatorMarker);
- if (prevEntry?.marker) {
- prevEntry.marker.setStyle(locatorStyleForEntry(prevEntry, locatorEntryCount(prevEntry)));
- }
- }
- stopSelectedLocatorPulse();
- selectedLocatorMarker = marker || null;
- if (selectedLocatorMarker) {
- setLocatorMarkerHighlight(selectedLocatorMarker, true);
- startSelectedLocatorPulse(selectedLocatorMarker);
- }
- }
- function isLocatorOverlay(marker) {
- const type = marker?.__trxType;
- return type === "bookmark" || type === "ft8" || type === "ft4" || type === "ft2" || type === "wspr";
- }
- function sendLocatorOverlayToBack(marker) {
- if (!isLocatorOverlay(marker) || typeof marker?.bringToBack !== "function") return;
- marker.bringToBack();
- }
- function renderMapLocatorChipRow(container, items, selectedSet, kind) {
- if (!container) return;
- container.replaceChildren();
- if (!Array.isArray(items) || items.length === 0) {
- container.innerHTML = `No ${kind === "band" ? "bands" : "sources"} available`;
- return;
- }
- let helperText = "";
- const sourceKeys = kind === "source" ? Object.keys(DEFAULT_MAP_SOURCE_FILTER) : [];
- const noneSelected = kind === "source" && sourceKeys.every((k) => !mapFilter[k]);
- if (kind === "source") {
- if (noneSelected) {
- helperText = "All sources visible — click to filter";
- }
- } else if (!(selectedSet instanceof Set) || selectedSet.size === 0) {
- helperText = `All ${kind === "band" ? "bands" : "sources"} visible by default`;
- }
- for (const item of items) {
- const btn = document.createElement("button");
- btn.type = "button";
- btn.className = "map-locator-chip";
- const isActive = kind === "source" ? !!mapFilter[item.key] : !!selectedSet?.has(item.key);
- if (kind === "source" && noneSelected) {
- btn.classList.add("is-default");
- } else if (!isActive) {
- btn.classList.add("is-inactive");
- }
- btn.dataset.filterKind = kind;
- btn.dataset.filterKey = item.key;
- btn.style.setProperty("--chip-color", item.color);
- btn.innerHTML = `${escapeMapHtml(item.label)}`;
- container.appendChild(btn);
- }
- if (helperText) {
- const hint = document.createElement("span");
- hint.className = "map-locator-empty";
- hint.textContent = helperText;
- container.appendChild(hint);
- }
- }
- function renderMapLocatorPhaseRow(container, phase) {
- if (!container) return;
- container.replaceChildren();
- const phases = [
- { key: "type", label: "Source" },
- { key: "band", label: "Band" }
- ];
- for (const item of phases) {
- const btn = document.createElement("button");
- btn.type = "button";
- btn.className = "map-locator-phase-btn";
- if (phase === item.key) btn.classList.add("is-active");
- btn.dataset.phase = item.key;
- btn.textContent = item.label;
- container.appendChild(btn);
- }
- }
- function renderMapLocatorLegend(phase, sourceItems, bandItems) {
- const legendEl = mapEl("map-band-legend");
- if (!legendEl) return;
- const isSourcePhase = phase === "type";
- const items = Array.isArray(isSourcePhase ? sourceItems : bandItems) ? isSourcePhase ? sourceItems : bandItems : [];
- if (items.length === 0) {
- legendEl.classList.add("is-empty");
- legendEl.replaceChildren();
- return;
- }
- legendEl.classList.remove("is-empty");
- const rows = items.map((item) => {
- const label = escapeMapHtml(item.label);
- const color = escapeMapHtml(item.color);
- return `${label}`;
- }).join("");
- const title = isSourcePhase ? "Source Colors" : "Band Colors";
- legendEl.innerHTML = `${title}
${rows}
`;
- }
- mapWindow.enableMapSourceFilter = function(key) {
- if (Object.prototype.hasOwnProperty.call(mapFilter, key) && !mapFilter[key]) {
- mapFilter[key] = true;
- rebuildMapLocatorFilters();
- applyMapFilter();
- }
- };
- function rebuildMapLocatorFilters() {
- const phaseEl = mapEl("map-locator-phase");
- const choiceEl = mapEl("map-locator-choice-filter");
- const choiceLabelEl = mapEl("map-locator-choice-label");
- const availableSources = /* @__PURE__ */ new Set();
- for (const entry of aisMarkers.values()) {
- if (entry?.visibleInHistoryWindow) {
- availableSources.add("ais");
- break;
- }
- }
- for (const entry of vdesMarkers.values()) {
- if (entry?.visibleInHistoryWindow) {
- availableSources.add("vdes");
- break;
- }
- }
- for (const entry of stationMarkers.values()) {
- if (entry?.type === "aprs" && entry?.visibleInHistoryWindow) {
- availableSources.add("aprs");
- break;
- }
- }
- const bandMap = /* @__PURE__ */ new Map();
- for (const entry of locatorMarkers.values()) {
- const sourceType = entry?.sourceType;
- if (!sourceType) continue;
- if ((sourceType === "ft8" || sourceType === "ft4" || sourceType === "ft2" || sourceType === "wspr") && !entry?.visibleInHistoryWindow) continue;
- availableSources.add(sourceType);
- const meta = entry?.bandMeta ?? /* @__PURE__ */ new Map();
- for (const [label, hz] of meta.entries()) {
- if (!bandMap.has(label)) {
- bandMap.set(label, {
- key: label,
- label,
- color: locatorBandChipColor(label),
- kind: "band",
- sortHz: Number.isFinite(hz) ? hz : 0
- });
- continue;
- }
- const existing = bandMap.get(label);
- if (existing && Number.isFinite(hz) && (existing.sortHz == null || !Number.isFinite(existing.sortHz) || hz > existing.sortHz)) {
- existing.sortHz = hz;
- }
- if (existing && !existing.color) {
- existing.color = locatorBandChipColor(label);
- }
- }
- }
- for (const key of Array.from(mapLocatorFilter.bands)) {
- if (!bandMap.has(key)) mapLocatorFilter.bands.delete(key);
- }
- const sourceItems = ["ais", "vdes", "aprs", "bookmark", "ft8", "ft4", "ft2", "wspr"].filter((key) => availableSources.has(key)).map((key) => ({
+ if (type === "ais") {
+ const key = marker?._aisMmsi ? String(marker._aisMmsi) : "";
+ const msg = aisMarkers.get(key)?.msg;
+ return [
key,
- label: mapSourceLabel(key),
- color: mapSourceColor(key),
- kind: "source"
- }));
- const bandItems = Array.from(bandMap.values()).sort((a, b) => (b.sortHz ?? 0) - (a.sortHz ?? 0) || a.label.localeCompare(b.label));
- renderMapLocatorLegend(mapLocatorFilter.phase, sourceItems, bandItems);
- if (!phaseEl || !choiceEl || !choiceLabelEl) return;
- renderMapLocatorPhaseRow(phaseEl, mapLocatorFilter.phase);
- if (mapLocatorFilter.phase === "band") {
- choiceLabelEl.textContent = "Visible Bands";
- renderMapLocatorChipRow(choiceEl, bandItems, mapLocatorFilter.bands, "band");
- } else {
- choiceLabelEl.textContent = "Visible Sources";
- renderMapLocatorChipRow(choiceEl, sourceItems, null, "source");
- }
- syncLocatorMarkerStyles();
- syncDecodeContactPathVisibility();
- }
- function markerPassesLocatorFilters(marker) {
- const meta = marker?._locatorFilterMeta;
- if (!meta) return true;
- if (mapLocatorFilter.phase === "band") {
- if (mapLocatorFilter.bands.size === 0) return true;
- if (!(meta.bands instanceof Set)) return false;
- for (const label of mapLocatorFilter.bands) {
- if (meta.bands.has(label)) return true;
- }
- return false;
- }
- return true;
- }
- function markerSearchText(marker) {
- const type = marker?.__trxType;
- if (type === "bookmark" || type === "ft8" || type === "ft4" || type === "ft2" || type === "wspr") {
- const entry = locatorEntryForMarker(marker);
- const parts = [];
- if (entry?.grid) parts.push(entry.grid);
- if (entry?.sourceType) parts.push(locatorSourceLabel(entry.sourceType));
- if (entry?.bandMeta instanceof Map) parts.push(...Array.from(entry.bandMeta.keys()));
- if (Array.isArray(entry?.bookmarks)) {
- for (const bm of entry.bookmarks) {
- if (bm?.name) parts.push(String(bm.name));
- if (bm?.locator) parts.push(String(bm.locator));
- if (bm?.mode) parts.push(String(bm.mode));
- if (bm?.category) parts.push(String(bm.category));
- if (bm?.comment) parts.push(String(bm.comment));
- if (Number.isFinite(bm?.freq_hz)) parts.push(String(Math.round(Number(bm.freq_hz))));
- }
- }
- if (entry?.stations instanceof Set) {
- parts.push(...Array.from(entry.stations.values()).map((v) => String(v)));
- }
- if (entry?.stationDetails instanceof Map) {
- for (const detail of entry.stationDetails.values()) {
- if (detail?.station) parts.push(String(detail.station));
- if (detail?.message) parts.push(String(detail.message));
- if (Number.isFinite(detail?.freq_hz)) parts.push(String(Math.round(Number(detail.freq_hz))));
- }
- }
- return parts.join(" ").toLowerCase();
- }
- if (type === "aprs") {
- const call = marker?._aprsCall ? String(marker._aprsCall) : "";
- const entry = stationMarkers.get(call);
- const info = entry?.info ? String(entry.info) : "";
- const pktRaw = entry?.pkt?.raw ? String(entry.pkt.raw) : "";
- return `${call} ${info} ${pktRaw}`.toLowerCase();
- }
- if (type === "ais") {
- const key = marker?._aisMmsi ? String(marker._aisMmsi) : "";
- const msg = aisMarkers.get(key)?.msg;
- return [
- key,
- msg?.name,
- msg?.callsign,
- msg?.destination,
- msg && Number.isFinite(msg.mmsi) ? String(msg.mmsi) : "",
- msg && Number.isFinite(msg.lat) ? String(msg.lat) : "",
- msg && Number.isFinite(msg.lon) ? String(msg.lon) : ""
- ].join(" ").toLowerCase();
- }
- if (type === "vdes") {
- const key = marker?._vdesKey ? String(marker._vdesKey) : "";
- const msg = vdesMarkers.get(key)?.msg;
- return [
- key,
- msg?.name,
- msg?.mmsi,
- msg?.message,
- msg?.raw,
- msg && Number.isFinite(msg.lat) ? String(msg.lat) : "",
- msg && Number.isFinite(msg.lon) ? String(msg.lon) : ""
- ].join(" ").toLowerCase();
- }
- return "";
- }
- function markerPassesSearchFilter(marker) {
- const query = String(mapSearchFilter || "").trim().toLowerCase();
- if (!query) return true;
- const terms = query.split(/\s+/).filter(Boolean);
- if (terms.length === 0) return true;
- const haystack = markerSearchText(marker);
- if (!haystack) return false;
- return terms.every((term) => haystack.includes(term));
- }
- function _receiverLocationKey(lat, lon) {
- return lat.toFixed(6) + "," + lon.toFixed(6);
- }
- function syncAprsReceiverMarker() {
- if (!aprsMap) return;
- const locGroups = {};
- const activeId = T.lastActiveRigId || T.serverActiveRigId || null;
- for (const rig of T.serverRigs) {
- if (!rig || !rig.remote) continue;
- const lat = rig.latitude, lon = rig.longitude;
- if (lat == null || lon == null || !Number.isFinite(lat) || !Number.isFinite(lon)) continue;
- const key = _receiverLocationKey(lat, lon);
- const group = locGroups[key] ??= { lat, lon, rigs: /* @__PURE__ */ new Set(), hasActive: false };
- group.rigs.add(rig.remote);
- if (rig.remote === activeId) group.hasActive = true;
- }
- if (T.serverLat != null && T.serverLon != null) {
- const key = _receiverLocationKey(T.serverLat, T.serverLon);
- const group = locGroups[key] ??= { lat: T.serverLat, lon: T.serverLon, rigs: /* @__PURE__ */ new Set(), hasActive: true };
- group.hasActive = true;
- }
- const seen = /* @__PURE__ */ new Set();
- let didInitialView = false;
- for (const [key, group] of Object.entries(locGroups)) {
- seen.add(key);
- const latLng = [group.lat, group.lon];
- const isActive = group.hasActive;
- let m = aprsMapReceiverMarkers[key];
- if (!m) {
- m = L.circleMarker(latLng, {
- radius: isActive ? 8 : 6,
- className: "trx-receiver-marker" + (isActive ? "" : " trx-receiver-marker-secondary"),
- fillOpacity: isActive ? 0.8 : 0.6
- }).addTo(aprsMap).bindPopup("");
- m._receiverLocKey = key;
- m._receiverRigs = group.rigs;
- aprsMapReceiverMarkers[key] = m;
- if (isActive && !didInitialView) {
- aprsMap.setView(latLng, Math.max(1, T.initialMapZoom));
- didInitialView = true;
- }
- } else {
- m.setLatLng(latLng);
- m._receiverRigs = group.rigs;
- m.setRadius(isActive ? 8 : 6);
- if (!aprsMap.hasLayer(m)) m.addTo(aprsMap);
- }
- }
- for (const key of Object.keys(aprsMapReceiverMarkers)) {
- if (!seen.has(key)) {
- const m = aprsMapReceiverMarkers[key];
- if (m && aprsMap.hasLayer(m)) m.removeFrom(aprsMap);
- Reflect.deleteProperty(aprsMapReceiverMarkers, key);
- }
- }
- }
- const satOverlays = /* @__PURE__ */ new Map();
- let satOverlaySeq = 0;
- mapWindow.addSatMapOverlay = function(msg) {
- if (!msg || !Array.isArray(msg.geo_bounds) || typeof msg.path !== "string") return;
- const bounds = msg.geo_bounds;
- if (!Array.isArray(bounds) || bounds.length !== 4) return;
- const latLngBounds = L.latLngBounds(
- [Number(bounds[0]), Number(bounds[1])],
- // SW
- [Number(bounds[2]), Number(bounds[3])]
- // NE
- );
- const key = `sat-${++satOverlaySeq}`;
- const overlay = L.imageOverlay(msg.path, latLngBounds, {
- opacity: 0.55,
- interactive: true,
- zIndex: 300
- });
- overlay.__trxType = "sat";
- overlay.__trxSatKey = key;
- overlay.__trxRigIds = msg.rig_id ? /* @__PURE__ */ new Set([msg.rig_id]) : /* @__PURE__ */ new Set();
- overlay.__trxHistoryVisible = true;
- mapMarkers.add(overlay);
- const decoder = "Meteor LRPT";
- const satellite = msg.satellite || "Unknown";
- const ts = msg.ts_ms ? new Date(msg.ts_ms).toLocaleString() : "";
- overlay.bindPopup(
- `${escapeMapHtml(decoder)}${escapeMapHtml(satellite)}
${escapeMapHtml(ts)}
` + (msg.path ? `
Download PNG` : "") + `
`
- );
- let track = null;
- if (msg.ground_track && Array.isArray(msg.ground_track) && msg.ground_track.length >= 2) {
- const latlngs = msg.ground_track.filter((pt) => Array.isArray(pt) && pt.length >= 2).map((pt) => [Number(pt[0]), Number(pt[1])]);
- track = L.polyline(latlngs, {
- color: mapSourceColor("sat"),
- weight: 2,
- opacity: 0.7,
- dashArray: "6, 4"
- });
- track.__trxType = "sat";
- track.__trxSatKey = key;
- track.__trxRigIds = overlay.__trxRigIds ?? /* @__PURE__ */ new Set();
- track.__trxHistoryVisible = true;
- mapMarkers.add(track);
- if (aprsMap) {
- track.addTo(aprsMap);
- }
- }
- satOverlays.set(key, { overlay, track, msg });
- if (aprsMap) {
- overlay.addTo(aprsMap);
- }
- applyMapFilter();
- };
- mapWindow.removeSatMapOverlay = function(key) {
- const entry = satOverlays.get(key);
- if (!entry) return;
- if (entry.overlay) {
- mapMarkers.delete(entry.overlay);
- if (aprsMap && aprsMap.hasLayer(entry.overlay)) entry.overlay.removeFrom(aprsMap);
- }
- if (entry.track) {
- mapMarkers.delete(entry.track);
- if (aprsMap && aprsMap.hasLayer(entry.track)) entry.track.removeFrom(aprsMap);
- }
- satOverlays.delete(key);
- };
- mapWindow.clearSatMapOverlays = function() {
- for (const [key] of satOverlays) {
- mapWindow.removeSatMapOverlay?.(key);
- }
- };
- mapWindow.clearMapMarkersByType = function(type) {
- if (type === "aprs") {
- selectedAprsTrackCall = null;
- stationMarkers.forEach((entry) => {
- if (entry && entry.marker) {
- if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
- mapMarkers.delete(entry.marker);
- }
- if (entry && entry.track) {
- if (aprsMap && aprsMap.hasLayer(entry.track)) entry.track.removeFrom(aprsMap);
- mapMarkers.delete(entry.track);
- }
- });
- stationMarkers.clear();
- return;
- }
- if (type === "ais") {
- aisMarkers.forEach((entry) => {
- if (entry && entry.marker) {
- if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
- mapMarkers.delete(entry.marker);
- }
- if (entry && entry.track) {
- if (aprsMap && aprsMap.hasLayer(entry.track)) entry.track.removeFrom(aprsMap);
- mapMarkers.delete(entry.track);
- }
- });
- selectedAisTrackMmsi = null;
- aisMarkers.clear();
- return;
- }
- if (type === "vdes") {
- vdesMarkers.forEach((entry) => {
- if (entry && entry.marker) {
- if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
- mapMarkers.delete(entry.marker);
- }
- });
- vdesMarkers.clear();
- return;
- }
- if (type === "sat") {
- mapWindow.clearSatMapOverlays?.();
- return;
- }
- if (type === "ft8" || type === "ft4" || type === "ft2" || type === "wspr") {
- const prefix = `${type}:`;
- for (const [key, entry] of locatorMarkers.entries()) {
- if (!key.startsWith(prefix)) continue;
- if (entry && entry.marker) {
- if (entry.marker === selectedLocatorMarker) {
- setSelectedLocatorMarker(null);
- clearMapRadioPath();
- }
- if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
- mapMarkers.delete(entry.marker);
- }
- locatorMarkers.delete(key);
- }
- rebuildMapLocatorFilters();
- rebuildDecodeContactPaths();
- }
- if (type === "bookmark") {
- for (const [key, entry] of locatorMarkers.entries()) {
- if (!key.startsWith("bookmark:")) continue;
- if (entry && entry.marker) {
- if (entry.marker === selectedLocatorMarker) {
- setSelectedLocatorMarker(null);
- clearMapRadioPath();
- }
- if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
- mapMarkers.delete(entry.marker);
- }
- locatorMarkers.delete(key);
- }
- rebuildMapLocatorFilters();
- }
- };
- function mapTileSpecForTheme(theme) {
- if (theme === "dark") {
- return {
- url: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
- options: {
- maxZoom: 19,
- subdomains: "abcd",
- attribution: '© OpenStreetMap © CARTO'
- }
- };
- }
- return {
- url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
- options: {
- maxZoom: 19,
- attribution: '© OpenStreetMap'
- }
- };
- }
- function updateMapBaseLayerForTheme(theme) {
- if (!aprsMap) return;
- if (aprsMapBaseLayer) {
- aprsMap.removeLayer(aprsMapBaseLayer);
- aprsMapBaseLayer = null;
- }
- const spec = mapTileSpecForTheme(theme);
- aprsMapBaseLayer = L.tileLayer(spec.url, spec.options).addTo(aprsMap);
- }
- function mapStageEl() {
- return mapEl("map-stage");
- }
- function mapIsFullscreen() {
- const stage = mapStageEl();
- if (!stage) return false;
- return document.fullscreenElement === stage || document.webkitFullscreenElement === stage || stage.classList.contains("map-fake-fullscreen");
- }
- function mapExitFakeFullscreen() {
- const stage = mapStageEl();
- if (!stage) return;
- stage.classList.remove("map-fake-fullscreen");
- document.body.classList.remove("map-fake-fullscreen-active");
- }
- function mapEnterFakeFullscreen() {
- const stage = mapStageEl();
- if (!stage) return;
- stage.classList.add("map-fake-fullscreen");
- document.body.classList.add("map-fake-fullscreen-active");
- }
- function updateMapFullscreenButton() {
- const btn = mapEl("map-fullscreen-btn");
- if (!btn) return;
- btn.textContent = mapIsFullscreen() ? "Exit Fullscreen" : "Fullscreen";
- }
- function applyMapOverlayPanelVisibility() {
- const panel = document.querySelector("#map-stage .map-overlay-panel");
- if (!panel) return;
- panel.classList.toggle("is-hidden", !mapOverlayPanelVisible);
- }
- function updateMapOverlayToggleButton() {
- const btn = mapEl("map-overlay-toggle-btn");
- if (!btn) return;
- btn.textContent = mapOverlayPanelVisible ? "Hide Filters" : "Show Filters";
- }
- async function toggleMapFullscreen() {
- const stage = mapStageEl();
- if (!stage) return;
- try {
- const webkitDocument = document;
- const isNative = document.fullscreenElement === stage || webkitDocument.webkitFullscreenElement === stage;
- const isFake = stage.classList.contains("map-fake-fullscreen");
- if (isNative) {
- if (document.exitFullscreen) await document.exitFullscreen();
- else if (webkitDocument.webkitExitFullscreen) await webkitDocument.webkitExitFullscreen();
- } else if (isFake) {
- mapExitFakeFullscreen();
- } else {
- const nativeFn = stage.requestFullscreen || stage.webkitRequestFullscreen;
- if (nativeFn) {
- try {
- await nativeFn.call(stage);
- } catch {
- mapEnterFakeFullscreen();
- }
- } else {
- mapEnterFakeFullscreen();
- }
- }
- } catch (err) {
- console.error("Map fullscreen toggle failed", err);
- } finally {
- updateMapFullscreenButton();
- requestAnimationFrame(() => {
- sizeAprsMapToViewport();
- });
- }
- }
- document.addEventListener("keydown", (e) => {
- if (e.key === "Escape") {
- const stage = mapStageEl();
- if (stage && stage.classList.contains("map-fake-fullscreen")) {
- mapExitFakeFullscreen();
- updateMapFullscreenButton();
- requestAnimationFrame(() => {
- sizeAprsMapToViewport();
- });
- }
- }
- });
- function materializeBufferedMapLayers() {
- if (!aprsMap) return;
- for (const [key, entry] of locatorMarkers) {
- if (!key.startsWith("bookmark:") || entry?.marker || !entry?.grid) continue;
- const bounds = maidenheadToBounds(entry.grid);
- if (!bounds) continue;
- entry.sourceType = "bookmark";
- entry.bandMeta = collectBandMeta((entry.bookmarks || []).map((bm) => Number(bm?.freq_hz)));
- entry.marker = L.rectangle(bounds, locatorStyleForEntry(entry, entry.bookmarks?.length || 1)).addTo(aprsMap).bindPopup(buildBookmarkLocatorPopupHtml(entry.grid, entry.bookmarks || []));
- entry.marker.__trxType = "bookmark";
- sendLocatorOverlayToBack(entry.marker);
- assignLocatorMarkerMeta(entry.marker, entry.sourceType, entry.bandMeta);
- mapMarkers.add(entry.marker);
- }
- pruneMapHistory();
- }
- let stageResizeObserver = null;
- function initAprsMap() {
- if (typeof L === "undefined") return;
- mapEl("aprs-map");
- sizeAprsMapToViewport();
- if (aprsMap) return;
- const hasLocation = T.serverLat != null && T.serverLon != null;
- const center = hasLocation && T.serverLat != null && T.serverLon != null ? [T.serverLat, T.serverLon] : [20, 0];
- const zoom = hasLocation ? T.initialMapZoom : 2;
- aprsMap = L.map("aprs-map").setView(center, zoom);
- const stage = mapStageEl();
- if (stage && typeof ResizeObserver !== "undefined") {
- if (stageResizeObserver) stageResizeObserver.disconnect();
- stageResizeObserver = new ResizeObserver(() => {
- sizeAprsMapToViewport();
- });
- stageResizeObserver.observe(stage);
- }
- updateMapBaseLayerForTheme(C.currentTheme());
- syncAprsReceiverMarker();
- aprsMap.on("popupopen", function(e) {
- const marker = e.popup._source;
- if (!marker) return;
- clearMapRadioPath();
- setSelectedLocatorMarker(null);
- if (selectedAprsTrackCall) {
- const prevEntry = stationMarkers.get(String(selectedAprsTrackCall));
- if (prevEntry && prevEntry.track && aprsMap && aprsMap.hasLayer(prevEntry.track)) {
- prevEntry.track.removeFrom(aprsMap);
- }
- selectedAprsTrackCall = null;
- }
- if (selectedAisTrackMmsi) {
- selectedAisTrackMmsi = null;
- syncSelectedAisTrackVisibility();
- }
- if (marker._receiverLocKey) {
- e.popup.setContent(buildReceiverPopupHtml(marker._receiverRigs || []));
- return;
- }
- if (!marker) return;
- const ll = typeof marker.getLatLng === "function" ? marker.getLatLng() : null;
- if (marker._aprsCall) {
- if (!ll) return;
- const entry = stationMarkers.get(marker._aprsCall);
- if (!entry) return;
- e.popup.setContent(buildAprsPopupHtml(marker._aprsCall, ll.lat, ll.lng, entry.info || "", entry.pkt));
- refreshAprsTrack(String(marker._aprsCall), entry);
- if (entry.track && aprsMap && mapFilter.aprs && !aprsMap.hasLayer(entry.track)) {
- entry.track.addTo(aprsMap);
- }
- selectedAprsTrackCall = String(marker._aprsCall);
- setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor("aprs"), "aprs-radio-path", marker.__trxRigIds);
- return;
- }
- if (marker._aisMmsi) {
- if (!ll) return;
- const entry = aisMarkers.get(String(marker._aisMmsi));
- if (!entry || !entry.msg) return;
- e.popup.setContent(buildAisPopupHtml(entry.msg));
- refreshAisTrack(String(marker._aisMmsi), entry);
- selectedAisTrackMmsi = String(marker._aisMmsi);
- syncSelectedAisTrackVisibility();
- setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor("ais"), "aprs-radio-path", marker.__trxRigIds);
- return;
- }
- if (marker._vdesKey) {
- if (!ll) return;
- const entry = vdesMarkers.get(String(marker._vdesKey));
- if (!entry || !entry.msg) return;
- e.popup.setContent(buildVdesPopupHtml(entry.msg));
- setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor("vdes"), "aprs-radio-path", marker.__trxRigIds);
- return;
- }
- if (marker.__trxType === "ft8" || marker.__trxType === "ft4" || marker.__trxType === "ft2" || marker.__trxType === "wspr") {
- const center2 = locatorMarkerCenter(marker);
- if (center2) {
- setSelectedLocatorMarker(marker);
- const lEntry = locatorEntryForMarker(marker);
- const lColor = lEntry ? locatorStyleForEntry(lEntry, locatorEntryCount(lEntry)).color ?? locatorFilterColor("ft8") : locatorFilterColor(marker.__trxType);
- setMapRadioPathTo(center2.lat, center2.lon, lColor, "locator-radio-path", marker.__trxRigIds);
- }
- } else if (marker.__trxType === "bookmark") {
- setSelectedLocatorMarker(marker);
- }
- });
- aprsMap.on("popupclose", function() {
- clearMapRadioPath();
- setSelectedLocatorMarker(null);
- if (selectedAprsTrackCall) {
- const entry = stationMarkers.get(String(selectedAprsTrackCall));
- if (entry && entry.track && aprsMap && aprsMap.hasLayer(entry.track)) {
- entry.track.removeFrom(aprsMap);
- }
- selectedAprsTrackCall = null;
- }
- if (selectedAisTrackMmsi) {
- selectedAisTrackMmsi = null;
- syncSelectedAisTrackVisibility();
- }
- });
- materializeBufferedMapLayers();
- const locatorPhaseEl = mapEl("map-locator-phase");
- const locatorChoiceEl = mapEl("map-locator-choice-filter");
- const mapSearchEl = mapEl("map-search-filter");
- const mapHistoryLimitEl = mapEl("map-history-limit");
- const mapP2pPathsToggleEl = mapEl("map-p2p-paths-toggle");
- const mapContactPathsToggleEl = mapEl("map-contact-paths-toggle");
- const fullscreenBtn = mapEl("map-fullscreen-btn");
- const overlayToggleBtn = mapEl("map-overlay-toggle-btn");
- if (locatorPhaseEl) {
- locatorPhaseEl.addEventListener("click", (e) => {
- const btn = e.target?.closest(".map-locator-phase-btn[data-phase]");
- if (!btn) return;
- const phase = String(btn.dataset.phase || "");
- if (phase !== "type" && phase !== "band") return;
- if (mapLocatorFilter.phase === phase) return;
- mapLocatorFilter.phase = phase;
- rebuildMapLocatorFilters();
- applyMapFilter();
- });
- }
- if (locatorChoiceEl) {
- locatorChoiceEl.addEventListener("click", (e) => {
- const chip = e.target?.closest(".map-locator-chip[data-filter-kind]");
- if (!chip) return;
- const kind = String(chip.dataset.filterKind || "");
- const key = String(chip.dataset.filterKey || "");
- if (!key) return;
- if (kind === "source" && Object.prototype.hasOwnProperty.call(mapFilter, key)) {
- const sourceKey = key;
- mapFilter[sourceKey] = !mapFilter[sourceKey];
- const srcKeys = Object.keys(DEFAULT_MAP_SOURCE_FILTER);
- const anySelected = srcKeys.some((k) => mapFilter[k]);
- if (anySelected && !mapFilter.aprs && selectedAprsTrackCall) {
- const entry = stationMarkers.get(String(selectedAprsTrackCall));
- if (entry && entry.track && aprsMap && aprsMap.hasLayer(entry.track)) {
- entry.track.removeFrom(aprsMap);
- }
- selectedAprsTrackCall = null;
- }
- if (anySelected && !mapFilter.ais && selectedAisTrackMmsi) {
- const entry = aisMarkers.get(String(selectedAisTrackMmsi));
- if (entry && entry.track && aprsMap && aprsMap.hasLayer(entry.track)) {
- entry.track.removeFrom(aprsMap);
- }
- selectedAisTrackMmsi = null;
- }
- } else if (kind === "band") {
- if (mapLocatorFilter.bands.has(key)) {
- mapLocatorFilter.bands.delete(key);
- } else {
- mapLocatorFilter.bands.add(key);
- }
- }
- rebuildMapLocatorFilters();
- applyMapFilter();
- });
- }
- const mapRigFilterEl = mapEl("map-rig-filter");
- if (mapRigFilterEl) {
- mapRigFilterEl.addEventListener("change", () => {
- mapRigFilter = mapRigFilterEl.value;
- applyMapFilter();
- });
- }
- if (mapSearchEl) {
- mapSearchEl.value = mapSearchFilter;
- mapSearchEl.addEventListener("input", () => {
- mapSearchFilter = String(mapSearchEl.value || "").trim();
- applyMapFilter();
- });
- }
- if (mapHistoryLimitEl) {
- mapHistoryLimitEl.value = String(mapHistoryLimitMinutes);
- mapHistoryLimitEl.addEventListener("change", () => {
- mapHistoryLimitMinutes = normalizeMapHistoryLimitMinutes(Number(mapHistoryLimitEl.value));
- mapHistoryLimitEl.value = String(mapHistoryLimitMinutes);
- saveSetting("mapHistoryLimitMinutes", mapHistoryLimitMinutes);
- pruneMapHistory();
- });
- }
- if (mapP2pPathsToggleEl) {
- updateMapP2pPathsToggle();
- mapP2pPathsToggleEl.addEventListener("click", () => {
- mapP2pRadioPathsEnabled = !mapP2pRadioPathsEnabled;
- saveSetting("mapP2pRadioPathsEnabled", mapP2pRadioPathsEnabled);
- updateMapP2pPathsToggle();
- if (!mapP2pRadioPathsEnabled) clearMapRadioPath();
- });
- }
- if (mapContactPathsToggleEl) {
- updateMapContactPathsToggle();
- mapContactPathsToggleEl.addEventListener("click", () => {
- mapDecodeContactPathsEnabled = !mapDecodeContactPathsEnabled;
- saveSetting("mapDecodeContactPathsEnabled", mapDecodeContactPathsEnabled);
- updateMapContactPathsToggle();
- syncDecodeContactPathVisibility();
- });
- }
- if (fullscreenBtn) {
- fullscreenBtn.addEventListener("click", () => {
- void toggleMapFullscreen();
- });
- updateMapFullscreenButton();
- }
- applyMapOverlayPanelVisibility();
- updateMapOverlayToggleButton();
- if (overlayToggleBtn) {
- overlayToggleBtn.addEventListener("click", () => {
- mapOverlayPanelVisible = !mapOverlayPanelVisible;
- saveSetting("mapOverlayPanelVisible", mapOverlayPanelVisible);
- applyMapOverlayPanelVisibility();
- updateMapOverlayToggleButton();
- });
- }
- if (!mapFullscreenListenerBound) {
- const onFullscreenChange = () => {
- updateMapFullscreenButton();
- sizeAprsMapToViewport();
- };
- document.addEventListener("fullscreenchange", onFullscreenChange);
- document.addEventListener("webkitfullscreenchange", onFullscreenChange);
- mapFullscreenListenerBound = true;
- }
- if (!mapHistoryPruneTimer) {
- mapHistoryPruneTimer = setInterval(() => {
- pruneMapHistory();
- }, 60 * 1e3);
- }
- rebuildMapLocatorFilters();
- }
- function sizeAprsMapToViewport() {
- const mapContainer = mapEl("aprs-map");
- const stage = mapStageEl();
- if (mapIsFullscreen() && stage) {
- const isFake = stage.classList.contains("map-fake-fullscreen");
- const stageHeight = isFake ? window.innerHeight : stage.clientHeight || stage.getBoundingClientRect().height;
- const target2 = Math.max(260, Math.floor(stageHeight));
- mapContainer.style.height = `${target2}px`;
- if (aprsMap) aprsMap.invalidateSize();
- return;
- }
- const mapRect = mapContainer.getBoundingClientRect();
- const width = mapContainer.clientWidth || mapRect.width;
- const footer = document.querySelector(".footer");
- let bottom = mapIsFullscreen() && stage ? stage.getBoundingClientRect().bottom : window.innerHeight;
- if (!mapIsFullscreen() && footer) {
- const fr = footer.getBoundingClientRect();
- if (fr.top > mapRect.top + 50) bottom = fr.top;
- }
- const available = Math.max(0, Math.floor(bottom - mapRect.top - 8));
- const widthDriven = width > 0 ? Math.floor(width / 1.55) : available;
- const viewportCap = mapIsFullscreen() ? Math.floor(window.innerHeight * 0.9) : Math.floor(window.innerHeight * 0.75);
- const minHeight = Math.min(260, available);
- const target = Math.max(minHeight, Math.min(available, viewportCap, widthDriven));
- mapContainer.style.height = `${target}px`;
- if (aprsMap) aprsMap.invalidateSize();
- }
- function aprsSymbolIcon(symbolTable, symbolCode) {
- if (!symbolTable || !symbolCode) return null;
- const table = symbolTable === "/" ? "primary" : "alternate";
- return L.divIcon({
- className: "",
- html: `${escapeMapHtml(symbolCode)}
`,
- iconSize: [24, 24],
- iconAnchor: [12, 12],
- popupAnchor: [0, -12]
- });
- }
- mapWindow.navigateToAprsMap = function(lat, lon) {
- T._activeTab = "map";
- document.querySelectorAll(".tab-bar .tab").forEach((t) => {
- t.classList.remove("active");
- });
- const mapTabBtn = document.querySelector(".tab-bar .tab[data-tab='map']");
- if (mapTabBtn) mapTabBtn.classList.add("active");
- document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none");
- const mapPanel = mapEl("tab-map");
- if (mapPanel) mapPanel.style.display = "";
- initAprsMap();
- sizeAprsMapToViewport();
- if (aprsMap) {
- requestAnimationFrame(() => {
- requestAnimationFrame(() => {
- sizeAprsMapToViewport();
- aprsMap?.invalidateSize();
- aprsMap?.setView([lat, lon], 13);
- });
- });
- }
- };
- mapWindow.navigateToMapLocator = function(grid, preferredType = null) {
- const normalizedGrid = String(grid || "").trim().toUpperCase();
- if (!/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalizedGrid)) return false;
- T._activeTab = "map";
- document.querySelectorAll(".tab-bar .tab").forEach((t) => {
- t.classList.remove("active");
- });
- const mapTabBtn = document.querySelector(".tab-bar .tab[data-tab='map']");
- if (mapTabBtn) mapTabBtn.classList.add("active");
- document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none");
- const mapPanel = mapEl("tab-map");
- if (mapPanel) mapPanel.style.display = "";
- initAprsMap();
- sizeAprsMapToViewport();
- if (!aprsMap) return false;
- const pref = preferredType === "wspr" ? "wspr" : preferredType === "ft4" ? "ft4" : preferredType === "ft2" ? "ft2" : preferredType === "ft8" ? "ft8" : null;
- const keys = pref ? [`${pref}:${normalizedGrid}`, `ft8:${normalizedGrid}`, `ft4:${normalizedGrid}`, `ft2:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`] : [`ft8:${normalizedGrid}`, `ft4:${normalizedGrid}`, `ft2:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`];
- let entry = null;
- for (const key of keys) {
- entry = locatorMarkers.get(key);
- if (entry?.marker) break;
- }
- if (!entry?.marker) return false;
- if (pref && Object.prototype.hasOwnProperty.call(mapFilter, pref) && !mapFilter[pref]) {
- mapFilter[pref] = true;
- rebuildMapLocatorFilters();
- applyMapFilter();
- }
- const marker = entry.marker;
- if (!aprsMap.hasLayer(marker)) {
- marker.addTo(aprsMap);
- sendLocatorOverlayToBack(marker);
- }
- const center = locatorMarkerCenter(marker);
- const focusMarker = () => {
- if (!aprsMap || !marker) return;
- sizeAprsMapToViewport();
- aprsMap.invalidateSize();
- if (center) {
- const targetZoom = Math.max(aprsMap.getZoom() || 0, 7);
- aprsMap.setView([center.lat, center.lon], targetZoom);
- if (marker.__trxType !== "bookmark") {
- const fEntry = locatorEntryForMarker(marker);
- const sourceType = marker.__trxType;
- const fColor = fEntry ? locatorStyleForEntry(fEntry, locatorEntryCount(fEntry)).color ?? locatorFilterColor(sourceType) : locatorFilterColor(sourceType);
- setMapRadioPathTo(center.lat, center.lon, fColor, "locator-radio-path", marker.__trxRigIds);
- }
- }
- setSelectedLocatorMarker(marker);
- if (typeof marker.openPopup === "function") marker.openPopup();
- };
- focusMarker();
- requestAnimationFrame(() => {
- requestAnimationFrame(focusMarker);
- });
- return true;
- };
- function buildReceiverPopupHtml(rigIds) {
- const call = T.serverCallsign || T.ownerCallsign || "Receiver";
- let meta = "";
- if (T.serverVersion) {
- meta = `trx-server v${escapeMapHtml(T.serverVersion)}`;
- if (T.serverBuildDate) meta += ` · ${escapeMapHtml(T.serverBuildDate)}`;
- }
- let rows = "";
- if (T.ownerCallsign && T.ownerCallsign !== T.serverCallsign) {
- rows += `| ${escapeMapHtml(T.ownerCallsign)} |
`;
- }
- const rigList = rigIds ? Array.from(rigIds) : [];
- const rigSet = rigList.length ? new Set(rigList) : null;
- const firstRig = rigSet ? T.serverRigs.find((r) => !!r.remote && rigSet.has(r.remote)) : null;
- const popupLat = firstRig ? firstRig.latitude : T.serverLat;
- const popupLon = firstRig ? firstRig.longitude : T.serverLon;
- if (popupLat != null && popupLon != null) {
- const grid = latLonToMaidenhead(popupLat, popupLon);
- rows += `| ${popupLat.toFixed(5)}, ${popupLon.toFixed(5)} (${escapeMapHtml(grid)}) |
`;
- }
- const rigsToShow = rigSet ? T.serverRigs.filter((r) => !!r.remote && rigSet.has(r.remote)) : T.serverRigs;
- for (const rig of rigsToShow) {
- const name = rig.display_name || `${rig.manufacturer} ${rig.model}`.trim();
- const active = rig.remote === T.serverActiveRigId ? ` ` : "";
- rows += `| ${escapeMapHtml(name)}${active} |
`;
- }
- return ``;
- }
- function buildAprsPopupHtml(call, lat, lon, info, pkt) {
- const age = pkt?._tsMs ? formatTimeAgo(pkt._tsMs) : pkt?._ts || null;
- const distKm = T.serverLat != null && T.serverLon != null ? haversineKm(T.serverLat, T.serverLon, lat, lon) : null;
- const distStr = distKm != null ? distKm < 1 ? `${Math.round(distKm * 1e3)} m` : `${distKm.toFixed(1)} km` : null;
- const path = pkt?.path || null;
- const type = pkt?.type || null;
- const meta = [age, distStr].filter(Boolean).join(" · ");
- let rows = "";
- if (type) rows += `| ${escapeMapHtml(type)} |
`;
- if (path) rows += `| ${escapeMapHtml(path)} |
`;
- if (lat != null && lon != null)
- rows += `| ${lat.toFixed(5)}, ${lon.toFixed(5)} |
`;
- return ``;
- }
- function buildAisPopupHtml(msg) {
- const age = msg?._tsMs ? formatTimeAgo(msg._tsMs) : null;
- const distKm = T.serverLat != null && T.serverLon != null && msg?.lat != null && msg?.lon != null ? haversineKm(T.serverLat, T.serverLon, msg.lat, msg.lon) : null;
- const distStr = distKm != null ? distKm < 1 ? `${Math.round(distKm * 1e3)} m` : `${distKm.toFixed(1)} km` : null;
- const meta = [age, distStr, msg?.channel ? `AIS ${escapeMapHtml(msg.channel)}` : null].filter(Boolean).join(" · ");
- let rows = "";
- rows += `| ${escapeMapHtml(String(msg.mmsi || "--"))} |
`;
- rows += `| ${escapeMapHtml(String(msg.message_type || "--"))} |
`;
- if (distStr) rows += `| ${distStr} from TRX |
`;
- if (msg?.sog_knots != null) rows += `| ${Number(msg.sog_knots).toFixed(1)} kn |
`;
- if (msg?.cog_deg != null) rows += `| ${Number(msg.cog_deg).toFixed(1)}° |
`;
- if (msg?.heading_deg != null) rows += `| ${Number(msg.heading_deg).toFixed(0)}° |
`;
- if (msg?.nav_status != null) rows += `| ${escapeMapHtml(String(msg.nav_status))} |
`;
- if (msg?.lat != null && msg?.lon != null) rows += `| ${msg.lat.toFixed(5)}, ${msg.lon.toFixed(5)} |
`;
- const info = [msg?.vessel_name, msg?.callsign, msg?.destination].filter(Boolean).map(escapeMapHtml).join(" · ");
- const vesselLabel = escapeMapHtml(msg?.vessel_name || `MMSI ${msg?.mmsi || "--"}`);
- const vesselUrl = mapWindow.buildAisVesselUrl ? mapWindow.buildAisVesselUrl(msg?.mmsi) : null;
- const vesselTitle = vesselUrl ? `${vesselLabel}` : vesselLabel;
- return ``;
- }
- function buildVdesPopupHtml(msg) {
- const age = msg.ts_ms == null ? "" : formatTimeAgo(msg.ts_ms);
- const distKm = T.serverLat != null && T.serverLon != null && msg?.lat != null && msg?.lon != null ? haversineKm(T.serverLat, T.serverLon, msg.lat, msg.lon) : null;
- const distStr = distKm != null ? distKm < 1 ? `${Math.round(distKm * 1e3)} m` : `${distKm.toFixed(1)} km` : null;
- const meta = [
- age,
- distStr,
- msg?.message_label ? escapeMapHtml(msg.message_label) : null,
- Number.isFinite(msg?.link_id) ? `LID ${Number(msg.link_id)}` : null
- ].filter(Boolean).join(" · ");
- let rows = "";
- if (distStr) rows += `| ${distStr} from TRX |
`;
- rows += `| ${escapeMapHtml(String(msg?.message_type ?? "--"))} |
`;
- if (Number.isFinite(msg?.source_id)) rows += `| ${escapeMapHtml(String(msg.source_id))} |
`;
- if (Number.isFinite(msg?.destination_id)) rows += `| ${escapeMapHtml(String(msg.destination_id))} |
`;
- if (msg?.lat != null && msg?.lon != null) rows += `| ${msg.lat.toFixed(5)}, ${msg.lon.toFixed(5)} |
`;
- if (Number.isFinite(msg?.sync_score)) rows += `| ${(Number(msg.sync_score) * 100).toFixed(0)}% |
`;
- if (msg?.fec_state) rows += `| ${escapeMapHtml(String(msg.fec_state))} |
`;
- const info = [
- msg?.vessel_name,
+ msg?.name,
msg?.callsign,
msg?.destination,
- msg?.payload_preview
- ].filter(Boolean).map(escapeMapHtml).join(" · ");
- const title = escapeMapHtml(msg?.vessel_name || msg?.callsign || "VDES Position");
- return ``;
+ msg && Number.isFinite(msg.mmsi) ? String(msg.mmsi) : "",
+ msg && Number.isFinite(msg.lat) ? String(msg.lat) : "",
+ msg && Number.isFinite(msg.lon) ? String(msg.lon) : ""
+ ].join(" ").toLowerCase();
}
- function aprsPositionsEqual(a, b) {
- if (!a || !b) return false;
- const aLat = Array.isArray(a) ? a[0] : a.lat;
- const aLon = Array.isArray(a) ? a[1] : a.lon;
- const bLat = Array.isArray(b) ? b[0] : b.lat;
- const bLon = Array.isArray(b) ? b[1] : b.lon;
- return Math.abs(aLat - bLat) < 1e-6 && Math.abs(aLon - bLon) < 1e-6;
+ if (type === "vdes") {
+ const key = marker?._vdesKey ? String(marker._vdesKey) : "";
+ const msg = vdesMarkers.get(key)?.msg;
+ return [
+ key,
+ msg?.name,
+ msg?.mmsi,
+ msg?.message,
+ msg?.raw,
+ msg && Number.isFinite(msg.lat) ? String(msg.lat) : "",
+ msg && Number.isFinite(msg.lon) ? String(msg.lon) : ""
+ ].join(" ").toLowerCase();
}
- function aisPositionsEqual(a, b) {
- if (!a || !b) return false;
- const aLat = Array.isArray(a) ? a[0] : a.lat;
- const aLon = Array.isArray(a) ? a[1] : a.lon;
- const bLat = Array.isArray(b) ? b[0] : b.lat;
- const bLon = Array.isArray(b) ? b[1] : b.lon;
- return Math.abs(aLat - bLat) < 1e-6 && Math.abs(aLon - bLon) < 1e-6;
+ return "";
+ }
+ function markerPassesSearchFilter(marker) {
+ const query = String(mapSearchFilter || "").trim().toLowerCase();
+ if (!query) return true;
+ const terms = query.split(/\s+/).filter(Boolean);
+ if (terms.length === 0) return true;
+ const haystack = markerSearchText(marker);
+ if (!haystack) return false;
+ return terms.every((term) => haystack.includes(term));
+ }
+ function _receiverLocationKey(lat, lon) {
+ return lat.toFixed(6) + "," + lon.toFixed(6);
+ }
+ function syncAprsReceiverMarker() {
+ if (!aprsMap) return;
+ const locGroups = {};
+ const activeId = T.lastActiveRigId || T.serverActiveRigId || null;
+ for (const rig of T.serverRigs) {
+ if (!rig || !rig.remote) continue;
+ const lat = rig.latitude, lon = rig.longitude;
+ if (lat == null || lon == null || !Number.isFinite(lat) || !Number.isFinite(lon)) continue;
+ const key = _receiverLocationKey(lat, lon);
+ const group = locGroups[key] ??= { lat, lon, rigs: /* @__PURE__ */ new Set(), hasActive: false };
+ group.rigs.add(rig.remote);
+ if (rig.remote === activeId) group.hasActive = true;
}
- function vdesMarkerKey(msg) {
- if (Number.isFinite(msg?.source_id)) return `src:${Number(msg.source_id)}`;
- if (Number.isFinite(msg?.mmsi) && Number(msg.mmsi) > 0) return `mmsi:${Number(msg.mmsi)}`;
- if (msg?.lat != null && msg?.lon != null) {
- return `pos:${Number(msg.lat).toFixed(4)}:${Number(msg.lon).toFixed(4)}:${Number(msg?.message_type ?? 0)}`;
- }
- return null;
+ if (T.serverLat != null && T.serverLon != null) {
+ const key = _receiverLocationKey(T.serverLat, T.serverLon);
+ const group = locGroups[key] ??= { lat: T.serverLat, lon: T.serverLon, rigs: /* @__PURE__ */ new Set(), hasActive: true };
+ group.hasActive = true;
}
- function _aprsAddMarkerToMap(call, entry) {
- if (!aprsMap || entry.lat == null || entry.lon == null) return;
- refreshAprsTrack(call, entry);
- const icon = aprsSymbolIcon(entry.symbolTable ?? "", entry.symbolCode ?? "");
- const popupContent = buildAprsPopupHtml(call, entry.lat, entry.lon, entry.info || "", entry.pkt);
- const marker = icon ? L.marker([entry.lat, entry.lon], { icon }).addTo(aprsMap).bindPopup(popupContent) : L.circleMarker([entry.lat, entry.lon], {
- radius: 6,
- color: "#00d17f",
- fillColor: "#00d17f",
- fillOpacity: 0.8
- }).addTo(aprsMap).bindPopup(popupContent);
- marker.__trxType = "aprs";
- marker.__trxRigIds = entry.rigIds || /* @__PURE__ */ new Set();
- marker._aprsCall = call;
- entry.marker = marker;
- mapMarkers.add(marker);
- }
- mapWindow.aprsMapAddStation = function(call, lat, lon, info, symbolTable, symbolCode, pkt) {
- const nextPoint = [lat, lon];
- const tsMs = Number.isFinite(pkt?._tsMs) ? Number(pkt._tsMs) : Date.now();
- const msgRigId = pkt?.rig_id || T.lastActiveRigId;
- const existing = stationMarkers.get(call);
- if (existing) {
- existing.pkt = pkt;
- existing.lat = lat;
- existing.lon = lon;
- existing.info = info;
- existing.symbolTable = symbolTable;
- existing.symbolCode = symbolCode;
- if (msgRigId) {
- if (!existing.rigIds) existing.rigIds = /* @__PURE__ */ new Set();
- existing.rigIds.add(msgRigId);
- }
- if (!Array.isArray(existing.trackHistory)) existing.trackHistory = [];
- const prevPoint = existing.trackHistory[existing.trackHistory.length - 1];
- if (!aprsPositionsEqual(prevPoint, nextPoint)) {
- existing.trackHistory.push({ lat, lon, tsMs });
- } else if (prevPoint) {
- prevPoint.tsMs = tsMs;
- }
- pruneAprsEntry(call, existing, mapHistoryCutoffMs());
- if (aprsMap && existing.marker && !T.decodeHistoryReplayActive) {
- existing.marker.setLatLng([lat, lon]);
- existing.marker.setPopupContent(buildAprsPopupHtml(call, lat, lon, info, pkt));
+ const seen = /* @__PURE__ */ new Set();
+ let didInitialView = false;
+ for (const [key, group] of Object.entries(locGroups)) {
+ seen.add(key);
+ const latLng = [group.lat, group.lon];
+ const isActive = group.hasActive;
+ let m = aprsMapReceiverMarkers[key];
+ if (!m) {
+ m = L.circleMarker(latLng, {
+ radius: isActive ? 8 : 6,
+ className: "trx-receiver-marker" + (isActive ? "" : " trx-receiver-marker-secondary"),
+ fillOpacity: isActive ? 0.8 : 0.6
+ }).addTo(aprsMap).bindPopup("");
+ m._receiverLocKey = key;
+ m._receiverRigs = group.rigs;
+ aprsMapReceiverMarkers[key] = m;
+ if (isActive && !didInitialView) {
+ aprsMap.setView(latLng, Math.max(1, T.initialMapZoom));
+ didInitialView = true;
}
} else {
- const entry = {
- marker: null,
- track: null,
- trackHistory: [{ lat, lon, tsMs }],
- trackPoints: [nextPoint],
- type: "aprs",
- pkt,
- lat,
- lon,
- info,
- symbolTable,
- symbolCode,
- rigIds: new Set(msgRigId ? [msgRigId] : [])
- };
- stationMarkers.set(call, entry);
- pruneAprsEntry(call, entry, mapHistoryCutoffMs());
- if (entry.visibleInHistoryWindow) ensureAprsMarker(call, entry);
- if (aprsMap) scheduleDecodeMapMaintenance();
+ m.setLatLng(latLng);
+ m._receiverRigs = group.rigs;
+ m.setRadius(isActive ? 8 : 6);
+ if (!aprsMap.hasLayer(m)) m.addTo(aprsMap);
}
- };
- function syncSelectedAisTrackVisibility() {
- if (!aprsMap) return;
- const map = aprsMap;
- const selectedKey = selectedAisTrackMmsi ? String(selectedAisTrackMmsi) : null;
- aisMarkers.forEach((entry, key) => {
- const track = entry?.track;
- if (!track) return;
- const shouldShow = !!selectedKey && selectedKey === String(key) && !!mapFilter.ais;
- const onMap = map.hasLayer(track);
- if (shouldShow && !onMap) {
- track.addTo(map);
+ }
+ for (const key of Object.keys(aprsMapReceiverMarkers)) {
+ if (!seen.has(key)) {
+ const m = aprsMapReceiverMarkers[key];
+ if (m && aprsMap.hasLayer(m)) m.removeFrom(aprsMap);
+ Reflect.deleteProperty(aprsMapReceiverMarkers, key);
+ }
+ }
+ }
+ const satOverlays = /* @__PURE__ */ new Map();
+ let satOverlaySeq = 0;
+ mapWindow.addSatMapOverlay = function(msg) {
+ if (!msg || !Array.isArray(msg.geo_bounds) || typeof msg.path !== "string") return;
+ const bounds = msg.geo_bounds;
+ if (!Array.isArray(bounds) || bounds.length !== 4) return;
+ const latLngBounds = L.latLngBounds(
+ [Number(bounds[0]), Number(bounds[1])],
+ // SW
+ [Number(bounds[2]), Number(bounds[3])]
+ // NE
+ );
+ const key = `sat-${++satOverlaySeq}`;
+ const overlay = L.imageOverlay(msg.path, latLngBounds, {
+ opacity: 0.55,
+ interactive: true,
+ zIndex: 300
+ });
+ overlay.__trxType = "sat";
+ overlay.__trxSatKey = key;
+ overlay.__trxRigIds = msg.rig_id ? /* @__PURE__ */ new Set([msg.rig_id]) : /* @__PURE__ */ new Set();
+ overlay.__trxHistoryVisible = true;
+ mapMarkers.add(overlay);
+ const decoder = "Meteor LRPT";
+ const satellite = msg.satellite || "Unknown";
+ const ts = msg.ts_ms ? new Date(msg.ts_ms).toLocaleString() : "";
+ overlay.bindPopup(
+ `${escapeMapHtml(decoder)}${escapeMapHtml(satellite)}
${escapeMapHtml(ts)}
` + (msg.path ? `
Download PNG` : "") + `
`
+ );
+ let track = null;
+ if (msg.ground_track && Array.isArray(msg.ground_track) && msg.ground_track.length >= 2) {
+ const latlngs = msg.ground_track.filter((pt) => Array.isArray(pt) && pt.length >= 2).map((pt) => [Number(pt[0]), Number(pt[1])]);
+ track = L.polyline(latlngs, {
+ color: mapSourceColor("sat"),
+ weight: 2,
+ opacity: 0.7,
+ dashArray: "6, 4"
+ });
+ track.__trxType = "sat";
+ track.__trxSatKey = key;
+ track.__trxRigIds = overlay.__trxRigIds ?? /* @__PURE__ */ new Set();
+ track.__trxHistoryVisible = true;
+ mapMarkers.add(track);
+ if (aprsMap) {
+ track.addTo(aprsMap);
+ }
+ }
+ satOverlays.set(key, { overlay, track, msg });
+ if (aprsMap) {
+ overlay.addTo(aprsMap);
+ }
+ applyMapFilter();
+ };
+ mapWindow.removeSatMapOverlay = function(key) {
+ const entry = satOverlays.get(key);
+ if (!entry) return;
+ if (entry.overlay) {
+ mapMarkers.delete(entry.overlay);
+ if (aprsMap && aprsMap.hasLayer(entry.overlay)) entry.overlay.removeFrom(aprsMap);
+ }
+ if (entry.track) {
+ mapMarkers.delete(entry.track);
+ if (aprsMap && aprsMap.hasLayer(entry.track)) entry.track.removeFrom(aprsMap);
+ }
+ satOverlays.delete(key);
+ };
+ mapWindow.clearSatMapOverlays = function() {
+ for (const [key] of satOverlays) {
+ mapWindow.removeSatMapOverlay?.(key);
+ }
+ };
+ mapWindow.clearMapMarkersByType = function(type) {
+ if (type === "aprs") {
+ selectedAprsTrackCall = null;
+ stationMarkers.forEach((entry) => {
+ if (entry && entry.marker) {
+ if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
+ mapMarkers.delete(entry.marker);
}
- if (!shouldShow && onMap) {
- track.removeFrom(map);
+ if (entry && entry.track) {
+ if (aprsMap && aprsMap.hasLayer(entry.track)) entry.track.removeFrom(aprsMap);
+ mapMarkers.delete(entry.track);
}
});
+ stationMarkers.clear();
+ return;
}
- function getAisAccentColor() {
- return getComputedStyle(document.documentElement).getPropertyValue("--accent-green").trim() || "#c24b1a";
+ if (type === "ais") {
+ aisMarkers.forEach((entry) => {
+ if (entry && entry.marker) {
+ if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
+ mapMarkers.delete(entry.marker);
+ }
+ if (entry && entry.track) {
+ if (aprsMap && aprsMap.hasLayer(entry.track)) entry.track.removeFrom(aprsMap);
+ mapMarkers.delete(entry.track);
+ }
+ });
+ selectedAisTrackMmsi = null;
+ aisMarkers.clear();
+ return;
}
- function aisMarkerOptionsFromMessage(msg) {
- const color = getAisAccentColor();
+ if (type === "vdes") {
+ vdesMarkers.forEach((entry) => {
+ if (entry && entry.marker) {
+ if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
+ mapMarkers.delete(entry.marker);
+ }
+ });
+ vdesMarkers.clear();
+ return;
+ }
+ if (type === "sat") {
+ mapWindow.clearSatMapOverlays?.();
+ return;
+ }
+ if (type === "ft8" || type === "ft4" || type === "ft2" || type === "wspr") {
+ const prefix = `${type}:`;
+ for (const [key, entry] of locatorMarkers.entries()) {
+ if (!key.startsWith(prefix)) continue;
+ if (entry && entry.marker) {
+ if (entry.marker === selectedLocatorMarker) {
+ setSelectedLocatorMarker(null);
+ clearMapRadioPath();
+ }
+ if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
+ mapMarkers.delete(entry.marker);
+ }
+ locatorMarkers.delete(key);
+ }
+ rebuildMapLocatorFilters();
+ rebuildDecodeContactPaths();
+ }
+ if (type === "bookmark") {
+ for (const [key, entry] of locatorMarkers.entries()) {
+ if (!key.startsWith("bookmark:")) continue;
+ if (entry && entry.marker) {
+ if (entry.marker === selectedLocatorMarker) {
+ setSelectedLocatorMarker(null);
+ clearMapRadioPath();
+ }
+ if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
+ mapMarkers.delete(entry.marker);
+ }
+ locatorMarkers.delete(key);
+ }
+ rebuildMapLocatorFilters();
+ }
+ };
+ function mapTileSpecForTheme(theme) {
+ if (theme === "dark") {
return {
- heading: msg?.heading_deg,
- course: msg?.cog_deg,
- speed: msg?.sog_knots,
- color,
- outline: "#00000055",
- size: 22
+ url: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png",
+ options: {
+ maxZoom: 19,
+ subdomains: "abcd",
+ attribution: '© OpenStreetMap © CARTO'
+ }
};
}
- function createAisMarker(lat, lon, msg) {
- if (typeof L !== "undefined" && typeof L.trxAisTrackSymbol === "function") {
- return L.trxAisTrackSymbol([lat, lon], aisMarkerOptionsFromMessage(msg));
+ return {
+ url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
+ options: {
+ maxZoom: 19,
+ attribution: '© OpenStreetMap'
}
- const color = getAisAccentColor();
- return L.circleMarker([lat, lon], {
- radius: 6,
- color,
- fillColor: color,
- fillOpacity: 0.82
+ };
+ }
+ function updateMapBaseLayerForTheme(theme) {
+ if (!aprsMap) return;
+ if (aprsMapBaseLayer) {
+ aprsMap.removeLayer(aprsMapBaseLayer);
+ aprsMapBaseLayer = null;
+ }
+ const spec = mapTileSpecForTheme(theme);
+ aprsMapBaseLayer = L.tileLayer(spec.url, spec.options).addTo(aprsMap);
+ }
+ function mapStageEl() {
+ return mapEl("map-stage");
+ }
+ function mapIsFullscreen() {
+ const stage = mapStageEl();
+ if (!stage) return false;
+ return document.fullscreenElement === stage || document.webkitFullscreenElement === stage || stage.classList.contains("map-fake-fullscreen");
+ }
+ function mapExitFakeFullscreen() {
+ const stage = mapStageEl();
+ if (!stage) return;
+ stage.classList.remove("map-fake-fullscreen");
+ document.body.classList.remove("map-fake-fullscreen-active");
+ }
+ function mapEnterFakeFullscreen() {
+ const stage = mapStageEl();
+ if (!stage) return;
+ stage.classList.add("map-fake-fullscreen");
+ document.body.classList.add("map-fake-fullscreen-active");
+ }
+ function updateMapFullscreenButton() {
+ const btn = mapEl("map-fullscreen-btn");
+ if (!btn) return;
+ btn.textContent = mapIsFullscreen() ? "Exit Fullscreen" : "Fullscreen";
+ }
+ function applyMapOverlayPanelVisibility() {
+ const panel = document.querySelector("#map-stage .map-overlay-panel");
+ if (!panel) return;
+ panel.classList.toggle("is-hidden", !mapOverlayPanelVisible);
+ }
+ function updateMapOverlayToggleButton() {
+ const btn = mapEl("map-overlay-toggle-btn");
+ if (!btn) return;
+ btn.textContent = mapOverlayPanelVisible ? "Hide Filters" : "Show Filters";
+ }
+ async function toggleMapFullscreen() {
+ const stage = mapStageEl();
+ if (!stage) return;
+ try {
+ const webkitDocument = document;
+ const isNative = document.fullscreenElement === stage || webkitDocument.webkitFullscreenElement === stage;
+ const isFake = stage.classList.contains("map-fake-fullscreen");
+ if (isNative) {
+ if (document.exitFullscreen) await document.exitFullscreen();
+ else if (webkitDocument.webkitExitFullscreen) await webkitDocument.webkitExitFullscreen();
+ } else if (isFake) {
+ mapExitFakeFullscreen();
+ } else {
+ const nativeFn = stage.requestFullscreen || stage.webkitRequestFullscreen;
+ if (nativeFn) {
+ try {
+ await nativeFn.call(stage);
+ } catch {
+ mapEnterFakeFullscreen();
+ }
+ } else {
+ mapEnterFakeFullscreen();
+ }
+ }
+ } catch (err) {
+ console.error("Map fullscreen toggle failed", err);
+ } finally {
+ updateMapFullscreenButton();
+ requestAnimationFrame(() => {
+ sizeAprsMapToViewport();
});
}
- function updateAisMarker(marker, msg, popupHtml) {
- if (!marker || msg.lat == null || msg.lon == null) return;
- marker.setLatLng([msg.lat, msg.lon]);
- if (typeof marker.setAisState === "function") {
- marker.setAisState(aisMarkerOptionsFromMessage(msg));
- }
- if (typeof marker.setStyle === "function" && typeof marker.setAisState !== "function") {
- const color = getAisAccentColor();
- marker.setRadius(6);
- marker.setStyle({
- color,
- fillColor: color,
- fillOpacity: 0.84
+ }
+ document.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") {
+ const stage = mapStageEl();
+ if (stage && stage.classList.contains("map-fake-fullscreen")) {
+ mapExitFakeFullscreen();
+ updateMapFullscreenButton();
+ requestAnimationFrame(() => {
+ sizeAprsMapToViewport();
});
}
- marker.setPopupContent(popupHtml);
}
- function refreshAisMarkerColors() {
- const color = getAisAccentColor();
- aisMarkers.forEach((entry) => {
- if (entry.marker) {
- if (typeof entry.marker.setAisState === "function") {
- entry.marker.setAisState(aisMarkerOptionsFromMessage(entry.msg || {}));
- } else if (typeof entry.marker.setStyle === "function") {
- entry.marker.setStyle({ color, fillColor: color });
- }
- }
- if (entry.track && typeof entry.track.setStyle === "function") {
- entry.track.setStyle({ color });
- }
+ });
+ function materializeBufferedMapLayers() {
+ if (!aprsMap) return;
+ for (const [key, entry] of locatorMarkers) {
+ if (!key.startsWith("bookmark:") || entry?.marker || !entry?.grid) continue;
+ const bounds = maidenheadToBounds(entry.grid);
+ if (!bounds) continue;
+ entry.sourceType = "bookmark";
+ entry.bandMeta = collectBandMeta((entry.bookmarks || []).map((bm) => Number(bm?.freq_hz)));
+ entry.marker = L.rectangle(bounds, locatorStyleForEntry(entry, entry.bookmarks?.length || 1)).addTo(aprsMap).bindPopup(buildBookmarkLocatorPopupHtml(entry.grid, entry.bookmarks || []));
+ entry.marker.__trxType = "bookmark";
+ sendLocatorOverlayToBack(entry.marker);
+ assignLocatorMarkerMeta(entry.marker, entry.sourceType, entry.bandMeta);
+ mapMarkers.add(entry.marker);
+ }
+ pruneMapHistory();
+ }
+ let stageResizeObserver = null;
+ function initAprsMap() {
+ if (typeof L === "undefined") return;
+ mapEl("aprs-map");
+ sizeAprsMapToViewport();
+ if (aprsMap) return;
+ const hasLocation = T.serverLat != null && T.serverLon != null;
+ const center = hasLocation && T.serverLat != null && T.serverLon != null ? [T.serverLat, T.serverLon] : [20, 0];
+ const zoom = hasLocation ? T.initialMapZoom : 2;
+ aprsMap = L.map("aprs-map").setView(center, zoom);
+ const stage = mapStageEl();
+ if (stage && typeof ResizeObserver !== "undefined") {
+ if (stageResizeObserver) stageResizeObserver.disconnect();
+ stageResizeObserver = new ResizeObserver(() => {
+ sizeAprsMapToViewport();
});
+ stageResizeObserver.observe(stage);
}
- mapWindow.aisMapAddVessel = function(msg) {
- if (msg == null || msg.lat == null || msg.lon == null || !Number.isFinite(msg.mmsi)) return;
- const key = String(msg.mmsi);
- const popupHtml = buildAisPopupHtml(msg);
- const nextPoint = [msg.lat, msg.lon];
- const tsMs = Number.isFinite(msg?._tsMs) ? Number(msg._tsMs) : Date.now();
- const msgRigId = msg?.rig_id || T.lastActiveRigId;
- const existing = aisMarkers.get(key);
- if (existing) {
- existing.msg = msg;
- if (msgRigId) {
- if (!existing.rigIds) existing.rigIds = /* @__PURE__ */ new Set();
- existing.rigIds.add(msgRigId);
- }
- if (!Array.isArray(existing.trackHistory)) existing.trackHistory = [];
- const prevPoint = existing.trackHistory[existing.trackHistory.length - 1];
- if (!aisPositionsEqual(prevPoint, nextPoint)) {
- existing.trackHistory.push({ lat: msg.lat, lon: msg.lon, tsMs });
- } else if (prevPoint) {
- prevPoint.tsMs = tsMs;
- }
- pruneAisEntry(key, existing, mapHistoryCutoffMs());
- if (aprsMap && existing.marker && !T.decodeHistoryReplayActive) {
- updateAisMarker(existing.marker, msg, popupHtml);
+ updateMapBaseLayerForTheme(C.currentTheme());
+ syncAprsReceiverMarker();
+ aprsMap.on("popupopen", function(e) {
+ const marker = e.popup._source;
+ if (!marker) return;
+ clearMapRadioPath();
+ setSelectedLocatorMarker(null);
+ if (selectedAprsTrackCall) {
+ const prevEntry = stationMarkers.get(String(selectedAprsTrackCall));
+ if (prevEntry && prevEntry.track && aprsMap && aprsMap.hasLayer(prevEntry.track)) {
+ prevEntry.track.removeFrom(aprsMap);
}
+ selectedAprsTrackCall = null;
+ }
+ if (selectedAisTrackMmsi) {
+ selectedAisTrackMmsi = null;
+ syncSelectedAisTrackVisibility();
+ }
+ if (marker._receiverLocKey) {
+ e.popup.setContent(buildReceiverPopupHtml(marker._receiverRigs || []));
return;
}
- const entry = {
- marker: null,
- track: null,
- trackHistory: [{ lat: msg.lat, lon: msg.lon, tsMs }],
- trackPoints: [nextPoint],
- msg,
- rigIds: new Set(msgRigId ? [msgRigId] : [])
- };
- aisMarkers.set(key, entry);
- pruneAisEntry(key, entry, mapHistoryCutoffMs());
- if (entry.visibleInHistoryWindow) ensureAisMarker(key, entry);
- scheduleDecodeMapMaintenance();
- };
- mapWindow.vdesMapAddPoint = function(msg) {
- if (msg == null || msg.lat == null || msg.lon == null) return;
- const key = vdesMarkerKey(msg);
- if (!key) return;
- const popupHtml = buildVdesPopupHtml(msg);
- const visible = Number.isFinite(Number(msg?._tsMs)) && Number(msg._tsMs) >= mapHistoryCutoffMs();
- const msgRigId = msg?.rig_id || T.lastActiveRigId;
- const existing = vdesMarkers.get(key);
- if (existing) {
- existing.msg = msg;
- existing.visibleInHistoryWindow = visible;
- if (msgRigId) {
- if (!existing.rigIds) existing.rigIds = /* @__PURE__ */ new Set();
- existing.rigIds.add(msgRigId);
- }
- if (!visible) {
- if (!C.decodeHistoryMapRenderingDeferred()) {
- setRetainedMapMarkerVisible(existing.marker, false);
- } else {
- C.markDecodeMapSyncPending();
- }
- return;
- }
- if (!C.decodeHistoryMapRenderingDeferred()) {
- ensureVdesMarker(key, existing);
- setRetainedMapMarkerVisible(existing.marker, true);
- } else {
- C.markDecodeMapSyncPending();
- }
- if (aprsMap && existing.marker && !T.decodeHistoryReplayActive) {
- existing.marker.setLatLng([msg.lat, msg.lon]);
- existing.marker.setPopupContent(popupHtml);
+ if (!marker) return;
+ const ll = typeof marker.getLatLng === "function" ? marker.getLatLng() : null;
+ if (marker._aprsCall) {
+ if (!ll) return;
+ const entry = stationMarkers.get(marker._aprsCall);
+ if (!entry) return;
+ e.popup.setContent(buildAprsPopupHtml(marker._aprsCall, ll.lat, ll.lng, entry.info || "", entry.pkt));
+ refreshAprsTrack(String(marker._aprsCall), entry);
+ if (entry.track && aprsMap && mapFilter.aprs && !aprsMap.hasLayer(entry.track)) {
+ entry.track.addTo(aprsMap);
}
+ selectedAprsTrackCall = String(marker._aprsCall);
+ setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor("aprs"), "aprs-radio-path", marker.__trxRigIds);
return;
}
- const entry = {
- marker: null,
- msg,
- visibleInHistoryWindow: visible,
- rigIds: new Set(msgRigId ? [msgRigId] : [])
- };
- vdesMarkers.set(key, entry);
- if (!visible) return;
- if (!C.decodeHistoryMapRenderingDeferred()) {
- ensureVdesMarker(key, entry);
- setRetainedMapMarkerVisible(entry.marker, true);
- } else {
- C.markDecodeMapSyncPending();
- }
- if (aprsMap && entry.marker && !T.decodeHistoryReplayActive) {
- entry.marker.setPopupContent(popupHtml);
- }
- scheduleDecodeMapMaintenance();
- };
- let reverseGeocodeLastKey = null;
- function reverseGeocodeLocation(lat, lon, grid) {
- const key = `${lat.toFixed(4)},${lon.toFixed(4)}`;
- if (key === reverseGeocodeLastKey) return;
- reverseGeocodeLastKey = key;
- const url = `https://nominatim.openstreetmap.org/reverse?lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lon)}&format=json&zoom=10&accept-language=en`;
- fetch(url, { headers: { "User-Agent": "trx-rs" } }).then((r) => r.ok ? r.json() : Promise.reject(new Error(`Reverse geocode failed: ${r.status}`))).then((data) => {
- const addr = data?.address;
- if (!addr) return;
- const city = addr.city || addr.town || addr.village || addr.hamlet || addr.municipality || addr.county || "";
- const country = addr.country || "";
- if (!city && !country) return;
- const label = city && country ? `${city}, ${country}` : city || country;
- T.lastCityLabel = label;
- if (T.locationSubtitle) {
- T.locationSubtitle.textContent = `Location: ${grid} · ${label}`;
- }
- C.updateDocumentTitle?.();
- }).catch(() => {
- });
- }
- function maidenheadToBounds(grid) {
- if (!grid || grid.length < 4) return null;
- const g = grid.toUpperCase();
- const A = "A".charCodeAt(0);
- const fieldLon = (g.charCodeAt(0) - A) * 20 - 180;
- const fieldLat = (g.charCodeAt(1) - A) * 10 - 90;
- const squareLon = parseInt(g.charAt(2), 10) * 2;
- const squareLat = parseInt(g.charAt(3), 10);
- let lon = fieldLon + squareLon;
- let lat = fieldLat + squareLat;
- let lonSpan = 2;
- let latSpan = 1;
- if (g.length >= 6) {
- const subLon = (g.charCodeAt(4) - A) * (5 / 60);
- const subLat = (g.charCodeAt(5) - A) * (2.5 / 60);
- lon += subLon;
- lat += subLat;
- lonSpan = 5 / 60;
- latSpan = 2.5 / 60;
- }
- return [
- [lat, lon],
- [lat + latSpan, lon + lonSpan]
- ];
- }
- function applyMapFilter() {
- if (!aprsMap) return;
- const map = aprsMap;
- const sourceKeys = Object.keys(DEFAULT_MAP_SOURCE_FILTER);
- const noneSelected = sourceKeys.every((k) => !mapFilter[k]);
- mapMarkers.forEach((marker) => {
- const type = marker.__trxType;
- const sourceVisible = noneSelected ? type == null || DEFAULT_MAP_SOURCE_FILTER[type] : type == null || !!mapFilter[type];
- const rigVisible = !mapRigFilter || marker.__trxType === "bookmark" || marker.__trxRigIds instanceof Set && marker.__trxRigIds.has(mapRigFilter);
- const visible = marker.__trxHistoryVisible !== false && markerPassesSearchFilter(marker) && markerPassesLocatorFilters(marker) && sourceVisible && rigVisible;
- const onMap = map.hasLayer(marker);
- if (visible && !onMap) {
- marker.addTo(map);
- sendLocatorOverlayToBack(marker);
- }
- if (!visible && onMap) marker.removeFrom(map);
- });
- syncSelectedAisTrackVisibility();
- syncDecodeContactPathVisibility();
- }
- function updateMapContactPathsToggle() {
- const btn = mapEl("map-contact-paths-toggle");
- if (!btn) return;
- btn.textContent = mapDecodeContactPathsEnabled ? "Contact Paths On" : "Contact Paths Off";
- btn.classList.toggle("is-active", mapDecodeContactPathsEnabled);
- }
- function updateMapP2pPathsToggle() {
- const btn = mapEl("map-p2p-paths-toggle");
- if (!btn) return;
- btn.textContent = mapP2pRadioPathsEnabled ? "TRX Paths On" : "TRX Paths Off";
- btn.classList.toggle("is-active", mapP2pRadioPathsEnabled);
- }
- function scheduleDecodeMapMaintenance() {
- if (C.decodeHistoryMapRenderingDeferred()) {
- C.markDecodeMapSyncPending();
+ if (marker._aisMmsi) {
+ if (!ll) return;
+ const entry = aisMarkers.get(String(marker._aisMmsi));
+ if (!entry || !entry.msg) return;
+ e.popup.setContent(buildAisPopupHtml(entry.msg));
+ refreshAisTrack(String(marker._aisMmsi), entry);
+ selectedAisTrackMmsi = String(marker._aisMmsi);
+ syncSelectedAisTrackVisibility();
+ setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor("ais"), "aprs-radio-path", marker.__trxRigIds);
return;
}
- scheduleUiFrameJob("decode-map-maintenance", () => {
- rebuildDecodeContactPaths();
+ if (marker._vdesKey) {
+ if (!ll) return;
+ const entry = vdesMarkers.get(String(marker._vdesKey));
+ if (!entry || !entry.msg) return;
+ e.popup.setContent(buildVdesPopupHtml(entry.msg));
+ setMapRadioPathTo(ll.lat, ll.lng, mapSourceColor("vdes"), "aprs-radio-path", marker.__trxRigIds);
+ return;
+ }
+ if (marker.__trxType === "ft8" || marker.__trxType === "ft4" || marker.__trxType === "ft2" || marker.__trxType === "wspr") {
+ const center2 = locatorMarkerCenter(marker);
+ if (center2) {
+ setSelectedLocatorMarker(marker);
+ const lEntry = locatorEntryForMarker(marker);
+ const lColor = lEntry ? locatorStyleForEntry(lEntry, locatorEntryCount(lEntry)).color ?? locatorFilterColor("ft8") : locatorFilterColor(marker.__trxType);
+ setMapRadioPathTo(center2.lat, center2.lon, lColor, "locator-radio-path", marker.__trxRigIds);
+ }
+ } else if (marker.__trxType === "bookmark") {
+ setSelectedLocatorMarker(marker);
+ }
+ });
+ aprsMap.on("popupclose", function() {
+ clearMapRadioPath();
+ setSelectedLocatorMarker(null);
+ if (selectedAprsTrackCall) {
+ const entry = stationMarkers.get(String(selectedAprsTrackCall));
+ if (entry && entry.track && aprsMap && aprsMap.hasLayer(entry.track)) {
+ entry.track.removeFrom(aprsMap);
+ }
+ selectedAprsTrackCall = null;
+ }
+ if (selectedAisTrackMmsi) {
+ selectedAisTrackMmsi = null;
+ syncSelectedAisTrackVisibility();
+ }
+ });
+ materializeBufferedMapLayers();
+ const locatorPhaseEl = mapEl("map-locator-phase");
+ const locatorChoiceEl = mapEl("map-locator-choice-filter");
+ const mapSearchEl = mapEl("map-search-filter");
+ const mapHistoryLimitEl = mapEl("map-history-limit");
+ const mapP2pPathsToggleEl = mapEl("map-p2p-paths-toggle");
+ const mapContactPathsToggleEl = mapEl("map-contact-paths-toggle");
+ const fullscreenBtn = mapEl("map-fullscreen-btn");
+ const overlayToggleBtn = mapEl("map-overlay-toggle-btn");
+ if (locatorPhaseEl) {
+ locatorPhaseEl.addEventListener("click", (e) => {
+ const btn = e.target?.closest(".map-locator-phase-btn[data-phase]");
+ if (!btn) return;
+ const phase = String(btn.dataset.phase || "");
+ if (phase !== "type" && phase !== "band") return;
+ if (mapLocatorFilter.phase === phase) return;
+ mapLocatorFilter.phase = phase;
rebuildMapLocatorFilters();
applyMapFilter();
});
}
- function formatDecodeLocatorTime(tsMs) {
- if (!Number.isFinite(tsMs)) return "--:--:--";
- return new Date(tsMs).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit"
- });
- }
- function formatMapPopupFreq(hz) {
- if (!Number.isFinite(hz)) return "--";
- const value = Number(hz);
- if (value >= 1e9) return `${(value / 1e9).toFixed(6).replace(/\.?0+$/, "")} GHz`;
- if (value >= 1e6) return `${(value / 1e6).toFixed(6).replace(/\.?0+$/, "")} MHz`;
- if (value >= 1e3) return `${(value / 1e3).toFixed(3).replace(/\.?0+$/, "")} kHz`;
- return `${Math.round(value)} Hz`;
- }
- function buildDecodeLocatorTooltipHtml(grid, entry, type) {
- const details = entry?.stationDetails instanceof Map ? Array.from(entry.stationDetails.values()) : [];
- details.sort((a, b) => Number(b?.ts_ms || 0) - Number(a?.ts_ms || 0));
- const title = type === "wspr" ? "WSPR" : "FT8";
- const rows = details.map((detail) => {
- const station = escapeMapHtml(String(detail?.source || detail?.station || detail?.target || "Unknown"));
- const freq = formatMapPopupFreq(Number(detail?.freq_hz));
- const meta = [
- detail?.target ? `to ${escapeMapHtml(String(detail.target))}` : null,
- Number.isFinite(detail?.snr_db) ? `${Number(detail.snr_db).toFixed(1)} dB` : null,
- Number.isFinite(detail?.dt_s) ? `dt ${Number(detail.dt_s).toFixed(2)}` : null,
- escapeMapHtml(freq)
- ].filter(Boolean).join(" · ");
- const remoteIds = detail?.remotes instanceof Set && detail.remotes.size > 0 ? Array.from(detail.remotes) : detail?.remote ? [detail.remote] : [];
- const rxHtml = remoteIds.map((rid) => {
- const label = _receiverLabel(rid);
- return label ? `${escapeMapHtml(label)}
` : "";
- }).filter(Boolean).join("");
- const message = detail?.message ? `${escapeMapHtml(String(detail.message))}
` : "";
- return `${station}${escapeMapHtml(formatDecodeLocatorTime(Number(detail?.ts_ms)))}
` + (meta ? `
${meta}
` : "") + rxHtml + message + `
`;
- }).join("");
- const count = Math.max(
- 1,
- details.length,
- entry?.stations instanceof Set ? entry.stations.size : 0
- );
- return `${escapeMapHtml(grid)}
${title} · ${count} station${count === 1 ? "" : "s"}
` + rows + `
`;
- }
- function rebuildDecodeContactPaths() {
- clearDecodeContactPaths();
- const stationLocators = /* @__PURE__ */ new Map();
- const directedMessages = [];
- for (const entry of locatorMarkers.values()) {
- if (!entry || entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "ft2" && entry.sourceType !== "wspr") continue;
- const grid = String(entry.grid || "").trim().toUpperCase();
- if (!grid || !(entry.stationDetails instanceof Map)) continue;
- for (const detail of entry.stationDetails.values()) {
- const source = String(detail?.source || detail?.station || "").trim().toUpperCase();
- const target = String(detail?.target || "").trim().toUpperCase();
- const tsMs = Number.isFinite(detail?.ts_ms) ? Number(detail.ts_ms) : 0;
- if (source) {
- const prev = stationLocators.get(source);
- if (!prev || tsMs >= prev.tsMs) {
- stationLocators.set(source, { grid, tsMs });
+ if (locatorChoiceEl) {
+ locatorChoiceEl.addEventListener("click", (e) => {
+ const chip = e.target?.closest(".map-locator-chip[data-filter-kind]");
+ if (!chip) return;
+ const kind = String(chip.dataset.filterKind || "");
+ const key = String(chip.dataset.filterKey || "");
+ if (!key) return;
+ if (kind === "source" && Object.prototype.hasOwnProperty.call(mapFilter, key)) {
+ const sourceKey = key;
+ mapFilter[sourceKey] = !mapFilter[sourceKey];
+ const srcKeys = Object.keys(DEFAULT_MAP_SOURCE_FILTER);
+ const anySelected = srcKeys.some((k) => mapFilter[k]);
+ if (anySelected && !mapFilter.aprs && selectedAprsTrackCall) {
+ const entry = stationMarkers.get(String(selectedAprsTrackCall));
+ if (entry && entry.track && aprsMap && aprsMap.hasLayer(entry.track)) {
+ entry.track.removeFrom(aprsMap);
}
+ selectedAprsTrackCall = null;
}
- if (source && target && source !== target) {
- const band = bandForHz(Number(detail?.freq_hz));
- directedMessages.push({
- source,
- target,
- sourceGrid: grid,
- sourceType: entry.sourceType,
- tsMs,
- bandLabel: band?.label || null,
- remote: detail?.remote || null
- });
- }
- }
- }
- for (const msg of directedMessages) {
- const targetLocator = stationLocators.get(msg.target);
- if (!targetLocator) continue;
- if (msg.sourceGrid === targetLocator.grid) continue;
- const sourceCenter = locatorToLatLon(msg.sourceGrid);
- const targetCenter = locatorToLatLon(targetLocator.grid);
- if (!sourceCenter || !targetCenter) continue;
- const distanceKm = haversineKm(sourceCenter.lat, sourceCenter.lon, targetCenter.lat, targetCenter.lon);
- const key = [msg.source, msg.target].sort().join("::");
- const prev = decodeContactPaths.get(key);
- if (prev?.tsMs != null && prev.tsMs > msg.tsMs) continue;
- decodeContactPaths.set(key, {
- pathKey: key,
- source: msg.source,
- target: msg.target,
- sourceGrid: msg.sourceGrid,
- targetGrid: targetLocator.grid,
- sourceType: msg.sourceType,
- bandLabel: msg.bandLabel,
- from: sourceCenter,
- to: targetCenter,
- tsMs: msg.tsMs,
- distanceKm,
- distanceText: formatDecodeContactDistance(distanceKm),
- line: null,
- labelMarker: null,
- remote: msg.remote
- });
- }
- syncDecodeContactPathVisibility();
- }
- function _receiverLabel(rigId) {
- if (!rigId) return null;
- const rig = T.serverRigs.find((r) => r.remote === rigId);
- const name = T.lastRigDisplayNames[rigId] || rigId;
- if (rig && rig.latitude != null && rig.longitude != null) {
- const grid = latLonToMaidenhead(rig.latitude, rig.longitude);
- return `${name} (${grid})`;
- }
- return name;
- }
- function renderMapQsoSummary() {
- const listEl = mapEl("map-qso-summary-list");
- if (!listEl) return;
- const cutoff = _statsHistoryCutoffMs();
- const entries = Array.from(decodeContactPaths.values()).filter((entry) => entry && Number.isFinite(entry.distanceKm) && _statsDetailPassesRigFilter(entry) && (!entry.tsMs || entry.tsMs >= cutoff)).sort((a, b) => {
- const distanceDelta = Number(b.distanceKm) - Number(a.distanceKm);
- if (Math.abs(distanceDelta) > 1e-3) return distanceDelta;
- return Number(b.tsMs || 0) - Number(a.tsMs || 0);
- }).slice(0, MAP_QSO_SUMMARY_LIMIT);
- if (selectedMapQsoKey && !entries.some((entry) => entry.pathKey === selectedMapQsoKey)) {
- selectedMapQsoKey = null;
- }
- if (entries.length === 0) {
- const empty = document.createElement("div");
- empty.className = "map-qso-summary-empty";
- empty.textContent = "No directed FT8 or WSPR contacts match the current map history and filters.";
- listEl.replaceChildren(empty);
- return;
- }
- const fragment = document.createDocumentFragment();
- entries.forEach((entry, index) => {
- const card = document.createElement("button");
- card.type = "button";
- card.className = "map-qso-card";
- card.classList.toggle("is-selected", entry.pathKey === selectedMapQsoKey);
- card.setAttribute("aria-pressed", entry.pathKey === selectedMapQsoKey ? "true" : "false");
- card.addEventListener("click", () => {
- selectedMapQsoKey = selectedMapQsoKey === entry.pathKey ? null : entry.pathKey ?? null;
- syncDecodeContactPathVisibility();
- if (selectedMapQsoKey && entry.sourceGrid) {
- mapWindow.navigateToMapLocator?.(entry.sourceGrid, entry.sourceType);
- }
- });
- const head = document.createElement("div");
- head.className = "map-qso-card-head";
- const rank = document.createElement("span");
- rank.className = "map-qso-card-rank";
- rank.textContent = `#${index + 1}`;
- head.appendChild(rank);
- const distance = document.createElement("span");
- distance.className = "map-qso-card-distance";
- distance.textContent = entry.distanceText || "--";
- head.appendChild(distance);
- const body = document.createElement("div");
- body.className = "map-qso-card-body";
- const pair = document.createElement("div");
- pair.className = "map-qso-card-pair";
- pair.textContent = `${entry.source || "Unknown"} <-> ${entry.target || "Unknown"}`;
- body.appendChild(pair);
- const meta = document.createElement("div");
- meta.className = "map-qso-card-meta";
- const sourceType = document.createElement("span");
- sourceType.className = "map-qso-card-pill";
- sourceType.textContent = String(entry.sourceType || "ft8").toUpperCase();
- meta.appendChild(sourceType);
- if (entry.bandLabel) {
- const band = document.createElement("span");
- band.className = "map-qso-card-pill map-qso-card-band";
- band.style.setProperty("--band-color", locatorBandChipColor(entry.bandLabel));
- band.textContent = entry.bandLabel;
- meta.appendChild(band);
- }
- const ageText = formatTimeAgo(Number(entry.tsMs));
- if (ageText) {
- const age = document.createElement("span");
- age.className = "map-qso-card-pill";
- age.textContent = ageText;
- meta.appendChild(age);
- }
- const rxLabel = entry.remote ? _receiverLabel(entry.remote) : null;
- if (rxLabel) {
- const rx = document.createElement("span");
- rx.className = "map-qso-card-pill map-qso-card-rx";
- rx.textContent = rxLabel;
- meta.appendChild(rx);
- }
- body.appendChild(meta);
- const grids = document.createElement("div");
- grids.className = "map-qso-card-grids";
- grids.textContent = `${entry.sourceGrid || "--"} <-> ${entry.targetGrid || "--"}`;
- body.appendChild(grids);
- card.appendChild(head);
- card.appendChild(body);
- fragment.appendChild(card);
- });
- listEl.replaceChildren(fragment);
- }
- function renderMapSignalSummary() {
- const listEl = mapEl("map-signal-summary-list");
- if (!listEl) return;
- const cutoff = _statsHistoryCutoffMs();
- const bestByStation = /* @__PURE__ */ new Map();
- for (const entry of locatorMarkers.values()) {
- if (!entry || entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "ft2" && entry.sourceType !== "wspr") continue;
- if (!(entry.stationDetails instanceof Map)) continue;
- for (const detail of entry.stationDetails.values()) {
- if (!Number.isFinite(detail?.snr_db)) continue;
- if (!_statsDetailPassesRigFilter(detail)) continue;
- if (detail.ts_ms && detail.ts_ms < cutoff) continue;
- const station = String(detail?.source || detail?.station || "").trim().toUpperCase();
- if (!station) continue;
- const snrDb = Number(detail.snr_db);
- const tsMs = Number.isFinite(detail?.ts_ms) ? Number(detail.ts_ms) : 0;
- const prev = bestByStation.get(station);
- if (!prev || snrDb > prev.snrDb || snrDb === prev.snrDb && tsMs > prev.tsMs) {
- bestByStation.set(station, {
- station,
- snrDb,
- tsMs,
- grid: entry.grid,
- sourceType: entry.sourceType,
- bandLabel: bandForHz(Number(detail?.freq_hz))?.label || null,
- remote: detail?.remote || null
- });
- }
- }
- }
- const entries = Array.from(bestByStation.values()).sort((a, b) => {
- const delta = b.snrDb - a.snrDb;
- if (Math.abs(delta) > 1e-3) return delta;
- return b.tsMs - a.tsMs;
- }).slice(0, MAP_QSO_SUMMARY_LIMIT);
- if (entries.length === 0) {
- const empty = document.createElement("div");
- empty.className = "map-qso-summary-empty";
- empty.textContent = "No decoded signals with SNR data in the current map history.";
- listEl.replaceChildren(empty);
- return;
- }
- const fragment = document.createDocumentFragment();
- entries.forEach((entry, index) => {
- const card = document.createElement("button");
- card.type = "button";
- card.className = "map-qso-card";
- if (entry.grid) {
- card.addEventListener("click", () => {
- mapWindow.navigateToMapLocator?.(entry.grid ?? "", entry.sourceType);
- });
- }
- const head = document.createElement("div");
- head.className = "map-qso-card-head";
- const rank = document.createElement("span");
- rank.className = "map-qso-card-rank";
- rank.textContent = `#${index + 1}`;
- head.appendChild(rank);
- const snr = document.createElement("span");
- snr.className = "map-qso-card-distance";
- snr.textContent = `${entry.snrDb >= 0 ? "+" : ""}${entry.snrDb.toFixed(0)} dB`;
- head.appendChild(snr);
- const body = document.createElement("div");
- body.className = "map-qso-card-body";
- const pair = document.createElement("div");
- pair.className = "map-qso-card-pair";
- pair.textContent = entry.station;
- body.appendChild(pair);
- const meta = document.createElement("div");
- meta.className = "map-qso-card-meta";
- const sourceType = document.createElement("span");
- sourceType.className = "map-qso-card-pill";
- sourceType.textContent = String(entry.sourceType || "ft8").toUpperCase();
- meta.appendChild(sourceType);
- if (entry.bandLabel) {
- const band = document.createElement("span");
- band.className = "map-qso-card-pill map-qso-card-band";
- band.style.setProperty("--band-color", locatorBandChipColor(entry.bandLabel));
- band.textContent = entry.bandLabel;
- meta.appendChild(band);
- }
- const ageText = formatTimeAgo(Number(entry.tsMs));
- if (ageText) {
- const age = document.createElement("span");
- age.className = "map-qso-card-pill";
- age.textContent = ageText;
- meta.appendChild(age);
- }
- const rxLabel = entry.remote ? _receiverLabel(entry.remote) : null;
- if (rxLabel) {
- const rx = document.createElement("span");
- rx.className = "map-qso-card-pill map-qso-card-rx";
- rx.textContent = rxLabel;
- meta.appendChild(rx);
- }
- body.appendChild(meta);
- const grids = document.createElement("div");
- grids.className = "map-qso-card-grids";
- grids.textContent = entry.grid || "--";
- body.appendChild(grids);
- card.appendChild(head);
- card.appendChild(body);
- fragment.appendChild(card);
- });
- listEl.replaceChildren(fragment);
- }
- function renderMapWeakSignalSummary() {
- const listEl = mapEl("map-weak-signal-summary-list");
- if (!listEl) return;
- const cutoff = _statsHistoryCutoffMs();
- const worstByStation = /* @__PURE__ */ new Map();
- for (const entry of locatorMarkers.values()) {
- if (!entry || entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "ft2" && entry.sourceType !== "wspr") continue;
- if (!(entry.stationDetails instanceof Map)) continue;
- for (const detail of entry.stationDetails.values()) {
- if (!Number.isFinite(detail?.snr_db)) continue;
- if (!_statsDetailPassesRigFilter(detail)) continue;
- if (detail.ts_ms && detail.ts_ms < cutoff) continue;
- const station = String(detail?.source || detail?.station || "").trim().toUpperCase();
- if (!station) continue;
- const snrDb = Number(detail.snr_db);
- const tsMs = Number.isFinite(detail?.ts_ms) ? Number(detail.ts_ms) : 0;
- const prev = worstByStation.get(station);
- if (!prev || snrDb < prev.snrDb || snrDb === prev.snrDb && tsMs > prev.tsMs) {
- worstByStation.set(station, {
- station,
- snrDb,
- tsMs,
- grid: entry.grid,
- sourceType: entry.sourceType,
- bandLabel: bandForHz(Number(detail?.freq_hz))?.label || null,
- remote: detail?.remote || null
- });
- }
- }
- }
- const entries = Array.from(worstByStation.values()).sort((a, b) => {
- const delta = a.snrDb - b.snrDb;
- if (Math.abs(delta) > 1e-3) return delta;
- return b.tsMs - a.tsMs;
- }).slice(0, MAP_QSO_SUMMARY_LIMIT);
- if (entries.length === 0) {
- const empty = document.createElement("div");
- empty.className = "map-qso-summary-empty";
- empty.textContent = "No decoded signals with SNR data in the current map history.";
- listEl.replaceChildren(empty);
- return;
- }
- const fragment = document.createDocumentFragment();
- entries.forEach((entry, index) => {
- const card = document.createElement("button");
- card.type = "button";
- card.className = "map-qso-card";
- if (entry.grid) {
- card.addEventListener("click", () => {
- mapWindow.navigateToMapLocator?.(entry.grid ?? "", entry.sourceType);
- });
- }
- const head = document.createElement("div");
- head.className = "map-qso-card-head";
- const rank = document.createElement("span");
- rank.className = "map-qso-card-rank";
- rank.textContent = `#${index + 1}`;
- head.appendChild(rank);
- const snr = document.createElement("span");
- snr.className = "map-qso-card-distance";
- snr.textContent = `${entry.snrDb >= 0 ? "+" : ""}${entry.snrDb.toFixed(0)} dB`;
- head.appendChild(snr);
- const body = document.createElement("div");
- body.className = "map-qso-card-body";
- const pair = document.createElement("div");
- pair.className = "map-qso-card-pair";
- pair.textContent = entry.station;
- body.appendChild(pair);
- const meta = document.createElement("div");
- meta.className = "map-qso-card-meta";
- const sourceType = document.createElement("span");
- sourceType.className = "map-qso-card-pill";
- sourceType.textContent = String(entry.sourceType || "ft8").toUpperCase();
- meta.appendChild(sourceType);
- if (entry.bandLabel) {
- const band = document.createElement("span");
- band.className = "map-qso-card-pill map-qso-card-band";
- band.style.setProperty("--band-color", locatorBandChipColor(entry.bandLabel));
- band.textContent = entry.bandLabel;
- meta.appendChild(band);
- }
- const ageText = formatTimeAgo(Number(entry.tsMs));
- if (ageText) {
- const age = document.createElement("span");
- age.className = "map-qso-card-pill";
- age.textContent = ageText;
- meta.appendChild(age);
- }
- const rxLabel = entry.remote ? _receiverLabel(entry.remote) : null;
- if (rxLabel) {
- const rx = document.createElement("span");
- rx.className = "map-qso-card-pill map-qso-card-rx";
- rx.textContent = rxLabel;
- meta.appendChild(rx);
- }
- body.appendChild(meta);
- const grids = document.createElement("div");
- grids.className = "map-qso-card-grids";
- grids.textContent = entry.grid || "--";
- body.appendChild(grids);
- card.appendChild(head);
- card.appendChild(body);
- fragment.appendChild(card);
- });
- listEl.replaceChildren(fragment);
- }
- let statsRigFilter = "";
- let statsHistoryLimitMinutes = 1440;
- const statsDecodeLog = [];
- const STATS_LOG_MAX = 5e4;
- const STATS_TYPE_COLORS = {
- ft8: "#4fc3f7",
- ft4: "#81c784",
- ft2: "#aed581",
- wspr: "#ffb74d",
- aprs: "#ce93d8",
- hf_aprs: "#ba68c8",
- ais: "#90a4ae",
- vdes: "#78909c",
- cw: "#fff176"
- };
- const STATS_DX_BUCKETS = [
- { label: "0–500 km", min: 0, max: 500 },
- { label: "500–1k", min: 500, max: 1e3 },
- { label: "1k–2k", min: 1e3, max: 2e3 },
- { label: "2k–5k", min: 2e3, max: 5e3 },
- { label: "5k–10k", min: 5e3, max: 1e4 },
- { label: "10k+ km", min: 1e4, max: Infinity }
- ];
- function _statsHistoryCutoffMs() {
- return Date.now() - statsHistoryLimitMinutes * 60 * 1e3;
- }
- function _statsDetailPassesRigFilter(detail) {
- if (!statsRigFilter) return true;
- if (detail?.remotes instanceof Set) return detail.remotes.has(statsRigFilter);
- return detail?.remote === statsRigFilter;
- }
- function updateStatsRigFilter() {
- const el = mapEl("stats-rig-filter");
- if (!el) return;
- const prev = el.value;
- while (el.options.length > 1) el.remove(1);
- for (const id of T.lastRigIds) {
- const opt = document.createElement("option");
- opt.value = id;
- opt.textContent = T.lastRigDisplayNames[id] || id;
- el.appendChild(opt);
- }
- if (prev && T.lastRigIds.includes(prev)) {
- el.value = prev;
- } else {
- el.value = "";
- statsRigFilter = "";
- }
- }
- function statsRecordDecode(type, remote, tsMs) {
- statsDecodeLog.push({ type: String(type || "unknown"), ts_ms: tsMs || Date.now(), remote: remote || null });
- if (statsDecodeLog.length > STATS_LOG_MAX) {
- statsDecodeLog.splice(0, statsDecodeLog.length - STATS_LOG_MAX);
- }
- }
- function _statsFilteredLog() {
- const cutoff = _statsHistoryCutoffMs();
- return statsDecodeLog.filter((e) => {
- if (e.ts_ms < cutoff) return false;
- if (statsRigFilter && e.remote && e.remote !== statsRigFilter) return false;
- return true;
- });
- }
- function renderStatsCounters() {
- const cutoff = _statsHistoryCutoffMs();
- const log = _statsFilteredLog();
- const totalDecodes = log.length;
- const uniqueStations = /* @__PURE__ */ new Set();
- const uniqueGrids = /* @__PURE__ */ new Set();
- for (const entry of locatorMarkers.values()) {
- if (!entry || !(entry.stationDetails instanceof Map)) continue;
- for (const detail of entry.stationDetails.values()) {
- if (detail?.ts_ms && detail.ts_ms < cutoff) continue;
- if (!_statsDetailPassesRigFilter(detail)) continue;
- const station = String(detail?.source || detail?.station || "").trim().toUpperCase();
- if (station) uniqueStations.add(station);
- }
- if (entry.grid) {
- const hasVisible = entry.stationDetails instanceof Map && Array.from(entry.stationDetails.values()).some(
- (d) => (!d.ts_ms || d.ts_ms >= cutoff) && _statsDetailPassesRigFilter(d)
- );
- if (hasVisible) uniqueGrids.add(entry.grid);
- }
- }
- const rateWindow = Date.now() - 6e4;
- const recentCount = log.filter((e) => e.ts_ms >= rateWindow).length;
- const setEl = (id, val) => {
- const el = mapEl(id);
- if (el) el.textContent = String(val);
- };
- setEl("stats-total-decodes", totalDecodes.toLocaleString());
- setEl("stats-unique-stations", uniqueStations.size.toLocaleString());
- setEl("stats-unique-grids", uniqueGrids.size.toLocaleString());
- setEl("stats-decode-rate", recentCount.toLocaleString());
- }
- function _renderBarChart(containerId, data, emptyMsg) {
- const el = mapEl(containerId);
- if (!el) return;
- if (!data || data.length === 0 || data.every((d) => d.count === 0)) {
- el.replaceChildren();
- const empty = document.createElement("div");
- empty.className = "stats-bar-empty";
- empty.textContent = emptyMsg || "No data available.";
- el.appendChild(empty);
- return;
- }
- const maxVal = Math.max(1, ...data.map((d) => d.count));
- const fragment = document.createDocumentFragment();
- for (const item of data) {
- const row = document.createElement("div");
- row.className = "stats-bar-row";
- const label = document.createElement("span");
- label.className = "stats-bar-label";
- label.textContent = item.label;
- row.appendChild(label);
- const track = document.createElement("div");
- track.className = "stats-bar-track";
- const fill = document.createElement("div");
- fill.className = "stats-bar-fill";
- fill.style.width = `${item.count / maxVal * 100}%`;
- fill.style.background = item.color || "var(--accent-green)";
- track.appendChild(fill);
- row.appendChild(track);
- const count = document.createElement("span");
- count.className = "stats-bar-count";
- count.textContent = item.count.toLocaleString();
- row.appendChild(count);
- fragment.appendChild(row);
- }
- el.replaceChildren(fragment);
- }
- function renderStatsDecodeTypes() {
- const log = _statsFilteredLog();
- const counts = {};
- for (const e of log) {
- counts[e.type] = (counts[e.type] || 0) + 1;
- }
- const data = Object.entries(counts).map(([type, count]) => ({
- label: type.toUpperCase(),
- count,
- color: STATS_TYPE_COLORS[type] || "#aaa"
- })).sort((a, b) => b.count - a.count);
- _renderBarChart("stats-decode-type-bars", data, "No decoded signals in the current history.");
- }
- function renderStatsBandActivity() {
- const cutoff = _statsHistoryCutoffMs();
- const bandCounts = {};
- for (const entry of locatorMarkers.values()) {
- if (!entry || !(entry.stationDetails instanceof Map)) continue;
- for (const detail of entry.stationDetails.values()) {
- if (detail?.ts_ms && detail.ts_ms < cutoff) continue;
- if (!_statsDetailPassesRigFilter(detail)) continue;
- if (!Number.isFinite(detail?.freq_hz)) continue;
- const band = bandForHz(Number(detail.freq_hz));
- if (band) {
- bandCounts[band.label] = (bandCounts[band.label] || 0) + 1;
- }
- }
- }
- const data = Object.entries(bandCounts).map(([label, count]) => ({
- label,
- count,
- color: locatorBandChipColor(label)
- })).sort((a, b) => b.count - a.count);
- _renderBarChart("stats-band-activity-bars", data, "No band activity data in the current history.");
- }
- function renderStatsRigCompare() {
- const section = mapEl("stats-rig-compare-section");
- if (!section) return;
- if (T.lastRigIds.length < 2) {
- section.style.display = "none";
- return;
- }
- section.style.display = "";
- const cutoff = _statsHistoryCutoffMs();
- const rigCounts = {};
- for (const e of statsDecodeLog) {
- if (e.ts_ms < cutoff) continue;
- const rid = e.remote || "unknown";
- rigCounts[rid] = (rigCounts[rid] || 0) + 1;
- }
- const data = Object.entries(rigCounts).map(([rid, count]) => ({
- label: T.lastRigDisplayNames[rid] || rid,
- count,
- color: "var(--accent-green)"
- })).sort((a, b) => b.count - a.count);
- _renderBarChart("stats-rig-compare-bars", data, "No decode data per receiver.");
- }
- function renderStatsDxHistogram() {
- const cutoff = _statsHistoryCutoffMs();
- const buckets = STATS_DX_BUCKETS.map((b) => ({ ...b, count: 0 }));
- for (const entry of decodeContactPaths.values()) {
- if (!entry || !Number.isFinite(entry.distanceKm)) continue;
- if (entry.tsMs && entry.tsMs < cutoff) continue;
- if (!_statsDetailPassesRigFilter(entry)) continue;
- const km = entry.distanceKm;
- if (km == null) continue;
- for (const b of buckets) {
- if (km >= b.min && km < b.max) {
- b.count++;
- break;
- }
- }
- }
- const data = buckets.map((b) => ({
- label: b.label,
- count: b.count,
- color: "#4fc3f7"
- }));
- _renderBarChart("stats-dx-histogram-bars", data, "No directed contact paths in the current history.");
- }
- let _statsRenderPending = false;
- let _statsControlsWired = false;
- function _wireStatsControls() {
- if (_statsControlsWired) return;
- const rigEl = mapEl("stats-rig-filter");
- const histEl = mapEl("stats-history-limit");
- if (!rigEl && !histEl) return;
- _statsControlsWired = true;
- if (rigEl) {
- rigEl.addEventListener("change", () => {
- statsRigFilter = rigEl.value;
- scheduleStatsRender();
- });
- }
- if (histEl) {
- histEl.value = String(statsHistoryLimitMinutes);
- histEl.addEventListener("change", () => {
- statsHistoryLimitMinutes = Number(histEl.value) || 1440;
- scheduleStatsRender();
- });
- }
- }
- function scheduleStatsRender() {
- if (_statsRenderPending) return;
- _statsRenderPending = true;
- requestAnimationFrame(() => {
- _statsRenderPending = false;
- _wireStatsControls();
- renderStatsCounters();
- renderStatsDecodeTypes();
- renderStatsBandActivity();
- renderStatsRigCompare();
- renderStatsDxHistogram();
- renderMapQsoSummary();
- renderMapSignalSummary();
- renderMapWeakSignalSummary();
- });
- }
- function buildBookmarkLocatorPopupHtml(grid, bookmarks) {
- const list = Array.isArray(bookmarks) ? bookmarks : [];
- const rows = list.map((bm) => {
- const title = escapeMapHtml(String(bm.name || "Bookmark"));
- const freq = escapeMapHtml(formatFreqForHumans(Number(bm.freq_hz)));
- const mode = bm.mode ? ` · ${escapeMapHtml(String(bm.mode))}` : "";
- return `${title} ${freq}${mode}`;
- }).join("
");
- return `${escapeMapHtml(grid)}
Bookmarks: ${list.length || 1}` + (rows ? `
${rows}` : "");
- }
- mapWindow.syncBookmarkMapLocators = function(bookmarks) {
- const list = Array.isArray(bookmarks) ? bookmarks : [];
- const grouped = /* @__PURE__ */ new Map();
- for (const bm of list) {
- const grid = String(bm?.locator || "").trim().toUpperCase();
- if (!grid) continue;
- const bounds = maidenheadToBounds(grid);
- if (!bounds) continue;
- const key = `bookmark:${grid}`;
- const bucket = grouped.get(key);
- if (bucket) {
- bucket.bookmarks.push(bm);
- } else {
- grouped.set(key, { grid, bounds, bookmarks: [bm] });
- }
- }
- for (const [key, entry] of locatorMarkers.entries()) {
- if (!key.startsWith("bookmark:")) continue;
- if (!grouped.has(key)) {
- if (entry && entry.marker) {
- if (entry.marker === selectedLocatorMarker) {
- setSelectedLocatorMarker(null);
- clearMapRadioPath();
+ if (anySelected && !mapFilter.ais && selectedAisTrackMmsi) {
+ const entry = aisMarkers.get(String(selectedAisTrackMmsi));
+ if (entry && entry.track && aprsMap && aprsMap.hasLayer(entry.track)) {
+ entry.track.removeFrom(aprsMap);
}
- if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
- mapMarkers.delete(entry.marker);
+ selectedAisTrackMmsi = null;
}
- locatorMarkers.delete(key);
- }
- }
- for (const [key, next] of grouped.entries()) {
- const existing = locatorMarkers.get(key);
- const popupHtml = buildBookmarkLocatorPopupHtml(next.grid, next.bookmarks);
- const bandMeta = collectBandMeta(next.bookmarks.map((bm) => Number(bm?.freq_hz)));
- if (existing) {
- existing.grid = next.grid;
- existing.bounds = next.bounds;
- existing.bookmarks = next.bookmarks;
- existing.sourceType = "bookmark";
- existing.bandMeta = bandMeta;
- if (existing.marker) {
- existing.marker.setBounds(next.bounds);
- existing.marker.setStyle(locatorStyleForEntry(existing, next.bookmarks.length));
- existing.marker.setPopupContent(popupHtml);
- sendLocatorOverlayToBack(existing.marker);
- assignLocatorMarkerMeta(existing.marker, existing.sourceType, existing.bandMeta);
+ } else if (kind === "band") {
+ if (mapLocatorFilter.bands.has(key)) {
+ mapLocatorFilter.bands.delete(key);
+ } else {
+ mapLocatorFilter.bands.add(key);
}
- continue;
- }
- const entry = {
- marker: null,
- grid: next.grid,
- bounds: next.bounds,
- bookmarks: next.bookmarks,
- sourceType: "bookmark",
- bandMeta
- };
- locatorMarkers.set(key, entry);
- if (aprsMap) {
- entry.marker = L.rectangle(next.bounds, locatorStyleForEntry(entry, next.bookmarks.length)).addTo(aprsMap).bindPopup(popupHtml);
- entry.marker.__trxType = "bookmark";
- sendLocatorOverlayToBack(entry.marker);
- assignLocatorMarkerMeta(entry.marker, entry.sourceType ?? "bookmark", entry.bandMeta);
- mapMarkers.add(entry.marker);
- }
- }
- rebuildMapLocatorFilters();
- applyMapFilter();
- };
- mapWindow.mapAddLocator = function(message, grids, type = "ft8", station = null, details = null) {
- if (!Array.isArray(grids) || grids.length === 0) return;
- const markerType = type === "wspr" ? "wspr" : type === "ft4" ? "ft4" : type === "ft2" ? "ft2" : "ft8";
- const msgRigId = details?.rig_id || T.lastActiveRigId;
- const unique = [...new Set(grids.map((g) => String(g).toUpperCase()))];
- const stationId = station && String(station).trim() ? String(station).trim().toUpperCase() : "";
- const locatorDetails = /* @__PURE__ */ new Map();
- if (Array.isArray(details?.locator_details)) {
- for (const locatorDetail of details.locator_details) {
- const grid = String(locatorDetail?.grid || "").trim().toUpperCase();
- if (!grid) continue;
- locatorDetails.set(grid, locatorDetail);
- }
- }
- for (const grid of unique) {
- const bounds = maidenheadToBounds(grid);
- if (!bounds) continue;
- const locatorDetail = locatorDetails.get(grid);
- const sourceId = locatorDetail?.source && String(locatorDetail.source).trim() ? String(locatorDetail.source).trim().toUpperCase() : "";
- const targetId = locatorDetail?.target && String(locatorDetail.target).trim() ? String(locatorDetail.target).trim().toUpperCase() : "";
- const detailStationId = sourceId || stationId;
- const detailEntry = {
- station: detailStationId || null,
- source: sourceId || null,
- target: targetId || null,
- ts_ms: details && Number.isFinite(details.ts_ms) ? Number(details.ts_ms) : null,
- snr_db: details && Number.isFinite(details.snr_db) ? Number(details.snr_db) : null,
- dt_s: details && Number.isFinite(details.dt_s) ? Number(details.dt_s) : null,
- freq_hz: details && Number.isFinite(details.freq_hz) ? Number(details.freq_hz) : null,
- message: String(details?.message || message || "").trim() || null,
- remote: msgRigId || null,
- remotes: new Set(msgRigId ? [msgRigId] : [])
- };
- const detailKey = detailStationId || `${targetId || "decode"}:${detailEntry.message || "decode"}:${detailEntry.ts_ms || Date.now()}`;
- const key = `${markerType}:${grid}`;
- const existing = locatorMarkers.get(key);
- if (existing) {
- existing.grid = grid;
- if (!(existing.allStationDetails instanceof Map)) {
- existing.allStationDetails = existing.stationDetails instanceof Map ? new Map(existing.stationDetails) : /* @__PURE__ */ new Map();
- }
- const prevDetail = existing.allStationDetails.get(detailKey);
- const mergedRemotes = prevDetail?.remotes instanceof Set ? new Set(prevDetail.remotes) : /* @__PURE__ */ new Set();
- if (msgRigId) mergedRemotes.add(msgRigId);
- existing.allStationDetails.set(detailKey, { ...detailEntry, remotes: mergedRemotes });
- existing.sourceType = markerType;
- if (msgRigId) {
- if (!existing.rigIds) existing.rigIds = /* @__PURE__ */ new Set();
- existing.rigIds.add(msgRigId);
- }
- pruneLocatorEntry(key, existing, mapHistoryCutoffMs());
- if (existing.marker) sendLocatorOverlayToBack(existing.marker);
- scheduleDecodeMapMaintenance();
- continue;
- }
- const allStationDetails = /* @__PURE__ */ new Map();
- allStationDetails.set(detailKey, { ...detailEntry });
- const entry = {
- marker: null,
- grid,
- stations: /* @__PURE__ */ new Set(),
- stationDetails: /* @__PURE__ */ new Map(),
- allStationDetails,
- sourceType: markerType,
- bandMeta: /* @__PURE__ */ new Map(),
- rigIds: new Set(msgRigId ? [msgRigId] : [])
- };
- locatorMarkers.set(key, entry);
- pruneLocatorEntry(key, entry, mapHistoryCutoffMs());
- if (entry.marker) sendLocatorOverlayToBack(entry.marker);
- }
- scheduleDecodeMapMaintenance();
- };
- document.querySelectorAll(".sub-tab-bar").forEach((bar) => {
- bar.addEventListener("click", (e) => {
- const btn = e.target?.closest(".sub-tab[data-subtab]");
- if (!btn) return;
- bar.querySelectorAll(".sub-tab").forEach((t) => {
- t.classList.remove("active");
- });
- btn.classList.add("active");
- const parent = bar.parentElement;
- if (!parent) return;
- parent.querySelectorAll(".sub-tab-panel").forEach((p) => p.style.display = "none");
- const nextPanel = parent.querySelector(`#subtab-${btn.dataset.subtab}`);
- if (nextPanel) nextPanel.style.display = "";
- if (btn.dataset.subtab === "cw" && mapWindow.refreshCwTonePicker) {
- requestAnimationFrame(() => {
- if (mapWindow.refreshCwTonePicker) mapWindow.refreshCwTonePicker();
- });
- }
- if (btn.dataset.subtab !== "sat" && typeof mapWindow.clearSatPredictionDom === "function") {
- mapWindow.clearSatPredictionDom();
}
+ rebuildMapLocatorFilters();
+ applyMapFilter();
});
- });
- window.addEventListener("resize", () => {
- const mapTab = mapEl("tab-map");
- if (!mapTab || mapTab.style.display === "none") return;
- sizeAprsMapToViewport();
- });
- function flushDeferredDecodeMapSync() {
- if (!T.decodeMapSyncPending || T.decodeHistoryReplayActive || !aprsMap) return;
- T.decodeMapSyncPending = false;
- scheduleUiFrameJob("decode-map-maintenance", () => {
+ }
+ const mapRigFilterEl = mapEl("map-rig-filter");
+ if (mapRigFilterEl) {
+ mapRigFilterEl.addEventListener("change", () => {
+ mapRigFilter = mapRigFilterEl.value;
+ applyMapFilter();
+ });
+ }
+ if (mapSearchEl) {
+ mapSearchEl.value = mapSearchFilter;
+ mapSearchEl.addEventListener("input", () => {
+ mapSearchFilter = String(mapSearchEl.value || "").trim();
+ applyMapFilter();
+ });
+ }
+ if (mapHistoryLimitEl) {
+ mapHistoryLimitEl.value = String(mapHistoryLimitMinutes);
+ mapHistoryLimitEl.addEventListener("change", () => {
+ mapHistoryLimitMinutes = normalizeMapHistoryLimitMinutes(Number(mapHistoryLimitEl.value));
+ mapHistoryLimitEl.value = String(mapHistoryLimitMinutes);
+ saveSetting("mapHistoryLimitMinutes", mapHistoryLimitMinutes);
pruneMapHistory();
});
}
- function autoInitIfVisible() {
- const panel = mapEl("tab-map");
- if (panel.style.display === "none") return;
- if (typeof L === "undefined") {
- window.setTimeout(autoInitIfVisible, 50);
+ if (mapP2pPathsToggleEl) {
+ updateMapP2pPathsToggle();
+ mapP2pPathsToggleEl.addEventListener("click", () => {
+ mapP2pRadioPathsEnabled = !mapP2pRadioPathsEnabled;
+ saveSetting("mapP2pRadioPathsEnabled", mapP2pRadioPathsEnabled);
+ updateMapP2pPathsToggle();
+ if (!mapP2pRadioPathsEnabled) clearMapRadioPath();
+ });
+ }
+ if (mapContactPathsToggleEl) {
+ updateMapContactPathsToggle();
+ mapContactPathsToggleEl.addEventListener("click", () => {
+ mapDecodeContactPathsEnabled = !mapDecodeContactPathsEnabled;
+ saveSetting("mapDecodeContactPathsEnabled", mapDecodeContactPathsEnabled);
+ updateMapContactPathsToggle();
+ syncDecodeContactPathVisibility();
+ });
+ }
+ if (fullscreenBtn) {
+ fullscreenBtn.addEventListener("click", () => {
+ void toggleMapFullscreen();
+ });
+ updateMapFullscreenButton();
+ }
+ applyMapOverlayPanelVisibility();
+ updateMapOverlayToggleButton();
+ if (overlayToggleBtn) {
+ overlayToggleBtn.addEventListener("click", () => {
+ mapOverlayPanelVisible = !mapOverlayPanelVisible;
+ saveSetting("mapOverlayPanelVisible", mapOverlayPanelVisible);
+ applyMapOverlayPanelVisibility();
+ updateMapOverlayToggleButton();
+ });
+ }
+ if (!mapFullscreenListenerBound) {
+ const onFullscreenChange = () => {
+ updateMapFullscreenButton();
+ sizeAprsMapToViewport();
+ };
+ document.addEventListener("fullscreenchange", onFullscreenChange);
+ document.addEventListener("webkitfullscreenchange", onFullscreenChange);
+ mapFullscreenListenerBound = true;
+ }
+ if (!mapHistoryPruneTimer) {
+ mapHistoryPruneTimer = setInterval(() => {
+ pruneMapHistory();
+ }, 60 * 1e3);
+ }
+ rebuildMapLocatorFilters();
+ }
+ function sizeAprsMapToViewport() {
+ const mapContainer = mapEl("aprs-map");
+ const stage = mapStageEl();
+ if (mapIsFullscreen() && stage) {
+ const isFake = stage.classList.contains("map-fake-fullscreen");
+ const stageHeight = isFake ? window.innerHeight : stage.clientHeight || stage.getBoundingClientRect().height;
+ const target2 = Math.max(260, Math.floor(stageHeight));
+ mapContainer.style.height = `${target2}px`;
+ if (aprsMap) aprsMap.invalidateSize();
+ return;
+ }
+ const mapRect = mapContainer.getBoundingClientRect();
+ const width = mapContainer.clientWidth || mapRect.width;
+ const footer = document.querySelector(".footer");
+ let bottom = mapIsFullscreen() && stage ? stage.getBoundingClientRect().bottom : window.innerHeight;
+ if (!mapIsFullscreen() && footer) {
+ const fr = footer.getBoundingClientRect();
+ if (fr.top > mapRect.top + 50) bottom = fr.top;
+ }
+ const available = Math.max(0, Math.floor(bottom - mapRect.top - 8));
+ const widthDriven = width > 0 ? Math.floor(width / 1.55) : available;
+ const viewportCap = mapIsFullscreen() ? Math.floor(window.innerHeight * 0.9) : Math.floor(window.innerHeight * 0.75);
+ const minHeight = Math.min(260, available);
+ const target = Math.max(minHeight, Math.min(available, viewportCap, widthDriven));
+ mapContainer.style.height = `${target}px`;
+ if (aprsMap) aprsMap.invalidateSize();
+ }
+ function aprsSymbolIcon(symbolTable, symbolCode) {
+ if (!symbolTable || !symbolCode) return null;
+ const table = symbolTable === "/" ? "primary" : "alternate";
+ return L.divIcon({
+ className: "",
+ html: `${escapeMapHtml(symbolCode)}
`,
+ iconSize: [24, 24],
+ iconAnchor: [12, 12],
+ popupAnchor: [0, -12]
+ });
+ }
+ mapWindow.navigateToAprsMap = function(lat, lon) {
+ T._activeTab = "map";
+ document.querySelectorAll(".tab-bar .tab").forEach((t) => {
+ t.classList.remove("active");
+ });
+ const mapTabBtn = document.querySelector(".tab-bar .tab[data-tab='map']");
+ if (mapTabBtn) mapTabBtn.classList.add("active");
+ document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none");
+ const mapPanel = mapEl("tab-map");
+ if (mapPanel) mapPanel.style.display = "";
+ initAprsMap();
+ sizeAprsMapToViewport();
+ if (aprsMap) {
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ sizeAprsMapToViewport();
+ aprsMap?.invalidateSize();
+ aprsMap?.setView([lat, lon], 13);
+ });
+ });
+ }
+ };
+ mapWindow.navigateToMapLocator = function(grid, preferredType = null) {
+ const normalizedGrid = String(grid || "").trim().toUpperCase();
+ if (!/^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalizedGrid)) return false;
+ T._activeTab = "map";
+ document.querySelectorAll(".tab-bar .tab").forEach((t) => {
+ t.classList.remove("active");
+ });
+ const mapTabBtn = document.querySelector(".tab-bar .tab[data-tab='map']");
+ if (mapTabBtn) mapTabBtn.classList.add("active");
+ document.querySelectorAll(".tab-panel").forEach((p) => p.style.display = "none");
+ const mapPanel = mapEl("tab-map");
+ if (mapPanel) mapPanel.style.display = "";
+ initAprsMap();
+ sizeAprsMapToViewport();
+ if (!aprsMap) return false;
+ const pref = preferredType === "wspr" ? "wspr" : preferredType === "ft4" ? "ft4" : preferredType === "ft2" ? "ft2" : preferredType === "ft8" ? "ft8" : null;
+ const keys = pref ? [`${pref}:${normalizedGrid}`, `ft8:${normalizedGrid}`, `ft4:${normalizedGrid}`, `ft2:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`] : [`ft8:${normalizedGrid}`, `ft4:${normalizedGrid}`, `ft2:${normalizedGrid}`, `wspr:${normalizedGrid}`, `bookmark:${normalizedGrid}`];
+ let entry = null;
+ for (const key of keys) {
+ entry = locatorMarkers.get(key);
+ if (entry?.marker) break;
+ }
+ if (!entry?.marker) return false;
+ if (pref && Object.prototype.hasOwnProperty.call(mapFilter, pref) && !mapFilter[pref]) {
+ mapFilter[pref] = true;
+ rebuildMapLocatorFilters();
+ applyMapFilter();
+ }
+ const marker = entry.marker;
+ if (!aprsMap.hasLayer(marker)) {
+ marker.addTo(aprsMap);
+ sendLocatorOverlayToBack(marker);
+ }
+ const center = locatorMarkerCenter(marker);
+ const focusMarker = () => {
+ if (!aprsMap || !marker) return;
+ sizeAprsMapToViewport();
+ aprsMap.invalidateSize();
+ if (center) {
+ const targetZoom = Math.max(aprsMap.getZoom() || 0, 7);
+ aprsMap.setView([center.lat, center.lon], targetZoom);
+ if (marker.__trxType !== "bookmark") {
+ const fEntry = locatorEntryForMarker(marker);
+ const sourceType = marker.__trxType;
+ const fColor = fEntry ? locatorStyleForEntry(fEntry, locatorEntryCount(fEntry)).color ?? locatorFilterColor(sourceType) : locatorFilterColor(sourceType);
+ setMapRadioPathTo(center.lat, center.lon, fColor, "locator-radio-path", marker.__trxRigIds);
+ }
+ }
+ setSelectedLocatorMarker(marker);
+ if (typeof marker.openPopup === "function") marker.openPopup();
+ };
+ focusMarker();
+ requestAnimationFrame(() => {
+ requestAnimationFrame(focusMarker);
+ });
+ return true;
+ };
+ function buildReceiverPopupHtml(rigIds) {
+ const call = T.serverCallsign || T.ownerCallsign || "Receiver";
+ let meta = "";
+ if (T.serverVersion) {
+ meta = `trx-server v${escapeMapHtml(T.serverVersion)}`;
+ if (T.serverBuildDate) meta += ` · ${escapeMapHtml(T.serverBuildDate)}`;
+ }
+ let rows = "";
+ if (T.ownerCallsign && T.ownerCallsign !== T.serverCallsign) {
+ rows += `| ${escapeMapHtml(T.ownerCallsign)} |
`;
+ }
+ const rigList = rigIds ? Array.from(rigIds) : [];
+ const rigSet = rigList.length ? new Set(rigList) : null;
+ const firstRig = rigSet ? T.serverRigs.find((r) => !!r.remote && rigSet.has(r.remote)) : null;
+ const popupLat = firstRig ? firstRig.latitude : T.serverLat;
+ const popupLon = firstRig ? firstRig.longitude : T.serverLon;
+ if (popupLat != null && popupLon != null) {
+ const grid = latLonToMaidenhead(popupLat, popupLon);
+ rows += `| ${popupLat.toFixed(5)}, ${popupLon.toFixed(5)} (${escapeMapHtml(grid)}) |
`;
+ }
+ const rigsToShow = rigSet ? T.serverRigs.filter((r) => !!r.remote && rigSet.has(r.remote)) : T.serverRigs;
+ for (const rig of rigsToShow) {
+ const name = rig.display_name || `${rig.manufacturer} ${rig.model}`.trim();
+ const active = rig.remote === T.serverActiveRigId ? ` ` : "";
+ rows += `| ${escapeMapHtml(name)}${active} |
`;
+ }
+ return ``;
+ }
+ function buildAprsPopupHtml(call, lat, lon, info, pkt) {
+ const age = pkt?._tsMs ? formatTimeAgo(pkt._tsMs) : pkt?._ts || null;
+ const distKm = T.serverLat != null && T.serverLon != null ? haversineKm(T.serverLat, T.serverLon, lat, lon) : null;
+ const distStr = distKm != null ? distKm < 1 ? `${Math.round(distKm * 1e3)} m` : `${distKm.toFixed(1)} km` : null;
+ const path = pkt?.path || null;
+ const type = pkt?.type || null;
+ const meta = [age, distStr].filter(Boolean).join(" · ");
+ let rows = "";
+ if (type) rows += `| ${escapeMapHtml(type)} |
`;
+ if (path) rows += `| ${escapeMapHtml(path)} |
`;
+ if (lat != null && lon != null)
+ rows += `| ${lat.toFixed(5)}, ${lon.toFixed(5)} |
`;
+ return ``;
+ }
+ function buildAisPopupHtml(msg) {
+ const age = msg?._tsMs ? formatTimeAgo(msg._tsMs) : null;
+ const distKm = T.serverLat != null && T.serverLon != null && msg?.lat != null && msg?.lon != null ? haversineKm(T.serverLat, T.serverLon, msg.lat, msg.lon) : null;
+ const distStr = distKm != null ? distKm < 1 ? `${Math.round(distKm * 1e3)} m` : `${distKm.toFixed(1)} km` : null;
+ const meta = [age, distStr, msg?.channel ? `AIS ${escapeMapHtml(msg.channel)}` : null].filter(Boolean).join(" · ");
+ let rows = "";
+ rows += `| ${escapeMapHtml(String(msg.mmsi || "--"))} |
`;
+ rows += `| ${escapeMapHtml(String(msg.message_type || "--"))} |
`;
+ if (distStr) rows += `| ${distStr} from TRX |
`;
+ if (msg?.sog_knots != null) rows += `| ${Number(msg.sog_knots).toFixed(1)} kn |
`;
+ if (msg?.cog_deg != null) rows += `| ${Number(msg.cog_deg).toFixed(1)}° |
`;
+ if (msg?.heading_deg != null) rows += `| ${Number(msg.heading_deg).toFixed(0)}° |
`;
+ if (msg?.nav_status != null) rows += `| ${escapeMapHtml(String(msg.nav_status))} |
`;
+ if (msg?.lat != null && msg?.lon != null) rows += `| ${msg.lat.toFixed(5)}, ${msg.lon.toFixed(5)} |
`;
+ const info = [msg?.vessel_name, msg?.callsign, msg?.destination].filter(Boolean).map(escapeMapHtml).join(" · ");
+ const vesselLabel = escapeMapHtml(msg?.vessel_name || `MMSI ${msg?.mmsi || "--"}`);
+ const vesselUrl = mapWindow.buildAisVesselUrl ? mapWindow.buildAisVesselUrl(msg?.mmsi) : null;
+ const vesselTitle = vesselUrl ? `${vesselLabel}` : vesselLabel;
+ return ``;
+ }
+ function buildVdesPopupHtml(msg) {
+ const age = msg.ts_ms == null ? "" : formatTimeAgo(msg.ts_ms);
+ const distKm = T.serverLat != null && T.serverLon != null && msg?.lat != null && msg?.lon != null ? haversineKm(T.serverLat, T.serverLon, msg.lat, msg.lon) : null;
+ const distStr = distKm != null ? distKm < 1 ? `${Math.round(distKm * 1e3)} m` : `${distKm.toFixed(1)} km` : null;
+ const meta = [
+ age,
+ distStr,
+ msg?.message_label ? escapeMapHtml(msg.message_label) : null,
+ Number.isFinite(msg?.link_id) ? `LID ${Number(msg.link_id)}` : null
+ ].filter(Boolean).join(" · ");
+ let rows = "";
+ if (distStr) rows += `| ${distStr} from TRX |
`;
+ rows += `| ${escapeMapHtml(String(msg?.message_type ?? "--"))} |
`;
+ if (Number.isFinite(msg?.source_id)) rows += `| ${escapeMapHtml(String(msg.source_id))} |
`;
+ if (Number.isFinite(msg?.destination_id)) rows += `| ${escapeMapHtml(String(msg.destination_id))} |
`;
+ if (msg?.lat != null && msg?.lon != null) rows += `| ${msg.lat.toFixed(5)}, ${msg.lon.toFixed(5)} |
`;
+ if (Number.isFinite(msg?.sync_score)) rows += `| ${(Number(msg.sync_score) * 100).toFixed(0)}% |
`;
+ if (msg?.fec_state) rows += `| ${escapeMapHtml(String(msg.fec_state))} |
`;
+ const info = [
+ msg?.vessel_name,
+ msg?.callsign,
+ msg?.destination,
+ msg?.payload_preview
+ ].filter(Boolean).map(escapeMapHtml).join(" · ");
+ const title = escapeMapHtml(msg?.vessel_name || msg?.callsign || "VDES Position");
+ return ``;
+ }
+ function aprsPositionsEqual(a, b) {
+ if (!a || !b) return false;
+ const aLat = Array.isArray(a) ? a[0] : a.lat;
+ const aLon = Array.isArray(a) ? a[1] : a.lon;
+ const bLat = Array.isArray(b) ? b[0] : b.lat;
+ const bLon = Array.isArray(b) ? b[1] : b.lon;
+ return Math.abs(aLat - bLat) < 1e-6 && Math.abs(aLon - bLon) < 1e-6;
+ }
+ function aisPositionsEqual(a, b) {
+ if (!a || !b) return false;
+ const aLat = Array.isArray(a) ? a[0] : a.lat;
+ const aLon = Array.isArray(a) ? a[1] : a.lon;
+ const bLat = Array.isArray(b) ? b[0] : b.lat;
+ const bLon = Array.isArray(b) ? b[1] : b.lon;
+ return Math.abs(aLat - bLat) < 1e-6 && Math.abs(aLon - bLon) < 1e-6;
+ }
+ function vdesMarkerKey(msg) {
+ if (Number.isFinite(msg?.source_id)) return `src:${Number(msg.source_id)}`;
+ if (Number.isFinite(msg?.mmsi) && Number(msg.mmsi) > 0) return `mmsi:${Number(msg.mmsi)}`;
+ if (msg?.lat != null && msg?.lon != null) {
+ return `pos:${Number(msg.lat).toFixed(4)}:${Number(msg.lon).toFixed(4)}:${Number(msg?.message_type ?? 0)}`;
+ }
+ return null;
+ }
+ function _aprsAddMarkerToMap(call, entry) {
+ if (!aprsMap || entry.lat == null || entry.lon == null) return;
+ refreshAprsTrack(call, entry);
+ const icon = aprsSymbolIcon(entry.symbolTable ?? "", entry.symbolCode ?? "");
+ const popupContent = buildAprsPopupHtml(call, entry.lat, entry.lon, entry.info || "", entry.pkt);
+ const marker = icon ? L.marker([entry.lat, entry.lon], { icon }).addTo(aprsMap).bindPopup(popupContent) : L.circleMarker([entry.lat, entry.lon], {
+ radius: 6,
+ color: "#00d17f",
+ fillColor: "#00d17f",
+ fillOpacity: 0.8
+ }).addTo(aprsMap).bindPopup(popupContent);
+ marker.__trxType = "aprs";
+ marker.__trxRigIds = entry.rigIds || /* @__PURE__ */ new Set();
+ marker._aprsCall = call;
+ entry.marker = marker;
+ mapMarkers.add(marker);
+ }
+ mapWindow.aprsMapAddStation = function(call, lat, lon, info, symbolTable, symbolCode, pkt) {
+ const nextPoint = [lat, lon];
+ const tsMs = Number.isFinite(pkt?._tsMs) ? Number(pkt._tsMs) : Date.now();
+ const msgRigId = pkt?.rig_id || T.lastActiveRigId;
+ const existing = stationMarkers.get(call);
+ if (existing) {
+ existing.pkt = pkt;
+ existing.lat = lat;
+ existing.lon = lon;
+ existing.info = info;
+ existing.symbolTable = symbolTable;
+ existing.symbolCode = symbolCode;
+ if (msgRigId) {
+ if (!existing.rigIds) existing.rigIds = /* @__PURE__ */ new Set();
+ existing.rigIds.add(msgRigId);
+ }
+ if (!Array.isArray(existing.trackHistory)) existing.trackHistory = [];
+ const prevPoint = existing.trackHistory[existing.trackHistory.length - 1];
+ if (!aprsPositionsEqual(prevPoint, nextPoint)) {
+ existing.trackHistory.push({ lat, lon, tsMs });
+ } else if (prevPoint) {
+ prevPoint.tsMs = tsMs;
+ }
+ pruneAprsEntry(call, existing, mapHistoryCutoffMs());
+ if (aprsMap && existing.marker && !T.decodeHistoryReplayActive) {
+ existing.marker.setLatLng([lat, lon]);
+ existing.marker.setPopupContent(buildAprsPopupHtml(call, lat, lon, info, pkt));
+ }
+ } else {
+ const entry = {
+ marker: null,
+ track: null,
+ trackHistory: [{ lat, lon, tsMs }],
+ trackPoints: [nextPoint],
+ type: "aprs",
+ pkt,
+ lat,
+ lon,
+ info,
+ symbolTable,
+ symbolCode,
+ rigIds: new Set(msgRigId ? [msgRigId] : [])
+ };
+ stationMarkers.set(call, entry);
+ pruneAprsEntry(call, entry, mapHistoryCutoffMs());
+ if (entry.visibleInHistoryWindow) ensureAprsMarker(call, entry);
+ if (aprsMap) scheduleDecodeMapMaintenance();
+ }
+ };
+ function syncSelectedAisTrackVisibility() {
+ if (!aprsMap) return;
+ const map = aprsMap;
+ const selectedKey = selectedAisTrackMmsi ? String(selectedAisTrackMmsi) : null;
+ aisMarkers.forEach((entry, key) => {
+ const track = entry?.track;
+ if (!track) return;
+ const shouldShow = !!selectedKey && selectedKey === String(key) && !!mapFilter.ais;
+ const onMap = map.hasLayer(track);
+ if (shouldShow && !onMap) {
+ track.addTo(map);
+ }
+ if (!shouldShow && onMap) {
+ track.removeFrom(map);
+ }
+ });
+ }
+ function getAisAccentColor() {
+ return getComputedStyle(document.documentElement).getPropertyValue("--accent-green").trim() || "#c24b1a";
+ }
+ function aisMarkerOptionsFromMessage(msg) {
+ const color = getAisAccentColor();
+ return {
+ heading: msg?.heading_deg,
+ course: msg?.cog_deg,
+ speed: msg?.sog_knots,
+ color,
+ outline: "#00000055",
+ size: 22
+ };
+ }
+ function createAisMarker(lat, lon, msg) {
+ if (typeof L !== "undefined" && typeof L.trxAisTrackSymbol === "function") {
+ return L.trxAisTrackSymbol([lat, lon], aisMarkerOptionsFromMessage(msg));
+ }
+ const color = getAisAccentColor();
+ return L.circleMarker([lat, lon], {
+ radius: 6,
+ color,
+ fillColor: color,
+ fillOpacity: 0.82
+ });
+ }
+ function updateAisMarker(marker, msg, popupHtml) {
+ if (!marker || msg.lat == null || msg.lon == null) return;
+ marker.setLatLng([msg.lat, msg.lon]);
+ if (typeof marker.setAisState === "function") {
+ marker.setAisState(aisMarkerOptionsFromMessage(msg));
+ }
+ if (typeof marker.setStyle === "function" && typeof marker.setAisState !== "function") {
+ const color = getAisAccentColor();
+ marker.setRadius(6);
+ marker.setStyle({
+ color,
+ fillColor: color,
+ fillOpacity: 0.84
+ });
+ }
+ marker.setPopupContent(popupHtml);
+ }
+ function refreshAisMarkerColors() {
+ const color = getAisAccentColor();
+ aisMarkers.forEach((entry) => {
+ if (entry.marker) {
+ if (typeof entry.marker.setAisState === "function") {
+ entry.marker.setAisState(aisMarkerOptionsFromMessage(entry.msg || {}));
+ } else if (typeof entry.marker.setStyle === "function") {
+ entry.marker.setStyle({ color, fillColor: color });
+ }
+ }
+ if (entry.track && typeof entry.track.setStyle === "function") {
+ entry.track.setStyle({ color });
+ }
+ });
+ }
+ mapWindow.aisMapAddVessel = function(msg) {
+ if (msg == null || msg.lat == null || msg.lon == null || !Number.isFinite(msg.mmsi)) return;
+ const key = String(msg.mmsi);
+ const popupHtml = buildAisPopupHtml(msg);
+ const nextPoint = [msg.lat, msg.lon];
+ const tsMs = Number.isFinite(msg?._tsMs) ? Number(msg._tsMs) : Date.now();
+ const msgRigId = msg?.rig_id || T.lastActiveRigId;
+ const existing = aisMarkers.get(key);
+ if (existing) {
+ existing.msg = msg;
+ if (msgRigId) {
+ if (!existing.rigIds) existing.rigIds = /* @__PURE__ */ new Set();
+ existing.rigIds.add(msgRigId);
+ }
+ if (!Array.isArray(existing.trackHistory)) existing.trackHistory = [];
+ const prevPoint = existing.trackHistory[existing.trackHistory.length - 1];
+ if (!aisPositionsEqual(prevPoint, nextPoint)) {
+ existing.trackHistory.push({ lat: msg.lat, lon: msg.lon, tsMs });
+ } else if (prevPoint) {
+ prevPoint.tsMs = tsMs;
+ }
+ pruneAisEntry(key, existing, mapHistoryCutoffMs());
+ if (aprsMap && existing.marker && !T.decodeHistoryReplayActive) {
+ updateAisMarker(existing.marker, msg, popupHtml);
+ }
+ return;
+ }
+ const entry = {
+ marker: null,
+ track: null,
+ trackHistory: [{ lat: msg.lat, lon: msg.lon, tsMs }],
+ trackPoints: [nextPoint],
+ msg,
+ rigIds: new Set(msgRigId ? [msgRigId] : [])
+ };
+ aisMarkers.set(key, entry);
+ pruneAisEntry(key, entry, mapHistoryCutoffMs());
+ if (entry.visibleInHistoryWindow) ensureAisMarker(key, entry);
+ scheduleDecodeMapMaintenance();
+ };
+ mapWindow.vdesMapAddPoint = function(msg) {
+ if (msg == null || msg.lat == null || msg.lon == null) return;
+ const key = vdesMarkerKey(msg);
+ if (!key) return;
+ const popupHtml = buildVdesPopupHtml(msg);
+ const visible = Number.isFinite(Number(msg?._tsMs)) && Number(msg._tsMs) >= mapHistoryCutoffMs();
+ const msgRigId = msg?.rig_id || T.lastActiveRigId;
+ const existing = vdesMarkers.get(key);
+ if (existing) {
+ existing.msg = msg;
+ existing.visibleInHistoryWindow = visible;
+ if (msgRigId) {
+ if (!existing.rigIds) existing.rigIds = /* @__PURE__ */ new Set();
+ existing.rigIds.add(msgRigId);
+ }
+ if (!visible) {
+ if (!C.decodeHistoryMapRenderingDeferred()) {
+ setRetainedMapMarkerVisible(existing.marker, false);
+ } else {
+ C.markDecodeMapSyncPending();
+ }
return;
}
- initAprsMap();
- requestAnimationFrame(() => requestAnimationFrame(sizeAprsMapToViewport));
+ if (!C.decodeHistoryMapRenderingDeferred()) {
+ ensureVdesMarker(key, existing);
+ setRetainedMapMarkerVisible(existing.marker, true);
+ } else {
+ C.markDecodeMapSyncPending();
+ }
+ if (aprsMap && existing.marker && !T.decodeHistoryReplayActive) {
+ existing.marker.setLatLng([msg.lat, msg.lon]);
+ existing.marker.setPopupContent(popupHtml);
+ }
+ return;
}
- modules.map = {
- initAprsMap,
- sizeAprsMapToViewport,
- syncAprsReceiverMarker,
- updateMapRigFilter,
- updateStatsRigFilter,
- statsRecordDecode,
- scheduleStatsRender,
- get aprsMap() {
- return aprsMap;
- },
- get stationMarkers() {
- return stationMarkers;
- },
- get locatorMarkers() {
- return locatorMarkers;
- },
- get aisMarkers() {
- return aisMarkers;
- },
- get vdesMarkers() {
- return vdesMarkers;
- },
- get decodeContactPaths() {
- return decodeContactPaths;
- },
- pruneMapHistory,
- aprsSymbolIcon,
- buildAprsPopupHtml,
- buildAisPopupHtml,
- buildVdesPopupHtml,
- ensureAprsMarker,
- ensureAisMarker,
- ensureVdesMarker,
- ensureDecodeLocatorMarker,
- aprsPositionsEqual,
- aisPositionsEqual,
- refreshAprsTrack,
- refreshAisTrack,
- updateAisMarker,
- createAisMarker,
- getAisAccentColor,
- refreshAisMarkerColors,
- setMapRadioPathTo,
- buildReceiverPopupHtml,
- rebuildDecodeContactPaths,
- syncDecodeContactPathVisibility,
- scheduleDecodeMapMaintenance,
- renderMapLocatorLegend,
- rebuildMapLocatorFilters,
- renderMapQsoSummary,
- renderMapSignalSummary,
- renderMapWeakSignalSummary,
- vdesMarkerKey,
- aisMarkerOptionsFromMessage,
- materializeBufferedMapLayers,
- flushDeferredDecodeMapSync,
- bandForHz,
- reverseGeocodeLocation
+ const entry = {
+ marker: null,
+ msg,
+ visibleInHistoryWindow: visible,
+ rigIds: new Set(msgRigId ? [msgRigId] : [])
};
- autoInitIfVisible();
- })();
+ vdesMarkers.set(key, entry);
+ if (!visible) return;
+ if (!C.decodeHistoryMapRenderingDeferred()) {
+ ensureVdesMarker(key, entry);
+ setRetainedMapMarkerVisible(entry.marker, true);
+ } else {
+ C.markDecodeMapSyncPending();
+ }
+ if (aprsMap && entry.marker && !T.decodeHistoryReplayActive) {
+ entry.marker.setPopupContent(popupHtml);
+ }
+ scheduleDecodeMapMaintenance();
+ };
+ let reverseGeocodeLastKey = null;
+ function reverseGeocodeLocation(lat, lon, grid) {
+ const key = `${lat.toFixed(4)},${lon.toFixed(4)}`;
+ if (key === reverseGeocodeLastKey) return;
+ reverseGeocodeLastKey = key;
+ const url = `https://nominatim.openstreetmap.org/reverse?lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lon)}&format=json&zoom=10&accept-language=en`;
+ fetch(url, { headers: { "User-Agent": "trx-rs" } }).then((r) => r.ok ? r.json() : Promise.reject(new Error(`Reverse geocode failed: ${r.status}`))).then((data) => {
+ const addr = data?.address;
+ if (!addr) return;
+ const city = addr.city || addr.town || addr.village || addr.hamlet || addr.municipality || addr.county || "";
+ const country = addr.country || "";
+ if (!city && !country) return;
+ const label = city && country ? `${city}, ${country}` : city || country;
+ T.lastCityLabel = label;
+ if (T.locationSubtitle) {
+ T.locationSubtitle.textContent = `Location: ${grid} · ${label}`;
+ }
+ C.updateDocumentTitle?.();
+ }).catch(() => {
+ });
+ }
+ function maidenheadToBounds(grid) {
+ if (!grid || grid.length < 4) return null;
+ const g = grid.toUpperCase();
+ const A = "A".charCodeAt(0);
+ const fieldLon = (g.charCodeAt(0) - A) * 20 - 180;
+ const fieldLat = (g.charCodeAt(1) - A) * 10 - 90;
+ const squareLon = parseInt(g.charAt(2), 10) * 2;
+ const squareLat = parseInt(g.charAt(3), 10);
+ let lon = fieldLon + squareLon;
+ let lat = fieldLat + squareLat;
+ let lonSpan = 2;
+ let latSpan = 1;
+ if (g.length >= 6) {
+ const subLon = (g.charCodeAt(4) - A) * (5 / 60);
+ const subLat = (g.charCodeAt(5) - A) * (2.5 / 60);
+ lon += subLon;
+ lat += subLat;
+ lonSpan = 5 / 60;
+ latSpan = 2.5 / 60;
+ }
+ return [
+ [lat, lon],
+ [lat + latSpan, lon + lonSpan]
+ ];
+ }
+ function applyMapFilter() {
+ if (!aprsMap) return;
+ const map = aprsMap;
+ const sourceKeys = Object.keys(DEFAULT_MAP_SOURCE_FILTER);
+ const noneSelected = sourceKeys.every((k) => !mapFilter[k]);
+ mapMarkers.forEach((marker) => {
+ const type = marker.__trxType;
+ const sourceVisible = noneSelected ? type == null || DEFAULT_MAP_SOURCE_FILTER[type] : type == null || !!mapFilter[type];
+ const rigVisible = !mapRigFilter || marker.__trxType === "bookmark" || marker.__trxRigIds instanceof Set && marker.__trxRigIds.has(mapRigFilter);
+ const visible = marker.__trxHistoryVisible !== false && markerPassesSearchFilter(marker) && markerPassesLocatorFilters(marker) && sourceVisible && rigVisible;
+ const onMap = map.hasLayer(marker);
+ if (visible && !onMap) {
+ marker.addTo(map);
+ sendLocatorOverlayToBack(marker);
+ }
+ if (!visible && onMap) marker.removeFrom(map);
+ });
+ syncSelectedAisTrackVisibility();
+ syncDecodeContactPathVisibility();
+ }
+ function updateMapContactPathsToggle() {
+ const btn = mapEl("map-contact-paths-toggle");
+ if (!btn) return;
+ btn.textContent = mapDecodeContactPathsEnabled ? "Contact Paths On" : "Contact Paths Off";
+ btn.classList.toggle("is-active", mapDecodeContactPathsEnabled);
+ }
+ function updateMapP2pPathsToggle() {
+ const btn = mapEl("map-p2p-paths-toggle");
+ if (!btn) return;
+ btn.textContent = mapP2pRadioPathsEnabled ? "TRX Paths On" : "TRX Paths Off";
+ btn.classList.toggle("is-active", mapP2pRadioPathsEnabled);
+ }
+ function scheduleDecodeMapMaintenance() {
+ if (C.decodeHistoryMapRenderingDeferred()) {
+ C.markDecodeMapSyncPending();
+ return;
+ }
+ scheduleUiFrameJob("decode-map-maintenance", () => {
+ rebuildDecodeContactPaths();
+ rebuildMapLocatorFilters();
+ applyMapFilter();
+ });
+ }
+ function formatDecodeLocatorTime(tsMs) {
+ if (!Number.isFinite(tsMs)) return "--:--:--";
+ return new Date(tsMs).toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit"
+ });
+ }
+ function formatMapPopupFreq(hz) {
+ if (!Number.isFinite(hz)) return "--";
+ const value = Number(hz);
+ if (value >= 1e9) return `${(value / 1e9).toFixed(6).replace(/\.?0+$/, "")} GHz`;
+ if (value >= 1e6) return `${(value / 1e6).toFixed(6).replace(/\.?0+$/, "")} MHz`;
+ if (value >= 1e3) return `${(value / 1e3).toFixed(3).replace(/\.?0+$/, "")} kHz`;
+ return `${Math.round(value)} Hz`;
+ }
+ function buildDecodeLocatorTooltipHtml(grid, entry, type) {
+ const details = entry?.stationDetails instanceof Map ? Array.from(entry.stationDetails.values()) : [];
+ details.sort((a, b) => Number(b?.ts_ms || 0) - Number(a?.ts_ms || 0));
+ const title = type === "wspr" ? "WSPR" : "FT8";
+ const rows = details.map((detail) => {
+ const station = escapeMapHtml(String(detail?.source || detail?.station || detail?.target || "Unknown"));
+ const freq = formatMapPopupFreq(Number(detail?.freq_hz));
+ const meta = [
+ detail?.target ? `to ${escapeMapHtml(String(detail.target))}` : null,
+ Number.isFinite(detail?.snr_db) ? `${Number(detail.snr_db).toFixed(1)} dB` : null,
+ Number.isFinite(detail?.dt_s) ? `dt ${Number(detail.dt_s).toFixed(2)}` : null,
+ escapeMapHtml(freq)
+ ].filter(Boolean).join(" · ");
+ const remoteIds = detail?.remotes instanceof Set && detail.remotes.size > 0 ? Array.from(detail.remotes) : detail?.remote ? [detail.remote] : [];
+ const rxHtml = remoteIds.map((rid) => {
+ const label = _receiverLabel(rid);
+ return label ? `${escapeMapHtml(label)}
` : "";
+ }).filter(Boolean).join("");
+ const message = detail?.message ? `${escapeMapHtml(String(detail.message))}
` : "";
+ return `${station}${escapeMapHtml(formatDecodeLocatorTime(Number(detail?.ts_ms)))}
` + (meta ? `
${meta}
` : "") + rxHtml + message + `
`;
+ }).join("");
+ const count = Math.max(
+ 1,
+ details.length,
+ entry?.stations instanceof Set ? entry.stations.size : 0
+ );
+ return `${escapeMapHtml(grid)}
${title} · ${count} station${count === 1 ? "" : "s"}
` + rows + `
`;
+ }
+ function rebuildDecodeContactPaths() {
+ clearDecodeContactPaths();
+ const stationLocators = /* @__PURE__ */ new Map();
+ const directedMessages = [];
+ for (const entry of locatorMarkers.values()) {
+ if (!entry || entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "ft2" && entry.sourceType !== "wspr") continue;
+ const grid = String(entry.grid || "").trim().toUpperCase();
+ if (!grid || !(entry.stationDetails instanceof Map)) continue;
+ for (const detail of entry.stationDetails.values()) {
+ const source = String(detail?.source || detail?.station || "").trim().toUpperCase();
+ const target = String(detail?.target || "").trim().toUpperCase();
+ const tsMs = Number.isFinite(detail?.ts_ms) ? Number(detail.ts_ms) : 0;
+ if (source) {
+ const prev = stationLocators.get(source);
+ if (!prev || tsMs >= prev.tsMs) {
+ stationLocators.set(source, { grid, tsMs });
+ }
+ }
+ if (source && target && source !== target) {
+ const band = bandForHz(Number(detail?.freq_hz));
+ directedMessages.push({
+ source,
+ target,
+ sourceGrid: grid,
+ sourceType: entry.sourceType,
+ tsMs,
+ bandLabel: band?.label || null,
+ remote: detail?.remote || null
+ });
+ }
+ }
+ }
+ for (const msg of directedMessages) {
+ const targetLocator = stationLocators.get(msg.target);
+ if (!targetLocator) continue;
+ if (msg.sourceGrid === targetLocator.grid) continue;
+ const sourceCenter = locatorToLatLon(msg.sourceGrid);
+ const targetCenter = locatorToLatLon(targetLocator.grid);
+ if (!sourceCenter || !targetCenter) continue;
+ const distanceKm = haversineKm(sourceCenter.lat, sourceCenter.lon, targetCenter.lat, targetCenter.lon);
+ const key = [msg.source, msg.target].sort().join("::");
+ const prev = decodeContactPaths.get(key);
+ if (prev?.tsMs != null && prev.tsMs > msg.tsMs) continue;
+ decodeContactPaths.set(key, {
+ pathKey: key,
+ source: msg.source,
+ target: msg.target,
+ sourceGrid: msg.sourceGrid,
+ targetGrid: targetLocator.grid,
+ sourceType: msg.sourceType,
+ bandLabel: msg.bandLabel,
+ from: sourceCenter,
+ to: targetCenter,
+ tsMs: msg.tsMs,
+ distanceKm,
+ distanceText: formatDecodeContactDistance(distanceKm),
+ line: null,
+ labelMarker: null,
+ remote: msg.remote
+ });
+ }
+ syncDecodeContactPathVisibility();
+ }
+ function _receiverLabel(rigId) {
+ if (!rigId) return null;
+ const rig = T.serverRigs.find((r) => r.remote === rigId);
+ const name = T.lastRigDisplayNames[rigId] || rigId;
+ if (rig && rig.latitude != null && rig.longitude != null) {
+ const grid = latLonToMaidenhead(rig.latitude, rig.longitude);
+ return `${name} (${grid})`;
+ }
+ return name;
+ }
+ function renderMapQsoSummary() {
+ const listEl = mapEl("map-qso-summary-list");
+ if (!listEl) return;
+ const cutoff = _statsHistoryCutoffMs();
+ const entries = Array.from(decodeContactPaths.values()).filter((entry) => entry && Number.isFinite(entry.distanceKm) && _statsDetailPassesRigFilter(entry) && (!entry.tsMs || entry.tsMs >= cutoff)).sort((a, b) => {
+ const distanceDelta = Number(b.distanceKm) - Number(a.distanceKm);
+ if (Math.abs(distanceDelta) > 1e-3) return distanceDelta;
+ return Number(b.tsMs || 0) - Number(a.tsMs || 0);
+ }).slice(0, MAP_QSO_SUMMARY_LIMIT);
+ if (selectedMapQsoKey && !entries.some((entry) => entry.pathKey === selectedMapQsoKey)) {
+ selectedMapQsoKey = null;
+ }
+ if (entries.length === 0) {
+ const empty = document.createElement("div");
+ empty.className = "map-qso-summary-empty";
+ empty.textContent = "No directed FT8 or WSPR contacts match the current map history and filters.";
+ listEl.replaceChildren(empty);
+ return;
+ }
+ const fragment = document.createDocumentFragment();
+ entries.forEach((entry, index) => {
+ const card = document.createElement("button");
+ card.type = "button";
+ card.className = "map-qso-card";
+ card.classList.toggle("is-selected", entry.pathKey === selectedMapQsoKey);
+ card.setAttribute("aria-pressed", entry.pathKey === selectedMapQsoKey ? "true" : "false");
+ card.addEventListener("click", () => {
+ selectedMapQsoKey = selectedMapQsoKey === entry.pathKey ? null : entry.pathKey ?? null;
+ syncDecodeContactPathVisibility();
+ if (selectedMapQsoKey && entry.sourceGrid) {
+ mapWindow.navigateToMapLocator?.(entry.sourceGrid, entry.sourceType);
+ }
+ });
+ const head = document.createElement("div");
+ head.className = "map-qso-card-head";
+ const rank = document.createElement("span");
+ rank.className = "map-qso-card-rank";
+ rank.textContent = `#${index + 1}`;
+ head.appendChild(rank);
+ const distance = document.createElement("span");
+ distance.className = "map-qso-card-distance";
+ distance.textContent = entry.distanceText || "--";
+ head.appendChild(distance);
+ const body = document.createElement("div");
+ body.className = "map-qso-card-body";
+ const pair = document.createElement("div");
+ pair.className = "map-qso-card-pair";
+ pair.textContent = `${entry.source || "Unknown"} <-> ${entry.target || "Unknown"}`;
+ body.appendChild(pair);
+ const meta = document.createElement("div");
+ meta.className = "map-qso-card-meta";
+ const sourceType = document.createElement("span");
+ sourceType.className = "map-qso-card-pill";
+ sourceType.textContent = String(entry.sourceType || "ft8").toUpperCase();
+ meta.appendChild(sourceType);
+ if (entry.bandLabel) {
+ const band = document.createElement("span");
+ band.className = "map-qso-card-pill map-qso-card-band";
+ band.style.setProperty("--band-color", locatorBandChipColor(entry.bandLabel));
+ band.textContent = entry.bandLabel;
+ meta.appendChild(band);
+ }
+ const ageText = formatTimeAgo(Number(entry.tsMs));
+ if (ageText) {
+ const age = document.createElement("span");
+ age.className = "map-qso-card-pill";
+ age.textContent = ageText;
+ meta.appendChild(age);
+ }
+ const rxLabel = entry.remote ? _receiverLabel(entry.remote) : null;
+ if (rxLabel) {
+ const rx = document.createElement("span");
+ rx.className = "map-qso-card-pill map-qso-card-rx";
+ rx.textContent = rxLabel;
+ meta.appendChild(rx);
+ }
+ body.appendChild(meta);
+ const grids = document.createElement("div");
+ grids.className = "map-qso-card-grids";
+ grids.textContent = `${entry.sourceGrid || "--"} <-> ${entry.targetGrid || "--"}`;
+ body.appendChild(grids);
+ card.appendChild(head);
+ card.appendChild(body);
+ fragment.appendChild(card);
+ });
+ listEl.replaceChildren(fragment);
+ }
+ function renderMapSignalSummary() {
+ const listEl = mapEl("map-signal-summary-list");
+ if (!listEl) return;
+ const cutoff = _statsHistoryCutoffMs();
+ const bestByStation = /* @__PURE__ */ new Map();
+ for (const entry of locatorMarkers.values()) {
+ if (!entry || entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "ft2" && entry.sourceType !== "wspr") continue;
+ if (!(entry.stationDetails instanceof Map)) continue;
+ for (const detail of entry.stationDetails.values()) {
+ if (!Number.isFinite(detail?.snr_db)) continue;
+ if (!_statsDetailPassesRigFilter(detail)) continue;
+ if (detail.ts_ms && detail.ts_ms < cutoff) continue;
+ const station = String(detail?.source || detail?.station || "").trim().toUpperCase();
+ if (!station) continue;
+ const snrDb = Number(detail.snr_db);
+ const tsMs = Number.isFinite(detail?.ts_ms) ? Number(detail.ts_ms) : 0;
+ const prev = bestByStation.get(station);
+ if (!prev || snrDb > prev.snrDb || snrDb === prev.snrDb && tsMs > prev.tsMs) {
+ bestByStation.set(station, {
+ station,
+ snrDb,
+ tsMs,
+ grid: entry.grid,
+ sourceType: entry.sourceType,
+ bandLabel: bandForHz(Number(detail?.freq_hz))?.label || null,
+ remote: detail?.remote || null
+ });
+ }
+ }
+ }
+ const entries = Array.from(bestByStation.values()).sort((a, b) => {
+ const delta = b.snrDb - a.snrDb;
+ if (Math.abs(delta) > 1e-3) return delta;
+ return b.tsMs - a.tsMs;
+ }).slice(0, MAP_QSO_SUMMARY_LIMIT);
+ if (entries.length === 0) {
+ const empty = document.createElement("div");
+ empty.className = "map-qso-summary-empty";
+ empty.textContent = "No decoded signals with SNR data in the current map history.";
+ listEl.replaceChildren(empty);
+ return;
+ }
+ const fragment = document.createDocumentFragment();
+ entries.forEach((entry, index) => {
+ const card = document.createElement("button");
+ card.type = "button";
+ card.className = "map-qso-card";
+ if (entry.grid) {
+ card.addEventListener("click", () => {
+ mapWindow.navigateToMapLocator?.(entry.grid ?? "", entry.sourceType);
+ });
+ }
+ const head = document.createElement("div");
+ head.className = "map-qso-card-head";
+ const rank = document.createElement("span");
+ rank.className = "map-qso-card-rank";
+ rank.textContent = `#${index + 1}`;
+ head.appendChild(rank);
+ const snr = document.createElement("span");
+ snr.className = "map-qso-card-distance";
+ snr.textContent = `${entry.snrDb >= 0 ? "+" : ""}${entry.snrDb.toFixed(0)} dB`;
+ head.appendChild(snr);
+ const body = document.createElement("div");
+ body.className = "map-qso-card-body";
+ const pair = document.createElement("div");
+ pair.className = "map-qso-card-pair";
+ pair.textContent = entry.station;
+ body.appendChild(pair);
+ const meta = document.createElement("div");
+ meta.className = "map-qso-card-meta";
+ const sourceType = document.createElement("span");
+ sourceType.className = "map-qso-card-pill";
+ sourceType.textContent = String(entry.sourceType || "ft8").toUpperCase();
+ meta.appendChild(sourceType);
+ if (entry.bandLabel) {
+ const band = document.createElement("span");
+ band.className = "map-qso-card-pill map-qso-card-band";
+ band.style.setProperty("--band-color", locatorBandChipColor(entry.bandLabel));
+ band.textContent = entry.bandLabel;
+ meta.appendChild(band);
+ }
+ const ageText = formatTimeAgo(Number(entry.tsMs));
+ if (ageText) {
+ const age = document.createElement("span");
+ age.className = "map-qso-card-pill";
+ age.textContent = ageText;
+ meta.appendChild(age);
+ }
+ const rxLabel = entry.remote ? _receiverLabel(entry.remote) : null;
+ if (rxLabel) {
+ const rx = document.createElement("span");
+ rx.className = "map-qso-card-pill map-qso-card-rx";
+ rx.textContent = rxLabel;
+ meta.appendChild(rx);
+ }
+ body.appendChild(meta);
+ const grids = document.createElement("div");
+ grids.className = "map-qso-card-grids";
+ grids.textContent = entry.grid || "--";
+ body.appendChild(grids);
+ card.appendChild(head);
+ card.appendChild(body);
+ fragment.appendChild(card);
+ });
+ listEl.replaceChildren(fragment);
+ }
+ function renderMapWeakSignalSummary() {
+ const listEl = mapEl("map-weak-signal-summary-list");
+ if (!listEl) return;
+ const cutoff = _statsHistoryCutoffMs();
+ const worstByStation = /* @__PURE__ */ new Map();
+ for (const entry of locatorMarkers.values()) {
+ if (!entry || entry.sourceType !== "ft8" && entry.sourceType !== "ft4" && entry.sourceType !== "ft2" && entry.sourceType !== "wspr") continue;
+ if (!(entry.stationDetails instanceof Map)) continue;
+ for (const detail of entry.stationDetails.values()) {
+ if (!Number.isFinite(detail?.snr_db)) continue;
+ if (!_statsDetailPassesRigFilter(detail)) continue;
+ if (detail.ts_ms && detail.ts_ms < cutoff) continue;
+ const station = String(detail?.source || detail?.station || "").trim().toUpperCase();
+ if (!station) continue;
+ const snrDb = Number(detail.snr_db);
+ const tsMs = Number.isFinite(detail?.ts_ms) ? Number(detail.ts_ms) : 0;
+ const prev = worstByStation.get(station);
+ if (!prev || snrDb < prev.snrDb || snrDb === prev.snrDb && tsMs > prev.tsMs) {
+ worstByStation.set(station, {
+ station,
+ snrDb,
+ tsMs,
+ grid: entry.grid,
+ sourceType: entry.sourceType,
+ bandLabel: bandForHz(Number(detail?.freq_hz))?.label || null,
+ remote: detail?.remote || null
+ });
+ }
+ }
+ }
+ const entries = Array.from(worstByStation.values()).sort((a, b) => {
+ const delta = a.snrDb - b.snrDb;
+ if (Math.abs(delta) > 1e-3) return delta;
+ return b.tsMs - a.tsMs;
+ }).slice(0, MAP_QSO_SUMMARY_LIMIT);
+ if (entries.length === 0) {
+ const empty = document.createElement("div");
+ empty.className = "map-qso-summary-empty";
+ empty.textContent = "No decoded signals with SNR data in the current map history.";
+ listEl.replaceChildren(empty);
+ return;
+ }
+ const fragment = document.createDocumentFragment();
+ entries.forEach((entry, index) => {
+ const card = document.createElement("button");
+ card.type = "button";
+ card.className = "map-qso-card";
+ if (entry.grid) {
+ card.addEventListener("click", () => {
+ mapWindow.navigateToMapLocator?.(entry.grid ?? "", entry.sourceType);
+ });
+ }
+ const head = document.createElement("div");
+ head.className = "map-qso-card-head";
+ const rank = document.createElement("span");
+ rank.className = "map-qso-card-rank";
+ rank.textContent = `#${index + 1}`;
+ head.appendChild(rank);
+ const snr = document.createElement("span");
+ snr.className = "map-qso-card-distance";
+ snr.textContent = `${entry.snrDb >= 0 ? "+" : ""}${entry.snrDb.toFixed(0)} dB`;
+ head.appendChild(snr);
+ const body = document.createElement("div");
+ body.className = "map-qso-card-body";
+ const pair = document.createElement("div");
+ pair.className = "map-qso-card-pair";
+ pair.textContent = entry.station;
+ body.appendChild(pair);
+ const meta = document.createElement("div");
+ meta.className = "map-qso-card-meta";
+ const sourceType = document.createElement("span");
+ sourceType.className = "map-qso-card-pill";
+ sourceType.textContent = String(entry.sourceType || "ft8").toUpperCase();
+ meta.appendChild(sourceType);
+ if (entry.bandLabel) {
+ const band = document.createElement("span");
+ band.className = "map-qso-card-pill map-qso-card-band";
+ band.style.setProperty("--band-color", locatorBandChipColor(entry.bandLabel));
+ band.textContent = entry.bandLabel;
+ meta.appendChild(band);
+ }
+ const ageText = formatTimeAgo(Number(entry.tsMs));
+ if (ageText) {
+ const age = document.createElement("span");
+ age.className = "map-qso-card-pill";
+ age.textContent = ageText;
+ meta.appendChild(age);
+ }
+ const rxLabel = entry.remote ? _receiverLabel(entry.remote) : null;
+ if (rxLabel) {
+ const rx = document.createElement("span");
+ rx.className = "map-qso-card-pill map-qso-card-rx";
+ rx.textContent = rxLabel;
+ meta.appendChild(rx);
+ }
+ body.appendChild(meta);
+ const grids = document.createElement("div");
+ grids.className = "map-qso-card-grids";
+ grids.textContent = entry.grid || "--";
+ body.appendChild(grids);
+ card.appendChild(head);
+ card.appendChild(body);
+ fragment.appendChild(card);
+ });
+ listEl.replaceChildren(fragment);
+ }
+ let statsRigFilter = "";
+ let statsHistoryLimitMinutes = 1440;
+ const statsDecodeLog = [];
+ const STATS_LOG_MAX = 5e4;
+ const STATS_TYPE_COLORS = {
+ ft8: "#4fc3f7",
+ ft4: "#81c784",
+ ft2: "#aed581",
+ wspr: "#ffb74d",
+ aprs: "#ce93d8",
+ hf_aprs: "#ba68c8",
+ ais: "#90a4ae",
+ vdes: "#78909c",
+ cw: "#fff176"
+ };
+ const STATS_DX_BUCKETS = [
+ { label: "0–500 km", min: 0, max: 500 },
+ { label: "500–1k", min: 500, max: 1e3 },
+ { label: "1k–2k", min: 1e3, max: 2e3 },
+ { label: "2k–5k", min: 2e3, max: 5e3 },
+ { label: "5k–10k", min: 5e3, max: 1e4 },
+ { label: "10k+ km", min: 1e4, max: Infinity }
+ ];
+ function _statsHistoryCutoffMs() {
+ return Date.now() - statsHistoryLimitMinutes * 60 * 1e3;
+ }
+ function _statsDetailPassesRigFilter(detail) {
+ if (!statsRigFilter) return true;
+ if (detail?.remotes instanceof Set) return detail.remotes.has(statsRigFilter);
+ return detail?.remote === statsRigFilter;
+ }
+ function updateStatsRigFilter() {
+ const el = mapEl("stats-rig-filter");
+ if (!el) return;
+ const prev = el.value;
+ while (el.options.length > 1) el.remove(1);
+ for (const id of T.lastRigIds) {
+ const opt = document.createElement("option");
+ opt.value = id;
+ opt.textContent = T.lastRigDisplayNames[id] || id;
+ el.appendChild(opt);
+ }
+ if (prev && T.lastRigIds.includes(prev)) {
+ el.value = prev;
+ } else {
+ el.value = "";
+ statsRigFilter = "";
+ }
+ }
+ function statsRecordDecode(type, remote, tsMs) {
+ statsDecodeLog.push({ type: String(type || "unknown"), ts_ms: tsMs || Date.now(), remote: remote || null });
+ if (statsDecodeLog.length > STATS_LOG_MAX) {
+ statsDecodeLog.splice(0, statsDecodeLog.length - STATS_LOG_MAX);
+ }
+ }
+ function _statsFilteredLog() {
+ const cutoff = _statsHistoryCutoffMs();
+ return statsDecodeLog.filter((e) => {
+ if (e.ts_ms < cutoff) return false;
+ if (statsRigFilter && e.remote && e.remote !== statsRigFilter) return false;
+ return true;
+ });
+ }
+ function renderStatsCounters() {
+ const cutoff = _statsHistoryCutoffMs();
+ const log = _statsFilteredLog();
+ const totalDecodes = log.length;
+ const uniqueStations = /* @__PURE__ */ new Set();
+ const uniqueGrids = /* @__PURE__ */ new Set();
+ for (const entry of locatorMarkers.values()) {
+ if (!entry || !(entry.stationDetails instanceof Map)) continue;
+ for (const detail of entry.stationDetails.values()) {
+ if (detail?.ts_ms && detail.ts_ms < cutoff) continue;
+ if (!_statsDetailPassesRigFilter(detail)) continue;
+ const station = String(detail?.source || detail?.station || "").trim().toUpperCase();
+ if (station) uniqueStations.add(station);
+ }
+ if (entry.grid) {
+ const hasVisible = entry.stationDetails instanceof Map && Array.from(entry.stationDetails.values()).some(
+ (d) => (!d.ts_ms || d.ts_ms >= cutoff) && _statsDetailPassesRigFilter(d)
+ );
+ if (hasVisible) uniqueGrids.add(entry.grid);
+ }
+ }
+ const rateWindow = Date.now() - 6e4;
+ const recentCount = log.filter((e) => e.ts_ms >= rateWindow).length;
+ const setEl = (id, val) => {
+ const el = mapEl(id);
+ if (el) el.textContent = String(val);
+ };
+ setEl("stats-total-decodes", totalDecodes.toLocaleString());
+ setEl("stats-unique-stations", uniqueStations.size.toLocaleString());
+ setEl("stats-unique-grids", uniqueGrids.size.toLocaleString());
+ setEl("stats-decode-rate", recentCount.toLocaleString());
+ }
+ function _renderBarChart(containerId, data, emptyMsg) {
+ const el = mapEl(containerId);
+ if (!el) return;
+ if (!data || data.length === 0 || data.every((d) => d.count === 0)) {
+ el.replaceChildren();
+ const empty = document.createElement("div");
+ empty.className = "stats-bar-empty";
+ empty.textContent = emptyMsg || "No data available.";
+ el.appendChild(empty);
+ return;
+ }
+ const maxVal = Math.max(1, ...data.map((d) => d.count));
+ const fragment = document.createDocumentFragment();
+ for (const item of data) {
+ const row = document.createElement("div");
+ row.className = "stats-bar-row";
+ const label = document.createElement("span");
+ label.className = "stats-bar-label";
+ label.textContent = item.label;
+ row.appendChild(label);
+ const track = document.createElement("div");
+ track.className = "stats-bar-track";
+ const fill = document.createElement("div");
+ fill.className = "stats-bar-fill";
+ fill.style.width = `${item.count / maxVal * 100}%`;
+ fill.style.background = item.color || "var(--accent-green)";
+ track.appendChild(fill);
+ row.appendChild(track);
+ const count = document.createElement("span");
+ count.className = "stats-bar-count";
+ count.textContent = item.count.toLocaleString();
+ row.appendChild(count);
+ fragment.appendChild(row);
+ }
+ el.replaceChildren(fragment);
+ }
+ function renderStatsDecodeTypes() {
+ const log = _statsFilteredLog();
+ const counts = {};
+ for (const e of log) {
+ counts[e.type] = (counts[e.type] || 0) + 1;
+ }
+ const data = Object.entries(counts).map(([type, count]) => ({
+ label: type.toUpperCase(),
+ count,
+ color: STATS_TYPE_COLORS[type] || "#aaa"
+ })).sort((a, b) => b.count - a.count);
+ _renderBarChart("stats-decode-type-bars", data, "No decoded signals in the current history.");
+ }
+ function renderStatsBandActivity() {
+ const cutoff = _statsHistoryCutoffMs();
+ const bandCounts = {};
+ for (const entry of locatorMarkers.values()) {
+ if (!entry || !(entry.stationDetails instanceof Map)) continue;
+ for (const detail of entry.stationDetails.values()) {
+ if (detail?.ts_ms && detail.ts_ms < cutoff) continue;
+ if (!_statsDetailPassesRigFilter(detail)) continue;
+ if (!Number.isFinite(detail?.freq_hz)) continue;
+ const band = bandForHz(Number(detail.freq_hz));
+ if (band) {
+ bandCounts[band.label] = (bandCounts[band.label] || 0) + 1;
+ }
+ }
+ }
+ const data = Object.entries(bandCounts).map(([label, count]) => ({
+ label,
+ count,
+ color: locatorBandChipColor(label)
+ })).sort((a, b) => b.count - a.count);
+ _renderBarChart("stats-band-activity-bars", data, "No band activity data in the current history.");
+ }
+ function renderStatsRigCompare() {
+ const section = mapEl("stats-rig-compare-section");
+ if (!section) return;
+ if (T.lastRigIds.length < 2) {
+ section.style.display = "none";
+ return;
+ }
+ section.style.display = "";
+ const cutoff = _statsHistoryCutoffMs();
+ const rigCounts = {};
+ for (const e of statsDecodeLog) {
+ if (e.ts_ms < cutoff) continue;
+ const rid = e.remote || "unknown";
+ rigCounts[rid] = (rigCounts[rid] || 0) + 1;
+ }
+ const data = Object.entries(rigCounts).map(([rid, count]) => ({
+ label: T.lastRigDisplayNames[rid] || rid,
+ count,
+ color: "var(--accent-green)"
+ })).sort((a, b) => b.count - a.count);
+ _renderBarChart("stats-rig-compare-bars", data, "No decode data per receiver.");
+ }
+ function renderStatsDxHistogram() {
+ const cutoff = _statsHistoryCutoffMs();
+ const buckets = STATS_DX_BUCKETS.map((b) => ({ ...b, count: 0 }));
+ for (const entry of decodeContactPaths.values()) {
+ if (!entry || !Number.isFinite(entry.distanceKm)) continue;
+ if (entry.tsMs && entry.tsMs < cutoff) continue;
+ if (!_statsDetailPassesRigFilter(entry)) continue;
+ const km = entry.distanceKm;
+ if (km == null) continue;
+ for (const b of buckets) {
+ if (km >= b.min && km < b.max) {
+ b.count++;
+ break;
+ }
+ }
+ }
+ const data = buckets.map((b) => ({
+ label: b.label,
+ count: b.count,
+ color: "#4fc3f7"
+ }));
+ _renderBarChart("stats-dx-histogram-bars", data, "No directed contact paths in the current history.");
+ }
+ let _statsRenderPending = false;
+ let _statsControlsWired = false;
+ function _wireStatsControls() {
+ if (_statsControlsWired) return;
+ const rigEl = mapEl("stats-rig-filter");
+ const histEl = mapEl("stats-history-limit");
+ if (!rigEl && !histEl) return;
+ _statsControlsWired = true;
+ if (rigEl) {
+ rigEl.addEventListener("change", () => {
+ statsRigFilter = rigEl.value;
+ scheduleStatsRender();
+ });
+ }
+ if (histEl) {
+ histEl.value = String(statsHistoryLimitMinutes);
+ histEl.addEventListener("change", () => {
+ statsHistoryLimitMinutes = Number(histEl.value) || 1440;
+ scheduleStatsRender();
+ });
+ }
+ }
+ function scheduleStatsRender() {
+ if (_statsRenderPending) return;
+ _statsRenderPending = true;
+ requestAnimationFrame(() => {
+ _statsRenderPending = false;
+ _wireStatsControls();
+ renderStatsCounters();
+ renderStatsDecodeTypes();
+ renderStatsBandActivity();
+ renderStatsRigCompare();
+ renderStatsDxHistogram();
+ renderMapQsoSummary();
+ renderMapSignalSummary();
+ renderMapWeakSignalSummary();
+ });
+ }
+ function buildBookmarkLocatorPopupHtml(grid, bookmarks) {
+ const list = Array.isArray(bookmarks) ? bookmarks : [];
+ const rows = list.map((bm) => {
+ const title = escapeMapHtml(String(bm.name || "Bookmark"));
+ const freq = escapeMapHtml(formatFreqForHumans(Number(bm.freq_hz)));
+ const mode = bm.mode ? ` · ${escapeMapHtml(String(bm.mode))}` : "";
+ return `${title} ${freq}${mode}`;
+ }).join("
");
+ return `${escapeMapHtml(grid)}
Bookmarks: ${list.length || 1}` + (rows ? `
${rows}` : "");
+ }
+ mapWindow.syncBookmarkMapLocators = function(bookmarks) {
+ const list = Array.isArray(bookmarks) ? bookmarks : [];
+ const grouped = /* @__PURE__ */ new Map();
+ for (const bm of list) {
+ const grid = String(bm?.locator || "").trim().toUpperCase();
+ if (!grid) continue;
+ const bounds = maidenheadToBounds(grid);
+ if (!bounds) continue;
+ const key = `bookmark:${grid}`;
+ const bucket = grouped.get(key);
+ if (bucket) {
+ bucket.bookmarks.push(bm);
+ } else {
+ grouped.set(key, { grid, bounds, bookmarks: [bm] });
+ }
+ }
+ for (const [key, entry] of locatorMarkers.entries()) {
+ if (!key.startsWith("bookmark:")) continue;
+ if (!grouped.has(key)) {
+ if (entry && entry.marker) {
+ if (entry.marker === selectedLocatorMarker) {
+ setSelectedLocatorMarker(null);
+ clearMapRadioPath();
+ }
+ if (aprsMap && aprsMap.hasLayer(entry.marker)) entry.marker.removeFrom(aprsMap);
+ mapMarkers.delete(entry.marker);
+ }
+ locatorMarkers.delete(key);
+ }
+ }
+ for (const [key, next] of grouped.entries()) {
+ const existing = locatorMarkers.get(key);
+ const popupHtml = buildBookmarkLocatorPopupHtml(next.grid, next.bookmarks);
+ const bandMeta = collectBandMeta(next.bookmarks.map((bm) => Number(bm?.freq_hz)));
+ if (existing) {
+ existing.grid = next.grid;
+ existing.bounds = next.bounds;
+ existing.bookmarks = next.bookmarks;
+ existing.sourceType = "bookmark";
+ existing.bandMeta = bandMeta;
+ if (existing.marker) {
+ existing.marker.setBounds(next.bounds);
+ existing.marker.setStyle(locatorStyleForEntry(existing, next.bookmarks.length));
+ existing.marker.setPopupContent(popupHtml);
+ sendLocatorOverlayToBack(existing.marker);
+ assignLocatorMarkerMeta(existing.marker, existing.sourceType, existing.bandMeta);
+ }
+ continue;
+ }
+ const entry = {
+ marker: null,
+ grid: next.grid,
+ bounds: next.bounds,
+ bookmarks: next.bookmarks,
+ sourceType: "bookmark",
+ bandMeta
+ };
+ locatorMarkers.set(key, entry);
+ if (aprsMap) {
+ entry.marker = L.rectangle(next.bounds, locatorStyleForEntry(entry, next.bookmarks.length)).addTo(aprsMap).bindPopup(popupHtml);
+ entry.marker.__trxType = "bookmark";
+ sendLocatorOverlayToBack(entry.marker);
+ assignLocatorMarkerMeta(entry.marker, entry.sourceType ?? "bookmark", entry.bandMeta);
+ mapMarkers.add(entry.marker);
+ }
+ }
+ rebuildMapLocatorFilters();
+ applyMapFilter();
+ };
+ mapWindow.mapAddLocator = function(message, grids, type = "ft8", station = null, details = null) {
+ if (!Array.isArray(grids) || grids.length === 0) return;
+ const markerType = type === "wspr" ? "wspr" : type === "ft4" ? "ft4" : type === "ft2" ? "ft2" : "ft8";
+ const msgRigId = details?.rig_id || T.lastActiveRigId;
+ const unique = [...new Set(grids.map((g) => String(g).toUpperCase()))];
+ const stationId = station && String(station).trim() ? String(station).trim().toUpperCase() : "";
+ const locatorDetails = /* @__PURE__ */ new Map();
+ if (Array.isArray(details?.locator_details)) {
+ for (const locatorDetail of details.locator_details) {
+ const grid = String(locatorDetail?.grid || "").trim().toUpperCase();
+ if (!grid) continue;
+ locatorDetails.set(grid, locatorDetail);
+ }
+ }
+ for (const grid of unique) {
+ const bounds = maidenheadToBounds(grid);
+ if (!bounds) continue;
+ const locatorDetail = locatorDetails.get(grid);
+ const sourceId = locatorDetail?.source && String(locatorDetail.source).trim() ? String(locatorDetail.source).trim().toUpperCase() : "";
+ const targetId = locatorDetail?.target && String(locatorDetail.target).trim() ? String(locatorDetail.target).trim().toUpperCase() : "";
+ const detailStationId = sourceId || stationId;
+ const detailEntry = {
+ station: detailStationId || null,
+ source: sourceId || null,
+ target: targetId || null,
+ ts_ms: details && Number.isFinite(details.ts_ms) ? Number(details.ts_ms) : null,
+ snr_db: details && Number.isFinite(details.snr_db) ? Number(details.snr_db) : null,
+ dt_s: details && Number.isFinite(details.dt_s) ? Number(details.dt_s) : null,
+ freq_hz: details && Number.isFinite(details.freq_hz) ? Number(details.freq_hz) : null,
+ message: String(details?.message || message || "").trim() || null,
+ remote: msgRigId || null,
+ remotes: new Set(msgRigId ? [msgRigId] : [])
+ };
+ const detailKey = detailStationId || `${targetId || "decode"}:${detailEntry.message || "decode"}:${detailEntry.ts_ms || Date.now()}`;
+ const key = `${markerType}:${grid}`;
+ const existing = locatorMarkers.get(key);
+ if (existing) {
+ existing.grid = grid;
+ if (!(existing.allStationDetails instanceof Map)) {
+ existing.allStationDetails = existing.stationDetails instanceof Map ? new Map(existing.stationDetails) : /* @__PURE__ */ new Map();
+ }
+ const prevDetail = existing.allStationDetails.get(detailKey);
+ const mergedRemotes = prevDetail?.remotes instanceof Set ? new Set(prevDetail.remotes) : /* @__PURE__ */ new Set();
+ if (msgRigId) mergedRemotes.add(msgRigId);
+ existing.allStationDetails.set(detailKey, { ...detailEntry, remotes: mergedRemotes });
+ existing.sourceType = markerType;
+ if (msgRigId) {
+ if (!existing.rigIds) existing.rigIds = /* @__PURE__ */ new Set();
+ existing.rigIds.add(msgRigId);
+ }
+ pruneLocatorEntry(key, existing, mapHistoryCutoffMs());
+ if (existing.marker) sendLocatorOverlayToBack(existing.marker);
+ scheduleDecodeMapMaintenance();
+ continue;
+ }
+ const allStationDetails = /* @__PURE__ */ new Map();
+ allStationDetails.set(detailKey, { ...detailEntry });
+ const entry = {
+ marker: null,
+ grid,
+ stations: /* @__PURE__ */ new Set(),
+ stationDetails: /* @__PURE__ */ new Map(),
+ allStationDetails,
+ sourceType: markerType,
+ bandMeta: /* @__PURE__ */ new Map(),
+ rigIds: new Set(msgRigId ? [msgRigId] : [])
+ };
+ locatorMarkers.set(key, entry);
+ pruneLocatorEntry(key, entry, mapHistoryCutoffMs());
+ if (entry.marker) sendLocatorOverlayToBack(entry.marker);
+ }
+ scheduleDecodeMapMaintenance();
+ };
+ document.querySelectorAll(".sub-tab-bar").forEach((bar) => {
+ bar.addEventListener("click", (e) => {
+ const btn = e.target?.closest(".sub-tab[data-subtab]");
+ if (!btn) return;
+ bar.querySelectorAll(".sub-tab").forEach((t) => {
+ t.classList.remove("active");
+ });
+ btn.classList.add("active");
+ const parent = bar.parentElement;
+ if (!parent) return;
+ parent.querySelectorAll(".sub-tab-panel").forEach((p) => p.style.display = "none");
+ const nextPanel = parent.querySelector(`#subtab-${btn.dataset.subtab}`);
+ if (nextPanel) nextPanel.style.display = "";
+ if (btn.dataset.subtab === "cw" && mapWindow.refreshCwTonePicker) {
+ requestAnimationFrame(() => {
+ if (mapWindow.refreshCwTonePicker) mapWindow.refreshCwTonePicker();
+ });
+ }
+ if (btn.dataset.subtab !== "sat" && typeof mapWindow.clearSatPredictionDom === "function") {
+ mapWindow.clearSatPredictionDom();
+ }
+ });
+ });
+ window.addEventListener("resize", () => {
+ const mapTab = mapEl("tab-map");
+ if (!mapTab || mapTab.style.display === "none") return;
+ sizeAprsMapToViewport();
+ });
+ function flushDeferredDecodeMapSync() {
+ if (!T.decodeMapSyncPending || T.decodeHistoryReplayActive || !aprsMap) return;
+ T.decodeMapSyncPending = false;
+ scheduleUiFrameJob("decode-map-maintenance", () => {
+ pruneMapHistory();
+ });
+ }
+ function autoInitIfVisible() {
+ const panel = mapEl("tab-map");
+ if (panel.style.display === "none") return;
+ if (typeof L === "undefined") {
+ window.setTimeout(autoInitIfVisible, 50);
+ return;
+ }
+ initAprsMap();
+ requestAnimationFrame(() => requestAnimationFrame(sizeAprsMapToViewport));
+ }
+ modules.map = {
+ initAprsMap,
+ sizeAprsMapToViewport,
+ syncAprsReceiverMarker,
+ updateMapRigFilter,
+ updateStatsRigFilter,
+ statsRecordDecode,
+ scheduleStatsRender,
+ get aprsMap() {
+ return aprsMap;
+ },
+ get stationMarkers() {
+ return stationMarkers;
+ },
+ get locatorMarkers() {
+ return locatorMarkers;
+ },
+ get aisMarkers() {
+ return aisMarkers;
+ },
+ get vdesMarkers() {
+ return vdesMarkers;
+ },
+ get decodeContactPaths() {
+ return decodeContactPaths;
+ },
+ pruneMapHistory,
+ aprsSymbolIcon,
+ buildAprsPopupHtml,
+ buildAisPopupHtml,
+ buildVdesPopupHtml,
+ ensureAprsMarker,
+ ensureAisMarker,
+ ensureVdesMarker,
+ ensureDecodeLocatorMarker,
+ aprsPositionsEqual,
+ aisPositionsEqual,
+ refreshAprsTrack,
+ refreshAisTrack,
+ updateAisMarker,
+ createAisMarker,
+ getAisAccentColor,
+ refreshAisMarkerColors,
+ setMapRadioPathTo,
+ buildReceiverPopupHtml,
+ rebuildDecodeContactPaths,
+ syncDecodeContactPathVisibility,
+ scheduleDecodeMapMaintenance,
+ renderMapLocatorLegend,
+ rebuildMapLocatorFilters,
+ renderMapQsoSummary,
+ renderMapSignalSummary,
+ renderMapWeakSignalSummary,
+ vdesMarkerKey,
+ aisMarkerOptionsFromMessage,
+ materializeBufferedMapLayers,
+ flushDeferredDecodeMapSync,
+ bandForHz,
+ reverseGeocodeLocation
+ };
+ autoInitIfVisible();
})();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sat-scheduler.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sat-scheduler.js
index 7a8848a0..1f8c88eb 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sat-scheduler.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sat-scheduler.js
@@ -1,266 +1,263 @@
-"use strict";
-(() => {
- // src/plugins/sat-scheduler.ts
- var satSchedulerWindow = window;
- (function() {
- "use strict";
- const dom = {
- enabled: document.getElementById("scheduler-sat-enabled"),
- pretune: document.getElementById("scheduler-sat-pretune"),
- body: document.getElementById("scheduler-sat-body"),
- tbody: document.getElementById("scheduler-sat-tbody"),
- addBtn: document.getElementById("scheduler-sat-add-btn"),
- passStatus: document.getElementById("scheduler-sat-pass-status"),
- formWrap: document.getElementById("sch-sat-form-wrap"),
- formTitle: document.getElementById("sch-sat-form-title"),
- form: document.getElementById("sch-sat-form"),
- formCancel: document.getElementById("sch-sat-form-cancel"),
- preset: document.getElementById("scheduler-sat-preset"),
- name: document.getElementById("scheduler-sat-name"),
- norad: document.getElementById("scheduler-sat-norad"),
- bookmark: document.getElementById("scheduler-sat-bookmark"),
- minEl: document.getElementById("scheduler-sat-min-el"),
- priority: document.getElementById("scheduler-sat-priority"),
- centerHz: document.getElementById("scheduler-sat-center-hz")
+// src/plugins/sat-scheduler.ts
+var satSchedulerWindow = window;
+(function() {
+ "use strict";
+ const dom = {
+ enabled: document.getElementById("scheduler-sat-enabled"),
+ pretune: document.getElementById("scheduler-sat-pretune"),
+ body: document.getElementById("scheduler-sat-body"),
+ tbody: document.getElementById("scheduler-sat-tbody"),
+ addBtn: document.getElementById("scheduler-sat-add-btn"),
+ passStatus: document.getElementById("scheduler-sat-pass-status"),
+ formWrap: document.getElementById("sch-sat-form-wrap"),
+ formTitle: document.getElementById("sch-sat-form-title"),
+ form: document.getElementById("sch-sat-form"),
+ formCancel: document.getElementById("sch-sat-form-cancel"),
+ preset: document.getElementById("scheduler-sat-preset"),
+ name: document.getElementById("scheduler-sat-name"),
+ norad: document.getElementById("scheduler-sat-norad"),
+ bookmark: document.getElementById("scheduler-sat-bookmark"),
+ minEl: document.getElementById("scheduler-sat-min-el"),
+ priority: document.getElementById("scheduler-sat-priority"),
+ centerHz: document.getElementById("scheduler-sat-center-hz")
+ };
+ let editIdx = null;
+ let eventsWired = false;
+ function getBridge() {
+ return satSchedulerWindow.trx?.modules?.scheduler ?? null;
+ }
+ function getConfig() {
+ const b = getBridge();
+ return b?.getConfig() ?? null;
+ }
+ function getStatus() {
+ const b = getBridge();
+ return b?.getStatus() ?? null;
+ }
+ function getBookmarks() {
+ const b = getBridge();
+ return b?.getBookmarks() ?? [];
+ }
+ function markDirty() {
+ getBridge()?.markDirty();
+ }
+ function bmName(id) {
+ const bm = getBookmarks().find(function(b) {
+ return b.id === id;
+ });
+ return bm ? bm.name : id;
+ }
+ function escHtml(s) {
+ return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+ }
+ function formatFreq(hz) {
+ if (hz >= 1e6) return (hz / 1e6).toFixed(3) + " MHz";
+ if (hz >= 1e3) return (hz / 1e3).toFixed(1) + " kHz";
+ return `${String(hz)} Hz`;
+ }
+ function getSatelliteEntries() {
+ const config = getConfig();
+ return config && config.satellites && Array.isArray(config.satellites.entries) ? config.satellites.entries : [];
+ }
+ function ensureSatelliteConfig() {
+ const config = getConfig();
+ if (!config) return { enabled: false, pretune_secs: 60, entries: [] };
+ if (!config.satellites) config.satellites = { enabled: false, pretune_secs: 60, entries: [] };
+ return config.satellites;
+ }
+ function collectSatelliteConfig() {
+ const enabled = dom.enabled ? dom.enabled.checked : false;
+ const pretune = dom.pretune ? parseInt(dom.pretune.value, 10) : 60;
+ return {
+ enabled,
+ pretune_secs: isNaN(pretune) || pretune < 0 ? 60 : pretune,
+ entries: getSatelliteEntries()
};
- let editIdx = null;
- let eventsWired = false;
- function getBridge() {
- return satSchedulerWindow.trx?.modules?.scheduler ?? null;
- }
- function getConfig() {
- const b = getBridge();
- return b?.getConfig() ?? null;
- }
- function getStatus() {
- const b = getBridge();
- return b?.getStatus() ?? null;
- }
- function getBookmarks() {
- const b = getBridge();
- return b?.getBookmarks() ?? [];
- }
- function markDirty() {
- getBridge()?.markDirty();
- }
- function bmName(id) {
- const bm = getBookmarks().find(function(b) {
- return b.id === id;
+ }
+ function renderSection() {
+ const config = getConfig();
+ const satCfg = config?.satellites;
+ const enabled = satCfg?.enabled ?? false;
+ if (dom.enabled) dom.enabled.checked = enabled;
+ if (dom.pretune) dom.pretune.value = String(satCfg?.pretune_secs ?? 60);
+ if (dom.body) dom.body.style.display = enabled ? "" : "none";
+ renderEntries();
+ renderPassStatus();
+ }
+ function renderEntries() {
+ if (!dom.tbody) return;
+ const entries = getSatelliteEntries();
+ const frag = document.createDocumentFragment();
+ entries.forEach(function(entry, idx) {
+ const tr = document.createElement("tr");
+ const tdSat = document.createElement("td");
+ tdSat.textContent = entry.satellite || "";
+ tr.appendChild(tdSat);
+ const tdNorad = document.createElement("td");
+ tdNorad.textContent = String(entry.norad_id || "");
+ tr.appendChild(tdNorad);
+ const tdBm = document.createElement("td");
+ tdBm.textContent = bmName(entry.bookmark_id);
+ tr.appendChild(tdBm);
+ const tdEl = document.createElement("td");
+ tdEl.textContent = `${String(entry.min_elevation_deg)}°`;
+ tr.appendChild(tdEl);
+ const tdPrio = document.createElement("td");
+ tdPrio.textContent = String(entry.priority || 0);
+ tr.appendChild(tdPrio);
+ const tdActions = document.createElement("td");
+ const editBtn = document.createElement("button");
+ editBtn.className = "sch-write";
+ editBtn.type = "button";
+ editBtn.textContent = "Edit";
+ editBtn.addEventListener("click", function() {
+ openForm(entry, idx);
});
- return bm ? bm.name : id;
- }
- function escHtml(s) {
- return s.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
- }
- function formatFreq(hz) {
- if (hz >= 1e6) return (hz / 1e6).toFixed(3) + " MHz";
- if (hz >= 1e3) return (hz / 1e3).toFixed(1) + " kHz";
- return `${String(hz)} Hz`;
- }
- function getSatelliteEntries() {
- const config = getConfig();
- return config && config.satellites && Array.isArray(config.satellites.entries) ? config.satellites.entries : [];
- }
- function ensureSatelliteConfig() {
- const config = getConfig();
- if (!config) return { enabled: false, pretune_secs: 60, entries: [] };
- if (!config.satellites) config.satellites = { enabled: false, pretune_secs: 60, entries: [] };
- return config.satellites;
- }
- function collectSatelliteConfig() {
- const enabled = dom.enabled ? dom.enabled.checked : false;
- const pretune = dom.pretune ? parseInt(dom.pretune.value, 10) : 60;
- return {
- enabled,
- pretune_secs: isNaN(pretune) || pretune < 0 ? 60 : pretune,
- entries: getSatelliteEntries()
- };
- }
- function renderSection() {
- const config = getConfig();
- const satCfg = config?.satellites;
- const enabled = satCfg?.enabled ?? false;
- if (dom.enabled) dom.enabled.checked = enabled;
- if (dom.pretune) dom.pretune.value = String(satCfg?.pretune_secs ?? 60);
- if (dom.body) dom.body.style.display = enabled ? "" : "none";
- renderEntries();
- renderPassStatus();
- }
- function renderEntries() {
- if (!dom.tbody) return;
- const entries = getSatelliteEntries();
- const frag = document.createDocumentFragment();
- entries.forEach(function(entry, idx) {
- const tr = document.createElement("tr");
- const tdSat = document.createElement("td");
- tdSat.textContent = entry.satellite || "";
- tr.appendChild(tdSat);
- const tdNorad = document.createElement("td");
- tdNorad.textContent = String(entry.norad_id || "");
- tr.appendChild(tdNorad);
- const tdBm = document.createElement("td");
- tdBm.textContent = bmName(entry.bookmark_id);
- tr.appendChild(tdBm);
- const tdEl = document.createElement("td");
- tdEl.textContent = `${String(entry.min_elevation_deg)}°`;
- tr.appendChild(tdEl);
- const tdPrio = document.createElement("td");
- tdPrio.textContent = String(entry.priority || 0);
- tr.appendChild(tdPrio);
- const tdActions = document.createElement("td");
- const editBtn = document.createElement("button");
- editBtn.className = "sch-write";
- editBtn.type = "button";
- editBtn.textContent = "Edit";
- editBtn.addEventListener("click", function() {
- openForm(entry, idx);
- });
- tdActions.appendChild(editBtn);
- const removeBtn = document.createElement("button");
- removeBtn.className = "sch-write";
- removeBtn.type = "button";
- removeBtn.textContent = "Remove";
- removeBtn.addEventListener("click", function() {
- removeEntry(idx);
- });
- tdActions.appendChild(removeBtn);
- tr.appendChild(tdActions);
- frag.appendChild(tr);
+ tdActions.appendChild(editBtn);
+ const removeBtn = document.createElement("button");
+ removeBtn.className = "sch-write";
+ removeBtn.type = "button";
+ removeBtn.textContent = "Remove";
+ removeBtn.addEventListener("click", function() {
+ removeEntry(idx);
});
- dom.tbody.replaceChildren(frag);
+ tdActions.appendChild(removeBtn);
+ tr.appendChild(tdActions);
+ frag.appendChild(tr);
+ });
+ dom.tbody.replaceChildren(frag);
+ }
+ function renderPassStatus() {
+ if (!dom.passStatus) return;
+ const entries = getSatelliteEntries();
+ if (entries.length === 0) {
+ dom.passStatus.innerHTML = "";
+ return;
}
- function renderPassStatus() {
- if (!dom.passStatus) return;
- const entries = getSatelliteEntries();
- if (entries.length === 0) {
- dom.passStatus.innerHTML = "";
- return;
- }
- const status = getStatus();
- if (status && status.active_satellite) {
- dom.passStatus.innerHTML = 'PASS ACTIVE: ' + escHtml(status.active_satellite) + "";
- } else {
- dom.passStatus.innerHTML = 'No satellite pass active. Predictions available in the SAT tab.';
- }
+ const status = getStatus();
+ if (status && status.active_satellite) {
+ dom.passStatus.innerHTML = 'PASS ACTIVE: ' + escHtml(status.active_satellite) + "";
+ } else {
+ dom.passStatus.innerHTML = 'No satellite pass active. Predictions available in the SAT tab.';
}
- function renderBookmarkSelect(selectedId) {
- const bookmarkSelect = dom.bookmark;
- if (!bookmarkSelect) return;
- bookmarkSelect.innerHTML = '';
- getBookmarks().forEach(function(bm) {
- const opt = document.createElement("option");
- opt.value = bm.id;
- opt.textContent = bm.name + " (" + formatFreq(bm.freq_hz) + " " + bm.mode + ")";
- if (bm.id === selectedId) opt.selected = true;
- bookmarkSelect.appendChild(opt);
- });
+ }
+ function renderBookmarkSelect(selectedId) {
+ const bookmarkSelect = dom.bookmark;
+ if (!bookmarkSelect) return;
+ bookmarkSelect.innerHTML = '';
+ getBookmarks().forEach(function(bm) {
+ const opt = document.createElement("option");
+ opt.value = bm.id;
+ opt.textContent = bm.name + " (" + formatFreq(bm.freq_hz) + " " + bm.mode + ")";
+ if (bm.id === selectedId) opt.selected = true;
+ bookmarkSelect.appendChild(opt);
+ });
+ }
+ function removeEntry(idx) {
+ const sat = ensureSatelliteConfig();
+ sat.entries.splice(idx, 1);
+ renderEntries();
+ markDirty();
+ }
+ function openForm(entry, idx) {
+ editIdx = idx != null ? idx : null;
+ if (dom.formTitle) dom.formTitle.textContent = entry ? "Edit Satellite" : "Add Satellite";
+ if (dom.preset) dom.preset.value = "";
+ if (dom.name) dom.name.value = entry ? entry.satellite || "" : "";
+ if (dom.norad) dom.norad.value = entry ? String(entry.norad_id || "") : "";
+ if (dom.minEl) dom.minEl.value = String(entry?.min_elevation_deg ?? 5);
+ if (dom.priority) dom.priority.value = String(entry?.priority ?? 0);
+ if (dom.centerHz) dom.centerHz.value = entry?.center_hz ? String(entry.center_hz) : "";
+ renderBookmarkSelect(entry ? entry.bookmark_id : null);
+ if (dom.formWrap) {
+ dom.formWrap.style.display = "flex";
+ if (dom.name) dom.name.focus();
}
- function removeEntry(idx) {
- const sat = ensureSatelliteConfig();
- sat.entries.splice(idx, 1);
- renderEntries();
- markDirty();
+ }
+ function closeForm() {
+ if (dom.formWrap) dom.formWrap.style.display = "none";
+ editIdx = null;
+ }
+ function onFormSubmit(e) {
+ e.preventDefault();
+ const satellite = dom.name ? dom.name.value.trim() : "";
+ const noradId = dom.norad ? parseInt(dom.norad.value, 10) : NaN;
+ const bmId = dom.bookmark ? dom.bookmark.value : "";
+ if (!satellite) {
+ satSchedulerWindow.trxUi?.notify("Enter a satellite name.", { kind: "error" });
+ dom.name?.focus();
+ return;
}
- function openForm(entry, idx) {
- editIdx = idx != null ? idx : null;
- if (dom.formTitle) dom.formTitle.textContent = entry ? "Edit Satellite" : "Add Satellite";
- if (dom.preset) dom.preset.value = "";
- if (dom.name) dom.name.value = entry ? entry.satellite || "" : "";
- if (dom.norad) dom.norad.value = entry ? String(entry.norad_id || "") : "";
- if (dom.minEl) dom.minEl.value = String(entry?.min_elevation_deg ?? 5);
- if (dom.priority) dom.priority.value = String(entry?.priority ?? 0);
- if (dom.centerHz) dom.centerHz.value = entry?.center_hz ? String(entry.center_hz) : "";
- renderBookmarkSelect(entry ? entry.bookmark_id : null);
- if (dom.formWrap) {
- dom.formWrap.style.display = "flex";
- if (dom.name) dom.name.focus();
- }
+ if (isNaN(noradId) || noradId <= 0) {
+ satSchedulerWindow.trxUi?.notify("Enter a valid NORAD catalog number.", { kind: "error" });
+ dom.norad?.focus();
+ return;
}
- function closeForm() {
- if (dom.formWrap) dom.formWrap.style.display = "none";
- editIdx = null;
+ if (!bmId) {
+ satSchedulerWindow.trxUi?.notify("Select a bookmark.", { kind: "error" });
+ dom.bookmark?.focus();
+ return;
}
- function onFormSubmit(e) {
- e.preventDefault();
- const satellite = dom.name ? dom.name.value.trim() : "";
- const noradId = dom.norad ? parseInt(dom.norad.value, 10) : NaN;
- const bmId = dom.bookmark ? dom.bookmark.value : "";
- if (!satellite) {
- satSchedulerWindow.trxUi?.notify("Enter a satellite name.", { kind: "error" });
- dom.name?.focus();
- return;
- }
- if (isNaN(noradId) || noradId <= 0) {
- satSchedulerWindow.trxUi?.notify("Enter a valid NORAD catalog number.", { kind: "error" });
- dom.norad?.focus();
- return;
- }
- if (!bmId) {
- satSchedulerWindow.trxUi?.notify("Select a bookmark.", { kind: "error" });
- dom.bookmark?.focus();
- return;
- }
- const minEl = dom.minEl ? parseFloat(dom.minEl.value) : 5;
- const prio = dom.priority ? parseInt(dom.priority.value, 10) : 0;
- const centerHzRaw = dom.centerHz ? parseInt(dom.centerHz.value, 10) : NaN;
- const sat = ensureSatelliteConfig();
- const existing = editIdx !== null ? sat.entries[editIdx] : void 0;
- const entryData = {
- id: existing?.id ?? `sat_${Date.now().toString(36)}`,
- satellite,
- norad_id: noradId,
- bookmark_id: bmId,
- min_elevation_deg: isNaN(minEl) ? 5 : minEl,
- priority: isNaN(prio) ? 0 : prio,
- center_hz: !isNaN(centerHzRaw) && centerHzRaw > 0 ? centerHzRaw : null,
- bookmark_ids: []
- };
- if (editIdx !== null) {
- sat.entries[editIdx] = entryData;
- } else {
- sat.entries.push(entryData);
- }
- closeForm();
- renderEntries();
- markDirty();
- }
- function onPresetChange() {
- if (!dom.preset || !dom.preset.value) return;
- const parts = dom.preset.value.split("|");
- if (dom.name) dom.name.value = parts[0] || "";
- if (dom.norad) dom.norad.value = parts[1] || "";
- }
- function wireEvents() {
- if (eventsWired) return;
- eventsWired = true;
- if (dom.enabled) {
- const enabledInput = dom.enabled;
- dom.enabled.addEventListener("change", function() {
- if (dom.body) dom.body.style.display = enabledInput.checked ? "" : "none";
- markDirty();
- });
- }
- if (dom.pretune) {
- dom.pretune.addEventListener("input", function() {
- markDirty();
- });
- }
- if (dom.addBtn) dom.addBtn.addEventListener("click", function() {
- openForm(null, null);
- });
- if (dom.form) dom.form.addEventListener("submit", onFormSubmit);
- if (dom.formCancel) dom.formCancel.addEventListener("click", closeForm);
- if (dom.preset) dom.preset.addEventListener("change", onPresetChange);
- }
- satSchedulerWindow.satScheduler = {
- wireEvents,
- renderSection,
- renderPassStatus,
- collectSatelliteConfig
+ const minEl = dom.minEl ? parseFloat(dom.minEl.value) : 5;
+ const prio = dom.priority ? parseInt(dom.priority.value, 10) : 0;
+ const centerHzRaw = dom.centerHz ? parseInt(dom.centerHz.value, 10) : NaN;
+ const sat = ensureSatelliteConfig();
+ const existing = editIdx !== null ? sat.entries[editIdx] : void 0;
+ const entryData = {
+ id: existing?.id ?? `sat_${Date.now().toString(36)}`,
+ satellite,
+ norad_id: noradId,
+ bookmark_id: bmId,
+ min_elevation_deg: isNaN(minEl) ? 5 : minEl,
+ priority: isNaN(prio) ? 0 : prio,
+ center_hz: !isNaN(centerHzRaw) && centerHzRaw > 0 ? centerHzRaw : null,
+ bookmark_ids: []
};
- if (getBridge()) {
- wireEvents();
- renderSection();
+ if (editIdx !== null) {
+ sat.entries[editIdx] = entryData;
+ } else {
+ sat.entries.push(entryData);
}
- })();
+ closeForm();
+ renderEntries();
+ markDirty();
+ }
+ function onPresetChange() {
+ if (!dom.preset || !dom.preset.value) return;
+ const parts = dom.preset.value.split("|");
+ if (dom.name) dom.name.value = parts[0] || "";
+ if (dom.norad) dom.norad.value = parts[1] || "";
+ }
+ function wireEvents() {
+ if (eventsWired) return;
+ eventsWired = true;
+ if (dom.enabled) {
+ const enabledInput = dom.enabled;
+ dom.enabled.addEventListener("change", function() {
+ if (dom.body) dom.body.style.display = enabledInput.checked ? "" : "none";
+ markDirty();
+ });
+ }
+ if (dom.pretune) {
+ dom.pretune.addEventListener("input", function() {
+ markDirty();
+ });
+ }
+ if (dom.addBtn) dom.addBtn.addEventListener("click", function() {
+ openForm(null, null);
+ });
+ if (dom.form) dom.form.addEventListener("submit", onFormSubmit);
+ if (dom.formCancel) dom.formCancel.addEventListener("click", closeForm);
+ if (dom.preset) dom.preset.addEventListener("change", onPresetChange);
+ }
+ satSchedulerWindow.satScheduler = {
+ wireEvents,
+ renderSection,
+ renderPassStatus,
+ collectSatelliteConfig
+ };
+ if (getBridge()) {
+ wireEvents();
+ renderSection();
+ }
})();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sat.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sat.js
index f826701a..6238a038 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sat.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/sat.js
@@ -1,482 +1,479 @@
-"use strict";
-(() => {
- // src/plugins/sat.ts
- var satWindow = window;
- var satDom = {
- status: document.getElementById("sat-status"),
- liveView: document.getElementById("sat-live-view"),
- historyView: document.getElementById("sat-history-view"),
- predictionsView: document.getElementById("sat-predictions-view"),
- liveLatest: document.getElementById("sat-live-latest"),
- historyList: document.getElementById("sat-history-list"),
- historyCount: document.getElementById("sat-history-count"),
- filterInput: document.getElementById("sat-filter"),
- sortSelect: document.getElementById("sat-sort"),
- typeFilter: document.getElementById("sat-type-filter"),
- lrptState: document.getElementById("sat-lrpt-state"),
- viewLiveBtn: document.getElementById("sat-view-live"),
- viewHistoryBtn: document.getElementById("sat-view-history"),
- viewPredBtn: document.getElementById("sat-view-predictions"),
- predFilter: document.getElementById("sat-pred-filter"),
- predMinEl: document.getElementById("sat-pred-min-el"),
- predCategory: document.getElementById("sat-pred-category"),
- predCurrentList: document.getElementById("sat-pred-current-list"),
- predUpcomingList: document.getElementById("sat-pred-list"),
- predCurrentSec: document.getElementById("sat-pred-current-section"),
- predUpcomingSec: document.getElementById("sat-pred-upcoming-section"),
- predStatus: document.getElementById("sat-pred-status")
- };
- var satImageHistory = [];
- var SAT_MAX_IMAGES = 100;
- var SAT_PRED_PAGE_SIZE = 50;
- var satPredShowAll = false;
- var satFilterText = "";
- var satActiveView = "live";
- var satPredData = [];
- var satPredFilterText = "";
- var satPredMinEl = 0;
- var satPredCategory = "all";
- var satPredSatCount = 0;
- var satPredCountdownTimer = null;
- function scheduleSatUi(key, job) {
- if (typeof satWindow.trxScheduleUiFrameJob === "function") {
- satWindow.trxScheduleUiFrameJob(key, job);
- return;
- }
- job();
+// src/plugins/sat.ts
+var satWindow = window;
+var satDom = {
+ status: document.getElementById("sat-status"),
+ liveView: document.getElementById("sat-live-view"),
+ historyView: document.getElementById("sat-history-view"),
+ predictionsView: document.getElementById("sat-predictions-view"),
+ liveLatest: document.getElementById("sat-live-latest"),
+ historyList: document.getElementById("sat-history-list"),
+ historyCount: document.getElementById("sat-history-count"),
+ filterInput: document.getElementById("sat-filter"),
+ sortSelect: document.getElementById("sat-sort"),
+ typeFilter: document.getElementById("sat-type-filter"),
+ lrptState: document.getElementById("sat-lrpt-state"),
+ viewLiveBtn: document.getElementById("sat-view-live"),
+ viewHistoryBtn: document.getElementById("sat-view-history"),
+ viewPredBtn: document.getElementById("sat-view-predictions"),
+ predFilter: document.getElementById("sat-pred-filter"),
+ predMinEl: document.getElementById("sat-pred-min-el"),
+ predCategory: document.getElementById("sat-pred-category"),
+ predCurrentList: document.getElementById("sat-pred-current-list"),
+ predUpcomingList: document.getElementById("sat-pred-list"),
+ predCurrentSec: document.getElementById("sat-pred-current-section"),
+ predUpcomingSec: document.getElementById("sat-pred-upcoming-section"),
+ predStatus: document.getElementById("sat-pred-status")
+};
+var satImageHistory = [];
+var SAT_MAX_IMAGES = 100;
+var SAT_PRED_PAGE_SIZE = 50;
+var satPredShowAll = false;
+var satFilterText = "";
+var satActiveView = "live";
+var satPredData = [];
+var satPredFilterText = "";
+var satPredMinEl = 0;
+var satPredCategory = "all";
+var satPredSatCount = 0;
+var satPredCountdownTimer = null;
+function scheduleSatUi(key, job) {
+ if (typeof satWindow.trxScheduleUiFrameJob === "function") {
+ satWindow.trxScheduleUiFrameJob(key, job);
+ return;
}
- function switchSatView(view) {
- const leavingPredictions = satActiveView === "predictions" && view !== "predictions";
- satActiveView = view;
- if (satDom.liveView) satDom.liveView.style.display = view === "live" ? "" : "none";
- if (satDom.historyView) satDom.historyView.style.display = view === "history" ? "" : "none";
- if (satDom.predictionsView) satDom.predictionsView.style.display = view === "predictions" ? "" : "none";
- if (satDom.viewLiveBtn) satDom.viewLiveBtn.classList.toggle("sat-view-active", view === "live");
- if (satDom.viewHistoryBtn) satDom.viewHistoryBtn.classList.toggle("sat-view-active", view === "history");
- if (satDom.viewPredBtn) satDom.viewPredBtn.classList.toggle("sat-view-active", view === "predictions");
- if (leavingPredictions) clearPredictionDom();
- if (view === "history") {
+ job();
+}
+function switchSatView(view) {
+ const leavingPredictions = satActiveView === "predictions" && view !== "predictions";
+ satActiveView = view;
+ if (satDom.liveView) satDom.liveView.style.display = view === "live" ? "" : "none";
+ if (satDom.historyView) satDom.historyView.style.display = view === "history" ? "" : "none";
+ if (satDom.predictionsView) satDom.predictionsView.style.display = view === "predictions" ? "" : "none";
+ if (satDom.viewLiveBtn) satDom.viewLiveBtn.classList.toggle("sat-view-active", view === "live");
+ if (satDom.viewHistoryBtn) satDom.viewHistoryBtn.classList.toggle("sat-view-active", view === "history");
+ if (satDom.viewPredBtn) satDom.viewPredBtn.classList.toggle("sat-view-active", view === "predictions");
+ if (leavingPredictions) clearPredictionDom();
+ if (view === "history") {
+ renderSatHistoryTable();
+ } else if (view === "predictions") {
+ satPredShowAll = false;
+ void loadSatPredictions();
+ }
+}
+function clearPredictionDom() {
+ stopCountdownTimer();
+ if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
+ if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
+}
+satWindow.clearSatPredictionDom = clearPredictionDom;
+satDom.viewLiveBtn?.addEventListener("click", () => {
+ switchSatView("live");
+});
+satDom.viewHistoryBtn?.addEventListener("click", () => {
+ switchSatView("history");
+});
+satDom.viewPredBtn?.addEventListener("click", () => {
+ switchSatView("predictions");
+});
+var lastSatLrptOn = null;
+satWindow.updateSatLiveState = function(update) {
+ if (!satDom.lrptState) return;
+ const lrptOn = !!update.lrpt_decode_enabled;
+ if (lrptOn !== lastSatLrptOn) {
+ lastSatLrptOn = lrptOn;
+ satDom.lrptState.textContent = lrptOn ? "Listening" : "Idle";
+ satDom.lrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle");
+ if (satDom.status) {
+ if (lrptOn) {
+ satDom.status.textContent = "Decoder active — waiting for signal";
+ } else {
+ satDom.status.textContent = "Decoder idle";
+ }
+ }
+ }
+};
+function renderSatLatestCard() {
+ if (!satDom.liveLatest) return;
+ if (satImageHistory.length === 0) {
+ satDom.liveLatest.innerHTML = 'No images decoded yet. Enable a decoder and wait for a satellite pass.
';
+ return;
+ }
+ const img = satImageHistory[0];
+ if (!img) return;
+ const typeName = "Meteor LRPT";
+ const satellite = img.satellite || "";
+ const channels = img.channels || img.channel_a || "";
+ const lines = img.mcu_count || img.line_count || 0;
+ const unit = "MCU rows";
+ const ts = img._ts || "--";
+ const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
+ const meta = [typeName];
+ if (satellite) meta.push(satellite);
+ if (channels) meta.push(channels);
+ meta.push(`${lines} ${unit}`);
+ meta.push(`${date} ${ts}`);
+ let html = ``;
+ html += `
Latest decoded image
`;
+ html += `
${meta.join(" · ")}
`;
+ if (img.path) {
+ html += `
Download PNG`;
+ }
+ if (img.geo_bounds) {
+ html += `
`;
+ }
+ html += `
`;
+ satDom.liveLatest.innerHTML = html;
+}
+function getSatFilteredHistory() {
+ let items = satImageHistory;
+ const typeVal = satDom.typeFilter ? satDom.typeFilter.value : "all";
+ if (typeVal === "lrpt") items = items.filter((i) => i._decoder === "lrpt");
+ if (satFilterText) {
+ items = items.filter((i) => {
+ const haystack = [
+ "meteor lrpt",
+ i.satellite || "",
+ i.channels || "",
+ i.channel_a || "",
+ i.channel_b || ""
+ ].join(" ").toUpperCase();
+ return haystack.includes(satFilterText);
+ });
+ }
+ const sortVal = satDom.sortSelect ? satDom.sortSelect.value : "newest";
+ if (sortVal === "oldest") items = items.slice().reverse();
+ return items;
+}
+function renderSatHistoryRow(img) {
+ const row = document.createElement("div");
+ row.className = "sat-history-row";
+ const typeName = "Meteor LRPT";
+ const typeClass = "sat-type-lrpt";
+ const ts = img._ts || "--";
+ const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: "short", day: "numeric" }) : "";
+ const satellite = img.satellite || "--";
+ const channels = img.channels || "--";
+ const lines = img.mcu_count || img.line_count || 0;
+ const unit = "MCU";
+ let link = img.path ? `PNG` : "--";
+ if (img.geo_bounds) {
+ link += ` Map`;
+ }
+ row.innerHTML = [
+ `${date} ${ts}`,
+ `${typeName}`,
+ `${satellite}`,
+ `${channels}`,
+ `${lines} ${unit}`,
+ `${link}`
+ ].join("");
+ return row;
+}
+function renderSatHistoryTable() {
+ if (!satDom.historyList) return;
+ const items = getSatFilteredHistory();
+ const fragment = document.createDocumentFragment();
+ for (const image of items) {
+ fragment.appendChild(renderSatHistoryRow(image));
+ }
+ satDom.historyList.replaceChildren(fragment);
+ if (satDom.historyCount) {
+ const total = satImageHistory.length;
+ const shown = items.length;
+ satDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? `${total} image${total === 1 ? "" : "s"}` : `${shown} of ${total} images`;
+ }
+}
+function addSatImage(img, decoder) {
+ const tsMs = Number.isFinite(img.ts_ms) ? Number(img.ts_ms) : Date.now();
+ img._tsMs = tsMs;
+ img._ts = new Date(tsMs).toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit"
+ });
+ img._decoder = decoder;
+ satImageHistory.unshift(img);
+ if (satImageHistory.length > SAT_MAX_IMAGES) {
+ satImageHistory = satImageHistory.slice(0, SAT_MAX_IMAGES);
+ }
+ scheduleSatUi("sat-latest", () => {
+ renderSatLatestCard();
+ });
+ if (satActiveView === "history") {
+ scheduleSatUi("sat-history", () => {
renderSatHistoryTable();
- } else if (view === "predictions") {
- satPredShowAll = false;
- void loadSatPredictions();
- }
- }
- function clearPredictionDom() {
- stopCountdownTimer();
- if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
- if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
- }
- satWindow.clearSatPredictionDom = clearPredictionDom;
- satDom.viewLiveBtn?.addEventListener("click", () => {
- switchSatView("live");
- });
- satDom.viewHistoryBtn?.addEventListener("click", () => {
- switchSatView("history");
- });
- satDom.viewPredBtn?.addEventListener("click", () => {
- switchSatView("predictions");
- });
- var lastSatLrptOn = null;
- satWindow.updateSatLiveState = function(update) {
- if (!satDom.lrptState) return;
- const lrptOn = !!update.lrpt_decode_enabled;
- if (lrptOn !== lastSatLrptOn) {
- lastSatLrptOn = lrptOn;
- satDom.lrptState.textContent = lrptOn ? "Listening" : "Idle";
- satDom.lrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle");
- if (satDom.status) {
- if (lrptOn) {
- satDom.status.textContent = "Decoder active — waiting for signal";
- } else {
- satDom.status.textContent = "Decoder idle";
- }
- }
- }
- };
- function renderSatLatestCard() {
- if (!satDom.liveLatest) return;
- if (satImageHistory.length === 0) {
- satDom.liveLatest.innerHTML = 'No images decoded yet. Enable a decoder and wait for a satellite pass.
';
- return;
- }
- const img = satImageHistory[0];
- if (!img) return;
- const typeName = "Meteor LRPT";
- const satellite = img.satellite || "";
- const channels = img.channels || img.channel_a || "";
- const lines = img.mcu_count || img.line_count || 0;
- const unit = "MCU rows";
- const ts = img._ts || "--";
- const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString() : "";
- const meta = [typeName];
- if (satellite) meta.push(satellite);
- if (channels) meta.push(channels);
- meta.push(`${lines} ${unit}`);
- meta.push(`${date} ${ts}`);
- let html = ``;
- html += `
Latest decoded image
`;
- html += `
${meta.join(" · ")}
`;
- if (img.path) {
- html += `
Download PNG`;
- }
- if (img.geo_bounds) {
- html += `
`;
- }
- html += `
`;
- satDom.liveLatest.innerHTML = html;
- }
- function getSatFilteredHistory() {
- let items = satImageHistory;
- const typeVal = satDom.typeFilter ? satDom.typeFilter.value : "all";
- if (typeVal === "lrpt") items = items.filter((i) => i._decoder === "lrpt");
- if (satFilterText) {
- items = items.filter((i) => {
- const haystack = [
- "meteor lrpt",
- i.satellite || "",
- i.channels || "",
- i.channel_a || "",
- i.channel_b || ""
- ].join(" ").toUpperCase();
- return haystack.includes(satFilterText);
- });
- }
- const sortVal = satDom.sortSelect ? satDom.sortSelect.value : "newest";
- if (sortVal === "oldest") items = items.slice().reverse();
- return items;
- }
- function renderSatHistoryRow(img) {
- const row = document.createElement("div");
- row.className = "sat-history-row";
- const typeName = "Meteor LRPT";
- const typeClass = "sat-type-lrpt";
- const ts = img._ts || "--";
- const date = img._tsMs ? new Date(img._tsMs).toLocaleDateString([], { month: "short", day: "numeric" }) : "";
- const satellite = img.satellite || "--";
- const channels = img.channels || "--";
- const lines = img.mcu_count || img.line_count || 0;
- const unit = "MCU";
- let link = img.path ? `PNG` : "--";
- if (img.geo_bounds) {
- link += ` Map`;
- }
- row.innerHTML = [
- `${date} ${ts}`,
- `${typeName}`,
- `${satellite}`,
- `${channels}`,
- `${lines} ${unit}`,
- `${link}`
- ].join("");
- return row;
- }
- function renderSatHistoryTable() {
- if (!satDom.historyList) return;
- const items = getSatFilteredHistory();
- const fragment = document.createDocumentFragment();
- for (const image of items) {
- fragment.appendChild(renderSatHistoryRow(image));
- }
- satDom.historyList.replaceChildren(fragment);
- if (satDom.historyCount) {
- const total = satImageHistory.length;
- const shown = items.length;
- satDom.historyCount.textContent = total === 0 ? "No images yet" : shown === total ? `${total} image${total === 1 ? "" : "s"}` : `${shown} of ${total} images`;
- }
- }
- function addSatImage(img, decoder) {
- const tsMs = Number.isFinite(img.ts_ms) ? Number(img.ts_ms) : Date.now();
- img._tsMs = tsMs;
- img._ts = new Date(tsMs).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit"
});
- img._decoder = decoder;
- satImageHistory.unshift(img);
- if (satImageHistory.length > SAT_MAX_IMAGES) {
- satImageHistory = satImageHistory.slice(0, SAT_MAX_IMAGES);
- }
- scheduleSatUi("sat-latest", () => {
- renderSatLatestCard();
- });
- if (satActiveView === "history") {
- scheduleSatUi("sat-history", () => {
- renderSatHistoryTable();
- });
- }
}
- function onServerLrptProgress(msg) {
- if (satDom.status && (msg.mcu_count ?? 0) > 0) {
- satDom.status.textContent = `Receiving — ${String(msg.mcu_count ?? 0)} MCU rows decoded`;
- }
+}
+function onServerLrptProgress(msg) {
+ if (satDom.status && (msg.mcu_count ?? 0) > 0) {
+ satDom.status.textContent = `Receiving — ${String(msg.mcu_count ?? 0)} MCU rows decoded`;
}
- function onServerLrptImage(msg) {
- if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
- addSatImage(msg, "lrpt");
- if (msg.geo_bounds && msg.path && satWindow.addSatMapOverlay) {
- satWindow.addSatMapOverlay(msg);
- }
+}
+function onServerLrptImage(msg) {
+ if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
+ addSatImage(msg, "lrpt");
+ if (msg.geo_bounds && msg.path && satWindow.addSatMapOverlay) {
+ satWindow.addSatMapOverlay(msg);
}
- function resetSatHistoryView() {
- satImageHistory = [];
- if (satDom.historyList) satDom.historyList.innerHTML = "";
- renderSatLatestCard();
- renderSatHistoryTable();
- satWindow.clearSatMapOverlays?.();
- }
- function pruneSatHistoryView() {
- renderSatHistoryTable();
- renderSatLatestCard();
- }
- satWindow.trxPluginRuntime.registerDecoder({
- id: "lrpt_image",
- onMessage: onServerLrptImage,
- reset: resetSatHistoryView,
- prune: pruneSatHistoryView
- });
- satWindow.trxPluginRuntime.registerDecoder({
- id: "lrpt_progress",
- onMessage: onServerLrptProgress
- });
- var lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
- lrptDecodeToggleBtn?.addEventListener("click", () => {
- void (async () => {
- try {
- await satWindow.takeSchedulerControlForDecoderDisable?.(lrptDecodeToggleBtn);
- await satWindow.postPath?.("/toggle_lrpt_decode");
- } catch (e) {
- console.error("LRPT toggle failed", e);
- }
- })();
- });
- var satFilterInput = satDom.filterInput;
- satFilterInput?.addEventListener("input", () => {
- satFilterText = satFilterInput.value.trim().toUpperCase();
- renderSatHistoryTable();
- });
- satDom.sortSelect?.addEventListener("change", () => {
- renderSatHistoryTable();
- });
- satDom.typeFilter?.addEventListener("change", () => {
- renderSatHistoryTable();
- });
- document.getElementById("settings-clear-sat-history")?.addEventListener("click", () => {
- void (async () => {
- if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
- try {
- await satWindow.postPath?.("/clear_lrpt_decode");
- resetSatHistoryView();
- } catch (e) {
- console.error("Weather satellite history clear failed", e);
- }
- })();
- });
- function azToCardinal(deg) {
- const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
- return dirs[Math.round(deg / 45) % 8] ?? "N";
- }
- function formatPredTime(ms) {
- const d = new Date(ms);
- const now = /* @__PURE__ */ new Date();
- const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
- const day = d.getUTCDay() !== now.getUTCDay() ? `${dayNames[d.getUTCDay()] ?? ""} ` : "";
- const hh = String(d.getUTCHours()).padStart(2, "0");
- const mm = String(d.getUTCMinutes()).padStart(2, "0");
- return `${day}${hh}:${mm}`;
- }
- function formatPredDuration(s) {
- if (s >= 60) return `${Math.round(s / 60)} min`;
- return `${s}s`;
- }
- function formatCountdown(ms) {
- const totalSec = Math.max(0, Math.floor(ms / 1e3));
- const m = Math.floor(totalSec / 60);
- const s = totalSec % 60;
- return `${m}:${String(s).padStart(2, "0")}`;
- }
- function elevationClass(deg) {
- if (deg >= 45) return "sat-pred-el-high";
- if (deg >= 10) return "sat-pred-el-mid";
- return "sat-pred-el-low";
- }
- function stopCountdownTimer() {
- if (satPredCountdownTimer) {
- clearInterval(satPredCountdownTimer);
- satPredCountdownTimer = null;
- }
- }
- function startCountdownTimer(container) {
- const countdownEls = container?.querySelectorAll(".sat-pred-col-countdown") ?? [];
- if (countdownEls.length === 0) return;
- satPredCountdownTimer = setInterval(() => {
- if (satActiveView !== "predictions") {
- stopCountdownTimer();
- return;
- }
- const n = Date.now();
- let anyActive = false;
- for (const el of countdownEls) {
- const los = Number.parseInt(el.dataset.los ?? "0", 10);
- const rem = los - n;
- if (rem > 0) {
- el.textContent = formatCountdown(rem);
- anyActive = true;
- } else {
- el.textContent = "0:00";
- }
- }
- if (!anyActive) {
- stopCountdownTimer();
- renderSatPredictions(getFilteredPredictions());
- }
- }, 1e3);
- }
- function buildCurrentPassRow(pass, now) {
- const row = document.createElement("div");
- row.className = "sat-pred-row-current";
- const dir = `${azToCardinal(pass.azimuth_aos_deg)} → ${azToCardinal(pass.azimuth_los_deg)}`;
- const remaining = Math.max(0, pass.los_ms - now);
- row.innerHTML = [
- `${pass.satellite}`,
- `${pass.max_elevation_deg.toFixed(1)}°`,
- `${formatPredTime(pass.aos_ms)}`,
- `${formatPredTime(pass.los_ms)}`,
- `${formatCountdown(remaining)}`,
- `${dir}`
- ].join("");
- return row;
- }
- function buildUpcomingPassRow(pass) {
- const row = document.createElement("div");
- row.className = "sat-pred-row";
- const dir = `${azToCardinal(pass.azimuth_aos_deg)} → ${azToCardinal(pass.azimuth_los_deg)}`;
- row.innerHTML = [
- `${formatPredTime(pass.aos_ms)}`,
- `${pass.satellite}`,
- `${pass.max_elevation_deg.toFixed(1)}°`,
- `${formatPredDuration(pass.duration_s)}`,
- `${dir}`
- ].join("");
- return row;
- }
- function getFilteredPredictions() {
- let items = satPredData;
- if (satPredCategory !== "all") items = items.filter((p) => p.category === satPredCategory);
- if (satPredMinEl > 0) items = items.filter((p) => p.max_elevation_deg >= satPredMinEl);
- if (satPredFilterText) items = items.filter((p) => p.satellite.toUpperCase().includes(satPredFilterText));
- return items;
- }
- function applyPredFilters() {
- renderSatPredictions(getFilteredPredictions());
- }
- var satPredictionFilter = satDom.predFilter;
- satPredictionFilter?.addEventListener("input", () => {
- satPredFilterText = satPredictionFilter.value.trim().toUpperCase();
- applyPredFilters();
- });
- var satPredictionMinElevation = satDom.predMinEl;
- satPredictionMinElevation?.addEventListener("change", () => {
- satPredMinEl = Number.parseInt(satPredictionMinElevation.value, 10) || 0;
- applyPredFilters();
- });
- var satPredictionCategory = satDom.predCategory;
- satPredictionCategory?.addEventListener("change", () => {
- satPredCategory = satPredictionCategory.value;
- applyPredFilters();
- });
- function renderSatPredictions(passes, error) {
- stopCountdownTimer();
- if (error) {
- if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
- if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
- if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
- if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
- if (satDom.predStatus) satDom.predStatus.textContent = error;
- return;
- }
- if (!Array.isArray(passes) || passes.length === 0) {
- if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
- if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
- if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
- if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
- if (satDom.predStatus) satDom.predStatus.textContent = "No passes found in the next 24 hours.";
- return;
- }
- const now = Date.now();
- const current = passes.filter((p) => p.aos_ms <= now && p.los_ms > now);
- const upcoming = passes.filter((p) => p.aos_ms > now);
- if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = current.length > 0 ? "" : "none";
- if (satDom.predCurrentList) {
- if (current.length === 0) {
- satDom.predCurrentList.innerHTML = "";
- } else {
- const frag = document.createDocumentFragment();
- for (const pass of current) frag.appendChild(buildCurrentPassRow(pass, now));
- satDom.predCurrentList.replaceChildren(frag);
- }
- }
- const upcomingLimit = satPredShowAll ? upcoming.length : SAT_PRED_PAGE_SIZE;
- const visibleUpcoming = upcoming.slice(0, upcomingLimit);
- const hiddenCount = upcoming.length - visibleUpcoming.length;
- if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = upcoming.length > 0 ? "" : "none";
- if (satDom.predUpcomingList) {
- const frag = document.createDocumentFragment();
- for (const pass of visibleUpcoming) frag.appendChild(buildUpcomingPassRow(pass));
- if (hiddenCount > 0) {
- const moreRow = document.createElement("div");
- moreRow.className = "sat-pred-row";
- moreRow.style.cursor = "pointer";
- moreRow.style.textAlign = "center";
- moreRow.innerHTML = `Show ${hiddenCount} more passes…`;
- moreRow.addEventListener("click", () => {
- satPredShowAll = true;
- renderSatPredictions(getFilteredPredictions());
- });
- frag.appendChild(moreRow);
- }
- satDom.predUpcomingList.replaceChildren(frag);
- }
- if (satDom.predStatus) {
- let text = `${current.length} active · ${upcoming.length} upcoming · times in UTC`;
- if (satPredSatCount > 0) text += ` · ${satPredSatCount} satellites tracked`;
- satDom.predStatus.textContent = text;
- }
- if (current.length > 0 && satActiveView === "predictions") {
- startCountdownTimer(satDom.predCurrentList);
- }
- }
- async function loadSatPredictions() {
- if (satDom.predStatus) satDom.predStatus.textContent = "Loading predictions…";
- if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
- if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
- try {
- const resp = await fetch("/sat_passes");
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
- const data = await resp.json();
- satPredSatCount = data.satellite_count || 0;
- if (data.error) {
- satPredData = [];
- renderSatPredictions([], data.error);
- } else {
- satPredData = data.passes || [];
- renderSatPredictions(getFilteredPredictions());
- }
- } catch (error) {
- renderSatPredictions([], `Failed to load predictions: ${error instanceof Error ? error.message : String(error)}`);
- }
- }
- satWindow.satShowOnMap = function(south, west, north, east) {
- if (typeof satWindow.enableMapSourceFilter === "function") {
- satWindow.enableMapSourceFilter("sat");
- }
- const lat = (south + north) / 2;
- const lon = (west + east) / 2;
- if (satWindow.navigateToAprsMap) {
- satWindow.navigateToAprsMap(lat, lon);
- }
- };
+}
+function resetSatHistoryView() {
+ satImageHistory = [];
+ if (satDom.historyList) satDom.historyList.innerHTML = "";
renderSatLatestCard();
renderSatHistoryTable();
-})();
+ satWindow.clearSatMapOverlays?.();
+}
+function pruneSatHistoryView() {
+ renderSatHistoryTable();
+ renderSatLatestCard();
+}
+satWindow.trxPluginRuntime.registerDecoder({
+ id: "lrpt_image",
+ onMessage: onServerLrptImage,
+ reset: resetSatHistoryView,
+ prune: pruneSatHistoryView
+});
+satWindow.trxPluginRuntime.registerDecoder({
+ id: "lrpt_progress",
+ onMessage: onServerLrptProgress
+});
+var lrptDecodeToggleBtn = document.getElementById("lrpt-decode-toggle-btn");
+lrptDecodeToggleBtn?.addEventListener("click", () => {
+ void (async () => {
+ try {
+ await satWindow.takeSchedulerControlForDecoderDisable?.(lrptDecodeToggleBtn);
+ await satWindow.postPath?.("/toggle_lrpt_decode");
+ } catch (e) {
+ console.error("LRPT toggle failed", e);
+ }
+ })();
+});
+var satFilterInput = satDom.filterInput;
+satFilterInput?.addEventListener("input", () => {
+ satFilterText = satFilterInput.value.trim().toUpperCase();
+ renderSatHistoryTable();
+});
+satDom.sortSelect?.addEventListener("change", () => {
+ renderSatHistoryTable();
+});
+satDom.typeFilter?.addEventListener("change", () => {
+ renderSatHistoryTable();
+});
+document.getElementById("settings-clear-sat-history")?.addEventListener("click", () => {
+ void (async () => {
+ if (!await satWindow.trxUi.confirm({ title: "Clear satellite history?", message: "All stored satellite decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
+ try {
+ await satWindow.postPath?.("/clear_lrpt_decode");
+ resetSatHistoryView();
+ } catch (e) {
+ console.error("Weather satellite history clear failed", e);
+ }
+ })();
+});
+function azToCardinal(deg) {
+ const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
+ return dirs[Math.round(deg / 45) % 8] ?? "N";
+}
+function formatPredTime(ms) {
+ const d = new Date(ms);
+ const now = /* @__PURE__ */ new Date();
+ const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+ const day = d.getUTCDay() !== now.getUTCDay() ? `${dayNames[d.getUTCDay()] ?? ""} ` : "";
+ const hh = String(d.getUTCHours()).padStart(2, "0");
+ const mm = String(d.getUTCMinutes()).padStart(2, "0");
+ return `${day}${hh}:${mm}`;
+}
+function formatPredDuration(s) {
+ if (s >= 60) return `${Math.round(s / 60)} min`;
+ return `${s}s`;
+}
+function formatCountdown(ms) {
+ const totalSec = Math.max(0, Math.floor(ms / 1e3));
+ const m = Math.floor(totalSec / 60);
+ const s = totalSec % 60;
+ return `${m}:${String(s).padStart(2, "0")}`;
+}
+function elevationClass(deg) {
+ if (deg >= 45) return "sat-pred-el-high";
+ if (deg >= 10) return "sat-pred-el-mid";
+ return "sat-pred-el-low";
+}
+function stopCountdownTimer() {
+ if (satPredCountdownTimer) {
+ clearInterval(satPredCountdownTimer);
+ satPredCountdownTimer = null;
+ }
+}
+function startCountdownTimer(container) {
+ const countdownEls = container?.querySelectorAll(".sat-pred-col-countdown") ?? [];
+ if (countdownEls.length === 0) return;
+ satPredCountdownTimer = setInterval(() => {
+ if (satActiveView !== "predictions") {
+ stopCountdownTimer();
+ return;
+ }
+ const n = Date.now();
+ let anyActive = false;
+ for (const el of countdownEls) {
+ const los = Number.parseInt(el.dataset.los ?? "0", 10);
+ const rem = los - n;
+ if (rem > 0) {
+ el.textContent = formatCountdown(rem);
+ anyActive = true;
+ } else {
+ el.textContent = "0:00";
+ }
+ }
+ if (!anyActive) {
+ stopCountdownTimer();
+ renderSatPredictions(getFilteredPredictions());
+ }
+ }, 1e3);
+}
+function buildCurrentPassRow(pass, now) {
+ const row = document.createElement("div");
+ row.className = "sat-pred-row-current";
+ const dir = `${azToCardinal(pass.azimuth_aos_deg)} → ${azToCardinal(pass.azimuth_los_deg)}`;
+ const remaining = Math.max(0, pass.los_ms - now);
+ row.innerHTML = [
+ `${pass.satellite}`,
+ `${pass.max_elevation_deg.toFixed(1)}°`,
+ `${formatPredTime(pass.aos_ms)}`,
+ `${formatPredTime(pass.los_ms)}`,
+ `${formatCountdown(remaining)}`,
+ `${dir}`
+ ].join("");
+ return row;
+}
+function buildUpcomingPassRow(pass) {
+ const row = document.createElement("div");
+ row.className = "sat-pred-row";
+ const dir = `${azToCardinal(pass.azimuth_aos_deg)} → ${azToCardinal(pass.azimuth_los_deg)}`;
+ row.innerHTML = [
+ `${formatPredTime(pass.aos_ms)}`,
+ `${pass.satellite}`,
+ `${pass.max_elevation_deg.toFixed(1)}°`,
+ `${formatPredDuration(pass.duration_s)}`,
+ `${dir}`
+ ].join("");
+ return row;
+}
+function getFilteredPredictions() {
+ let items = satPredData;
+ if (satPredCategory !== "all") items = items.filter((p) => p.category === satPredCategory);
+ if (satPredMinEl > 0) items = items.filter((p) => p.max_elevation_deg >= satPredMinEl);
+ if (satPredFilterText) items = items.filter((p) => p.satellite.toUpperCase().includes(satPredFilterText));
+ return items;
+}
+function applyPredFilters() {
+ renderSatPredictions(getFilteredPredictions());
+}
+var satPredictionFilter = satDom.predFilter;
+satPredictionFilter?.addEventListener("input", () => {
+ satPredFilterText = satPredictionFilter.value.trim().toUpperCase();
+ applyPredFilters();
+});
+var satPredictionMinElevation = satDom.predMinEl;
+satPredictionMinElevation?.addEventListener("change", () => {
+ satPredMinEl = Number.parseInt(satPredictionMinElevation.value, 10) || 0;
+ applyPredFilters();
+});
+var satPredictionCategory = satDom.predCategory;
+satPredictionCategory?.addEventListener("change", () => {
+ satPredCategory = satPredictionCategory.value;
+ applyPredFilters();
+});
+function renderSatPredictions(passes, error) {
+ stopCountdownTimer();
+ if (error) {
+ if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
+ if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
+ if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
+ if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
+ if (satDom.predStatus) satDom.predStatus.textContent = error;
+ return;
+ }
+ if (!Array.isArray(passes) || passes.length === 0) {
+ if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
+ if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
+ if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
+ if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
+ if (satDom.predStatus) satDom.predStatus.textContent = "No passes found in the next 24 hours.";
+ return;
+ }
+ const now = Date.now();
+ const current = passes.filter((p) => p.aos_ms <= now && p.los_ms > now);
+ const upcoming = passes.filter((p) => p.aos_ms > now);
+ if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = current.length > 0 ? "" : "none";
+ if (satDom.predCurrentList) {
+ if (current.length === 0) {
+ satDom.predCurrentList.innerHTML = "";
+ } else {
+ const frag = document.createDocumentFragment();
+ for (const pass of current) frag.appendChild(buildCurrentPassRow(pass, now));
+ satDom.predCurrentList.replaceChildren(frag);
+ }
+ }
+ const upcomingLimit = satPredShowAll ? upcoming.length : SAT_PRED_PAGE_SIZE;
+ const visibleUpcoming = upcoming.slice(0, upcomingLimit);
+ const hiddenCount = upcoming.length - visibleUpcoming.length;
+ if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = upcoming.length > 0 ? "" : "none";
+ if (satDom.predUpcomingList) {
+ const frag = document.createDocumentFragment();
+ for (const pass of visibleUpcoming) frag.appendChild(buildUpcomingPassRow(pass));
+ if (hiddenCount > 0) {
+ const moreRow = document.createElement("div");
+ moreRow.className = "sat-pred-row";
+ moreRow.style.cursor = "pointer";
+ moreRow.style.textAlign = "center";
+ moreRow.innerHTML = `Show ${hiddenCount} more passes…`;
+ moreRow.addEventListener("click", () => {
+ satPredShowAll = true;
+ renderSatPredictions(getFilteredPredictions());
+ });
+ frag.appendChild(moreRow);
+ }
+ satDom.predUpcomingList.replaceChildren(frag);
+ }
+ if (satDom.predStatus) {
+ let text = `${current.length} active · ${upcoming.length} upcoming · times in UTC`;
+ if (satPredSatCount > 0) text += ` · ${satPredSatCount} satellites tracked`;
+ satDom.predStatus.textContent = text;
+ }
+ if (current.length > 0 && satActiveView === "predictions") {
+ startCountdownTimer(satDom.predCurrentList);
+ }
+}
+async function loadSatPredictions() {
+ if (satDom.predStatus) satDom.predStatus.textContent = "Loading predictions…";
+ if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
+ if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
+ try {
+ const resp = await fetch("/sat_passes");
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ const data = await resp.json();
+ satPredSatCount = data.satellite_count || 0;
+ if (data.error) {
+ satPredData = [];
+ renderSatPredictions([], data.error);
+ } else {
+ satPredData = data.passes || [];
+ renderSatPredictions(getFilteredPredictions());
+ }
+ } catch (error) {
+ renderSatPredictions([], `Failed to load predictions: ${error instanceof Error ? error.message : String(error)}`);
+ }
+}
+satWindow.satShowOnMap = function(south, west, north, east) {
+ if (typeof satWindow.enableMapSourceFilter === "function") {
+ satWindow.enableMapSourceFilter("sat");
+ }
+ const lat = (south + north) / 2;
+ const lon = (west + east) / 2;
+ if (satWindow.navigateToAprsMap) {
+ satWindow.navigateToAprsMap(lat, lon);
+ }
+};
+renderSatLatestCard();
+renderSatHistoryTable();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/scheduler.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/scheduler.js
index 289e8bb9..be748f6c 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/scheduler.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/scheduler.js
@@ -1,1128 +1,652 @@
-"use strict";
-(() => {
- // src/plugins/scheduler.ts
- var schedulerWindow = window;
- var wiredElements = /* @__PURE__ */ new WeakSet();
- function schedulerEl(id) {
- const element = document.getElementById(id);
- if (!element) throw new Error(`Missing scheduler element #${id}`);
- return element;
+// src/plugins/scheduler.ts
+var schedulerWindow = window;
+var wiredElements = /* @__PURE__ */ new WeakSet();
+function schedulerEl(id) {
+ const element = document.getElementById(id);
+ if (!element) throw new Error(`Missing scheduler element #${id}`);
+ return element;
+}
+(function() {
+ "use strict";
+ let schedulerRole = null;
+ let currentRigId = null;
+ let currentConfig = null;
+ let currentSchedulerStatus = null;
+ let bookmarkList = [];
+ let statusInterval = null;
+ let interleaveTicker = null;
+ let schedulerStepPending = false;
+ let schEntryEditIdx = null;
+ let schedulerDirty = false;
+ function initScheduler(rigId, role) {
+ schedulerRole = role;
+ currentRigId = rigId || null;
+ if (currentRigId) loadScheduler();
+ startStatusPolling();
+ startInterleaveTicker();
}
- (function() {
- "use strict";
- let schedulerRole = null;
- let currentRigId = null;
- let currentConfig = null;
- let currentSchedulerStatus = null;
- let bookmarkList = [];
- let statusInterval = null;
- let interleaveTicker = null;
- let schedulerStepPending = false;
- let schEntryEditIdx = null;
- let schedulerDirty = false;
- function initScheduler(rigId, role) {
- schedulerRole = role;
- currentRigId = rigId || null;
- if (currentRigId) loadScheduler();
- startStatusPolling();
- startInterleaveTicker();
+ function destroyScheduler() {
+ if (statusInterval) {
+ clearInterval(statusInterval);
+ statusInterval = null;
}
- function destroyScheduler() {
- if (statusInterval) {
- clearInterval(statusInterval);
- statusInterval = null;
- }
- if (interleaveTicker) {
- clearInterval(interleaveTicker);
- interleaveTicker = null;
- }
+ if (interleaveTicker) {
+ clearInterval(interleaveTicker);
+ interleaveTicker = null;
}
- function setSchedulerRig(rigId) {
- const nextRigId = rigId || null;
- if (nextRigId === currentRigId) return;
- currentRigId = nextRigId;
+ }
+ function setSchedulerRig(rigId) {
+ const nextRigId = rigId || null;
+ if (nextRigId === currentRigId) return;
+ currentRigId = nextRigId;
+ renderSchedulerInterleaveStatus();
+ if (!currentRigId) return;
+ loadScheduler();
+ pollStatus();
+ }
+ function apiGetScheduler(rigId) {
+ return fetch("/scheduler/" + encodeURIComponent(rigId)).then(function(r) {
+ if (!r.ok) throw new Error("HTTP " + r.status);
+ return r.json();
+ });
+ }
+ function apiPutScheduler(rigId, config) {
+ return fetch("/scheduler/" + encodeURIComponent(rigId), {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(config)
+ }).then(function(r) {
+ if (!r.ok) throw new Error("HTTP " + r.status);
+ return r.json();
+ });
+ }
+ function apiDeleteScheduler(rigId) {
+ return fetch("/scheduler/" + encodeURIComponent(rigId), {
+ method: "DELETE"
+ }).then(function(r) {
+ if (!r.ok) throw new Error("HTTP " + r.status);
+ return r.json();
+ });
+ }
+ function apiGetStatus(rigId) {
+ return fetch("/scheduler/" + encodeURIComponent(rigId) + "/status").then(
+ function(r) {
+ if (!r.ok) throw new Error("HTTP " + r.status);
+ return r.json();
+ }
+ );
+ }
+ function apiActivateSchedulerEntry(rigId, entryId) {
+ return fetch("/scheduler/" + encodeURIComponent(rigId) + "/activate", {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ entry_id: entryId })
+ }).then(function(r) {
+ if (!r.ok) throw new Error("HTTP " + r.status);
+ return r.json();
+ });
+ }
+ function apiGetBookmarks() {
+ const url = currentRigId ? "/bookmarks?scope=" + encodeURIComponent(currentRigId) : "/bookmarks";
+ return fetch(url).then(function(r) {
+ return r.ok ? r.json() : [];
+ });
+ }
+ function loadScheduler() {
+ const rig = currentRigId;
+ if (!rig) return;
+ Promise.all([apiGetScheduler(rig), apiGetBookmarks()]).then(function([config, bms]) {
+ currentConfig = config;
+ bookmarkList = Array.isArray(bms) ? bms : [];
+ populateTsBookmarkSelect();
+ renderScheduler();
+ clearSchedulerDirty();
renderSchedulerInterleaveStatus();
- if (!currentRigId) return;
- loadScheduler();
- pollStatus();
- }
- function apiGetScheduler(rigId) {
- return fetch("/scheduler/" + encodeURIComponent(rigId)).then(function(r) {
- if (!r.ok) throw new Error("HTTP " + r.status);
- return r.json();
- });
- }
- function apiPutScheduler(rigId, config) {
- return fetch("/scheduler/" + encodeURIComponent(rigId), {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(config)
- }).then(function(r) {
- if (!r.ok) throw new Error("HTTP " + r.status);
- return r.json();
- });
- }
- function apiDeleteScheduler(rigId) {
- return fetch("/scheduler/" + encodeURIComponent(rigId), {
- method: "DELETE"
- }).then(function(r) {
- if (!r.ok) throw new Error("HTTP " + r.status);
- return r.json();
- });
- }
- function apiGetStatus(rigId) {
- return fetch("/scheduler/" + encodeURIComponent(rigId) + "/status").then(
- function(r) {
- if (!r.ok) throw new Error("HTTP " + r.status);
- return r.json();
- }
- );
- }
- function apiActivateSchedulerEntry(rigId, entryId) {
- return fetch("/scheduler/" + encodeURIComponent(rigId) + "/activate", {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ entry_id: entryId })
- }).then(function(r) {
- if (!r.ok) throw new Error("HTTP " + r.status);
- return r.json();
- });
- }
- function apiGetBookmarks() {
- const url = currentRigId ? "/bookmarks?scope=" + encodeURIComponent(currentRigId) : "/bookmarks";
- return fetch(url).then(function(r) {
- return r.ok ? r.json() : [];
- });
- }
- function loadScheduler() {
- const rig = currentRigId;
- if (!rig) return;
- Promise.all([apiGetScheduler(rig), apiGetBookmarks()]).then(function([config, bms]) {
- currentConfig = config;
- bookmarkList = Array.isArray(bms) ? bms : [];
- populateTsBookmarkSelect();
- renderScheduler();
- clearSchedulerDirty();
- renderSchedulerInterleaveStatus();
- }).catch(function(error) {
- console.error("scheduler load failed", error);
- renderSchedulerInterleaveStatus();
- });
- }
- function startStatusPolling() {
- if (statusInterval) clearInterval(statusInterval);
- statusInterval = setInterval(pollStatus, 15e3);
- pollStatus();
- }
- function startInterleaveTicker() {
- if (interleaveTicker) clearInterval(interleaveTicker);
- interleaveTicker = setInterval(renderSchedulerInterleaveStatus, 1e3);
+ }).catch(function(error) {
+ console.error("scheduler load failed", error);
renderSchedulerInterleaveStatus();
+ });
+ }
+ function startStatusPolling() {
+ if (statusInterval) clearInterval(statusInterval);
+ statusInterval = setInterval(pollStatus, 15e3);
+ pollStatus();
+ }
+ function startInterleaveTicker() {
+ if (interleaveTicker) clearInterval(interleaveTicker);
+ interleaveTicker = setInterval(renderSchedulerInterleaveStatus, 1e3);
+ renderSchedulerInterleaveStatus();
+ }
+ function schedulerUtcSeconds() {
+ return Math.floor(Date.now() / 1e3);
+ }
+ function schedulerUtcMinuteInfo() {
+ const secs = schedulerUtcSeconds();
+ const secsIntoDay = (secs % 86400 + 86400) % 86400;
+ return {
+ minuteOfDay: Math.floor(secsIntoDay / 60),
+ secondOfMinute: secsIntoDay % 60
+ };
+ }
+ function schedulerEntryIsActive(entry, nowMin) {
+ const start = Number(entry && entry.start_min);
+ const end = Number(entry && entry.end_min);
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return false;
+ if (start === end) return true;
+ if (start < end) return nowMin >= start && nowMin < end;
+ return nowMin >= start || nowMin < end;
+ }
+ function schedulerEntryCurrentWindowStart(entry, nowMin) {
+ const start = Number(entry && entry.start_min);
+ const end = Number(entry && entry.end_min);
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return Number.NEGATIVE_INFINITY;
+ if (start === end) return 0;
+ if (start < end) return start;
+ return nowMin >= start ? start : start - 1440;
+ }
+ function schedulerEntryDisplayName(entry) {
+ if (!entry) return "Scheduler entry";
+ if (entry.label) return String(entry.label);
+ const bookmarkName = bmName(entry.bookmark_id);
+ return bookmarkName || "Scheduler entry";
+ }
+ function schedulerInterleaveState(config) {
+ if (!config || config.mode !== "time_span") {
+ return { activeEntries: [], currentIndex: -1, remainingSec: 0, cycleMin: 0 };
}
- function schedulerUtcSeconds() {
- return Math.floor(Date.now() / 1e3);
+ const entries = Array.isArray(config.entries) ? config.entries : [];
+ const minuteInfo = schedulerUtcMinuteInfo();
+ const nowMin = minuteInfo.minuteOfDay;
+ const active = entries.filter(function(entry) {
+ return schedulerEntryIsActive(entry, nowMin);
+ });
+ if (active.length === 0) {
+ return { activeEntries: [], currentIndex: -1, remainingSec: 0, cycleMin: 0 };
}
- function schedulerUtcMinuteInfo() {
- const secs = schedulerUtcSeconds();
- const secsIntoDay = (secs % 86400 + 86400) % 86400;
- return {
- minuteOfDay: Math.floor(secsIntoDay / 60),
- secondOfMinute: secsIntoDay % 60
- };
+ if (active.length === 1) {
+ return { activeEntries: active, currentIndex: 0, remainingSec: 0, cycleMin: 0 };
}
- function schedulerEntryIsActive(entry, nowMin) {
- const start = Number(entry && entry.start_min);
- const end = Number(entry && entry.end_min);
- if (!Number.isFinite(start) || !Number.isFinite(end)) return false;
- if (start === end) return true;
- if (start < end) return nowMin >= start && nowMin < end;
- return nowMin >= start || nowMin < end;
+ const exclIdx = active.findIndex(function(e) {
+ return e.exclusive;
+ });
+ if (exclIdx >= 0) {
+ const exclusive = active[exclIdx];
+ return { activeEntries: exclusive ? [exclusive] : [], currentIndex: 0, remainingSec: 0, cycleMin: 0 };
}
- function schedulerEntryCurrentWindowStart(entry, nowMin) {
- const start = Number(entry && entry.start_min);
- const end = Number(entry && entry.end_min);
- if (!Number.isFinite(start) || !Number.isFinite(end)) return Number.NEGATIVE_INFINITY;
- if (start === end) return 0;
- if (start < end) return start;
- return nowMin >= start ? start : start - 1440;
+ const defaultInterleave = Number(config.interleave_min);
+ const durations = active.map(function(entry) {
+ const own = Number(entry && entry.interleave_min);
+ if (Number.isFinite(own) && own > 0) return Math.floor(own);
+ if (Number.isFinite(defaultInterleave) && defaultInterleave > 0) return Math.floor(defaultInterleave);
+ return 0;
+ });
+ const cycleMin = durations.reduce(function(sum, value) {
+ return sum + value;
+ }, 0);
+ if (!(cycleMin > 0)) {
+ return { activeEntries: active, currentIndex: 0, remainingSec: 0, cycleMin: 0 };
}
- function schedulerEntryDisplayName(entry) {
- if (!entry) return "Scheduler entry";
- if (entry.label) return String(entry.label);
- const bookmarkName = bmName(entry.bookmark_id);
- return bookmarkName || "Scheduler entry";
+ const statusEntryId = currentSchedulerStatus && currentSchedulerStatus.last_entry_id ? String(currentSchedulerStatus.last_entry_id) : "";
+ const statusIndex = statusEntryId ? active.findIndex(function(entry) {
+ return String(entry && entry.id || "") === statusEntryId;
+ }) : -1;
+ const statusAppliedUtc = currentSchedulerStatus && Number.isFinite(Number(currentSchedulerStatus.last_applied_utc)) ? Number(currentSchedulerStatus.last_applied_utc) : null;
+ if (statusIndex >= 0 && statusAppliedUtc != null) {
+ const manualDurationMin = durations[statusIndex] ?? 0;
+ const elapsedSec = Math.max(0, schedulerUtcSeconds() - statusAppliedUtc);
+ const remainingSec2 = manualDurationMin > 0 ? Math.max(1, manualDurationMin * 60 - elapsedSec) : 0;
+ if (remainingSec2 > 0) {
+ return {
+ activeEntries: active,
+ currentIndex: statusIndex,
+ remainingSec: remainingSec2,
+ cycleMin
+ };
+ }
}
- function schedulerInterleaveState(config) {
- if (!config || config.mode !== "time_span") {
- return { activeEntries: [], currentIndex: -1, remainingSec: 0, cycleMin: 0 };
- }
- const entries = Array.isArray(config.entries) ? config.entries : [];
- const minuteInfo = schedulerUtcMinuteInfo();
- const nowMin = minuteInfo.minuteOfDay;
- const active = entries.filter(function(entry) {
- return schedulerEntryIsActive(entry, nowMin);
- });
- if (active.length === 0) {
- return { activeEntries: [], currentIndex: -1, remainingSec: 0, cycleMin: 0 };
- }
- if (active.length === 1) {
- return { activeEntries: active, currentIndex: 0, remainingSec: 0, cycleMin: 0 };
- }
- const exclIdx = active.findIndex(function(e) {
- return e.exclusive;
- });
- if (exclIdx >= 0) {
- const exclusive = active[exclIdx];
- return { activeEntries: exclusive ? [exclusive] : [], currentIndex: 0, remainingSec: 0, cycleMin: 0 };
- }
- const defaultInterleave = Number(config.interleave_min);
- const durations = active.map(function(entry) {
- const own = Number(entry && entry.interleave_min);
- if (Number.isFinite(own) && own > 0) return Math.floor(own);
- if (Number.isFinite(defaultInterleave) && defaultInterleave > 0) return Math.floor(defaultInterleave);
- return 0;
- });
- const cycleMin = durations.reduce(function(sum, value) {
- return sum + value;
- }, 0);
- if (!(cycleMin > 0)) {
- return { activeEntries: active, currentIndex: 0, remainingSec: 0, cycleMin: 0 };
- }
- const statusEntryId = currentSchedulerStatus && currentSchedulerStatus.last_entry_id ? String(currentSchedulerStatus.last_entry_id) : "";
- const statusIndex = statusEntryId ? active.findIndex(function(entry) {
- return String(entry && entry.id || "") === statusEntryId;
- }) : -1;
- const statusAppliedUtc = currentSchedulerStatus && Number.isFinite(Number(currentSchedulerStatus.last_applied_utc)) ? Number(currentSchedulerStatus.last_applied_utc) : null;
- if (statusIndex >= 0 && statusAppliedUtc != null) {
- const manualDurationMin = durations[statusIndex] ?? 0;
- const elapsedSec = Math.max(0, schedulerUtcSeconds() - statusAppliedUtc);
- const remainingSec2 = manualDurationMin > 0 ? Math.max(1, manualDurationMin * 60 - elapsedSec) : 0;
- if (remainingSec2 > 0) {
- return {
- activeEntries: active,
- currentIndex: statusIndex,
- remainingSec: remainingSec2,
- cycleMin
- };
- }
- }
- const overlapStart = active.reduce(function(maxStart, entry) {
- return Math.max(maxStart, schedulerEntryCurrentWindowStart(entry, nowMin));
- }, Number.NEGATIVE_INFINITY);
- if (!Number.isFinite(overlapStart)) {
- return { activeEntries: active, currentIndex: 0, remainingSec: 0, cycleMin: 0 };
- }
- const nowMinPrecise = minuteInfo.minuteOfDay + minuteInfo.secondOfMinute / 60;
- const posMin = ((nowMinPrecise - overlapStart) % cycleMin + cycleMin) % cycleMin;
- let cumulative = 0;
- let slotStart = 0;
- let currentIndex = 0;
- let currentDuration = 0;
- for (let i = 0; i < durations.length; i += 1) {
- const duration = durations[i] ?? 0;
- const nextCumulative = cumulative + duration;
- if (posMin < nextCumulative) {
- slotStart = cumulative;
- cumulative = nextCumulative;
- currentIndex = i;
- currentDuration = duration;
- break;
- }
+ const overlapStart = active.reduce(function(maxStart, entry) {
+ return Math.max(maxStart, schedulerEntryCurrentWindowStart(entry, nowMin));
+ }, Number.NEGATIVE_INFINITY);
+ if (!Number.isFinite(overlapStart)) {
+ return { activeEntries: active, currentIndex: 0, remainingSec: 0, cycleMin: 0 };
+ }
+ const nowMinPrecise = minuteInfo.minuteOfDay + minuteInfo.secondOfMinute / 60;
+ const posMin = ((nowMinPrecise - overlapStart) % cycleMin + cycleMin) % cycleMin;
+ let cumulative = 0;
+ let slotStart = 0;
+ let currentIndex = 0;
+ let currentDuration = 0;
+ for (let i = 0; i < durations.length; i += 1) {
+ const duration = durations[i] ?? 0;
+ const nextCumulative = cumulative + duration;
+ if (posMin < nextCumulative) {
+ slotStart = cumulative;
cumulative = nextCumulative;
+ currentIndex = i;
+ currentDuration = duration;
+ break;
}
- if (!(currentDuration > 0)) {
- return { activeEntries: active, currentIndex: 0, remainingSec: 0, cycleMin: 0 };
+ cumulative = nextCumulative;
+ }
+ if (!(currentDuration > 0)) {
+ return { activeEntries: active, currentIndex: 0, remainingSec: 0, cycleMin: 0 };
+ }
+ const elapsedSlotSec = Math.max(0, Math.floor((posMin - slotStart) * 60));
+ const remainingSec = Math.max(1, currentDuration * 60 - elapsedSlotSec);
+ return {
+ activeEntries: active,
+ currentIndex,
+ remainingSec,
+ cycleMin
+ };
+ }
+ function renderSchedulerInterleaveStatus() {
+ const wrap = schedulerEl("scheduler-cycle-status");
+ if (!wrap) return;
+ const state = schedulerInterleaveState(currentConfig);
+ const isActive = state.activeEntries.length > 1 && state.cycleMin > 0;
+ wrap.style.display = isActive ? "" : "none";
+ if (isActive) {
+ const activeName = schedulerEntryDisplayName(state.activeEntries[state.currentIndex]);
+ const totalSlotSec = state.cycleMin > 0 ? state.cycleMin * 60 / state.activeEntries.length : 0;
+ const elapsedPct = totalSlotSec > 0 ? Math.min(100, Math.max(0, (totalSlotSec - state.remainingSec) / totalSlotSec * 100)) : 0;
+ const ringFill = schedulerEl("interleave-ring-fill");
+ if (ringFill) ringFill.setAttribute("stroke-dashoffset", String(100 - elapsedPct));
+ const nameEl = schedulerEl("interleave-active-name");
+ if (nameEl) nameEl.textContent = activeName;
+ const countdownEl = schedulerEl("interleave-countdown");
+ if (countdownEl) countdownEl.textContent = "next in " + state.remainingSec + "s · " + state.cycleMin + "m cycle";
+ }
+ renderTimelineNeedle();
+ renderSchedulerStepControls();
+ }
+ function renderSchedulerStepControls() {
+ const prevBtn = schedulerEl("scheduler-prev-btn");
+ const nextBtn = schedulerEl("scheduler-next-btn");
+ if (!prevBtn || !nextBtn) return;
+ const state = schedulerInterleaveState(currentConfig);
+ const enabled = schedulerRole === "control" && !!currentRigId && !schedulerStepPending && state.activeEntries.length > 1;
+ prevBtn.disabled = !enabled;
+ nextBtn.disabled = !enabled;
+ const hint = enabled ? "Select a different active scheduler entry" : "Available only when multiple scheduler entries are active";
+ prevBtn.title = hint;
+ nextBtn.title = hint;
+ }
+ function pollStatus() {
+ const rig = currentRigId;
+ if (!rig) return;
+ apiGetStatus(rig).then(function(st) {
+ currentSchedulerStatus = st || null;
+ renderStatus(st);
+ renderSchedulerInterleaveStatus();
+ renderActivityLog();
+ renderSatPassStatus();
+ }).catch(function() {
+ });
+ }
+ function renderStatus(st) {
+ const el = schedulerEl("scheduler-status-card");
+ if (!el) return;
+ if (!st || !st.active && !st.last_bookmark_id) {
+ el.textContent = "No activity yet.";
+ return;
+ }
+ const statusEntryId = st.last_entry_id ? String(st.last_entry_id) : "";
+ const entry = statusEntryId && currentConfig && Array.isArray(currentConfig.entries) ? currentConfig.entries.find(function(item) {
+ return String(item && item.id || "") === statusEntryId;
+ }) : null;
+ const name = entry ? schedulerEntryDisplayName(entry) : st.last_bookmark_name || st.last_bookmark_id || "—";
+ let ts = "";
+ if (st.last_applied_utc) {
+ const d = new Date(st.last_applied_utc * 1e3);
+ ts = " at " + d.toUTCString();
+ }
+ const satLabel = st.active_satellite ? " [SAT: " + st.active_satellite + "]" : "";
+ let details = "";
+ if (st.freq_hz) {
+ details += formatFreq(st.freq_hz);
+ if (st.mode) details += " · " + st.mode;
+ if (st.active_decoders && st.active_decoders.length > 0) {
+ details += " · " + st.active_decoders.join(", ") + " active";
}
- const elapsedSlotSec = Math.max(0, Math.floor((posMin - slotStart) * 60));
- const remainingSec = Math.max(1, currentDuration * 60 - elapsedSlotSec);
- return {
- activeEntries: active,
- currentIndex,
- remainingSec,
- cycleMin
- };
}
- function renderSchedulerInterleaveStatus() {
- const wrap = schedulerEl("scheduler-cycle-status");
- if (!wrap) return;
- const state = schedulerInterleaveState(currentConfig);
- const isActive = state.activeEntries.length > 1 && state.cycleMin > 0;
- wrap.style.display = isActive ? "" : "none";
- if (isActive) {
- const activeName = schedulerEntryDisplayName(state.activeEntries[state.currentIndex]);
- const totalSlotSec = state.cycleMin > 0 ? state.cycleMin * 60 / state.activeEntries.length : 0;
- const elapsedPct = totalSlotSec > 0 ? Math.min(100, Math.max(0, (totalSlotSec - state.remainingSec) / totalSlotSec * 100)) : 0;
- const ringFill = schedulerEl("interleave-ring-fill");
- if (ringFill) ringFill.setAttribute("stroke-dashoffset", String(100 - elapsedPct));
- const nameEl = schedulerEl("interleave-active-name");
- if (nameEl) nameEl.textContent = activeName;
- const countdownEl = schedulerEl("interleave-countdown");
- if (countdownEl) countdownEl.textContent = "next in " + state.remainingSec + "s · " + state.cycleMin + "m cycle";
- }
- renderTimelineNeedle();
- renderSchedulerStepControls();
+ if (details) {
+ el.innerHTML = "Last applied: " + escHtml(name) + satLabel + ts + '
' + escHtml(details) + "";
+ } else {
+ el.textContent = "Last applied: " + name + satLabel + ts;
}
- function renderSchedulerStepControls() {
- const prevBtn = schedulerEl("scheduler-prev-btn");
- const nextBtn = schedulerEl("scheduler-next-btn");
- if (!prevBtn || !nextBtn) return;
- const state = schedulerInterleaveState(currentConfig);
- const enabled = schedulerRole === "control" && !!currentRigId && !schedulerStepPending && state.activeEntries.length > 1;
- prevBtn.disabled = !enabled;
- nextBtn.disabled = !enabled;
- const hint = enabled ? "Select a different active scheduler entry" : "Available only when multiple scheduler entries are active";
- prevBtn.title = hint;
- nextBtn.title = hint;
- }
- function pollStatus() {
- const rig = currentRigId;
- if (!rig) return;
- apiGetStatus(rig).then(function(st) {
- currentSchedulerStatus = st || null;
- renderStatus(st);
- renderSchedulerInterleaveStatus();
- renderActivityLog();
- renderSatPassStatus();
- }).catch(function() {
- });
- }
- function renderStatus(st) {
- const el = schedulerEl("scheduler-status-card");
- if (!el) return;
- if (!st || !st.active && !st.last_bookmark_id) {
- el.textContent = "No activity yet.";
+ }
+ function apiGetSchedulerLog(rigId) {
+ return fetch("/scheduler/" + encodeURIComponent(rigId) + "/log").then(function(r) {
+ return r.ok ? r.json() : [];
+ });
+ }
+ function renderActivityLog() {
+ const wrap = schedulerEl("scheduler-activity-log-wrap");
+ const container = schedulerEl("scheduler-activity-log");
+ if (!wrap || !container || !currentRigId) return;
+ apiGetSchedulerLog(currentRigId).then(function(entries) {
+ if (!entries || entries.length === 0) {
+ wrap.style.display = "none";
return;
}
- const statusEntryId = st.last_entry_id ? String(st.last_entry_id) : "";
- const entry = statusEntryId && currentConfig && Array.isArray(currentConfig.entries) ? currentConfig.entries.find(function(item) {
- return String(item && item.id || "") === statusEntryId;
- }) : null;
- const name = entry ? schedulerEntryDisplayName(entry) : st.last_bookmark_name || st.last_bookmark_id || "—";
- let ts = "";
- if (st.last_applied_utc) {
- const d = new Date(st.last_applied_utc * 1e3);
- ts = " at " + d.toUTCString();
- }
- const satLabel = st.active_satellite ? " [SAT: " + st.active_satellite + "]" : "";
- let details = "";
- if (st.freq_hz) {
- details += formatFreq(st.freq_hz);
- if (st.mode) details += " · " + st.mode;
- if (st.active_decoders && st.active_decoders.length > 0) {
- details += " · " + st.active_decoders.join(", ") + " active";
- }
- }
- if (details) {
- el.innerHTML = "Last applied: " + escHtml(name) + satLabel + ts + '
' + escHtml(details) + "";
- } else {
- el.textContent = "Last applied: " + name + satLabel + ts;
- }
- }
- function apiGetSchedulerLog(rigId) {
- return fetch("/scheduler/" + encodeURIComponent(rigId) + "/log").then(function(r) {
- return r.ok ? r.json() : [];
- });
- }
- function renderActivityLog() {
- const wrap = schedulerEl("scheduler-activity-log-wrap");
- const container = schedulerEl("scheduler-activity-log");
- if (!wrap || !container || !currentRigId) return;
- apiGetSchedulerLog(currentRigId).then(function(entries) {
- if (!entries || entries.length === 0) {
- wrap.style.display = "none";
- return;
- }
- wrap.style.display = "";
- const html = entries.slice().reverse().map(function(e) {
- const d = new Date(e.utc * 1e3);
- const ts = d.toUTCString();
- const action = e.action || "unknown";
- const label = e.entry_label || "";
- const bm = e.bookmark_name || "";
- return '' + escHtml(ts) + ' ' + escHtml(action) + " " + (bm ? '' + escHtml(bm) + "" : "") + (label ? ' (' + escHtml(label) + ")" : "") + "
";
- }).join("");
- container.innerHTML = html;
- }).catch(function() {
- });
- }
- function renderScheduler() {
- const panel = schedulerEl("scheduler-panel");
- if (!panel) return;
- const mode = currentConfig && currentConfig.mode || "disabled";
- const isControl = schedulerRole === "control";
- setSelected("scheduler-mode-select", mode);
- const satEnabled = currentConfig && currentConfig.satellites && currentConfig.satellites.enabled;
- const controlRow = document.querySelector(".scheduler-control-row");
- if (controlRow) controlRow.style.display = mode !== "disabled" || satEnabled ? "" : "none";
- const glSection = schedulerEl("scheduler-grayline-section");
- const tsSection = schedulerEl("scheduler-timespan-section");
- if (glSection) glSection.style.display = mode === "grayline" ? "" : "none";
- if (tsSection) tsSection.style.display = mode === "time_span" ? "" : "none";
- renderSatelliteSection();
- if (mode === "grayline" && currentConfig && currentConfig.grayline) {
- const gl = currentConfig.grayline;
- const lat = gl.lat ?? schedulerWindow.serverLat ?? "";
- const lon = gl.lon ?? schedulerWindow.serverLon ?? "";
- setInputValue("scheduler-gl-lat", lat != null ? lat : "");
- setInputValue("scheduler-gl-lon", lon != null ? lon : "");
- const gridEl = schedulerEl("scheduler-gl-grid");
- if (gridEl) {
- gridEl.value = latLonToGrid(lat, lon);
- }
- setInputValue("scheduler-gl-window", gl.transition_window_min != null ? gl.transition_window_min : 20);
- renderBookmarkSelect("scheduler-gl-dawn", gl.dawn_bookmark_id);
- renderBookmarkSelect("scheduler-gl-day", gl.day_bookmark_id);
- renderBookmarkSelect("scheduler-gl-dusk", gl.dusk_bookmark_id);
- renderBookmarkSelect("scheduler-gl-night", gl.night_bookmark_id);
- } else if (mode === "grayline") {
- const lat = schedulerWindow.serverLat ?? "";
- const lon = schedulerWindow.serverLon ?? "";
- setInputValue("scheduler-gl-lat", lat != null ? lat : "");
- setInputValue("scheduler-gl-lon", lon != null ? lon : "");
- const gridEl2 = schedulerEl("scheduler-gl-grid");
- if (gridEl2 && lat !== "" && lon !== "") {
- gridEl2.value = latLonToGrid(lat, lon);
- }
- setInputValue("scheduler-gl-window", 20);
- renderBookmarkSelect("scheduler-gl-dawn", null);
- renderBookmarkSelect("scheduler-gl-day", null);
- renderBookmarkSelect("scheduler-gl-dusk", null);
- renderBookmarkSelect("scheduler-gl-night", null);
- } else {
- renderBookmarkSelect("scheduler-gl-dawn", null);
- renderBookmarkSelect("scheduler-gl-day", null);
- renderBookmarkSelect("scheduler-gl-dusk", null);
- renderBookmarkSelect("scheduler-gl-night", null);
- }
- const ilEl = schedulerEl("scheduler-ts-interleave");
- if (ilEl) {
- const il = currentConfig && currentConfig.interleave_min;
- ilEl.value = il ? String(il) : "";
- }
- renderTimespanEntries();
- const formEls = panel.querySelectorAll("input, select, button.sch-write");
- formEls.forEach(function(el) {
- el.disabled = !isControl;
- });
- const saveBtn = schedulerEl("scheduler-save-btn");
- if (saveBtn) {
- saveBtn.style.display = isControl ? "" : "none";
- }
- const resetBtn = schedulerEl("scheduler-reset-btn");
- if (resetBtn) {
- resetBtn.style.display = isControl ? "" : "none";
- }
- }
- function setSelected(id, value) {
- const el = schedulerEl(id);
- if (el) el.value = value;
- }
- function setInputValue(id, value) {
- const el = schedulerEl(id);
- if (el) el.value = String(value);
- }
- function renderBookmarkSelect(id, selectedId) {
- const sel = schedulerEl(id);
- if (!sel) return;
- sel.innerHTML = '';
- bookmarkList.forEach(function(bm) {
- const opt = document.createElement("option");
- opt.value = bm.id;
- opt.textContent = bm.name + " (" + formatFreq(bm.freq_hz) + " " + bm.mode + ")";
- if (bm.id === selectedId) opt.selected = true;
- sel.appendChild(opt);
- });
- }
- function formatFreq(hz) {
- if (hz >= 1e6) return (hz / 1e6).toFixed(3) + " MHz";
- if (hz >= 1e3) return (hz / 1e3).toFixed(1) + " kHz";
- return hz + " Hz";
- }
- function schOpenEntryForm(entry, idx) {
- schEntryEditIdx = idx != null ? idx : null;
- const titleEl = schedulerEl("sch-entry-form-title");
- if (titleEl) titleEl.textContent = entry ? "Edit Entry" : "Add Entry";
- const startEl = schedulerEl("scheduler-ts-start");
- const endEl = schedulerEl("scheduler-ts-end");
- const bmEl = schedulerEl("scheduler-ts-bookmark");
- const labelEl = schedulerEl("scheduler-ts-label");
- const ilEl = schedulerEl("scheduler-ts-entry-interleave");
- const centerHzEl = schedulerEl("scheduler-ts-center-hz");
- if (startEl) startEl.value = entry ? minToHHMM(entry.start_min) : "";
- if (endEl) endEl.value = entry ? minToHHMM(entry.end_min) : "";
- if (bmEl) bmEl.value = entry ? entry.bookmark_id || "" : "";
- if (labelEl) labelEl.value = entry ? entry.label || "" : "";
- if (ilEl) ilEl.value = entry?.interleave_min ? String(entry.interleave_min) : "";
- if (centerHzEl) centerHzEl.value = entry?.center_hz ? String(entry.center_hz) : "";
- const recordEl = schedulerEl("scheduler-ts-entry-record");
- if (recordEl) recordEl.checked = !!(entry && entry.record);
- pendingExtraBmIds = entry && Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids.slice() : [];
- renderExtraBmList();
- const wrap = schedulerEl("sch-entry-form-wrap");
- if (wrap) {
- wrap.style.display = "block";
- if (startEl) startEl.focus();
- }
- }
- function schCloseEntryForm() {
- const wrap = schedulerEl("sch-entry-form-wrap");
- if (wrap) wrap.style.display = "none";
- schEntryEditIdx = null;
- pendingExtraBmIds = [];
- }
- function schEntryFormSubmit(e) {
- e.preventDefault();
- const startEl = schedulerEl("scheduler-ts-start");
- const endEl = schedulerEl("scheduler-ts-end");
- const bmEl = schedulerEl("scheduler-ts-bookmark");
- const labelEl = schedulerEl("scheduler-ts-label");
- const ilEl = schedulerEl("scheduler-ts-entry-interleave");
- const centerHzEl = schedulerEl("scheduler-ts-center-hz");
- if (!startEl || !endEl || !bmEl) return;
- const bmId = bmEl.value;
- if (!bmId) {
- schedulerWindow.trxUi.notify?.("Select a primary bookmark before saving.", { kind: "error" });
- return;
- }
- const startMin = hhmmToMin(startEl.value);
- const endMin = hhmmToMin(endEl.value);
- const label = labelEl ? labelEl.value.trim() : "";
- const ilVal = ilEl ? parseInt(ilEl.value, 10) : NaN;
- const entryInterleave = !isNaN(ilVal) && ilVal > 0 ? ilVal : null;
- const centerHzRaw = centerHzEl ? parseInt(centerHzEl.value, 10) : NaN;
- const centerHz = !isNaN(centerHzRaw) && centerHzRaw > 0 ? centerHzRaw : null;
- const extraBmIds = pendingExtraBmIds.slice();
- currentConfig ??= { remote: currentRigId, mode: "time_span", grayline: null, entries: [] };
- const config = currentConfig;
- const recordCb = schedulerEl("scheduler-ts-entry-record");
- const entryRecord = recordCb ? recordCb.checked : false;
- const entryData = {
- id: "ts_" + Date.now().toString(36),
- start_min: startMin,
- end_min: endMin,
- bookmark_id: bmId,
- label: label || null,
- interleave_min: entryInterleave,
- center_hz: centerHz,
- bookmark_ids: extraBmIds,
- record: entryRecord,
- exclusive: false
- };
- if (schEntryEditIdx !== null) {
- const existing = config.entries[schEntryEditIdx];
- if (existing?.id) entryData.id = existing.id;
- config.entries[schEntryEditIdx] = entryData;
- } else {
- config.entries.push(entryData);
- }
- schCloseEntryForm();
- renderTimespanEntries();
- markSchedulerDirty();
- }
- const TIMELINE_COLORS = ["#38bdf8", "#f59e0b", "#a78bfa", "#34d399", "#fb7185", "#60a5fa"];
- function renderTimeline() {
- const container = schedulerEl("scheduler-ts-timeline");
- if (!container) return;
- const entries = currentConfig && Array.isArray(currentConfig.entries) ? currentConfig.entries : [];
- if (entries.length === 0) {
- container.innerHTML = "";
- return;
- }
- const W = 1e3;
- const H = 80;
- const BAR_Y = 6;
- const BAR_H = 30;
- const TICK_Y = BAR_Y + BAR_H + 2;
- let svg = '";
- container.innerHTML = svg;
- container.querySelectorAll(".sch-timeline-seg").forEach(function(seg) {
- seg.addEventListener("click", function() {
- const i = parseInt(seg.getAttribute("data-idx") ?? "", 10);
- const entry = currentConfig && currentConfig.entries ? currentConfig.entries[i] : null;
- if (entry) schOpenEntryForm(entry, i);
- });
- });
- const svgEl = container.querySelector("svg");
- if (svgEl) {
- const timelineSvg = svgEl;
- svgEl.addEventListener("click", function(e) {
- if (e.target instanceof Element && e.target.classList.contains("sch-timeline-seg")) return;
- const rect = timelineSvg.getBoundingClientRect();
- const xPct = (e.clientX - rect.left) / rect.width;
- const clickMin = Math.floor(xPct * 1440);
- const startHour = Math.floor(clickMin / 60);
- const startMin = startHour * 60;
- const endMin = (startHour + 1) % 24 * 60;
- schOpenEntryForm(null);
- const startEl = schedulerEl("scheduler-ts-start");
- const endEl = schedulerEl("scheduler-ts-end");
- if (startEl) startEl.value = minToHHMM(startMin);
- if (endEl) endEl.value = minToHHMM(endMin);
- });
- svgEl.style.cursor = "crosshair";
- }
- }
- function timelineNeedleSvg() {
- const info = schedulerUtcMinuteInfo();
- const nowMin = info.minuteOfDay + info.secondOfMinute / 60;
- const x = nowMin / 1440 * 1e3;
- return '';
- }
- function renderTimelineNeedle() {
- const g = schedulerEl("sch-timeline-needle-g");
- if (g) g.innerHTML = timelineNeedleSvg();
- }
- function schInlineEdit(tr, entry, idx) {
- const bmOptions = bookmarkList.map(function(bm) {
- const sel = bm.id === entry.bookmark_id ? " selected" : "";
- return '";
+ wrap.style.display = "";
+ const html = entries.slice().reverse().map(function(e) {
+ const d = new Date(e.utc * 1e3);
+ const ts = d.toUTCString();
+ const action = e.action || "unknown";
+ const label = e.entry_label || "";
+ const bm = e.bookmark_name || "";
+ return '' + escHtml(ts) + ' ' + escHtml(action) + " " + (bm ? '' + escHtml(bm) + "" : "") + (label ? ' (' + escHtml(label) + ")" : "") + "
";
}).join("");
- const extraBmOptions = '' + bookmarkList.map(function(bm) {
- return '";
- }).join("");
- const inlineExtraIds = Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids.slice() : [];
- tr.innerHTML = '⠇ | | | ' + (entry.center_hz ? formatFreq(entry.center_hz) : "—") + ' | | | | | | | ';
- tr.classList.add("sch-inline-editing");
- const chipsContainer = tr.querySelector(".sch-inline-extra-chips");
- const extraPick = tr.querySelector(".sch-inline-extra-pick");
- const extraAddBtn = tr.querySelector(".sch-inline-extra-add");
- if (!chipsContainer || !extraPick || !extraAddBtn) return;
- const chips = chipsContainer;
- const picker = extraPick;
- function renderInlineExtraChips() {
- chips.innerHTML = "";
- inlineExtraIds.forEach(function(id, i) {
- const chip = document.createElement("span");
- chip.className = "sch-extra-bm-chip";
- const rmBtn = document.createElement("span");
- rmBtn.className = "sch-extra-bm-chip-rm";
- rmBtn.textContent = "×";
- rmBtn.title = "Remove";
- rmBtn.addEventListener("click", function() {
- inlineExtraIds.splice(i, 1);
- renderInlineExtraChips();
- });
- chip.appendChild(rmBtn);
- chip.appendChild(document.createTextNode(" " + bmName(id)));
- chips.appendChild(chip);
- });
- Array.from(picker.options).forEach(function(opt) {
- if (opt.value) opt.disabled = inlineExtraIds.includes(opt.value);
- });
- }
- renderInlineExtraChips();
- extraAddBtn.addEventListener("click", function() {
- if (!picker.value) return;
- if (!inlineExtraIds.includes(picker.value)) {
- inlineExtraIds.push(picker.value);
- renderInlineExtraChips();
- }
- picker.value = "";
- });
- const exclEl = tr.querySelector('[data-field="exclusive"]');
- const ilInput = tr.querySelector('[data-field="interleave"]');
- if (exclEl && ilInput) {
- const exclusiveInput = exclEl;
- const interleaveInput = ilInput;
- exclEl.addEventListener("change", function() {
- interleaveInput.disabled = exclusiveInput.checked;
- if (exclusiveInput.checked) interleaveInput.value = "";
- });
- }
- tr.querySelector(".sch-inline-save")?.addEventListener("click", function() {
- const startEl = tr.querySelector('[data-field="start"]');
- const endEl = tr.querySelector('[data-field="end"]');
- const bmEl = tr.querySelector('[data-field="bookmark"]');
- const labelEl = tr.querySelector('[data-field="label"]');
- const ilEl = tr.querySelector('[data-field="interleave"]');
- const recEl = tr.querySelector('[data-field="record"]');
- const exEl = tr.querySelector('[data-field="exclusive"]');
- if (!startEl || !endEl || !bmEl || !labelEl || !ilEl || !recEl) return;
- if (bmEl && !bmEl.value) {
- schedulerWindow.trxUi.notify?.("Select a bookmark before saving.", { kind: "error" });
- bmEl.focus();
- return;
- }
- entry.start_min = hhmmToMin(startEl.value);
- entry.end_min = hhmmToMin(endEl.value);
- entry.bookmark_id = bmEl.value;
- entry.label = labelEl.value.trim() || null;
- entry.exclusive = exEl ? exEl.checked : false;
- const ilVal = parseInt(ilEl.value, 10);
- entry.interleave_min = entry.exclusive ? null : !isNaN(ilVal) && ilVal > 0 ? ilVal : null;
- entry.bookmark_ids = inlineExtraIds.slice();
- entry.record = recEl.checked;
- if (currentConfig) currentConfig.entries[idx] = entry;
- renderTimespanEntries();
- markSchedulerDirty();
- });
- tr.querySelector(".sch-inline-cancel")?.addEventListener("click", function() {
- renderTimespanEntries();
- });
- }
- function renderTimespanEntries() {
- const tbody = schedulerEl("scheduler-ts-tbody");
- if (!tbody) return;
- tbody.innerHTML = "";
- const entries = currentConfig && Array.isArray(currentConfig.entries) ? currentConfig.entries : [];
- entries.forEach(function(entry, idx) {
- const tr = document.createElement("tr");
- if (currentSchedulerStatus && currentSchedulerStatus.last_entry_id && entry.id && String(entry.id) === String(currentSchedulerStatus.last_entry_id)) {
- tr.classList.add("sch-active");
- }
- const il = entry.exclusive ? "Exclusive" : entry.interleave_min ? String(entry.interleave_min) + " min" : "—";
- const allDay = entry.start_min === entry.end_min;
- const centerCell = entry.center_hz ? formatFreq(entry.center_hz) : "—";
- const extraIds = Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids : [];
- const extraCell = extraIds.length ? extraIds.map(function(id) {
- return escHtml(bmName(id));
- }).join(", ") : "—";
- tr.innerHTML = '⠇ | ' + (allDay ? "All day" : minToHHMM(entry.start_min) + ' (' + minToLocal(entry.start_min) + ")") + " | " + (allDay ? "—" : minToHHMM(entry.end_min) + ' (' + minToLocal(entry.end_min) + ")") + " | " + centerCell + " | " + escHtml(bmName(entry.bookmark_id)) + " | " + extraCell + " | " + escHtml(entry.label || "") + " | " + il + " | " + (entry.record ? "Yes" : "") + ' | | ';
- tbody.appendChild(tr);
- });
- tbody.querySelectorAll(".sch-edit-btn").forEach(function(btn) {
- btn.addEventListener("click", function() {
- const i = parseInt(btn.dataset.idx ?? "", 10);
- const entry = currentConfig && currentConfig.entries ? currentConfig.entries[i] : null;
- const row = btn.closest("tr");
- if (entry && row) schInlineEdit(row, entry, i);
- });
- });
- tbody.querySelectorAll(".sch-remove-btn").forEach(function(btn) {
- btn.addEventListener("click", function() {
- removeEntry(parseInt(btn.dataset.idx ?? "", 10));
- });
- });
- (function() {
- const handles = tbody.querySelectorAll(".sch-drag-handle");
- let dragIdx = null;
- handles.forEach(function(handle, idx) {
- const row = handle.parentElement;
- if (!row) return;
- const dragRow = row;
- handle.addEventListener("dragstart", function(event) {
- const e = event;
- dragIdx = idx;
- dragRow.classList.add("sch-dragging");
- if (e.dataTransfer) {
- e.dataTransfer.effectAllowed = "move";
- e.dataTransfer.setData("text/plain", String(idx));
- }
- });
- dragRow.addEventListener("dragover", function(event) {
- const e = event;
- e.preventDefault();
- if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
- dragRow.classList.add("sch-drag-over");
- });
- dragRow.addEventListener("dragleave", function() {
- dragRow.classList.remove("sch-drag-over");
- });
- dragRow.addEventListener("drop", function(event) {
- const e = event;
- e.preventDefault();
- dragRow.classList.remove("sch-drag-over");
- if (dragIdx === null || dragIdx === idx) return;
- if (!currentConfig) return;
- const entries2 = currentConfig.entries;
- const moved = entries2.splice(dragIdx, 1)[0];
- if (moved) entries2.splice(idx, 0, moved);
- renderTimespanEntries();
- markSchedulerDirty();
- });
- handle.addEventListener("dragend", function() {
- dragRow.classList.remove("sch-dragging");
- dragIdx = null;
- });
- });
- })();
- renderTimeline();
- }
- function bmName(id) {
- const bm = bookmarkList.find(function(b) {
- return b.id === id;
- });
- return bm ? bm.name : String(id || "");
- }
- function minToLocal(min) {
- const now = /* @__PURE__ */ new Date();
- const utcMidnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
- const utcMs = utcMidnight.getTime() + min * 6e4;
- const local = new Date(utcMs);
- return String(local.getHours()).padStart(2, "0") + ":" + String(local.getMinutes()).padStart(2, "0");
- }
- function minToHHMM(min) {
- const h = Math.floor(min / 60) % 24;
- const m = min % 60;
- return String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0");
- }
- function hhmmToMin(str) {
- const parts = str.split(":");
- return parseInt(parts[0] || "0", 10) * 60 + parseInt(parts[1] || "0", 10);
- }
- function gridToLatLon(grid) {
- grid = String(grid).toUpperCase().trim();
- if (grid.length < 4) return null;
- const lonField = grid.charCodeAt(0) - 65;
- const latField = grid.charCodeAt(1) - 65;
- const lonSquare = parseInt(grid.charAt(2), 10);
- const latSquare = parseInt(grid.charAt(3), 10);
- if (isNaN(lonSquare) || isNaN(latSquare) || lonField < 0 || lonField > 17 || latField < 0 || latField > 17) return null;
- let lon = lonField * 20 + lonSquare * 2 - 180;
- let lat = latField * 10 + latSquare * 1 - 90;
- if (grid.length >= 6) {
- const lonSub = grid.charCodeAt(4) - 65;
- const latSub = grid.charCodeAt(5) - 65;
- if (lonSub >= 0 && lonSub < 24 && latSub >= 0 && latSub < 24) {
- lon += lonSub * (2 / 24) + 1 / 24;
- lat += latSub * (1 / 24) + 0.5 / 24;
- }
- } else {
- lon += 1;
- lat += 0.5;
- }
- return { lat, lon };
- }
- function latLonToGrid(lat, lon) {
- lon += 180;
- lat += 90;
- if (isNaN(lon) || isNaN(lat)) return "";
- const lonField = String.fromCharCode(65 + Math.floor(lon / 20));
- const latField = String.fromCharCode(65 + Math.floor(lat / 10));
- const lonSquare = Math.floor(lon % 20 / 2);
- const latSquare = Math.floor(lat % 10);
- const lonSub = String.fromCharCode(97 + Math.floor(lon % 2 / 2 * 24));
- const latSub = String.fromCharCode(97 + Math.floor(lat % 1 * 24));
- return lonField + latField + lonSquare + latSquare + lonSub + latSub;
- }
- function escHtml(s) {
- return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
- }
- function schedulerSelectRelativeEntry(delta) {
- const state = schedulerInterleaveState(currentConfig);
- const rigId = currentRigId;
- if (!rigId || schedulerStepPending || state.activeEntries.length <= 1) return;
- const count = state.activeEntries.length;
- const currentIndex = state.currentIndex >= 0 ? state.currentIndex : 0;
- const targetIndex = (currentIndex + delta + count) % count;
- const target = state.activeEntries[targetIndex];
- if (!target || !target.id) return;
- const targetId = target.id;
- schedulerStepPending = true;
- renderSchedulerStepControls();
- Promise.resolve(schedulerWindow.vchanTakeSchedulerControl?.() ?? null).then(function() {
- return apiActivateSchedulerEntry(rigId, targetId);
- }).then(function(status) {
- currentSchedulerStatus = status || null;
- return Promise.resolve(
- schedulerWindow.vchanToggleSchedulerRelease?.() ?? null
- ).then(function() {
- renderStatus(status);
- renderSchedulerInterleaveStatus();
- showSchedulerToast("Selected " + schedulerEntryDisplayName(target) + ".");
- pollStatus();
- });
- }).catch(function(error) {
- console.error("scheduler entry selection failed", error);
- showSchedulerToast("Scheduler entry selection failed: " + (error instanceof Error ? error.message : String(error)), true);
- }).finally(function() {
- schedulerStepPending = false;
- renderSchedulerStepControls();
- });
- }
- function removeEntry(idx) {
- if (!currentConfig || !currentConfig.entries) return;
- currentConfig.entries.splice(idx, 1);
- renderTimespanEntries();
- markSchedulerDirty();
- }
- function bookmarkExists(id) {
- if (!id) return true;
- return bookmarkList.some(function(bm) {
- return bm.id === id;
- });
- }
- function saveScheduler() {
- const rig = currentRigId;
- if (!rig) return;
- const modeEl = schedulerEl("scheduler-mode-select");
- const rawMode = modeEl ? modeEl.value : "disabled";
- const mode = rawMode === "grayline" || rawMode === "time_span" ? rawMode : "disabled";
- const config = {
- remote: rig,
- mode,
- grayline: null,
- entries: []
- };
- if (mode === "grayline") {
- const lat = parseFloat(schedulerEl("scheduler-gl-lat").value);
- const lon = parseFloat(schedulerEl("scheduler-gl-lon").value);
- const win = parseInt(schedulerEl("scheduler-gl-window").value, 10);
- config.grayline = {
- lat: isNaN(lat) ? 0 : lat,
- lon: isNaN(lon) ? 0 : lon,
- transition_window_min: isNaN(win) ? 20 : win,
- dawn_bookmark_id: selectVal("scheduler-gl-dawn") || null,
- day_bookmark_id: selectVal("scheduler-gl-day") || null,
- dusk_bookmark_id: selectVal("scheduler-gl-dusk") || null,
- night_bookmark_id: selectVal("scheduler-gl-night") || null
- };
- } else if (mode === "time_span") {
- config.entries = currentConfig && currentConfig.entries ? currentConfig.entries : [];
- const ilVal = parseInt(schedulerEl("scheduler-ts-interleave").value, 10);
- config.interleave_min = isNaN(ilVal) || ilVal <= 0 ? null : ilVal;
- }
- config.satellites = collectSatelliteConfig();
- const missingBmErrors = [];
- if (mode === "grayline" && config.grayline) {
- const gl = config.grayline;
- const glFields = [
- ["dawn_bookmark_id", "Grayline dawn"],
- ["day_bookmark_id", "Grayline day"],
- ["dusk_bookmark_id", "Grayline dusk"],
- ["night_bookmark_id", "Grayline night"]
- ];
- glFields.forEach(function(pair) {
- const bookmarkId = gl[pair[0]];
- if (typeof bookmarkId === "string" && !bookmarkExists(bookmarkId)) missingBmErrors.push(pair[1] + " (bookmark " + bookmarkId + ")");
- });
- }
- if (mode === "time_span" && Array.isArray(config.entries)) {
- config.entries.forEach(function(entry, idx) {
- const label = entry.label || "Entry #" + (idx + 1);
- if (!bookmarkExists(entry.bookmark_id)) {
- missingBmErrors.push(label + " primary bookmark (" + entry.bookmark_id + ")");
- }
- const extras = Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids : [];
- extras.forEach(function(id) {
- if (!bookmarkExists(id)) {
- missingBmErrors.push(label + " extra channel (" + id + ")");
- }
- });
- });
- }
- if (config.satellites && Array.isArray(config.satellites.entries)) {
- config.satellites.entries.forEach(function(sat, idx) {
- const satLabel = sat.satellite || "Satellite #" + (idx + 1);
- if (!bookmarkExists(sat.bookmark_id)) {
- missingBmErrors.push(satLabel + " bookmark (" + sat.bookmark_id + ")");
- }
- });
- }
- if (missingBmErrors.length > 0) {
- showSchedulerToast("Missing bookmarks: " + missingBmErrors.join("; "), true);
- return;
- }
- const btn = schedulerEl("scheduler-save-btn");
- if (btn) btn.disabled = true;
- apiPutScheduler(rig, config).then(function(saved) {
- currentConfig = saved;
- renderScheduler();
- clearSchedulerDirty();
- showSchedulerToast("Scheduler saved.");
- }).catch(function(error) {
- showSchedulerToast("Save failed: " + (error instanceof Error ? error.message : String(error)), true);
- }).finally(function() {
- if (btn) btn.disabled = false;
- });
- }
- function selectVal(id) {
- const el = schedulerEl(id);
- return el ? el.value : "";
- }
- async function resetScheduler() {
- const rig = currentRigId;
- if (!rig) return;
- if (!await schedulerWindow.trxUi.confirm({ title: "Reset scheduler?", message: "This rig's scheduler configuration will be reset to Disabled.", confirmLabel: "Reset" })) return;
- apiDeleteScheduler(rig).then(function() {
- currentConfig = {
- remote: rig,
- mode: "disabled",
- grayline: null,
- entries: []
- };
- renderScheduler();
- clearSchedulerDirty();
- showSchedulerToast("Scheduler reset.");
- }).catch(function(error) {
- showSchedulerToast("Reset failed: " + (error instanceof Error ? error.message : String(error)), true);
- });
- }
- function markSchedulerDirty() {
- if (schedulerDirty) return;
- schedulerDirty = true;
- const btn = schedulerEl("scheduler-save-btn");
- if (btn) btn.classList.add("sch-dirty");
- }
- function clearSchedulerDirty() {
- schedulerDirty = false;
- const btn = schedulerEl("scheduler-save-btn");
- if (btn) btn.classList.remove("sch-dirty");
- }
- function showSchedulerToast(msg, isError = false) {
- const el = schedulerEl("scheduler-toast");
- if (!el) return;
- el.textContent = msg;
- el.style.background = isError ? "var(--color-error, #c00)" : "var(--accent-green)";
- el.style.display = "block";
- setTimeout(function() {
- el.style.display = "none";
- }, 3e3);
- }
- function wireSchedulerEvents() {
- const modeEl = schedulerEl("scheduler-mode-select");
- if (modeEl) {
- modeEl.addEventListener("change", function() {
- const mode = modeEl.value === "grayline" || modeEl.value === "time_span" ? modeEl.value : "disabled";
- currentConfig ??= { remote: currentRigId, mode, grayline: null, entries: [] };
- currentConfig.mode = mode;
- renderScheduler();
- });
- }
- const saveBtn = schedulerEl("scheduler-save-btn");
- if (saveBtn) saveBtn.addEventListener("click", saveScheduler);
- const resetBtn = schedulerEl("scheduler-reset-btn");
- if (resetBtn) resetBtn.addEventListener("click", () => {
- void resetScheduler();
- });
- const addBtn = schedulerEl("scheduler-ts-add-btn");
- if (addBtn) addBtn.addEventListener("click", function() {
- schOpenEntryForm(null);
- });
- const entryForm = schedulerEl("sch-entry-form");
- if (entryForm) entryForm.addEventListener("submit", schEntryFormSubmit);
- const cancelBtn = schedulerEl("sch-entry-form-cancel");
- if (cancelBtn) cancelBtn.addEventListener("click", schCloseEntryForm);
- const prevBtn = schedulerEl("scheduler-prev-btn");
- if (prevBtn) prevBtn.addEventListener("click", function() {
- schedulerSelectRelativeEntry(-1);
- });
- const nextBtn = schedulerEl("scheduler-next-btn");
- if (nextBtn) nextBtn.addEventListener("click", function() {
- schedulerSelectRelativeEntry(1);
- });
- const schPanel = schedulerEl("scheduler-panel");
- if (schPanel && !wiredElements.has(schPanel)) {
- wiredElements.add(schPanel);
- schPanel.addEventListener("input", function(e) {
- if (!(e.target instanceof Element) || e.target.closest("#sch-entry-form") || e.target.closest("#sch-sat-form")) return;
- markSchedulerDirty();
- });
- schPanel.addEventListener("change", function(e) {
- if (!(e.target instanceof Element) || e.target.closest("#sch-entry-form") || e.target.closest("#sch-sat-form")) return;
- markSchedulerDirty();
- });
- }
+ container.innerHTML = html;
+ }).catch(function() {
+ });
+ }
+ function renderScheduler() {
+ const panel = schedulerEl("scheduler-panel");
+ if (!panel) return;
+ const mode = currentConfig && currentConfig.mode || "disabled";
+ const isControl = schedulerRole === "control";
+ setSelected("scheduler-mode-select", mode);
+ const satEnabled = currentConfig && currentConfig.satellites && currentConfig.satellites.enabled;
+ const controlRow = document.querySelector(".scheduler-control-row");
+ if (controlRow) controlRow.style.display = mode !== "disabled" || satEnabled ? "" : "none";
+ const glSection = schedulerEl("scheduler-grayline-section");
+ const tsSection = schedulerEl("scheduler-timespan-section");
+ if (glSection) glSection.style.display = mode === "grayline" ? "" : "none";
+ if (tsSection) tsSection.style.display = mode === "time_span" ? "" : "none";
+ renderSatelliteSection();
+ if (mode === "grayline" && currentConfig && currentConfig.grayline) {
+ const gl = currentConfig.grayline;
+ const lat = gl.lat ?? schedulerWindow.serverLat ?? "";
+ const lon = gl.lon ?? schedulerWindow.serverLon ?? "";
+ setInputValue("scheduler-gl-lat", lat != null ? lat : "");
+ setInputValue("scheduler-gl-lon", lon != null ? lon : "");
const gridEl = schedulerEl("scheduler-gl-grid");
if (gridEl) {
- gridEl.addEventListener("input", function() {
- const ll = gridToLatLon(gridEl.value);
- if (ll) {
- setInputValue("scheduler-gl-lat", ll.lat.toFixed(3));
- setInputValue("scheduler-gl-lon", ll.lon.toFixed(3));
- markSchedulerDirty();
+ gridEl.value = latLonToGrid(lat, lon);
+ }
+ setInputValue("scheduler-gl-window", gl.transition_window_min != null ? gl.transition_window_min : 20);
+ renderBookmarkSelect("scheduler-gl-dawn", gl.dawn_bookmark_id);
+ renderBookmarkSelect("scheduler-gl-day", gl.day_bookmark_id);
+ renderBookmarkSelect("scheduler-gl-dusk", gl.dusk_bookmark_id);
+ renderBookmarkSelect("scheduler-gl-night", gl.night_bookmark_id);
+ } else if (mode === "grayline") {
+ const lat = schedulerWindow.serverLat ?? "";
+ const lon = schedulerWindow.serverLon ?? "";
+ setInputValue("scheduler-gl-lat", lat != null ? lat : "");
+ setInputValue("scheduler-gl-lon", lon != null ? lon : "");
+ const gridEl2 = schedulerEl("scheduler-gl-grid");
+ if (gridEl2 && lat !== "" && lon !== "") {
+ gridEl2.value = latLonToGrid(lat, lon);
+ }
+ setInputValue("scheduler-gl-window", 20);
+ renderBookmarkSelect("scheduler-gl-dawn", null);
+ renderBookmarkSelect("scheduler-gl-day", null);
+ renderBookmarkSelect("scheduler-gl-dusk", null);
+ renderBookmarkSelect("scheduler-gl-night", null);
+ } else {
+ renderBookmarkSelect("scheduler-gl-dawn", null);
+ renderBookmarkSelect("scheduler-gl-day", null);
+ renderBookmarkSelect("scheduler-gl-dusk", null);
+ renderBookmarkSelect("scheduler-gl-night", null);
+ }
+ const ilEl = schedulerEl("scheduler-ts-interleave");
+ if (ilEl) {
+ const il = currentConfig && currentConfig.interleave_min;
+ ilEl.value = il ? String(il) : "";
+ }
+ renderTimespanEntries();
+ const formEls = panel.querySelectorAll("input, select, button.sch-write");
+ formEls.forEach(function(el) {
+ el.disabled = !isControl;
+ });
+ const saveBtn = schedulerEl("scheduler-save-btn");
+ if (saveBtn) {
+ saveBtn.style.display = isControl ? "" : "none";
+ }
+ const resetBtn = schedulerEl("scheduler-reset-btn");
+ if (resetBtn) {
+ resetBtn.style.display = isControl ? "" : "none";
+ }
+ }
+ function setSelected(id, value) {
+ const el = schedulerEl(id);
+ if (el) el.value = value;
+ }
+ function setInputValue(id, value) {
+ const el = schedulerEl(id);
+ if (el) el.value = String(value);
+ }
+ function renderBookmarkSelect(id, selectedId) {
+ const sel = schedulerEl(id);
+ if (!sel) return;
+ sel.innerHTML = '';
+ bookmarkList.forEach(function(bm) {
+ const opt = document.createElement("option");
+ opt.value = bm.id;
+ opt.textContent = bm.name + " (" + formatFreq(bm.freq_hz) + " " + bm.mode + ")";
+ if (bm.id === selectedId) opt.selected = true;
+ sel.appendChild(opt);
+ });
+ }
+ function formatFreq(hz) {
+ if (hz >= 1e6) return (hz / 1e6).toFixed(3) + " MHz";
+ if (hz >= 1e3) return (hz / 1e3).toFixed(1) + " kHz";
+ return hz + " Hz";
+ }
+ function schOpenEntryForm(entry, idx) {
+ schEntryEditIdx = idx != null ? idx : null;
+ const titleEl = schedulerEl("sch-entry-form-title");
+ if (titleEl) titleEl.textContent = entry ? "Edit Entry" : "Add Entry";
+ const startEl = schedulerEl("scheduler-ts-start");
+ const endEl = schedulerEl("scheduler-ts-end");
+ const bmEl = schedulerEl("scheduler-ts-bookmark");
+ const labelEl = schedulerEl("scheduler-ts-label");
+ const ilEl = schedulerEl("scheduler-ts-entry-interleave");
+ const centerHzEl = schedulerEl("scheduler-ts-center-hz");
+ if (startEl) startEl.value = entry ? minToHHMM(entry.start_min) : "";
+ if (endEl) endEl.value = entry ? minToHHMM(entry.end_min) : "";
+ if (bmEl) bmEl.value = entry ? entry.bookmark_id || "" : "";
+ if (labelEl) labelEl.value = entry ? entry.label || "" : "";
+ if (ilEl) ilEl.value = entry?.interleave_min ? String(entry.interleave_min) : "";
+ if (centerHzEl) centerHzEl.value = entry?.center_hz ? String(entry.center_hz) : "";
+ const recordEl = schedulerEl("scheduler-ts-entry-record");
+ if (recordEl) recordEl.checked = !!(entry && entry.record);
+ pendingExtraBmIds = entry && Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids.slice() : [];
+ renderExtraBmList();
+ const wrap = schedulerEl("sch-entry-form-wrap");
+ if (wrap) {
+ wrap.style.display = "block";
+ if (startEl) startEl.focus();
+ }
+ }
+ function schCloseEntryForm() {
+ const wrap = schedulerEl("sch-entry-form-wrap");
+ if (wrap) wrap.style.display = "none";
+ schEntryEditIdx = null;
+ pendingExtraBmIds = [];
+ }
+ function schEntryFormSubmit(e) {
+ e.preventDefault();
+ const startEl = schedulerEl("scheduler-ts-start");
+ const endEl = schedulerEl("scheduler-ts-end");
+ const bmEl = schedulerEl("scheduler-ts-bookmark");
+ const labelEl = schedulerEl("scheduler-ts-label");
+ const ilEl = schedulerEl("scheduler-ts-entry-interleave");
+ const centerHzEl = schedulerEl("scheduler-ts-center-hz");
+ if (!startEl || !endEl || !bmEl) return;
+ const bmId = bmEl.value;
+ if (!bmId) {
+ schedulerWindow.trxUi.notify?.("Select a primary bookmark before saving.", { kind: "error" });
+ return;
+ }
+ const startMin = hhmmToMin(startEl.value);
+ const endMin = hhmmToMin(endEl.value);
+ const label = labelEl ? labelEl.value.trim() : "";
+ const ilVal = ilEl ? parseInt(ilEl.value, 10) : NaN;
+ const entryInterleave = !isNaN(ilVal) && ilVal > 0 ? ilVal : null;
+ const centerHzRaw = centerHzEl ? parseInt(centerHzEl.value, 10) : NaN;
+ const centerHz = !isNaN(centerHzRaw) && centerHzRaw > 0 ? centerHzRaw : null;
+ const extraBmIds = pendingExtraBmIds.slice();
+ currentConfig ??= { remote: currentRigId, mode: "time_span", grayline: null, entries: [] };
+ const config = currentConfig;
+ const recordCb = schedulerEl("scheduler-ts-entry-record");
+ const entryRecord = recordCb ? recordCb.checked : false;
+ const entryData = {
+ id: "ts_" + Date.now().toString(36),
+ start_min: startMin,
+ end_min: endMin,
+ bookmark_id: bmId,
+ label: label || null,
+ interleave_min: entryInterleave,
+ center_hz: centerHz,
+ bookmark_ids: extraBmIds,
+ record: entryRecord,
+ exclusive: false
+ };
+ if (schEntryEditIdx !== null) {
+ const existing = config.entries[schEntryEditIdx];
+ if (existing?.id) entryData.id = existing.id;
+ config.entries[schEntryEditIdx] = entryData;
+ } else {
+ config.entries.push(entryData);
+ }
+ schCloseEntryForm();
+ renderTimespanEntries();
+ markSchedulerDirty();
+ }
+ const TIMELINE_COLORS = ["#38bdf8", "#f59e0b", "#a78bfa", "#34d399", "#fb7185", "#60a5fa"];
+ function renderTimeline() {
+ const container = schedulerEl("scheduler-ts-timeline");
+ if (!container) return;
+ const entries = currentConfig && Array.isArray(currentConfig.entries) ? currentConfig.entries : [];
+ if (entries.length === 0) {
+ container.innerHTML = "";
+ return;
+ }
+ const W = 1e3;
+ const H = 80;
+ const BAR_Y = 6;
+ const BAR_H = 30;
+ const TICK_Y = BAR_Y + BAR_H + 2;
+ let svg = '";
+ container.innerHTML = svg;
+ container.querySelectorAll(".sch-timeline-seg").forEach(function(seg) {
+ seg.addEventListener("click", function() {
+ const i = parseInt(seg.getAttribute("data-idx") ?? "", 10);
+ const entry = currentConfig && currentConfig.entries ? currentConfig.entries[i] : null;
+ if (entry) schOpenEntryForm(entry, i);
+ });
+ });
+ const svgEl = container.querySelector("svg");
+ if (svgEl) {
+ const timelineSvg = svgEl;
+ svgEl.addEventListener("click", function(e) {
+ if (e.target instanceof Element && e.target.classList.contains("sch-timeline-seg")) return;
+ const rect = timelineSvg.getBoundingClientRect();
+ const xPct = (e.clientX - rect.left) / rect.width;
+ const clickMin = Math.floor(xPct * 1440);
+ const startHour = Math.floor(clickMin / 60);
+ const startMin = startHour * 60;
+ const endMin = (startHour + 1) % 24 * 60;
+ schOpenEntryForm(null);
+ const startEl = schedulerEl("scheduler-ts-start");
+ const endEl = schedulerEl("scheduler-ts-end");
+ if (startEl) startEl.value = minToHHMM(startMin);
+ if (endEl) endEl.value = minToHHMM(endMin);
+ });
+ svgEl.style.cursor = "crosshair";
+ }
+ }
+ function timelineNeedleSvg() {
+ const info = schedulerUtcMinuteInfo();
+ const nowMin = info.minuteOfDay + info.secondOfMinute / 60;
+ const x = nowMin / 1440 * 1e3;
+ return '';
+ }
+ function renderTimelineNeedle() {
+ const g = schedulerEl("sch-timeline-needle-g");
+ if (g) g.innerHTML = timelineNeedleSvg();
+ }
+ function schInlineEdit(tr, entry, idx) {
+ const bmOptions = bookmarkList.map(function(bm) {
+ const sel = bm.id === entry.bookmark_id ? " selected" : "";
+ return '";
+ }).join("");
+ const extraBmOptions = '' + bookmarkList.map(function(bm) {
+ return '";
+ }).join("");
+ const inlineExtraIds = Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids.slice() : [];
+ tr.innerHTML = '⠇ | | | ' + (entry.center_hz ? formatFreq(entry.center_hz) : "—") + ' | | | | | | | ';
+ tr.classList.add("sch-inline-editing");
+ const chipsContainer = tr.querySelector(".sch-inline-extra-chips");
+ const extraPick = tr.querySelector(".sch-inline-extra-pick");
+ const extraAddBtn = tr.querySelector(".sch-inline-extra-add");
+ if (!chipsContainer || !extraPick || !extraAddBtn) return;
+ const chips = chipsContainer;
+ const picker = extraPick;
+ function renderInlineExtraChips() {
+ chips.innerHTML = "";
+ inlineExtraIds.forEach(function(id, i) {
const chip = document.createElement("span");
chip.className = "sch-extra-bm-chip";
const rmBtn = document.createElement("span");
@@ -1130,94 +654,567 @@
rmBtn.textContent = "×";
rmBtn.title = "Remove";
rmBtn.addEventListener("click", function() {
- pendingExtraBmIds.splice(idx, 1);
- renderExtraBmList();
+ inlineExtraIds.splice(i, 1);
+ renderInlineExtraChips();
});
chip.appendChild(rmBtn);
- const label = document.createTextNode(" " + (bm ? bm.name : id));
- chip.appendChild(label);
- container.appendChild(chip);
+ chip.appendChild(document.createTextNode(" " + bmName(id)));
+ chips.appendChild(chip);
});
- const pick = schedulerEl("scheduler-ts-extra-bm-pick");
- if (pick) {
- Array.from(pick.options).forEach(function(opt) {
- if (opt.value) {
- opt.disabled = pendingExtraBmIds.includes(opt.value);
+ Array.from(picker.options).forEach(function(opt) {
+ if (opt.value) opt.disabled = inlineExtraIds.includes(opt.value);
+ });
+ }
+ renderInlineExtraChips();
+ extraAddBtn.addEventListener("click", function() {
+ if (!picker.value) return;
+ if (!inlineExtraIds.includes(picker.value)) {
+ inlineExtraIds.push(picker.value);
+ renderInlineExtraChips();
+ }
+ picker.value = "";
+ });
+ const exclEl = tr.querySelector('[data-field="exclusive"]');
+ const ilInput = tr.querySelector('[data-field="interleave"]');
+ if (exclEl && ilInput) {
+ const exclusiveInput = exclEl;
+ const interleaveInput = ilInput;
+ exclEl.addEventListener("change", function() {
+ interleaveInput.disabled = exclusiveInput.checked;
+ if (exclusiveInput.checked) interleaveInput.value = "";
+ });
+ }
+ tr.querySelector(".sch-inline-save")?.addEventListener("click", function() {
+ const startEl = tr.querySelector('[data-field="start"]');
+ const endEl = tr.querySelector('[data-field="end"]');
+ const bmEl = tr.querySelector('[data-field="bookmark"]');
+ const labelEl = tr.querySelector('[data-field="label"]');
+ const ilEl = tr.querySelector('[data-field="interleave"]');
+ const recEl = tr.querySelector('[data-field="record"]');
+ const exEl = tr.querySelector('[data-field="exclusive"]');
+ if (!startEl || !endEl || !bmEl || !labelEl || !ilEl || !recEl) return;
+ if (bmEl && !bmEl.value) {
+ schedulerWindow.trxUi.notify?.("Select a bookmark before saving.", { kind: "error" });
+ bmEl.focus();
+ return;
+ }
+ entry.start_min = hhmmToMin(startEl.value);
+ entry.end_min = hhmmToMin(endEl.value);
+ entry.bookmark_id = bmEl.value;
+ entry.label = labelEl.value.trim() || null;
+ entry.exclusive = exEl ? exEl.checked : false;
+ const ilVal = parseInt(ilEl.value, 10);
+ entry.interleave_min = entry.exclusive ? null : !isNaN(ilVal) && ilVal > 0 ? ilVal : null;
+ entry.bookmark_ids = inlineExtraIds.slice();
+ entry.record = recEl.checked;
+ if (currentConfig) currentConfig.entries[idx] = entry;
+ renderTimespanEntries();
+ markSchedulerDirty();
+ });
+ tr.querySelector(".sch-inline-cancel")?.addEventListener("click", function() {
+ renderTimespanEntries();
+ });
+ }
+ function renderTimespanEntries() {
+ const tbody = schedulerEl("scheduler-ts-tbody");
+ if (!tbody) return;
+ tbody.innerHTML = "";
+ const entries = currentConfig && Array.isArray(currentConfig.entries) ? currentConfig.entries : [];
+ entries.forEach(function(entry, idx) {
+ const tr = document.createElement("tr");
+ if (currentSchedulerStatus && currentSchedulerStatus.last_entry_id && entry.id && String(entry.id) === String(currentSchedulerStatus.last_entry_id)) {
+ tr.classList.add("sch-active");
+ }
+ const il = entry.exclusive ? "Exclusive" : entry.interleave_min ? String(entry.interleave_min) + " min" : "—";
+ const allDay = entry.start_min === entry.end_min;
+ const centerCell = entry.center_hz ? formatFreq(entry.center_hz) : "—";
+ const extraIds = Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids : [];
+ const extraCell = extraIds.length ? extraIds.map(function(id) {
+ return escHtml(bmName(id));
+ }).join(", ") : "—";
+ tr.innerHTML = '⠇ | ' + (allDay ? "All day" : minToHHMM(entry.start_min) + ' (' + minToLocal(entry.start_min) + ")") + " | " + (allDay ? "—" : minToHHMM(entry.end_min) + ' (' + minToLocal(entry.end_min) + ")") + " | " + centerCell + " | " + escHtml(bmName(entry.bookmark_id)) + " | " + extraCell + " | " + escHtml(entry.label || "") + " | " + il + " | " + (entry.record ? "Yes" : "") + ' | | ';
+ tbody.appendChild(tr);
+ });
+ tbody.querySelectorAll(".sch-edit-btn").forEach(function(btn) {
+ btn.addEventListener("click", function() {
+ const i = parseInt(btn.dataset.idx ?? "", 10);
+ const entry = currentConfig && currentConfig.entries ? currentConfig.entries[i] : null;
+ const row = btn.closest("tr");
+ if (entry && row) schInlineEdit(row, entry, i);
+ });
+ });
+ tbody.querySelectorAll(".sch-remove-btn").forEach(function(btn) {
+ btn.addEventListener("click", function() {
+ removeEntry(parseInt(btn.dataset.idx ?? "", 10));
+ });
+ });
+ (function() {
+ const handles = tbody.querySelectorAll(".sch-drag-handle");
+ let dragIdx = null;
+ handles.forEach(function(handle, idx) {
+ const row = handle.parentElement;
+ if (!row) return;
+ const dragRow = row;
+ handle.addEventListener("dragstart", function(event) {
+ const e = event;
+ dragIdx = idx;
+ dragRow.classList.add("sch-dragging");
+ if (e.dataTransfer) {
+ e.dataTransfer.effectAllowed = "move";
+ e.dataTransfer.setData("text/plain", String(idx));
+ }
+ });
+ dragRow.addEventListener("dragover", function(event) {
+ const e = event;
+ e.preventDefault();
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
+ dragRow.classList.add("sch-drag-over");
+ });
+ dragRow.addEventListener("dragleave", function() {
+ dragRow.classList.remove("sch-drag-over");
+ });
+ dragRow.addEventListener("drop", function(event) {
+ const e = event;
+ e.preventDefault();
+ dragRow.classList.remove("sch-drag-over");
+ if (dragIdx === null || dragIdx === idx) return;
+ if (!currentConfig) return;
+ const entries2 = currentConfig.entries;
+ const moved = entries2.splice(dragIdx, 1)[0];
+ if (moved) entries2.splice(idx, 0, moved);
+ renderTimespanEntries();
+ markSchedulerDirty();
+ });
+ handle.addEventListener("dragend", function() {
+ dragRow.classList.remove("sch-dragging");
+ dragIdx = null;
+ });
+ });
+ })();
+ renderTimeline();
+ }
+ function bmName(id) {
+ const bm = bookmarkList.find(function(b) {
+ return b.id === id;
+ });
+ return bm ? bm.name : String(id || "");
+ }
+ function minToLocal(min) {
+ const now = /* @__PURE__ */ new Date();
+ const utcMidnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
+ const utcMs = utcMidnight.getTime() + min * 6e4;
+ const local = new Date(utcMs);
+ return String(local.getHours()).padStart(2, "0") + ":" + String(local.getMinutes()).padStart(2, "0");
+ }
+ function minToHHMM(min) {
+ const h = Math.floor(min / 60) % 24;
+ const m = min % 60;
+ return String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0");
+ }
+ function hhmmToMin(str) {
+ const parts = str.split(":");
+ return parseInt(parts[0] || "0", 10) * 60 + parseInt(parts[1] || "0", 10);
+ }
+ function gridToLatLon(grid) {
+ grid = String(grid).toUpperCase().trim();
+ if (grid.length < 4) return null;
+ const lonField = grid.charCodeAt(0) - 65;
+ const latField = grid.charCodeAt(1) - 65;
+ const lonSquare = parseInt(grid.charAt(2), 10);
+ const latSquare = parseInt(grid.charAt(3), 10);
+ if (isNaN(lonSquare) || isNaN(latSquare) || lonField < 0 || lonField > 17 || latField < 0 || latField > 17) return null;
+ let lon = lonField * 20 + lonSquare * 2 - 180;
+ let lat = latField * 10 + latSquare * 1 - 90;
+ if (grid.length >= 6) {
+ const lonSub = grid.charCodeAt(4) - 65;
+ const latSub = grid.charCodeAt(5) - 65;
+ if (lonSub >= 0 && lonSub < 24 && latSub >= 0 && latSub < 24) {
+ lon += lonSub * (2 / 24) + 1 / 24;
+ lat += latSub * (1 / 24) + 0.5 / 24;
+ }
+ } else {
+ lon += 1;
+ lat += 0.5;
+ }
+ return { lat, lon };
+ }
+ function latLonToGrid(lat, lon) {
+ lon += 180;
+ lat += 90;
+ if (isNaN(lon) || isNaN(lat)) return "";
+ const lonField = String.fromCharCode(65 + Math.floor(lon / 20));
+ const latField = String.fromCharCode(65 + Math.floor(lat / 10));
+ const lonSquare = Math.floor(lon % 20 / 2);
+ const latSquare = Math.floor(lat % 10);
+ const lonSub = String.fromCharCode(97 + Math.floor(lon % 2 / 2 * 24));
+ const latSub = String.fromCharCode(97 + Math.floor(lat % 1 * 24));
+ return lonField + latField + lonSquare + latSquare + lonSub + latSub;
+ }
+ function escHtml(s) {
+ return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+ }
+ function schedulerSelectRelativeEntry(delta) {
+ const state = schedulerInterleaveState(currentConfig);
+ const rigId = currentRigId;
+ if (!rigId || schedulerStepPending || state.activeEntries.length <= 1) return;
+ const count = state.activeEntries.length;
+ const currentIndex = state.currentIndex >= 0 ? state.currentIndex : 0;
+ const targetIndex = (currentIndex + delta + count) % count;
+ const target = state.activeEntries[targetIndex];
+ if (!target || !target.id) return;
+ const targetId = target.id;
+ schedulerStepPending = true;
+ renderSchedulerStepControls();
+ Promise.resolve(schedulerWindow.vchanTakeSchedulerControl?.() ?? null).then(function() {
+ return apiActivateSchedulerEntry(rigId, targetId);
+ }).then(function(status) {
+ currentSchedulerStatus = status || null;
+ return Promise.resolve(
+ schedulerWindow.vchanToggleSchedulerRelease?.() ?? null
+ ).then(function() {
+ renderStatus(status);
+ renderSchedulerInterleaveStatus();
+ showSchedulerToast("Selected " + schedulerEntryDisplayName(target) + ".");
+ pollStatus();
+ });
+ }).catch(function(error) {
+ console.error("scheduler entry selection failed", error);
+ showSchedulerToast("Scheduler entry selection failed: " + (error instanceof Error ? error.message : String(error)), true);
+ }).finally(function() {
+ schedulerStepPending = false;
+ renderSchedulerStepControls();
+ });
+ }
+ function removeEntry(idx) {
+ if (!currentConfig || !currentConfig.entries) return;
+ currentConfig.entries.splice(idx, 1);
+ renderTimespanEntries();
+ markSchedulerDirty();
+ }
+ function bookmarkExists(id) {
+ if (!id) return true;
+ return bookmarkList.some(function(bm) {
+ return bm.id === id;
+ });
+ }
+ function saveScheduler() {
+ const rig = currentRigId;
+ if (!rig) return;
+ const modeEl = schedulerEl("scheduler-mode-select");
+ const rawMode = modeEl ? modeEl.value : "disabled";
+ const mode = rawMode === "grayline" || rawMode === "time_span" ? rawMode : "disabled";
+ const config = {
+ remote: rig,
+ mode,
+ grayline: null,
+ entries: []
+ };
+ if (mode === "grayline") {
+ const lat = parseFloat(schedulerEl("scheduler-gl-lat").value);
+ const lon = parseFloat(schedulerEl("scheduler-gl-lon").value);
+ const win = parseInt(schedulerEl("scheduler-gl-window").value, 10);
+ config.grayline = {
+ lat: isNaN(lat) ? 0 : lat,
+ lon: isNaN(lon) ? 0 : lon,
+ transition_window_min: isNaN(win) ? 20 : win,
+ dawn_bookmark_id: selectVal("scheduler-gl-dawn") || null,
+ day_bookmark_id: selectVal("scheduler-gl-day") || null,
+ dusk_bookmark_id: selectVal("scheduler-gl-dusk") || null,
+ night_bookmark_id: selectVal("scheduler-gl-night") || null
+ };
+ } else if (mode === "time_span") {
+ config.entries = currentConfig && currentConfig.entries ? currentConfig.entries : [];
+ const ilVal = parseInt(schedulerEl("scheduler-ts-interleave").value, 10);
+ config.interleave_min = isNaN(ilVal) || ilVal <= 0 ? null : ilVal;
+ }
+ config.satellites = collectSatelliteConfig();
+ const missingBmErrors = [];
+ if (mode === "grayline" && config.grayline) {
+ const gl = config.grayline;
+ const glFields = [
+ ["dawn_bookmark_id", "Grayline dawn"],
+ ["day_bookmark_id", "Grayline day"],
+ ["dusk_bookmark_id", "Grayline dusk"],
+ ["night_bookmark_id", "Grayline night"]
+ ];
+ glFields.forEach(function(pair) {
+ const bookmarkId = gl[pair[0]];
+ if (typeof bookmarkId === "string" && !bookmarkExists(bookmarkId)) missingBmErrors.push(pair[1] + " (bookmark " + bookmarkId + ")");
+ });
+ }
+ if (mode === "time_span" && Array.isArray(config.entries)) {
+ config.entries.forEach(function(entry, idx) {
+ const label = entry.label || "Entry #" + (idx + 1);
+ if (!bookmarkExists(entry.bookmark_id)) {
+ missingBmErrors.push(label + " primary bookmark (" + entry.bookmark_id + ")");
+ }
+ const extras = Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids : [];
+ extras.forEach(function(id) {
+ if (!bookmarkExists(id)) {
+ missingBmErrors.push(label + " extra channel (" + id + ")");
+ }
+ });
+ });
+ }
+ if (config.satellites && Array.isArray(config.satellites.entries)) {
+ config.satellites.entries.forEach(function(sat, idx) {
+ const satLabel = sat.satellite || "Satellite #" + (idx + 1);
+ if (!bookmarkExists(sat.bookmark_id)) {
+ missingBmErrors.push(satLabel + " bookmark (" + sat.bookmark_id + ")");
+ }
+ });
+ }
+ if (missingBmErrors.length > 0) {
+ showSchedulerToast("Missing bookmarks: " + missingBmErrors.join("; "), true);
+ return;
+ }
+ const btn = schedulerEl("scheduler-save-btn");
+ if (btn) btn.disabled = true;
+ apiPutScheduler(rig, config).then(function(saved) {
+ currentConfig = saved;
+ renderScheduler();
+ clearSchedulerDirty();
+ showSchedulerToast("Scheduler saved.");
+ }).catch(function(error) {
+ showSchedulerToast("Save failed: " + (error instanceof Error ? error.message : String(error)), true);
+ }).finally(function() {
+ if (btn) btn.disabled = false;
+ });
+ }
+ function selectVal(id) {
+ const el = schedulerEl(id);
+ return el ? el.value : "";
+ }
+ async function resetScheduler() {
+ const rig = currentRigId;
+ if (!rig) return;
+ if (!await schedulerWindow.trxUi.confirm({ title: "Reset scheduler?", message: "This rig's scheduler configuration will be reset to Disabled.", confirmLabel: "Reset" })) return;
+ apiDeleteScheduler(rig).then(function() {
+ currentConfig = {
+ remote: rig,
+ mode: "disabled",
+ grayline: null,
+ entries: []
+ };
+ renderScheduler();
+ clearSchedulerDirty();
+ showSchedulerToast("Scheduler reset.");
+ }).catch(function(error) {
+ showSchedulerToast("Reset failed: " + (error instanceof Error ? error.message : String(error)), true);
+ });
+ }
+ function markSchedulerDirty() {
+ if (schedulerDirty) return;
+ schedulerDirty = true;
+ const btn = schedulerEl("scheduler-save-btn");
+ if (btn) btn.classList.add("sch-dirty");
+ }
+ function clearSchedulerDirty() {
+ schedulerDirty = false;
+ const btn = schedulerEl("scheduler-save-btn");
+ if (btn) btn.classList.remove("sch-dirty");
+ }
+ function showSchedulerToast(msg, isError = false) {
+ const el = schedulerEl("scheduler-toast");
+ if (!el) return;
+ el.textContent = msg;
+ el.style.background = isError ? "var(--color-error, #c00)" : "var(--accent-green)";
+ el.style.display = "block";
+ setTimeout(function() {
+ el.style.display = "none";
+ }, 3e3);
+ }
+ function wireSchedulerEvents() {
+ const modeEl = schedulerEl("scheduler-mode-select");
+ if (modeEl) {
+ modeEl.addEventListener("change", function() {
+ const mode = modeEl.value === "grayline" || modeEl.value === "time_span" ? modeEl.value : "disabled";
+ currentConfig ??= { remote: currentRigId, mode, grayline: null, entries: [] };
+ currentConfig.mode = mode;
+ renderScheduler();
+ });
+ }
+ const saveBtn = schedulerEl("scheduler-save-btn");
+ if (saveBtn) saveBtn.addEventListener("click", saveScheduler);
+ const resetBtn = schedulerEl("scheduler-reset-btn");
+ if (resetBtn) resetBtn.addEventListener("click", () => {
+ void resetScheduler();
+ });
+ const addBtn = schedulerEl("scheduler-ts-add-btn");
+ if (addBtn) addBtn.addEventListener("click", function() {
+ schOpenEntryForm(null);
+ });
+ const entryForm = schedulerEl("sch-entry-form");
+ if (entryForm) entryForm.addEventListener("submit", schEntryFormSubmit);
+ const cancelBtn = schedulerEl("sch-entry-form-cancel");
+ if (cancelBtn) cancelBtn.addEventListener("click", schCloseEntryForm);
+ const prevBtn = schedulerEl("scheduler-prev-btn");
+ if (prevBtn) prevBtn.addEventListener("click", function() {
+ schedulerSelectRelativeEntry(-1);
+ });
+ const nextBtn = schedulerEl("scheduler-next-btn");
+ if (nextBtn) nextBtn.addEventListener("click", function() {
+ schedulerSelectRelativeEntry(1);
+ });
+ const schPanel = schedulerEl("scheduler-panel");
+ if (schPanel && !wiredElements.has(schPanel)) {
+ wiredElements.add(schPanel);
+ schPanel.addEventListener("input", function(e) {
+ if (!(e.target instanceof Element) || e.target.closest("#sch-entry-form") || e.target.closest("#sch-sat-form")) return;
+ markSchedulerDirty();
+ });
+ schPanel.addEventListener("change", function(e) {
+ if (!(e.target instanceof Element) || e.target.closest("#sch-entry-form") || e.target.closest("#sch-sat-form")) return;
+ markSchedulerDirty();
+ });
+ }
+ const gridEl = schedulerEl("scheduler-gl-grid");
+ if (gridEl) {
+ gridEl.addEventListener("input", function() {
+ const ll = gridToLatLon(gridEl.value);
+ if (ll) {
+ setInputValue("scheduler-gl-lat", ll.lat.toFixed(3));
+ setInputValue("scheduler-gl-lon", ll.lon.toFixed(3));
+ markSchedulerDirty();
+ }
+ });
+ }
+ const latEl = schedulerEl("scheduler-gl-lat");
+ const lonEl = schedulerEl("scheduler-gl-lon");
+ [latEl, lonEl].forEach(function(el) {
+ if (el) {
+ el.addEventListener("input", function() {
+ const la = parseFloat(schedulerEl("scheduler-gl-lat").value);
+ const lo = parseFloat(schedulerEl("scheduler-gl-lon").value);
+ const gEl = schedulerEl("scheduler-gl-grid");
+ if (gEl && !isNaN(la) && !isNaN(lo)) {
+ gEl.value = latLonToGrid(la, lo);
}
});
}
- }
- function wireExtraBmAdd() {
- const addBtn = schedulerEl("scheduler-ts-extra-bm-add");
- if (!addBtn || wiredElements.has(addBtn)) return;
- wiredElements.add(addBtn);
- addBtn.addEventListener("click", function() {
- const pick = schedulerEl("scheduler-ts-extra-bm-pick");
- if (!pick || !pick.value) return;
- if (!pendingExtraBmIds.includes(pick.value)) {
- pendingExtraBmIds.push(pick.value);
- renderExtraBmList();
- }
- pick.value = "";
- });
- }
- function renderSatelliteSection() {
- schedulerWindow.satScheduler?.renderSection();
- }
- function renderSatPassStatus() {
- schedulerWindow.satScheduler?.renderPassStatus();
- }
- function collectSatelliteConfig() {
- return schedulerWindow.satScheduler ? schedulerWindow.satScheduler.collectSatelliteConfig() : { enabled: false, pretune_secs: 60, entries: [] };
- }
- function wireSatelliteEvents() {
- schedulerWindow.satScheduler?.wireEvents();
- }
- function isInputFocused() {
- const el = document.activeElement;
- if (!el) return false;
- const tag = el.tagName;
- return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el instanceof HTMLElement && el.isContentEditable;
- }
- document.addEventListener("keydown", function(e) {
- if (isInputFocused()) return;
- if (e.shiftKey && e.key === "R") {
- e.preventDefault();
- const releaseBtn = schedulerEl("scheduler-release-btn");
- if (releaseBtn && !releaseBtn.disabled) releaseBtn.click();
- } else if (e.shiftKey && e.key === "N") {
- e.preventDefault();
- schedulerSelectRelativeEntry(1);
- } else if (e.shiftKey && e.key === "P") {
- e.preventDefault();
- schedulerSelectRelativeEntry(-1);
- }
});
- (function() {
- const details = document.querySelector(".sch-ts-details");
- if (!details) return;
- const schedulerDetails = details;
- const key = "sch-details-open";
- const saved = localStorage.getItem(key);
- if (saved !== null) schedulerDetails.open = saved === "1";
- details.addEventListener("toggle", function() {
- localStorage.setItem(key, schedulerDetails.open ? "1" : "0");
+ wireExtraBmAdd();
+ wireSatelliteEvents();
+ }
+ function populateTsBookmarkSelect() {
+ const sel = schedulerEl("scheduler-ts-bookmark");
+ const extraSel = schedulerEl("scheduler-ts-extra-bm-pick");
+ [sel, extraSel].forEach(function(el) {
+ if (!el) return;
+ const prev = el.value;
+ el.innerHTML = '';
+ bookmarkList.forEach(function(bm) {
+ const opt = document.createElement("option");
+ opt.value = bm.id;
+ opt.textContent = bm.name + " (" + formatFreq(bm.freq_hz) + " " + bm.mode + ")";
+ el.appendChild(opt);
+ });
+ if (prev) el.value = prev;
+ });
+ }
+ let pendingExtraBmIds = [];
+ function renderExtraBmList() {
+ const container = schedulerEl("scheduler-ts-extra-bm-list");
+ if (!container) return;
+ container.innerHTML = "";
+ pendingExtraBmIds.forEach(function(id, idx) {
+ const bm = bookmarkList.find(function(b) {
+ return b.id === id;
+ });
+ const chip = document.createElement("span");
+ chip.className = "sch-extra-bm-chip";
+ const rmBtn = document.createElement("span");
+ rmBtn.className = "sch-extra-bm-chip-rm";
+ rmBtn.textContent = "×";
+ rmBtn.title = "Remove";
+ rmBtn.addEventListener("click", function() {
+ pendingExtraBmIds.splice(idx, 1);
+ renderExtraBmList();
+ });
+ chip.appendChild(rmBtn);
+ const label = document.createTextNode(" " + (bm ? bm.name : id));
+ chip.appendChild(label);
+ container.appendChild(chip);
+ });
+ const pick = schedulerEl("scheduler-ts-extra-bm-pick");
+ if (pick) {
+ Array.from(pick.options).forEach(function(opt) {
+ if (opt.value) {
+ opt.disabled = pendingExtraBmIds.includes(opt.value);
+ }
});
- })();
- const schedulerService = {
- initialize: initScheduler,
- destroy: destroyScheduler,
- wireEvents: wireSchedulerEvents,
- setRig: setSchedulerRig,
- getConfig: () => currentConfig,
- getStatus: () => currentSchedulerStatus,
- getBookmarks: () => bookmarkList,
- markDirty: markSchedulerDirty
- };
- schedulerWindow.trx.modules.scheduler = schedulerService;
- if (schedulerWindow.authRole != null) {
- initScheduler(schedulerWindow.lastActiveRigId ?? null, schedulerWindow.authRole);
- wireSchedulerEvents();
}
+ }
+ function wireExtraBmAdd() {
+ const addBtn = schedulerEl("scheduler-ts-extra-bm-add");
+ if (!addBtn || wiredElements.has(addBtn)) return;
+ wiredElements.add(addBtn);
+ addBtn.addEventListener("click", function() {
+ const pick = schedulerEl("scheduler-ts-extra-bm-pick");
+ if (!pick || !pick.value) return;
+ if (!pendingExtraBmIds.includes(pick.value)) {
+ pendingExtraBmIds.push(pick.value);
+ renderExtraBmList();
+ }
+ pick.value = "";
+ });
+ }
+ function renderSatelliteSection() {
+ schedulerWindow.satScheduler?.renderSection();
+ }
+ function renderSatPassStatus() {
+ schedulerWindow.satScheduler?.renderPassStatus();
+ }
+ function collectSatelliteConfig() {
+ return schedulerWindow.satScheduler ? schedulerWindow.satScheduler.collectSatelliteConfig() : { enabled: false, pretune_secs: 60, entries: [] };
+ }
+ function wireSatelliteEvents() {
+ schedulerWindow.satScheduler?.wireEvents();
+ }
+ function isInputFocused() {
+ const el = document.activeElement;
+ if (!el) return false;
+ const tag = el.tagName;
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el instanceof HTMLElement && el.isContentEditable;
+ }
+ document.addEventListener("keydown", function(e) {
+ if (isInputFocused()) return;
+ if (e.shiftKey && e.key === "R") {
+ e.preventDefault();
+ const releaseBtn = schedulerEl("scheduler-release-btn");
+ if (releaseBtn && !releaseBtn.disabled) releaseBtn.click();
+ } else if (e.shiftKey && e.key === "N") {
+ e.preventDefault();
+ schedulerSelectRelativeEntry(1);
+ } else if (e.shiftKey && e.key === "P") {
+ e.preventDefault();
+ schedulerSelectRelativeEntry(-1);
+ }
+ });
+ (function() {
+ const details = document.querySelector(".sch-ts-details");
+ if (!details) return;
+ const schedulerDetails = details;
+ const key = "sch-details-open";
+ const saved = localStorage.getItem(key);
+ if (saved !== null) schedulerDetails.open = saved === "1";
+ details.addEventListener("toggle", function() {
+ localStorage.setItem(key, schedulerDetails.open ? "1" : "0");
+ });
})();
+ const schedulerService = {
+ initialize: initScheduler,
+ destroy: destroyScheduler,
+ wireEvents: wireSchedulerEvents,
+ setRig: setSchedulerRig,
+ getConfig: () => currentConfig,
+ getStatus: () => currentSchedulerStatus,
+ getBookmarks: () => bookmarkList,
+ markDirty: markSchedulerDirty
+ };
+ schedulerWindow.trx.modules.scheduler = schedulerService;
+ if (schedulerWindow.authRole != null) {
+ initScheduler(schedulerWindow.lastActiveRigId ?? null, schedulerWindow.authRole);
+ wireSchedulerEvents();
+ }
})();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vchan.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vchan.js
index 41be5773..07b4edca 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vchan.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/vchan.js
@@ -1,459 +1,456 @@
-"use strict";
-(() => {
- // src/plugins/vchan.ts
- var vchanWindow = window;
- var vchanSessionId = null;
- var vchanRigId = null;
- var vchanChannels = [];
- var vchanActiveId = null;
- var schedulerReleaseState = null;
- var schedulerReleasePollTimer = null;
- function vchanFmtFreq(hz) {
- if (!Number.isFinite(hz) || hz <= 0) return "--";
- if (hz >= 1e9) return (hz / 1e9).toFixed(4).replace(/\.?0+$/, "") + " GHz";
- if (hz >= 1e6) return (hz / 1e6).toFixed(4).replace(/\.?0+$/, "") + " MHz";
- if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + " kHz";
- return `${String(hz)} Hz`;
+// src/plugins/vchan.ts
+var vchanWindow = window;
+var vchanSessionId = null;
+var vchanRigId = null;
+var vchanChannels = [];
+var vchanActiveId = null;
+var schedulerReleaseState = null;
+var schedulerReleasePollTimer = null;
+function vchanFmtFreq(hz) {
+ if (!Number.isFinite(hz) || hz <= 0) return "--";
+ if (hz >= 1e9) return (hz / 1e9).toFixed(4).replace(/\.?0+$/, "") + " GHz";
+ if (hz >= 1e6) return (hz / 1e6).toFixed(4).replace(/\.?0+$/, "") + " MHz";
+ if (hz >= 1e3) return (hz / 1e3).toFixed(1).replace(/\.?0+$/, "") + " kHz";
+ return `${String(hz)} Hz`;
+}
+function schedulerReleaseSummaryText(state) {
+ if (!state) return "Scheduler is controlling the rig.";
+ const connected = Number(state.connected_sessions) || 0;
+ const released = Number(state.released_sessions) || 0;
+ if (connected === 0) return "Scheduler can control the rig.";
+ if (state.all_released) {
+ return connected === 1 ? "Scheduler is controlling the rig." : `Scheduler is controlling the rig for all ${connected} users.`;
}
- function schedulerReleaseSummaryText(state) {
- if (!state) return "Scheduler is controlling the rig.";
- const connected = Number(state.connected_sessions) || 0;
- const released = Number(state.released_sessions) || 0;
- if (connected === 0) return "Scheduler can control the rig.";
- if (state.all_released) {
- return connected === 1 ? "Scheduler is controlling the rig." : `Scheduler is controlling the rig for all ${connected} users.`;
+ if (!state.current_session_released) {
+ const othersReleased = Math.max(released, 0);
+ return othersReleased > 0 ? `You are holding control. ${othersReleased} other user${othersReleased === 1 ? "" : "s"} already released it.` : "You are holding control. Release it to return control to the scheduler.";
+ }
+ const blocking = Math.max(connected - released, 0);
+ return blocking > 0 ? `Scheduler is waiting for ${blocking} user${blocking === 1 ? "" : "s"} to stop manual tuning.` : "Scheduler can control the rig.";
+}
+function vchanRenderSchedulerRelease() {
+ const btn = document.getElementById("scheduler-release-btn");
+ const status = document.getElementById("scheduler-release-status");
+ if (!btn || !status) return;
+ const currentReleased = !!(schedulerReleaseState && schedulerReleaseState.current_session_released);
+ btn.disabled = !vchanSessionId || currentReleased;
+ btn.classList.toggle("active", !currentReleased);
+ btn.textContent = "Release to Scheduler";
+ status.textContent = schedulerReleaseSummaryText(schedulerReleaseState);
+}
+async function vchanPollSchedulerRelease() {
+ if (!vchanSessionId) {
+ schedulerReleaseState = null;
+ vchanRenderSchedulerRelease();
+ return;
+ }
+ try {
+ const resp = await fetch(`/scheduler-control?session_id=${encodeURIComponent(vchanSessionId)}`);
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ schedulerReleaseState = await resp.json();
+ vchanRenderSchedulerRelease();
+ } catch (e) {
+ console.error("scheduler release status failed", e);
+ }
+}
+function vchanStartSchedulerReleasePolling() {
+ if (schedulerReleasePollTimer) {
+ clearInterval(schedulerReleasePollTimer);
+ }
+ schedulerReleasePollTimer = setInterval(() => {
+ void vchanPollSchedulerRelease();
+ }, 1e4);
+}
+async function vchanToggleSchedulerRelease() {
+ if (!vchanSessionId) return;
+ const rigId = vchanRigId || vchanWindow.lastActiveRigId || null;
+ try {
+ const resp = await fetch("/scheduler-control", {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ session_id: vchanSessionId, released: true, remote: rigId })
+ });
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ schedulerReleaseState = await resp.json();
+ vchanRenderSchedulerRelease();
+ } catch (e) {
+ console.error("scheduler release toggle failed", e);
+ }
+}
+async function vchanTakeSchedulerControl() {
+ if (!vchanSessionId) return;
+ if (schedulerReleaseState && !schedulerReleaseState.current_session_released) return;
+ try {
+ const resp = await fetch("/scheduler-control", {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ session_id: vchanSessionId, released: false })
+ });
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ schedulerReleaseState = await resp.json();
+ vchanRenderSchedulerRelease();
+ } catch (e) {
+ console.error("scheduler control takeover failed", e);
+ }
+}
+vchanWindow.vchanTakeSchedulerControl = vchanTakeSchedulerControl;
+function vchanHandleSession(data) {
+ try {
+ const d = JSON.parse(data);
+ vchanSessionId = d.session_id || null;
+ void vchanPollSchedulerRelease();
+ } catch (e) {
+ console.warn("vchan: bad session event", e);
+ }
+}
+function vchanHandleChannels(data) {
+ try {
+ const d = JSON.parse(data);
+ vchanRigId = d.remote || null;
+ vchanChannels = d.channels || [];
+ const ids = new Set(vchanChannels.map((c) => c.id));
+ const primaryChannel = vchanChannels[0];
+ if (!vchanActiveId && primaryChannel && vchanSessionId) {
+ void vchanAutoJoinPrimary(primaryChannel.id);
+ } else if (vchanActiveId && !ids.has(vchanActiveId)) {
+ vchanActiveId = vchanChannels[0]?.id ?? null;
+ vchanReconnectAudio();
}
- if (!state.current_session_released) {
- const othersReleased = Math.max(released, 0);
- return othersReleased > 0 ? `You are holding control. ${othersReleased} other user${othersReleased === 1 ? "" : "s"} already released it.` : "You are holding control. Release it to return control to the scheduler.";
+ vchanRender();
+ vchanRenderSchedulerRelease();
+ vchanWindow.renderRdsOverlays?.();
+ } catch (e) {
+ console.warn("vchan: bad channels event", e);
+ }
+}
+vchanWindow.vchanHandleSession = vchanHandleSession;
+vchanWindow.vchanHandleChannels = vchanHandleChannels;
+function vchanRender() {
+ const picker = document.getElementById("vchan-picker");
+ if (!picker) return;
+ picker.innerHTML = "";
+ vchanChannels.forEach((ch) => {
+ const btn = document.createElement("button");
+ btn.type = "button";
+ btn.title = `Ch ${ch.index}: ${vchanFmtFreq(ch.freq_hz)} ${ch.mode} · ${ch.subscribers} subscriber${ch.subscribers !== 1 ? "s" : ""}`;
+ if (ch.id === vchanActiveId) btn.classList.add("active");
+ const label = document.createElement("span");
+ label.className = "vchan-label";
+ label.textContent = `${ch.index}: ${vchanFmtFreq(ch.freq_hz)} ${ch.mode}`;
+ btn.appendChild(label);
+ if (!ch.permanent) {
+ const del = document.createElement("span");
+ del.className = "vchan-del";
+ del.textContent = "×";
+ del.title = "Delete channel";
+ del.addEventListener("click", (e) => {
+ e.stopPropagation();
+ void vchanDelete(ch.id);
+ });
+ btn.appendChild(del);
}
- const blocking = Math.max(connected - released, 0);
- return blocking > 0 ? `Scheduler is waiting for ${blocking} user${blocking === 1 ? "" : "s"} to stop manual tuning.` : "Scheduler can control the rig.";
+ btn.addEventListener("click", () => {
+ if (ch.id !== vchanActiveId) void vchanSubscribe(ch.id);
+ });
+ picker.appendChild(btn);
+ });
+ const addBtn = document.createElement("button");
+ addBtn.type = "button";
+ addBtn.className = "vchan-add";
+ addBtn.textContent = "+";
+ addBtn.title = "Allocate new virtual channel at current frequency";
+ addBtn.addEventListener("click", () => {
+ void vchanAllocate();
+ });
+ picker.appendChild(addBtn);
+ vchanSyncAccentUI();
+ if (vchanWindow.updateDocumentTitle && vchanWindow.activeChannelRds) {
+ vchanWindow.updateDocumentTitle(vchanWindow.activeChannelRds());
}
- function vchanRenderSchedulerRelease() {
- const btn = document.getElementById("scheduler-release-btn");
- const status = document.getElementById("scheduler-release-status");
- if (!btn || !status) return;
- const currentReleased = !!(schedulerReleaseState && schedulerReleaseState.current_session_released);
- btn.disabled = !vchanSessionId || currentReleased;
- btn.classList.toggle("active", !currentReleased);
- btn.textContent = "Release to Scheduler";
- status.textContent = schedulerReleaseSummaryText(schedulerReleaseState);
- }
- async function vchanPollSchedulerRelease() {
- if (!vchanSessionId) {
- schedulerReleaseState = null;
- vchanRenderSchedulerRelease();
+ vchanRenderSchedulerRelease();
+}
+async function vchanAllocate() {
+ if (!vchanSessionId || !vchanRigId) return;
+ const freqHz = typeof vchanWindow.lastFreqHz === "number" && vchanWindow.lastFreqHz > 0 ? vchanWindow.lastFreqHz : 0;
+ const modeEl = document.getElementById("mode");
+ const mode = modeEl ? modeEl.value || "USB" : "USB";
+ try {
+ const resp = await fetch(`/channels/${encodeURIComponent(vchanRigId)}`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ session_id: vchanSessionId, freq_hz: freqHz, mode })
+ });
+ if (!resp.ok) {
+ const msg = await resp.text().catch(() => String(resp.status));
+ console.warn("vchan: allocate failed —", msg);
return;
}
- try {
- const resp = await fetch(`/scheduler-control?session_id=${encodeURIComponent(vchanSessionId)}`);
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
- schedulerReleaseState = await resp.json();
- vchanRenderSchedulerRelease();
- } catch (e) {
- console.error("scheduler release status failed", e);
- }
+ const ch = await resp.json();
+ vchanActiveId = ch.id;
+ vchanRender();
+ vchanReconnectAudio();
+ } catch (e) {
+ console.error("vchan: allocate error", e);
}
- function vchanStartSchedulerReleasePolling() {
- if (schedulerReleasePollTimer) {
- clearInterval(schedulerReleasePollTimer);
+}
+async function vchanDelete(channelId) {
+ if (!vchanRigId) return;
+ try {
+ const resp = await fetch(
+ `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}`,
+ { method: "DELETE" }
+ );
+ if (!resp.ok) {
+ console.warn("vchan: delete failed", resp.status);
}
- schedulerReleasePollTimer = setInterval(() => {
- void vchanPollSchedulerRelease();
- }, 1e4);
+ } catch (e) {
+ console.error("vchan: delete error", e);
}
- async function vchanToggleSchedulerRelease() {
- if (!vchanSessionId) return;
- const rigId = vchanRigId || vchanWindow.lastActiveRigId || null;
- try {
- const resp = await fetch("/scheduler-control", {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ session_id: vchanSessionId, released: true, remote: rigId })
- });
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
- schedulerReleaseState = await resp.json();
- vchanRenderSchedulerRelease();
- } catch (e) {
- console.error("scheduler release toggle failed", e);
- }
- }
- async function vchanTakeSchedulerControl() {
- if (!vchanSessionId) return;
- if (schedulerReleaseState && !schedulerReleaseState.current_session_released) return;
- try {
- const resp = await fetch("/scheduler-control", {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ session_id: vchanSessionId, released: false })
- });
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
- schedulerReleaseState = await resp.json();
- vchanRenderSchedulerRelease();
- } catch (e) {
- console.error("scheduler control takeover failed", e);
- }
- }
- vchanWindow.vchanTakeSchedulerControl = vchanTakeSchedulerControl;
- function vchanHandleSession(data) {
- try {
- const d = JSON.parse(data);
- vchanSessionId = d.session_id || null;
- void vchanPollSchedulerRelease();
- } catch (e) {
- console.warn("vchan: bad session event", e);
- }
- }
- function vchanHandleChannels(data) {
- try {
- const d = JSON.parse(data);
- vchanRigId = d.remote || null;
- vchanChannels = d.channels || [];
- const ids = new Set(vchanChannels.map((c) => c.id));
- const primaryChannel = vchanChannels[0];
- if (!vchanActiveId && primaryChannel && vchanSessionId) {
- void vchanAutoJoinPrimary(primaryChannel.id);
- } else if (vchanActiveId && !ids.has(vchanActiveId)) {
- vchanActiveId = vchanChannels[0]?.id ?? null;
- vchanReconnectAudio();
- }
- vchanRender();
- vchanRenderSchedulerRelease();
- vchanWindow.renderRdsOverlays?.();
- } catch (e) {
- console.warn("vchan: bad channels event", e);
- }
- }
- vchanWindow.vchanHandleSession = vchanHandleSession;
- vchanWindow.vchanHandleChannels = vchanHandleChannels;
- function vchanRender() {
- const picker = document.getElementById("vchan-picker");
- if (!picker) return;
- picker.innerHTML = "";
- vchanChannels.forEach((ch) => {
- const btn = document.createElement("button");
- btn.type = "button";
- btn.title = `Ch ${ch.index}: ${vchanFmtFreq(ch.freq_hz)} ${ch.mode} · ${ch.subscribers} subscriber${ch.subscribers !== 1 ? "s" : ""}`;
- if (ch.id === vchanActiveId) btn.classList.add("active");
- const label = document.createElement("span");
- label.className = "vchan-label";
- label.textContent = `${ch.index}: ${vchanFmtFreq(ch.freq_hz)} ${ch.mode}`;
- btn.appendChild(label);
- if (!ch.permanent) {
- const del = document.createElement("span");
- del.className = "vchan-del";
- del.textContent = "×";
- del.title = "Delete channel";
- del.addEventListener("click", (e) => {
- e.stopPropagation();
- void vchanDelete(ch.id);
- });
- btn.appendChild(del);
- }
- btn.addEventListener("click", () => {
- if (ch.id !== vchanActiveId) void vchanSubscribe(ch.id);
- });
- picker.appendChild(btn);
- });
- const addBtn = document.createElement("button");
- addBtn.type = "button";
- addBtn.className = "vchan-add";
- addBtn.textContent = "+";
- addBtn.title = "Allocate new virtual channel at current frequency";
- addBtn.addEventListener("click", () => {
- void vchanAllocate();
- });
- picker.appendChild(addBtn);
- vchanSyncAccentUI();
- if (vchanWindow.updateDocumentTitle && vchanWindow.activeChannelRds) {
- vchanWindow.updateDocumentTitle(vchanWindow.activeChannelRds());
- }
- vchanRenderSchedulerRelease();
- }
- async function vchanAllocate() {
- if (!vchanSessionId || !vchanRigId) return;
- const freqHz = typeof vchanWindow.lastFreqHz === "number" && vchanWindow.lastFreqHz > 0 ? vchanWindow.lastFreqHz : 0;
- const modeEl = document.getElementById("mode");
- const mode = modeEl ? modeEl.value || "USB" : "USB";
- try {
- const resp = await fetch(`/channels/${encodeURIComponent(vchanRigId)}`, {
+}
+async function vchanAutoJoinPrimary(channelId) {
+ if (!vchanSessionId || !vchanRigId) return;
+ try {
+ const resp = await fetch(
+ `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}/subscribe`,
+ {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ session_id: vchanSessionId, freq_hz: freqHz, mode })
- });
- if (!resp.ok) {
- const msg = await resp.text().catch(() => String(resp.status));
- console.warn("vchan: allocate failed —", msg);
- return;
+ body: JSON.stringify({ session_id: vchanSessionId })
}
- const ch = await resp.json();
- vchanActiveId = ch.id;
- vchanRender();
- vchanReconnectAudio();
- } catch (e) {
- console.error("vchan: allocate error", e);
+ );
+ if (!resp.ok) {
+ console.warn("vchan: auto-join primary failed", resp.status);
+ return;
}
+ vchanActiveId = channelId;
+ vchanRender();
+ } catch (e) {
+ console.error("vchan: auto-join error", e);
}
- async function vchanDelete(channelId) {
- if (!vchanRigId) return;
- try {
- const resp = await fetch(
- `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}`,
- { method: "DELETE" }
- );
- if (!resp.ok) {
- console.warn("vchan: delete failed", resp.status);
+}
+async function vchanSubscribe(channelId) {
+ if (!vchanSessionId || !vchanRigId) return;
+ try {
+ await vchanTakeSchedulerControl();
+ const resp = await fetch(
+ `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}/subscribe`,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ session_id: vchanSessionId })
}
- } catch (e) {
- console.error("vchan: delete error", e);
+ );
+ if (!resp.ok) {
+ console.warn("vchan: subscribe failed", resp.status);
+ return;
}
+ vchanActiveId = channelId;
+ vchanRender();
+ vchanSyncModeDisplay();
+ vchanReconnectAudio();
+ } catch (e) {
+ console.error("vchan: subscribe error", e);
}
- async function vchanAutoJoinPrimary(channelId) {
- if (!vchanSessionId || !vchanRigId) return;
- try {
- const resp = await fetch(
- `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}/subscribe`,
- {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ session_id: vchanSessionId })
- }
- );
- if (!resp.ok) {
- console.warn("vchan: auto-join primary failed", resp.status);
- return;
- }
- vchanActiveId = channelId;
- vchanRender();
- } catch (e) {
- console.error("vchan: auto-join error", e);
- }
+}
+function vchanReconnectAudio() {
+ const ch = vchanIsOnVirtual() ? vchanActiveChannel() : null;
+ vchanWindow._audioChannelOverride = ch?.id ?? null;
+ if (!vchanWindow.rxActive) return;
+ vchanWindow.stopRxAudio?.();
+ setTimeout(() => {
+ vchanWindow.startRxAudio?.();
+ }, 300);
+}
+function vchanApplyCapabilities(caps) {
+ const picker = document.getElementById("vchan-picker");
+ if (!picker) return;
+ picker.style.display = caps && caps.filter_controls ? "" : "none";
+ vchanRenderSchedulerRelease();
+}
+vchanWindow.vchanApplyCapabilities = vchanApplyCapabilities;
+function vchanIsOnVirtual() {
+ if (!vchanActiveId || vchanChannels.length === 0) return false;
+ return vchanActiveId !== vchanChannels[0]?.id;
+}
+vchanWindow.vchanIsOnVirtual = vchanIsOnVirtual;
+function vchanActiveChannel() {
+ return vchanChannels.find((c) => c.id === vchanActiveId) || null;
+}
+function vchanUpdateFreqDisplay() {
+ const ch = vchanActiveChannel();
+ if (!ch) return;
+ const el = document.getElementById("freq");
+ if (!el) return;
+ if (vchanWindow.formatFreqForStep && typeof vchanWindow.jogUnit === "number") {
+ el.value = vchanWindow.formatFreqForStep(ch.freq_hz, vchanWindow.jogUnit);
+ } else {
+ el.value = (ch.freq_hz / 1e6).toFixed(6).replace(/\.?0+$/, "");
}
- async function vchanSubscribe(channelId) {
- if (!vchanSessionId || !vchanRigId) return;
- try {
- await vchanTakeSchedulerControl();
- const resp = await fetch(
- `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(channelId)}/subscribe`,
- {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ session_id: vchanSessionId })
- }
- );
- if (!resp.ok) {
- console.warn("vchan: subscribe failed", resp.status);
- return;
- }
- vchanActiveId = channelId;
- vchanRender();
- vchanSyncModeDisplay();
- vchanReconnectAudio();
- } catch (e) {
- console.error("vchan: subscribe error", e);
- }
- }
- function vchanReconnectAudio() {
- const ch = vchanIsOnVirtual() ? vchanActiveChannel() : null;
- vchanWindow._audioChannelOverride = ch?.id ?? null;
- if (!vchanWindow.rxActive) return;
- vchanWindow.stopRxAudio?.();
- setTimeout(() => {
- vchanWindow.startRxAudio?.();
- }, 300);
- }
- function vchanApplyCapabilities(caps) {
- const picker = document.getElementById("vchan-picker");
- if (!picker) return;
- picker.style.display = caps && caps.filter_controls ? "" : "none";
- vchanRenderSchedulerRelease();
- }
- vchanWindow.vchanApplyCapabilities = vchanApplyCapabilities;
- function vchanIsOnVirtual() {
- if (!vchanActiveId || vchanChannels.length === 0) return false;
- return vchanActiveId !== vchanChannels[0]?.id;
- }
- vchanWindow.vchanIsOnVirtual = vchanIsOnVirtual;
- function vchanActiveChannel() {
- return vchanChannels.find((c) => c.id === vchanActiveId) || null;
- }
- function vchanUpdateFreqDisplay() {
+}
+function vchanSyncModeDisplay() {
+ const modeEl = document.getElementById("mode");
+ if (!modeEl) return;
+ if (vchanIsOnVirtual()) {
const ch = vchanActiveChannel();
- if (!ch) return;
- const el = document.getElementById("freq");
- if (!el) return;
- if (vchanWindow.formatFreqForStep && typeof vchanWindow.jogUnit === "number") {
- el.value = vchanWindow.formatFreqForStep(ch.freq_hz, vchanWindow.jogUnit);
- } else {
- el.value = (ch.freq_hz / 1e6).toFixed(6).replace(/\.?0+$/, "");
- }
+ if (ch && ch.mode) modeEl.value = ch.mode.toUpperCase();
}
- function vchanSyncModeDisplay() {
- const modeEl = document.getElementById("mode");
- if (!modeEl) return;
- if (vchanIsOnVirtual()) {
- const ch = vchanActiveChannel();
- if (ch && ch.mode) modeEl.value = ch.mode.toUpperCase();
+ const modeUpper = (modeEl.value || "").toUpperCase();
+ if (typeof vchanWindow.lastModeName === "string") {
+ if (modeUpper === "WFM" && vchanWindow.lastModeName !== "WFM") {
+ vchanWindow.setJogDivisor?.(10);
+ vchanWindow.resetRdsDisplay?.();
+ } else if (modeUpper !== "WFM" && vchanWindow.lastModeName === "WFM") {
+ vchanWindow.resetRdsDisplay?.();
}
- const modeUpper = (modeEl.value || "").toUpperCase();
- if (typeof vchanWindow.lastModeName === "string") {
- if (modeUpper === "WFM" && vchanWindow.lastModeName !== "WFM") {
- vchanWindow.setJogDivisor?.(10);
- vchanWindow.resetRdsDisplay?.();
- } else if (modeUpper !== "WFM" && vchanWindow.lastModeName === "WFM") {
- vchanWindow.resetRdsDisplay?.();
+ vchanWindow.lastModeName = modeUpper;
+ }
+ vchanWindow.updateWfmControls?.();
+ vchanWindow.updateSdrSquelchControlVisibility?.();
+ if (vchanWindow.refreshRdsUi) {
+ vchanWindow.refreshRdsUi();
+ } else {
+ vchanWindow.positionRdsPsOverlay?.();
+ }
+}
+function vchanSyncBwDisplay() {
+ if (!vchanIsOnVirtual()) return;
+ const ch = vchanActiveChannel();
+ if (!ch) return;
+ const bwEl = document.getElementById("spectrum-bw-input");
+ if (!bwEl) return;
+ let bwHz = ch.bandwidth_hz || 0;
+ if (bwHz === 0 && vchanWindow.mwDefaultsForMode) {
+ bwHz = vchanWindow.mwDefaultsForMode(ch.mode)[0] || 0;
+ }
+ if (bwHz > 0) {
+ bwEl.value = (bwHz / 1e3).toFixed(3).replace(/\.?0+$/, "");
+ vchanWindow.currentBandwidthHz = bwHz;
+ }
+}
+function vchanSyncAccentUI() {
+ const onVirtual = vchanIsOnVirtual();
+ const freqEl = document.getElementById("freq");
+ const bwEl = document.getElementById("spectrum-bw-input");
+ if (freqEl) freqEl.classList.toggle("vchan-ch-active", onVirtual);
+ if (bwEl) bwEl.classList.toggle("vchan-ch-active", onVirtual);
+ if (onVirtual) {
+ vchanUpdateFreqDisplay();
+ vchanSyncModeDisplay();
+ vchanSyncBwDisplay();
+ } else {
+ origRefreshFreqDisplay?.();
+ }
+ if (vchanWindow.updateDocumentTitle && vchanWindow.activeChannelRds) {
+ vchanWindow.updateDocumentTitle(vchanWindow.activeChannelRds());
+ }
+}
+var origRefreshFreqDisplay = null;
+function vchanSetChannelFreq(freqHz) {
+ if (!vchanRigId || !vchanActiveId) return;
+ if (vchanWindow.lastSpectrumData && vchanWindow.lastSpectrumData.sample_rate > 0) {
+ const halfSpan = vchanWindow.lastSpectrumData.sample_rate / 2;
+ const center = vchanWindow.lastSpectrumData.center_hz;
+ if (Math.abs(freqHz - center) > halfSpan) {
+ if (vchanWindow.showHint) {
+ vchanWindow.showHint(
+ `Out of SDR bandwidth (center ${(center / 1e6).toFixed(3)} MHz ±${(halfSpan / 1e3).toFixed(0)} kHz)`,
+ 3e3
+ );
}
- vchanWindow.lastModeName = modeUpper;
- }
- vchanWindow.updateWfmControls?.();
- vchanWindow.updateSdrSquelchControlVisibility?.();
- if (vchanWindow.refreshRdsUi) {
- vchanWindow.refreshRdsUi();
- } else {
- vchanWindow.positionRdsPsOverlay?.();
+ return;
}
}
- function vchanSyncBwDisplay() {
- if (!vchanIsOnVirtual()) return;
- const ch = vchanActiveChannel();
- if (!ch) return;
- const bwEl = document.getElementById("spectrum-bw-input");
- if (!bwEl) return;
- let bwHz = ch.bandwidth_hz || 0;
- if (bwHz === 0 && vchanWindow.mwDefaultsForMode) {
- bwHz = vchanWindow.mwDefaultsForMode(ch.mode)[0] || 0;
+ void vchanTakeSchedulerControl();
+ void fetch(
+ `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/freq`,
+ {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ freq_hz: Math.round(freqHz) })
}
- if (bwHz > 0) {
- bwEl.value = (bwHz / 1e3).toFixed(3).replace(/\.?0+$/, "");
- vchanWindow.currentBandwidthHz = bwHz;
- }
- }
- function vchanSyncAccentUI() {
- const onVirtual = vchanIsOnVirtual();
- const freqEl = document.getElementById("freq");
- const bwEl = document.getElementById("spectrum-bw-input");
- if (freqEl) freqEl.classList.toggle("vchan-ch-active", onVirtual);
- if (bwEl) bwEl.classList.toggle("vchan-ch-active", onVirtual);
- if (onVirtual) {
- vchanUpdateFreqDisplay();
- vchanSyncModeDisplay();
- vchanSyncBwDisplay();
- } else {
- origRefreshFreqDisplay?.();
- }
- if (vchanWindow.updateDocumentTitle && vchanWindow.activeChannelRds) {
- vchanWindow.updateDocumentTitle(vchanWindow.activeChannelRds());
- }
- }
- var origRefreshFreqDisplay = null;
- function vchanSetChannelFreq(freqHz) {
- if (!vchanRigId || !vchanActiveId) return;
- if (vchanWindow.lastSpectrumData && vchanWindow.lastSpectrumData.sample_rate > 0) {
- const halfSpan = vchanWindow.lastSpectrumData.sample_rate / 2;
- const center = vchanWindow.lastSpectrumData.center_hz;
- if (Math.abs(freqHz - center) > halfSpan) {
- if (vchanWindow.showHint) {
- vchanWindow.showHint(
- `Out of SDR bandwidth (center ${(center / 1e6).toFixed(3)} MHz ±${(halfSpan / 1e3).toFixed(0)} kHz)`,
- 3e3
- );
- }
- return;
- }
- }
- void vchanTakeSchedulerControl();
- void fetch(
- `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/freq`,
+ ).catch((error) => {
+ console.error("vchan: set freq error", error);
+ });
+}
+async function vchanSetChannelBandwidth(bwHz) {
+ if (!vchanRigId || !vchanActiveId) return;
+ try {
+ await vchanTakeSchedulerControl();
+ const resp = await fetch(
+ `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/bw`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ freq_hz: Math.round(freqHz) })
+ body: JSON.stringify({ bandwidth_hz: Math.round(bwHz) })
}
- ).catch((error) => {
- console.error("vchan: set freq error", error);
+ );
+ if (!resp.ok) console.warn("vchan: set bw failed", resp.status);
+ } catch (e) {
+ console.error("vchan: set bw error", e);
+ }
+}
+async function vchanSetChannelMode(mode) {
+ if (!vchanRigId || !vchanActiveId) return;
+ try {
+ await vchanTakeSchedulerControl();
+ const resp = await fetch(
+ `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/mode`,
+ {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ mode })
+ }
+ );
+ if (!resp.ok) console.warn("vchan: set mode failed", resp.status);
+ } catch (e) {
+ console.error("vchan: set mode error", e);
+ }
+}
+vchanWindow.vchanInterceptMode = async function(mode) {
+ if (!vchanIsOnVirtual()) return false;
+ await vchanSetChannelMode(mode);
+ return true;
+};
+vchanWindow.vchanInterceptBandwidth = async function(bwHz) {
+ if (!vchanIsOnVirtual()) return false;
+ await vchanSetChannelBandwidth(bwHz);
+ return true;
+};
+(function() {
+ const original = vchanWindow.setRigFrequency;
+ vchanWindow.setRigFrequency = function(freqHz) {
+ if (vchanIsOnVirtual()) {
+ if (vchanWindow.applyLocalTunedFrequency) {
+ if (typeof vchanWindow._freqOptimisticSeq === "number") {
+ vchanWindow._freqOptimisticSeq += 1;
+ vchanWindow._freqOptimisticHz = Math.round(freqHz);
+ }
+ vchanWindow.applyLocalTunedFrequency(Math.round(freqHz));
+ }
+ vchanSetChannelFreq(freqHz);
+ return;
+ }
+ void vchanTakeSchedulerControl();
+ original?.(freqHz);
+ };
+})();
+(function initSchedulerReleaseControl() {
+ const btn = document.getElementById("scheduler-release-btn");
+ if (btn) {
+ btn.addEventListener("click", () => {
+ void vchanToggleSchedulerRelease();
});
}
- async function vchanSetChannelBandwidth(bwHz) {
- if (!vchanRigId || !vchanActiveId) return;
- try {
- await vchanTakeSchedulerControl();
- const resp = await fetch(
- `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/bw`,
- {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ bandwidth_hz: Math.round(bwHz) })
- }
- );
- if (!resp.ok) console.warn("vchan: set bw failed", resp.status);
- } catch (e) {
- console.error("vchan: set bw error", e);
+ vchanStartSchedulerReleasePolling();
+ vchanRenderSchedulerRelease();
+})();
+(function() {
+ origRefreshFreqDisplay = vchanWindow.refreshFreqDisplay ?? null;
+ vchanWindow.refreshFreqDisplay = function() {
+ if (vchanIsOnVirtual()) {
+ vchanUpdateFreqDisplay();
+ return;
}
- }
- async function vchanSetChannelMode(mode) {
- if (!vchanRigId || !vchanActiveId) return;
- try {
- await vchanTakeSchedulerControl();
- const resp = await fetch(
- `/channels/${encodeURIComponent(vchanRigId)}/${encodeURIComponent(vchanActiveId)}/mode`,
- {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ mode })
- }
- );
- if (!resp.ok) console.warn("vchan: set mode failed", resp.status);
- } catch (e) {
- console.error("vchan: set mode error", e);
- }
- }
- vchanWindow.vchanInterceptMode = async function(mode) {
- if (!vchanIsOnVirtual()) return false;
- await vchanSetChannelMode(mode);
- return true;
- };
- vchanWindow.vchanInterceptBandwidth = async function(bwHz) {
- if (!vchanIsOnVirtual()) return false;
- await vchanSetChannelBandwidth(bwHz);
- return true;
+ origRefreshFreqDisplay?.();
};
- (function() {
- const original = vchanWindow.setRigFrequency;
- vchanWindow.setRigFrequency = function(freqHz) {
- if (vchanIsOnVirtual()) {
- if (vchanWindow.applyLocalTunedFrequency) {
- if (typeof vchanWindow._freqOptimisticSeq === "number") {
- vchanWindow._freqOptimisticSeq += 1;
- vchanWindow._freqOptimisticHz = Math.round(freqHz);
- }
- vchanWindow.applyLocalTunedFrequency(Math.round(freqHz));
- }
- vchanSetChannelFreq(freqHz);
- return;
- }
- void vchanTakeSchedulerControl();
- original?.(freqHz);
- };
- })();
- (function initSchedulerReleaseControl() {
- const btn = document.getElementById("scheduler-release-btn");
- if (btn) {
- btn.addEventListener("click", () => {
- void vchanToggleSchedulerRelease();
- });
- }
- vchanStartSchedulerReleasePolling();
- vchanRenderSchedulerRelease();
- })();
- (function() {
- origRefreshFreqDisplay = vchanWindow.refreshFreqDisplay ?? null;
- vchanWindow.refreshFreqDisplay = function() {
- if (vchanIsOnVirtual()) {
- vchanUpdateFreqDisplay();
- return;
- }
- origRefreshFreqDisplay?.();
- };
- })();
})();
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 19338739..eb756fcf 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,271 +1,268 @@
-"use strict";
-(() => {
- // 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;
+// 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;
+}
+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;
}
- 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 = `${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;
- }
- 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";
- }
- vdesWindow.updateVdesBar = updateVdesBar;
- vdesWindow.clearVdesBar = function() {
- resetVdesHistoryView();
- };
- function resetVdesHistoryView() {
- if (vdesMessagesEl) vdesMessagesEl.innerHTML = "";
- vdesMessageHistory = [];
- updateVdesBar();
+ 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();
}
- function renderVdesHistory() {
- pruneVdesMessageHistory();
- if (!vdesMessagesEl) {
- updateVdesSummary();
- return;
- }
- const fragment = document.createDocumentFragment();
- for (const message of vdesMessageHistory) {
- fragment.appendChild(renderVdesRow(message));
- }
- vdesMessagesEl.replaceChildren(fragment);
+ 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 = `${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;
+ }
+ 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";
+}
+vdesWindow.updateVdesBar = updateVdesBar;
+vdesWindow.clearVdesBar = function() {
+ resetVdesHistoryView();
+};
+function resetVdesHistoryView() {
+ if (vdesMessagesEl) vdesMessagesEl.innerHTML = "";
+ vdesMessageHistory = [];
+ updateVdesBar();
+ renderVdesHistory();
+}
+function renderVdesHistory() {
+ pruneVdesMessageHistory();
+ if (!vdesMessagesEl) {
updateVdesSummary();
+ return;
}
- 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([], {
+ 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"
});
- 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 vdesWindow.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);
}
+ normalized.push(next);
}
- function pruneVdesHistoryView() {
- pruneVdesMessageHistory();
- updateVdesBar();
+ 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 vdesWindow.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();
- }
- updateVdesSummary();
- window.trxPluginRuntime.registerDecoder({
- id: "vdes",
- onMessage: onServerVdes,
- onBatch: onServerVdesBatch,
- restore: onServerVdesBatch,
- reset: resetVdesHistoryView,
- prune: pruneVdesHistoryView
});
-})();
+}
+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
+});
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wefax.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wefax.js
index f3934838..09bae50f 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wefax.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wefax.js
@@ -1,335 +1,332 @@
-"use strict";
-(() => {
- // 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, "&").replace(//g, ">").replace(/"/g, """);
- }
- 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");
+// 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;
});
- if (wefaxDom.viewHistoryBtn) wefaxDom.viewHistoryBtn.addEventListener("click", function() {
- switchWefaxView("history");
+}
+function escapeHtml(s) {
+ return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+}
+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");
});
- function resetLiveCanvas(pixelsPerLine) {
- const canvas = wefaxDom.liveCanvas;
- if (!canvas) return;
- wefaxLivePixelsPerLine = pixelsPerLine;
- wefaxLiveLineCount = 0;
- canvas.width = pixelsPerLine;
- canvas.height = 800;
+ 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.fillStyle = "#000";
- wefaxLiveCtx.fillRect(0, 0, canvas.width, canvas.height);
- if (wefaxDom.liveContainer) wefaxDom.liveContainer.style.display = "";
+ wefaxLiveCtx.putImageData(old, 0, 0);
}
- 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++;
+ 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;
}
- function renderWefaxLatestCard() {
- if (!wefaxDom.liveLatest) return;
- if (wefaxImageHistory.length === 0) {
- wefaxDom.liveLatest.innerHTML = 'No images decoded yet. Enable the decoder and tune to a WEFAX station.
';
- 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 = '';
- html += '
Latest decoded image
';
- html += '
' + escapeHtml(meta) + "
";
- if (imgSrc) {
- html += '
View full image';
- }
- html += "
";
- wefaxDom.liveLatest.innerHTML = html;
+ wefaxLiveCtx.putImageData(imgData, 0, y);
+ wefaxLiveLineCount++;
+}
+function renderWefaxLatestCard() {
+ if (!wefaxDom.liveLatest) return;
+ if (wefaxImageHistory.length === 0) {
+ wefaxDom.liveLatest.innerHTML = 'No images decoded yet. Enable the decoder and tune to a WEFAX station.
';
+ return;
}
- 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;
+ 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 = '';
+ html += '
Latest decoded image
';
+ html += '
' + escapeHtml(meta) + "
";
+ if (imgSrc) {
+ html += '
View full image';
}
- 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 ? '
View' : "--";
- row.innerHTML = [
- "
" + escapeHtml(date + " " + ts) + "",
- "
" + escapeHtml(String(ioc)) + "",
- "
" + escapeHtml(String(lpm)) + "",
- `
${String(lines)}`,
- "
" + link + ""
- ].join("");
- return row;
+ html += "
";
+ 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;
+ });
}
- 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`;
+ 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 ? 'View' : "--";
+ row.innerHTML = [
+ "" + escapeHtml(date + " " + ts) + "",
+ "" + escapeHtml(String(ioc)) + "",
+ "" + escapeHtml(String(lpm)) + "",
+ `${String(lines)}`,
+ "" + link + ""
+ ].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`;
+ }
+}
+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 {
}
}
- 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([], {
+ 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 (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 (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"
});
- 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 (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 (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 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");
- resetWefaxHistoryView();
- } catch (e) {
- console.error("WEFAX clear failed", e);
- }
- })();
- });
+ wefaxImageHistory = messages.concat(wefaxImageHistory);
+ pruneWefaxHistory();
+ scheduleWefaxUi("wefax-latest", renderWefaxLatestCard);
+ if (wefaxActiveView === "history") {
+ scheduleWefaxUi("wefax-history", renderWefaxHistoryTable);
}
+}
+function pruneWefaxHistoryView() {
+ pruneWefaxHistory();
+ renderWefaxHistoryTable();
renderWefaxLatestCard();
- wefaxWindow.trxPluginRuntime.registerDecoder({
- id: "wefax",
- onMessage: onServerWefax,
- restore: restoreWefaxHistory,
- prune: pruneWefaxHistoryView,
- reset: resetWefaxHistoryView
+}
+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);
});
- wefaxWindow.trxPluginRuntime.registerDecoder({
- id: "wefax_progress",
- onMessage: onServerWefaxProgress
+}
+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");
+ resetWefaxHistoryView();
+ } catch (e) {
+ console.error("WEFAX clear failed", e);
+ }
+ })();
+ });
+}
+renderWefaxLatestCard();
+wefaxWindow.trxPluginRuntime.registerDecoder({
+ id: "wefax",
+ onMessage: onServerWefax,
+ restore: restoreWefaxHistory,
+ prune: pruneWefaxHistoryView,
+ reset: resetWefaxHistoryView
+});
+wefaxWindow.trxPluginRuntime.registerDecoder({
+ id: "wefax_progress",
+ onMessage: onServerWefaxProgress
+});
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wspr.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wspr.js
index 81f1ad59..60f26a9f 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wspr.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/wspr.js
@@ -1,255 +1,105 @@
-"use strict";
-(() => {
- // src/plugins/wspr.ts
- var wsprWindow = window;
- var wsprStatus = document.getElementById("wspr-status");
- var wsprPeriodEl = document.getElementById("wspr-period");
- var wsprMessagesEl = document.getElementById("wspr-messages");
- var wsprFilterInput = document.getElementById("wspr-filter");
- var WSPR_PERIOD_SECONDS = 120;
- var wsprFilterText = "";
- var wsprMessageHistory = [];
- function finiteNumber(value) {
- const number = typeof value === "number" ? value : Number(value);
- return Number.isFinite(number) ? number : null;
- }
- function currentWsprHistoryRetentionMs() {
- return typeof wsprWindow.getDecodeHistoryRetentionMs === "function" ? wsprWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
- }
- function pruneWsprMessageHistory() {
- const cutoffMs = Date.now() - currentWsprHistoryRetentionMs();
- wsprMessageHistory = wsprMessageHistory.filter((msg) => Number(msg._tsMs ?? msg.ts_ms) >= cutoffMs);
- }
- function scheduleWsprHistoryRender() {
- if (typeof wsprWindow.trxScheduleUiFrameJob === "function") {
- wsprWindow.trxScheduleUiFrameJob("wspr-history", () => {
- renderWsprHistory();
- });
- return;
- }
- renderWsprHistory();
- }
- function fmtWsprTime(tsMs) {
- if (!tsMs) return "--:--:--";
- return new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
- }
- function updateWsprPeriodTimer() {
- if (!wsprPeriodEl) return;
- const nowSec = Math.floor(Date.now() / 1e3);
- const remaining = WSPR_PERIOD_SECONDS - nowSec % WSPR_PERIOD_SECONDS;
- const mm = String(Math.floor(remaining / 60)).padStart(2, "0");
- const ss = String(remaining % 60).padStart(2, "0");
- wsprPeriodEl.textContent = `Next slot ${mm}:${ss}`;
- }
- updateWsprPeriodTimer();
- setInterval(updateWsprPeriodTimer, 500);
- function renderWsprRow(msg) {
- const row = document.createElement("div");
- row.className = "ft8-row";
- row.dataset.decoder = "wspr";
- const snr = finiteNumber(msg.snr_db);
- const delta = finiteNumber(msg.dt_s);
- const baseHz = finiteNumber(wsprWindow.ft8BaseHz);
- const offsetHz = finiteNumber(msg.freq_hz);
- const rfHz = offsetHz !== null && baseHz !== null ? baseHz + offsetHz : null;
- const freq = rfHz?.toFixed(0) ?? "--";
- const message = msg.message ?? "";
- row.dataset.message = message.toUpperCase();
- row.innerHTML = `${fmtWsprTime(msg.ts_ms)}${snr?.toFixed(1) ?? "--"}${delta?.toFixed(2) ?? "--"}${freq}${renderWsprMessage(message)}`;
- applyWsprFilterToRow(row);
- return row;
- }
- function renderWsprHistory() {
- pruneWsprMessageHistory();
- if (!wsprMessagesEl) return;
- const fragment = document.createDocumentFragment();
- for (let i = 0; i < wsprMessageHistory.length; i += 1) {
- const message = wsprMessageHistory[i];
- if (message) fragment.appendChild(renderWsprRow(message));
- }
- wsprMessagesEl.replaceChildren(fragment);
- }
- function addWsprMessage(msg) {
- msg._tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
- wsprMessageHistory.unshift(msg);
- pruneWsprMessageHistory();
- scheduleWsprHistoryRender();
- }
- function normalizeServerWsprMessage(msg) {
- const raw = msg.message ?? "";
- const grids = extractAllGrids(raw);
- const station = extractLikelyCallsign(raw);
- const baseHz = finiteNumber(wsprWindow.ft8BaseHz);
- const offsetHz = finiteNumber(msg.freq_hz);
- const rfHz = offsetHz !== null && baseHz !== null ? baseHz + offsetHz : offsetHz;
- return {
- raw,
- grids,
- station,
- rfHz,
- history: {
- receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null,
- ts_ms: msg.ts_ms,
- snr_db: msg.snr_db,
- dt_s: msg.dt_s,
- freq_hz: msg.freq_hz,
- message: raw
- }
- };
- }
- function onServerWsprBatch(messages) {
- if (!Array.isArray(messages) || messages.length === 0) return;
- if (wsprStatus) wsprStatus.textContent = "Receiving";
- const normalized = [];
- for (const msg of messages) {
- const next = normalizeServerWsprMessage(msg);
- if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
- wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
- ...msg,
- ...next.rfHz === null ? {} : { freq_hz: next.rfHz }
- });
- }
- next.history._tsMs = Number.isFinite(next.history.ts_ms) ? Number(next.history.ts_ms) : Date.now();
- normalized.push(next.history);
- }
- normalized.reverse();
- wsprMessageHistory = normalized.concat(wsprMessageHistory);
- pruneWsprMessageHistory();
- scheduleWsprHistoryRender();
- }
- function pruneWsprHistoryView() {
- pruneWsprMessageHistory();
- renderWsprHistory();
- }
- function escapeWsprHtml(input) {
- return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
- }
- function renderWsprMessage(message) {
- let out = "";
- let i = 0;
- while (i < message.length) {
- const ch = message[i];
- if (isAlphaNum(ch)) {
- let j = i + 1;
- while (j < message.length && isAlphaNum(message[j])) j++;
- const token = message.slice(i, j);
- const grid = token.toUpperCase();
- if (isMaidenheadGridToken(grid)) {
- out += `${grid}`;
- } else {
- out += escapeWsprHtml(token);
- }
- i = j;
- } else {
- out += escapeWsprHtml(ch ?? "");
- i += 1;
- }
- }
- return out;
- }
- function extractAllGrids(message) {
- const out = [];
- const seen = /* @__PURE__ */ new Set();
- const parts = message.toUpperCase().split(/[^A-Z0-9]+/);
- for (const token of parts) {
- if (!token) continue;
- if (isMaidenheadGridToken(token) && !seen.has(token)) {
- seen.add(token);
- out.push(token);
- }
- }
- return out;
- }
- function extractLikelyCallsign(message) {
- const parts = message.toUpperCase().split(/[^A-Z0-9/]+/);
- for (const token of parts) {
- if (!token) continue;
- if (token.length < 3 || token.length > 12) continue;
- if (token === "CQ" || token === "DE" || token === "QRZ" || token === "DX") continue;
- if (isMaidenheadGridToken(token)) continue;
- if (/^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token)) return token;
- }
- return null;
- }
- function isFtxFarewellToken(token) {
- const normalized = token.trim().toUpperCase();
- return normalized === "RR73" || normalized === "73" || normalized === "RR";
- }
- function isMaidenheadGridToken(token) {
- const normalized = token.trim().toUpperCase();
- return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && !isFtxFarewellToken(normalized);
- }
- function isAlphaNum(ch) {
- return ch !== void 0 && /[A-Za-z0-9]/.test(ch);
- }
- function activateWsprHistoryLocator(target) {
- if (!(target instanceof Element)) return false;
- const locatorEl = target.closest(".ft8-locator[data-locator-grid]");
- if (!locatorEl) return false;
- const grid = (locatorEl.dataset.locatorGrid || "").toUpperCase();
- if (!grid) return false;
- if (typeof wsprWindow.navigateToMapLocator === "function") {
- wsprWindow.navigateToMapLocator(grid, "wspr");
- }
- return true;
- }
- function applyWsprFilterToRow(row) {
- if (!wsprFilterText) {
- row.style.display = "";
- return;
- }
- const message = row.dataset.message || "";
- row.style.display = message.includes(wsprFilterText) ? "" : "none";
- }
- function resetWsprHistoryView() {
- if (wsprMessagesEl) wsprMessagesEl.innerHTML = "";
- wsprMessageHistory = [];
- renderWsprHistory();
- if (wsprWindow.clearMapMarkersByType) wsprWindow.clearMapMarkersByType("wspr");
- }
- if (wsprFilterInput) {
- wsprFilterInput.addEventListener("input", () => {
- wsprFilterText = wsprFilterInput.value.trim().toUpperCase();
+// src/plugins/wspr.ts
+var wsprWindow = window;
+var wsprStatus = document.getElementById("wspr-status");
+var wsprPeriodEl = document.getElementById("wspr-period");
+var wsprMessagesEl = document.getElementById("wspr-messages");
+var wsprFilterInput = document.getElementById("wspr-filter");
+var WSPR_PERIOD_SECONDS = 120;
+var wsprFilterText = "";
+var wsprMessageHistory = [];
+function finiteNumber(value) {
+ const number = typeof value === "number" ? value : Number(value);
+ return Number.isFinite(number) ? number : null;
+}
+function currentWsprHistoryRetentionMs() {
+ return typeof wsprWindow.getDecodeHistoryRetentionMs === "function" ? wsprWindow.getDecodeHistoryRetentionMs() : 24 * 60 * 60 * 1e3;
+}
+function pruneWsprMessageHistory() {
+ const cutoffMs = Date.now() - currentWsprHistoryRetentionMs();
+ wsprMessageHistory = wsprMessageHistory.filter((msg) => Number(msg._tsMs ?? msg.ts_ms) >= cutoffMs);
+}
+function scheduleWsprHistoryRender() {
+ if (typeof wsprWindow.trxScheduleUiFrameJob === "function") {
+ wsprWindow.trxScheduleUiFrameJob("wspr-history", () => {
renderWsprHistory();
});
+ return;
}
- if (wsprMessagesEl) {
- wsprMessagesEl.addEventListener("click", (event) => {
- if (!activateWsprHistoryLocator(event.target)) return;
- event.preventDefault();
- event.stopPropagation();
- });
- wsprMessagesEl.addEventListener("keydown", (event) => {
- if (event.key !== "Enter" && event.key !== " ") return;
- if (!activateWsprHistoryLocator(event.target)) return;
- event.preventDefault();
- event.stopPropagation();
- });
+ renderWsprHistory();
+}
+function fmtWsprTime(tsMs) {
+ if (!tsMs) return "--:--:--";
+ return new Date(tsMs).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
+}
+function updateWsprPeriodTimer() {
+ if (!wsprPeriodEl) return;
+ const nowSec = Math.floor(Date.now() / 1e3);
+ const remaining = WSPR_PERIOD_SECONDS - nowSec % WSPR_PERIOD_SECONDS;
+ const mm = String(Math.floor(remaining / 60)).padStart(2, "0");
+ const ss = String(remaining % 60).padStart(2, "0");
+ wsprPeriodEl.textContent = `Next slot ${mm}:${ss}`;
+}
+updateWsprPeriodTimer();
+setInterval(updateWsprPeriodTimer, 500);
+function renderWsprRow(msg) {
+ const row = document.createElement("div");
+ row.className = "ft8-row";
+ row.dataset.decoder = "wspr";
+ const snr = finiteNumber(msg.snr_db);
+ const delta = finiteNumber(msg.dt_s);
+ const baseHz = finiteNumber(wsprWindow.ft8BaseHz);
+ const offsetHz = finiteNumber(msg.freq_hz);
+ const rfHz = offsetHz !== null && baseHz !== null ? baseHz + offsetHz : null;
+ const freq = rfHz?.toFixed(0) ?? "--";
+ const message = msg.message ?? "";
+ row.dataset.message = message.toUpperCase();
+ row.innerHTML = `${fmtWsprTime(msg.ts_ms)}${snr?.toFixed(1) ?? "--"}${delta?.toFixed(2) ?? "--"}${freq}${renderWsprMessage(message)}`;
+ applyWsprFilterToRow(row);
+ return row;
+}
+function renderWsprHistory() {
+ pruneWsprMessageHistory();
+ if (!wsprMessagesEl) return;
+ const fragment = document.createDocumentFragment();
+ for (let i = 0; i < wsprMessageHistory.length; i += 1) {
+ const message = wsprMessageHistory[i];
+ if (message) fragment.appendChild(renderWsprRow(message));
}
- var wsprDecodeToggleBtn = document.getElementById("wspr-decode-toggle-btn");
- wsprDecodeToggleBtn?.addEventListener("click", () => {
- void (async () => {
- try {
- await wsprWindow.takeSchedulerControlForDecoderDisable?.(wsprDecodeToggleBtn);
- await wsprWindow.postPath?.("/toggle_wspr_decode");
- } catch (error) {
- console.error("WSPR toggle failed", error);
- }
- })();
- });
- document.getElementById("settings-clear-wspr-history")?.addEventListener("click", () => {
- void (async () => {
- if (!await wsprWindow.trxUi.confirm({ title: "Clear WSPR history?", message: "All stored WSPR decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
- try {
- await wsprWindow.postPath?.("/clear_wspr_decode");
- resetWsprHistoryView();
- } catch (error) {
- console.error("WSPR history clear failed", error);
- }
- })();
- });
- function onServerWspr(msg) {
- if (wsprStatus) wsprStatus.textContent = "Receiving";
+ wsprMessagesEl.replaceChildren(fragment);
+}
+function addWsprMessage(msg) {
+ msg._tsMs = Number.isFinite(msg.ts_ms) ? Number(msg.ts_ms) : Date.now();
+ wsprMessageHistory.unshift(msg);
+ pruneWsprMessageHistory();
+ scheduleWsprHistoryRender();
+}
+function normalizeServerWsprMessage(msg) {
+ const raw = msg.message ?? "";
+ const grids = extractAllGrids(raw);
+ const station = extractLikelyCallsign(raw);
+ const baseHz = finiteNumber(wsprWindow.ft8BaseHz);
+ const offsetHz = finiteNumber(msg.freq_hz);
+ const rfHz = offsetHz !== null && baseHz !== null ? baseHz + offsetHz : offsetHz;
+ return {
+ raw,
+ grids,
+ station,
+ rfHz,
+ history: {
+ receiver: wsprWindow.getDecodeRigMeta ? wsprWindow.getDecodeRigMeta() : null,
+ ts_ms: msg.ts_ms,
+ snr_db: msg.snr_db,
+ dt_s: msg.dt_s,
+ freq_hz: msg.freq_hz,
+ message: raw
+ }
+ };
+}
+function onServerWsprBatch(messages) {
+ if (!Array.isArray(messages) || messages.length === 0) return;
+ if (wsprStatus) wsprStatus.textContent = "Receiving";
+ const normalized = [];
+ for (const msg of messages) {
const next = normalizeServerWsprMessage(msg);
if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
@@ -257,14 +107,161 @@
...next.rfHz === null ? {} : { freq_hz: next.rfHz }
});
}
- addWsprMessage(next.history);
+ next.history._tsMs = Number.isFinite(next.history.ts_ms) ? Number(next.history.ts_ms) : Date.now();
+ normalized.push(next.history);
}
- wsprWindow.trxPluginRuntime.registerDecoder({
- id: "wspr",
- onMessage: onServerWspr,
- onBatch: onServerWsprBatch,
- restore: onServerWsprBatch,
- prune: pruneWsprHistoryView,
- reset: resetWsprHistoryView
+ normalized.reverse();
+ wsprMessageHistory = normalized.concat(wsprMessageHistory);
+ pruneWsprMessageHistory();
+ scheduleWsprHistoryRender();
+}
+function pruneWsprHistoryView() {
+ pruneWsprMessageHistory();
+ renderWsprHistory();
+}
+function escapeWsprHtml(input) {
+ return input.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
+}
+function renderWsprMessage(message) {
+ let out = "";
+ let i = 0;
+ while (i < message.length) {
+ const ch = message[i];
+ if (isAlphaNum(ch)) {
+ let j = i + 1;
+ while (j < message.length && isAlphaNum(message[j])) j++;
+ const token = message.slice(i, j);
+ const grid = token.toUpperCase();
+ if (isMaidenheadGridToken(grid)) {
+ out += `${grid}`;
+ } else {
+ out += escapeWsprHtml(token);
+ }
+ i = j;
+ } else {
+ out += escapeWsprHtml(ch ?? "");
+ i += 1;
+ }
+ }
+ return out;
+}
+function extractAllGrids(message) {
+ const out = [];
+ const seen = /* @__PURE__ */ new Set();
+ const parts = message.toUpperCase().split(/[^A-Z0-9]+/);
+ for (const token of parts) {
+ if (!token) continue;
+ if (isMaidenheadGridToken(token) && !seen.has(token)) {
+ seen.add(token);
+ out.push(token);
+ }
+ }
+ return out;
+}
+function extractLikelyCallsign(message) {
+ const parts = message.toUpperCase().split(/[^A-Z0-9/]+/);
+ for (const token of parts) {
+ if (!token) continue;
+ if (token.length < 3 || token.length > 12) continue;
+ if (token === "CQ" || token === "DE" || token === "QRZ" || token === "DX") continue;
+ if (isMaidenheadGridToken(token)) continue;
+ if (/^[A-Z0-9/]{1,5}\d[A-Z0-9/]{1,6}$/.test(token)) return token;
+ }
+ return null;
+}
+function isFtxFarewellToken(token) {
+ const normalized = token.trim().toUpperCase();
+ return normalized === "RR73" || normalized === "73" || normalized === "RR";
+}
+function isMaidenheadGridToken(token) {
+ const normalized = token.trim().toUpperCase();
+ return /^[A-R]{2}\d{2}(?:[A-X]{2})?$/.test(normalized) && !isFtxFarewellToken(normalized);
+}
+function isAlphaNum(ch) {
+ return ch !== void 0 && /[A-Za-z0-9]/.test(ch);
+}
+function activateWsprHistoryLocator(target) {
+ if (!(target instanceof Element)) return false;
+ const locatorEl = target.closest(".ft8-locator[data-locator-grid]");
+ if (!locatorEl) return false;
+ const grid = (locatorEl.dataset.locatorGrid || "").toUpperCase();
+ if (!grid) return false;
+ if (typeof wsprWindow.navigateToMapLocator === "function") {
+ wsprWindow.navigateToMapLocator(grid, "wspr");
+ }
+ return true;
+}
+function applyWsprFilterToRow(row) {
+ if (!wsprFilterText) {
+ row.style.display = "";
+ return;
+ }
+ const message = row.dataset.message || "";
+ row.style.display = message.includes(wsprFilterText) ? "" : "none";
+}
+function resetWsprHistoryView() {
+ if (wsprMessagesEl) wsprMessagesEl.innerHTML = "";
+ wsprMessageHistory = [];
+ renderWsprHistory();
+ if (wsprWindow.clearMapMarkersByType) wsprWindow.clearMapMarkersByType("wspr");
+}
+if (wsprFilterInput) {
+ wsprFilterInput.addEventListener("input", () => {
+ wsprFilterText = wsprFilterInput.value.trim().toUpperCase();
+ renderWsprHistory();
});
-})();
+}
+if (wsprMessagesEl) {
+ wsprMessagesEl.addEventListener("click", (event) => {
+ if (!activateWsprHistoryLocator(event.target)) return;
+ event.preventDefault();
+ event.stopPropagation();
+ });
+ wsprMessagesEl.addEventListener("keydown", (event) => {
+ if (event.key !== "Enter" && event.key !== " ") return;
+ if (!activateWsprHistoryLocator(event.target)) return;
+ event.preventDefault();
+ event.stopPropagation();
+ });
+}
+var wsprDecodeToggleBtn = document.getElementById("wspr-decode-toggle-btn");
+wsprDecodeToggleBtn?.addEventListener("click", () => {
+ void (async () => {
+ try {
+ await wsprWindow.takeSchedulerControlForDecoderDisable?.(wsprDecodeToggleBtn);
+ await wsprWindow.postPath?.("/toggle_wspr_decode");
+ } catch (error) {
+ console.error("WSPR toggle failed", error);
+ }
+ })();
+});
+document.getElementById("settings-clear-wspr-history")?.addEventListener("click", () => {
+ void (async () => {
+ if (!await wsprWindow.trxUi.confirm({ title: "Clear WSPR history?", message: "All stored WSPR decodes will be permanently removed.", confirmLabel: "Clear history" })) return;
+ try {
+ await wsprWindow.postPath?.("/clear_wspr_decode");
+ resetWsprHistoryView();
+ } catch (error) {
+ console.error("WSPR history clear failed", error);
+ }
+ })();
+});
+function onServerWspr(msg) {
+ if (wsprStatus) wsprStatus.textContent = "Receiving";
+ const next = normalizeServerWsprMessage(msg);
+ if (next.grids.length > 0 && wsprWindow.mapAddLocator) {
+ wsprWindow.mapAddLocator(next.raw, next.grids, "wspr", next.station, {
+ ...msg,
+ ...next.rfHz === null ? {} : { freq_hz: next.rfHz }
+ });
+ }
+ addWsprMessage(next.history);
+}
+wsprWindow.trxPluginRuntime.registerDecoder({
+ id: "wspr",
+ onMessage: onServerWspr,
+ onBatch: onServerWsprBatch,
+ restore: onServerWsprBatch,
+ prune: pruneWsprHistoryView,
+ reset: resetWsprHistoryView
+});
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 ac072ec0..3246eace 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
@@ -67,7 +67,10 @@ await build({
},
outdir: outputDir,
bundle: true,
- format: "iife",
+ format: "esm",
+ splitting: true,
+ entryNames: "[name]",
+ chunkNames: "chunk-[hash]",
platform: "browser",
target: "es2022",
sourcemap: false,
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/aprs.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/aprs.test.mjs
index a8b95aa9..6955db3c 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/aprs.test.mjs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/aprs.test.mjs
@@ -6,6 +6,7 @@ import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import vm from "node:vm";
+import { bundleEntry } from "./bundle-entry.mjs";
test("APRS entry normalizes positioned packets without remote symbol assets", async () => {
const forwarded = [];
@@ -26,7 +27,7 @@ test("APRS entry normalizes positioned packets without remote symbol assets", as
console,
});
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
- const source = await readFile(new URL("../../assets/web/generated/aprs.js", import.meta.url), "utf8");
+ const source = await bundleEntry(new URL("../src/plugins/aprs.ts", import.meta.url));
assert.equal(source.includes("raw.githubusercontent.com"), false);
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context);
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/bundle-entry.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/bundle-entry.mjs
new file mode 100644
index 00000000..1b2d04ad
--- /dev/null
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/bundle-entry.mjs
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: 2026 Stan Grams
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+import { build } from "esbuild";
+
+export async function bundleEntry(entryUrl) {
+ const result = await build({
+ entryPoints: [entryUrl.pathname],
+ bundle: true,
+ format: "iife",
+ platform: "browser",
+ target: "es2022",
+ write: false,
+ });
+ const output = result.outputFiles[0];
+ if (!output) throw new Error(`No bundle output for ${entryUrl.pathname}`);
+ return output.text;
+}
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ftx-family.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ftx-family.test.mjs
index 548a8e41..e1373dc3 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ftx-family.test.mjs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/ftx-family.test.mjs
@@ -6,6 +6,7 @@ import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import vm from "node:vm";
+import { bundleEntry } from "./bundle-entry.mjs";
test("FT2 entry normalizes audio offsets and registers typed callbacks", async () => {
let barRenderer;
@@ -30,7 +31,7 @@ test("FT2 entry normalizes audio offsets and registers typed callbacks", async (
console,
});
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
- const source = await readFile(new URL("../../assets/web/generated/ft2.js", import.meta.url), "utf8");
+ const source = await bundleEntry(new URL("../src/plugins/ft2.ts", import.meta.url));
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context);
@@ -62,7 +63,7 @@ test("FT8 entry installs shared parsing without relying on script globals", asyn
console,
});
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
- const source = await readFile(new URL("../../assets/web/generated/ft8.js", import.meta.url), "utf8");
+ const source = await bundleEntry(new URL("../src/plugins/ft8.ts", import.meta.url));
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context);
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/hf-aprs.test.mjs b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/hf-aprs.test.mjs
index ab061222..224bd318 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/hf-aprs.test.mjs
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/tests/hf-aprs.test.mjs
@@ -6,6 +6,7 @@ import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import vm from "node:vm";
+import { bundleEntry } from "./bundle-entry.mjs";
test("HF APRS entry uses shared typed normalization and local symbols", async () => {
const window = { trxUi: { confirm: async () => true } };
@@ -22,7 +23,7 @@ test("HF APRS entry uses shared typed normalization and local symbols", async ()
console,
});
const runtime = await readFile(new URL("../../assets/web/generated/plugin-runtime.js", import.meta.url), "utf8");
- const source = await readFile(new URL("../../assets/web/generated/hf-aprs.js", import.meta.url), "utf8");
+ const source = await bundleEntry(new URL("../src/plugins/hf-aprs.ts", import.meta.url));
assert.equal(source.includes("raw.githubusercontent.com"), false);
new vm.Script(runtime).runInContext(context);
new vm.Script(source).runInContext(context);