diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js
index 67087b20..6c49c1e8 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/app.js
@@ -1258,7 +1258,7 @@ function applyRigList(activeRigId, rigIds, displayNames) {
updateRigIdentitySummary(lastActiveRigId);
window.trxUi?.setActiveRig(lastActiveRigId);
if (rigListChanged) {
- if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId);
+ window.trx.modules.scheduler?.setRig(lastActiveRigId);
if (typeof setBackgroundDecodeRig === "function") setBackgroundDecodeRig(lastActiveRigId);
if (typeof bmPopulateScopePicker === "function") bmPopulateScopePicker();
if (typeof bmFetch === "function") bmFetch(document.getElementById("bm-category-filter")?.value || "");
@@ -3593,7 +3593,7 @@ async function switchRigFromSelect(selectEl) {
updateRigSubtitle(lastActiveRigId);
updateRigIdentitySummary(lastActiveRigId);
window.trxUi?.setActiveRig(lastActiveRigId);
- if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId);
+ window.trx.modules.scheduler?.setRig(lastActiveRigId);
if (typeof setBackgroundDecodeRig === "function") setBackgroundDecodeRig(lastActiveRigId);
if (typeof bmFetch === "function") bmFetch(document.getElementById("bm-category-filter")?.value || "");
window.trx.modules.map?.syncAprsReceiverMarker();
@@ -4333,10 +4333,8 @@ async function initializeApp() {
}
}
function initSettingsUI() {
- if (typeof initScheduler === "function") {
- initScheduler(lastActiveRigId, authRole);
- wireSchedulerEvents();
- }
+ window.trx.modules.scheduler?.initialize(lastActiveRigId, authRole);
+ window.trx.modules.scheduler?.wireEvents();
if (typeof initBackgroundDecode === "function") {
initBackgroundDecode(lastActiveRigId, authRole);
wireBackgroundDecodeEvents();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js
index 08fe1bbe..090bf81f 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/plugin-loader.js
@@ -10,7 +10,7 @@ const pluginGroups = {
};
const loaded = /* @__PURE__ */ new Set();
const loading = /* @__PURE__ */ new Map();
-const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js", "/sat.js", "/sat-scheduler.js", "/vchan.js", "/bookmarks.js"]);
+const modulePlugins = /* @__PURE__ */ new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js", "/sat.js", "/sat-scheduler.js", "/vchan.js", "/bookmarks.js", "/scheduler.js"]);
function loadLegacyScript(path) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
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 fcd55ae2..7a8848a0 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
@@ -24,8 +24,9 @@
centerHz: document.getElementById("scheduler-sat-center-hz")
};
let editIdx = null;
+ let eventsWired = false;
function getBridge() {
- return satSchedulerWindow.schedulerBridge ?? null;
+ return satSchedulerWindow.trx?.modules?.scheduler ?? null;
}
function getConfig() {
const b = getBridge();
@@ -230,6 +231,8 @@
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() {
@@ -255,5 +258,9 @@
renderPassStatus,
collectSatelliteConfig
};
+ if (getBridge()) {
+ wireEvents();
+ renderSection();
+ }
})();
})();
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 e5493205..289e8bb9 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,1195 +1,1223 @@
"use strict";
-(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();
+(() => {
+ // 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 destroyScheduler() {
- if (statusInterval) {
- clearInterval(statusInterval);
- statusInterval = null;
+ (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();
}
- if (interleaveTicker) {
- clearInterval(interleaveTicker);
- interleaveTicker = null;
+ function destroyScheduler() {
+ if (statusInterval) {
+ clearInterval(statusInterval);
+ statusInterval = null;
+ }
+ if (interleaveTicker) {
+ clearInterval(interleaveTicker);
+ interleaveTicker = null;
+ }
}
- }
- 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) {
+ 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 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() {
- var 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(e) {
- console.error("scheduler load failed", e);
- 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 };
+ });
}
- 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 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();
+ });
}
- if (active.length === 1) {
- return { activeEntries: active, currentIndex: 0, remainingSec: 0, cycleMin: 0 };
+ 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();
+ });
}
- var exclIdx = active.findIndex(function(e) {
- return e.exclusive;
- });
- if (exclIdx >= 0) {
- return { activeEntries: [active[exclIdx]], 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];
- 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 nextCumulative = cumulative + durations[i];
- if (posMin < nextCumulative) {
- slotStart = cumulative;
- cumulative = nextCumulative;
- currentIndex = i;
- currentDuration = durations[i];
- break;
- }
- 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 = document.getElementById("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) {
- var activeName = schedulerEntryDisplayName(state.activeEntries[state.currentIndex]);
- var totalSlotSec = state.cycleMin > 0 ? state.cycleMin * 60 / state.activeEntries.length : 0;
- var elapsedPct = totalSlotSec > 0 ? Math.min(100, Math.max(0, (totalSlotSec - state.remainingSec) / totalSlotSec * 100)) : 0;
- var ringFill = document.getElementById("interleave-ring-fill");
- if (ringFill) ringFill.setAttribute("stroke-dashoffset", String(100 - elapsedPct));
- var nameEl = document.getElementById("interleave-active-name");
- if (nameEl) nameEl.textContent = activeName;
- var countdownEl = document.getElementById("interleave-countdown");
- if (countdownEl) countdownEl.textContent = "next in " + state.remainingSec + "s · " + state.cycleMin + "m cycle";
- }
- renderTimelineNeedle();
- renderSchedulerStepControls();
- }
- function renderSchedulerStepControls() {
- const prevBtn = document.getElementById("scheduler-prev-btn");
- const nextBtn = document.getElementById("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 = document.getElementById("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 + "]" : "";
- var 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() {
- var wrap = document.getElementById("scheduler-activity-log-wrap");
- var container = document.getElementById("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 = "";
- var html = entries.slice().reverse().map(function(e) {
- var d = new Date(e.utc * 1e3);
- var ts = d.toUTCString();
- var action = e.action || "unknown";
- var label = e.entry_label || "";
- var 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 = document.getElementById("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 = document.getElementById("scheduler-grayline-section");
- const tsSection = document.getElementById("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 != null ? gl.lat : typeof serverLat !== "undefined" ? serverLat : "";
- const lon = gl.lon != null ? gl.lon : typeof serverLon !== "undefined" ? serverLon : "";
- setInputValue("scheduler-gl-lat", lat != null ? lat : "");
- setInputValue("scheduler-gl-lon", lon != null ? lon : "");
- var gridEl = document.getElementById("scheduler-gl-grid");
- if (gridEl && lat !== "" && lon !== "") {
- 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 = typeof serverLat !== "undefined" ? serverLat : "";
- const lon = typeof serverLon !== "undefined" ? serverLon : "";
- setInputValue("scheduler-gl-lat", lat != null ? lat : "");
- setInputValue("scheduler-gl-lon", lon != null ? lon : "");
- var gridEl2 = document.getElementById("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 = document.getElementById("scheduler-ts-interleave");
- if (ilEl) {
- const il = currentConfig && currentConfig.interleave_min;
- ilEl.value = il ? il : "";
- }
- renderTimespanEntries();
- const formEls = panel.querySelectorAll("input, select, button.sch-write");
- formEls.forEach(function(el) {
- el.disabled = !isControl;
- });
- const saveBtn = document.getElementById("scheduler-save-btn");
- if (saveBtn) {
- saveBtn.style.display = isControl ? "" : "none";
- }
- const resetBtn = document.getElementById("scheduler-reset-btn");
- if (resetBtn) {
- resetBtn.style.display = isControl ? "" : "none";
- }
- }
- function setSelected(id, value) {
- const el = document.getElementById(id);
- if (el) el.value = value;
- }
- function setInputValue(id, value) {
- const el = document.getElementById(id);
- if (el) el.value = value;
- }
- function renderBookmarkSelect(id, selectedId) {
- const sel = document.getElementById(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 = document.getElementById("sch-entry-form-title");
- if (titleEl) titleEl.textContent = entry ? "Edit Entry" : "Add Entry";
- const startEl = document.getElementById("scheduler-ts-start");
- const endEl = document.getElementById("scheduler-ts-end");
- const bmEl = document.getElementById("scheduler-ts-bookmark");
- const labelEl = document.getElementById("scheduler-ts-label");
- const ilEl = document.getElementById("scheduler-ts-entry-interleave");
- const centerHzEl = document.getElementById("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 && entry.interleave_min ? entry.interleave_min : "";
- if (centerHzEl) centerHzEl.value = entry && entry.center_hz ? entry.center_hz : "";
- const recordEl = document.getElementById("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 = document.getElementById("sch-entry-form-wrap");
- if (wrap) {
- wrap.style.display = "block";
- if (startEl) startEl.focus();
- }
- }
- function schCloseEntryForm() {
- const wrap = document.getElementById("sch-entry-form-wrap");
- if (wrap) wrap.style.display = "none";
- schEntryEditIdx = null;
- pendingExtraBmIds = [];
- }
- function schEntryFormSubmit(e) {
- e.preventDefault();
- const startEl = document.getElementById("scheduler-ts-start");
- const endEl = document.getElementById("scheduler-ts-end");
- const bmEl = document.getElementById("scheduler-ts-bookmark");
- const labelEl = document.getElementById("scheduler-ts-label");
- const ilEl = document.getElementById("scheduler-ts-entry-interleave");
- const centerHzEl = document.getElementById("scheduler-ts-center-hz");
- if (!startEl || !endEl || !bmEl) return;
- const bmId = bmEl.value;
- if (!bmId) {
- window.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();
- if (!currentConfig) {
- currentConfig = { remote: currentRigId, mode: "time_span", entries: [] };
- }
- if (!currentConfig.entries) currentConfig.entries = [];
- const recordCb = document.getElementById("scheduler-ts-entry-record");
- const entryRecord = recordCb ? recordCb.checked : false;
- const entryData = {
- start_min: startMin,
- end_min: endMin,
- bookmark_id: bmId,
- label: label || null,
- interleave_min: entryInterleave,
- center_hz: centerHz,
- bookmark_ids: extraBmIds,
- record: entryRecord
- };
- if (schEntryEditIdx !== null) {
- const existing = currentConfig.entries[schEntryEditIdx];
- entryData.id = existing ? existing.id : "ts_" + Date.now().toString(36);
- currentConfig.entries[schEntryEditIdx] = entryData;
- } else {
- entryData.id = "ts_" + Date.now().toString(36);
- currentConfig.entries.push(entryData);
- }
- schCloseEntryForm();
- renderTimespanEntries();
- markSchedulerDirty();
- }
- var TIMELINE_COLORS = ["#38bdf8", "#f59e0b", "#a78bfa", "#34d399", "#fb7185", "#60a5fa"];
- function renderTimeline() {
- var container = document.getElementById("scheduler-ts-timeline");
- if (!container) return;
- var entries = currentConfig && Array.isArray(currentConfig.entries) ? currentConfig.entries : [];
- if (entries.length === 0) {
- container.innerHTML = "";
- return;
- }
- var W = 1e3;
- var H = 80;
- var BAR_Y = 6;
- var BAR_H = 30;
- var TICK_Y = BAR_Y + BAR_H + 2;
- var svg = '";
- container.innerHTML = svg;
- container.querySelectorAll(".sch-timeline-seg").forEach(function(seg) {
- seg.addEventListener("click", function() {
- var i = parseInt(seg.getAttribute("data-idx"), 10);
- var entry = currentConfig && currentConfig.entries ? currentConfig.entries[i] : null;
- if (entry) schOpenEntryForm(entry, i);
- });
- });
- var svgEl = container.querySelector("svg");
- if (svgEl) {
- svgEl.addEventListener("click", function(e) {
- if (e.target.classList.contains("sch-timeline-seg")) return;
- var rect = svgEl.getBoundingClientRect();
- var xPct = (e.clientX - rect.left) / rect.width;
- var clickMin = Math.floor(xPct * 1440);
- var startHour = Math.floor(clickMin / 60);
- var startMin = startHour * 60;
- var endMin = (startHour + 1) % 24 * 60;
- schOpenEntryForm(null, null);
- var startEl = document.getElementById("scheduler-ts-start");
- var endEl = document.getElementById("scheduler-ts-end");
- if (startEl) startEl.value = minToHHMM(startMin);
- if (endEl) endEl.value = minToHHMM(endMin);
- });
- svgEl.style.cursor = "crosshair";
- }
- }
- function timelineNeedleSvg() {
- var info = schedulerUtcMinuteInfo();
- var nowMin = info.minuteOfDay + info.secondOfMinute / 60;
- var x = nowMin / 1440 * 1e3;
- return '';
- }
- function renderTimelineNeedle() {
- var g = document.getElementById("sch-timeline-needle-g");
- if (g) g.innerHTML = timelineNeedleSvg();
- }
- function schInlineEdit(tr, entry, idx) {
- var bmOptions = bookmarkList.map(function(bm) {
- var sel = bm.id === entry.bookmark_id ? " selected" : "";
- return '";
- }).join("");
- var extraBmOptions = '' + bookmarkList.map(function(bm) {
- return '";
- }).join("");
- var 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");
- var chipsContainer = tr.querySelector(".sch-inline-extra-chips");
- var extraPick = tr.querySelector(".sch-inline-extra-pick");
- var extraAddBtn = tr.querySelector(".sch-inline-extra-add");
- function renderInlineExtraChips() {
- chipsContainer.innerHTML = "";
- inlineExtraIds.forEach(function(id, i) {
- var chip = document.createElement("span");
- chip.className = "sch-extra-bm-chip";
- var 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)));
- chipsContainer.appendChild(chip);
- });
- Array.from(extraPick.options).forEach(function(opt) {
- if (opt.value) opt.disabled = inlineExtraIds.includes(opt.value);
+ 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();
});
}
- renderInlineExtraChips();
- extraAddBtn.addEventListener("click", function() {
- if (!extraPick.value) return;
- if (!inlineExtraIds.includes(extraPick.value)) {
- inlineExtraIds.push(extraPick.value);
- renderInlineExtraChips();
- }
- extraPick.value = "";
- });
- var exclEl = tr.querySelector('[data-field="exclusive"]');
- var ilInput = tr.querySelector('[data-field="interleave"]');
- if (exclEl && ilInput) {
- exclEl.addEventListener("change", function() {
- ilInput.disabled = exclEl.checked;
- if (exclEl.checked) ilInput.value = "";
+ function apiGetBookmarks() {
+ const url = currentRigId ? "/bookmarks?scope=" + encodeURIComponent(currentRigId) : "/bookmarks";
+ return fetch(url).then(function(r) {
+ return r.ok ? r.json() : [];
});
}
- tr.querySelector(".sch-inline-save").addEventListener("click", function() {
- var startEl = tr.querySelector('[data-field="start"]');
- var endEl = tr.querySelector('[data-field="end"]');
- var bmEl = tr.querySelector('[data-field="bookmark"]');
- var labelEl = tr.querySelector('[data-field="label"]');
- var ilEl = tr.querySelector('[data-field="interleave"]');
- var recEl = tr.querySelector('[data-field="record"]');
- var exEl = tr.querySelector('[data-field="exclusive"]');
- if (bmEl && !bmEl.value) {
- window.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;
- var 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;
- currentConfig.entries[idx] = entry;
- renderTimespanEntries();
- markSchedulerDirty();
- });
- tr.querySelector(".sch-inline-cancel").addEventListener("click", function() {
- renderTimespanEntries();
- });
- }
- function renderTimespanEntries() {
- const tbody = document.getElementById("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;
- if (entry) schInlineEdit(btn.closest("tr"), entry, i);
- });
- });
- tbody.querySelectorAll(".sch-remove-btn").forEach(function(btn) {
- btn.addEventListener("click", function() {
- removeEntry(parseInt(btn.dataset.idx, 10));
- });
- });
- (function() {
- var handles = tbody.querySelectorAll(".sch-drag-handle");
- var dragIdx = null;
- handles.forEach(function(handle, idx) {
- var row = handle.parentElement;
- handle.addEventListener("dragstart", function(e) {
- dragIdx = idx;
- row.classList.add("sch-dragging");
- e.dataTransfer.effectAllowed = "move";
- e.dataTransfer.setData("text/plain", String(idx));
- });
- row.addEventListener("dragover", function(e) {
- e.preventDefault();
- e.dataTransfer.dropEffect = "move";
- row.classList.add("sch-drag-over");
- });
- row.addEventListener("dragleave", function() {
- row.classList.remove("sch-drag-over");
- });
- row.addEventListener("drop", function(e) {
- e.preventDefault();
- row.classList.remove("sch-drag-over");
- if (dragIdx === null || dragIdx === idx) return;
- var entries2 = currentConfig.entries;
- var moved = entries2.splice(dragIdx, 1)[0];
- entries2.splice(idx, 0, moved);
- renderTimespanEntries();
- markSchedulerDirty();
- });
- handle.addEventListener("dragend", function() {
- row.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) {
- var now = /* @__PURE__ */ new Date();
- var utcMidnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
- var utcMs = utcMidnight.getTime() + min * 6e4;
- var 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;
- var lonField = grid.charCodeAt(0) - 65;
- var latField = grid.charCodeAt(1) - 65;
- var lonSquare = parseInt(grid.charAt(2), 10);
- var latSquare = parseInt(grid.charAt(3), 10);
- if (isNaN(lonSquare) || isNaN(latSquare) || lonField < 0 || lonField > 17 || latField < 0 || latField > 17) return null;
- var lon = lonField * 20 + lonSquare * 2 - 180;
- var lat = latField * 10 + latSquare * 1 - 90;
- if (grid.length >= 6) {
- var lonSub = grid.charCodeAt(4) - 65;
- var 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 = parseFloat(lon) + 180;
- lat = parseFloat(lat) + 90;
- if (isNaN(lon) || isNaN(lat)) return "";
- var lonField = String.fromCharCode(65 + Math.floor(lon / 20));
- var latField = String.fromCharCode(65 + Math.floor(lat / 10));
- var lonSquare = Math.floor(lon % 20 / 2);
- var latSquare = Math.floor(lat % 10);
- var lonSub = String.fromCharCode(97 + Math.floor(lon % 2 / 2 * 24));
- var 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);
- if (!currentRigId || 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;
- schedulerStepPending = true;
- renderSchedulerStepControls();
- Promise.resolve(typeof vchanTakeSchedulerControl === "function" ? vchanTakeSchedulerControl() : null).then(function() {
- return apiActivateSchedulerEntry(currentRigId, target.id);
- }).then(function(status) {
- currentSchedulerStatus = status || null;
- return Promise.resolve(
- typeof vchanToggleSchedulerRelease === "function" ? vchanToggleSchedulerRelease() : null
- ).then(function() {
- renderStatus(status);
- renderSchedulerInterleaveStatus();
- showSchedulerToast("Selected " + schedulerEntryDisplayName(target) + ".");
- pollStatus();
- });
- }).catch(function(e) {
- console.error("scheduler entry selection failed", e);
- showSchedulerToast("Scheduler entry selection failed: " + e.message, 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 = document.getElementById("scheduler-mode-select");
- const mode = modeEl ? modeEl.value : "disabled";
- const config = {
- remote: rig,
- mode,
- grayline: null,
- entries: []
- };
- if (mode === "grayline") {
- const lat = parseFloat(document.getElementById("scheduler-gl-lat").value);
- const lon = parseFloat(document.getElementById("scheduler-gl-lon").value);
- const win = parseInt(document.getElementById("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(document.getElementById("scheduler-ts-interleave").value, 10);
- config.interleave_min = isNaN(ilVal) || ilVal <= 0 ? null : ilVal;
- }
- config.satellites = collectSatelliteConfig();
- var missingBmErrors = [];
- if (mode === "grayline" && config.grayline) {
- var gl = config.grayline;
- var 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) {
- if (!bookmarkExists(gl[pair[0]])) missingBmErrors.push(pair[1] + " (bookmark " + gl[pair[0]] + ")");
- });
- }
- if (mode === "time_span" && Array.isArray(config.entries)) {
- config.entries.forEach(function(entry, idx) {
- var label = entry.label || "Entry #" + (idx + 1);
- if (!bookmarkExists(entry.bookmark_id)) {
- missingBmErrors.push(label + " primary bookmark (" + entry.bookmark_id + ")");
- }
- var 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) {
- var satLabel = sat.name || "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 = document.getElementById("scheduler-save-btn");
- if (btn) btn.disabled = true;
- apiPutScheduler(rig, config).then(function(saved) {
- currentConfig = saved;
- renderScheduler();
- clearSchedulerDirty();
- showSchedulerToast("Scheduler saved.");
- }).catch(function(e) {
- showSchedulerToast("Save failed: " + e.message, true);
- }).finally(function() {
- if (btn) btn.disabled = false;
- });
- }
- function selectVal(id) {
- const el = document.getElementById(id);
- return el ? el.value : "";
- }
- async function resetScheduler() {
- const rig = currentRigId;
- if (!rig) return;
- if (!await window.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(e) {
- showSchedulerToast("Reset failed: " + e.message, true);
- });
- }
- function markSchedulerDirty() {
- if (schedulerDirty) return;
- schedulerDirty = true;
- var btn = document.getElementById("scheduler-save-btn");
- if (btn) btn.classList.add("sch-dirty");
- }
- function clearSchedulerDirty() {
- schedulerDirty = false;
- var btn = document.getElementById("scheduler-save-btn");
- if (btn) btn.classList.remove("sch-dirty");
- }
- function showSchedulerToast(msg, isError) {
- const el = document.getElementById("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 = document.getElementById("scheduler-mode-select");
- if (modeEl) {
- modeEl.addEventListener("change", function() {
- if (!currentConfig) currentConfig = { remote: currentRigId, mode: modeEl.value, entries: [] };
- currentConfig.mode = modeEl.value;
+ 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();
});
}
- const saveBtn = document.getElementById("scheduler-save-btn");
- if (saveBtn) saveBtn.addEventListener("click", saveScheduler);
- const resetBtn = document.getElementById("scheduler-reset-btn");
- if (resetBtn) resetBtn.addEventListener("click", resetScheduler);
- const addBtn = document.getElementById("scheduler-ts-add-btn");
- if (addBtn) addBtn.addEventListener("click", function() {
- schOpenEntryForm(null, null);
- });
- const entryForm = document.getElementById("sch-entry-form");
- if (entryForm) entryForm.addEventListener("submit", schEntryFormSubmit);
- const cancelBtn = document.getElementById("sch-entry-form-cancel");
- if (cancelBtn) cancelBtn.addEventListener("click", schCloseEntryForm);
- const prevBtn = document.getElementById("scheduler-prev-btn");
- if (prevBtn) prevBtn.addEventListener("click", function() {
- schedulerSelectRelativeEntry(-1);
- });
- const nextBtn = document.getElementById("scheduler-next-btn");
- if (nextBtn) nextBtn.addEventListener("click", function() {
- schedulerSelectRelativeEntry(1);
- });
- var schPanel = document.getElementById("scheduler-panel");
- if (schPanel && !schPanel._dirtyWired) {
- schPanel._dirtyWired = true;
- schPanel.addEventListener("input", function(e) {
- if (e.target.closest("#sch-entry-form") || e.target.closest("#sch-sat-form")) return;
- markSchedulerDirty();
- });
- schPanel.addEventListener("change", function(e) {
- if (e.target.closest("#sch-entry-form") || e.target.closest("#sch-sat-form")) return;
- markSchedulerDirty();
- });
+ function startStatusPolling() {
+ if (statusInterval) clearInterval(statusInterval);
+ statusInterval = setInterval(pollStatus, 15e3);
+ pollStatus();
}
- var gridEl = document.getElementById("scheduler-gl-grid");
- if (gridEl) {
- gridEl.addEventListener("input", function() {
- var ll = gridToLatLon(gridEl.value);
- if (ll) {
- setInputValue("scheduler-gl-lat", ll.lat.toFixed(3));
- setInputValue("scheduler-gl-lon", ll.lon.toFixed(3));
- markSchedulerDirty();
- }
- });
+ function startInterleaveTicker() {
+ if (interleaveTicker) clearInterval(interleaveTicker);
+ interleaveTicker = setInterval(renderSchedulerInterleaveStatus, 1e3);
+ renderSchedulerInterleaveStatus();
}
- var latEl = document.getElementById("scheduler-gl-lat");
- var lonEl = document.getElementById("scheduler-gl-lon");
- [latEl, lonEl].forEach(function(el) {
- if (el) {
- el.addEventListener("input", function() {
- var la = parseFloat(document.getElementById("scheduler-gl-lat").value);
- var lo = parseFloat(document.getElementById("scheduler-gl-lon").value);
- var gEl = document.getElementById("scheduler-gl-grid");
- if (gEl && !isNaN(la) && !isNaN(lo)) {
- gEl.value = latLonToGrid(la, lo);
- }
- });
+ 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 };
}
- });
- wireExtraBmAdd();
- wireSatelliteEvents();
- }
- function populateTsBookmarkSelect() {
- const sel = document.getElementById("scheduler-ts-bookmark");
- const extraSel = document.getElementById("scheduler-ts-extra-bm-pick");
- [sel, extraSel].forEach(function(el) {
+ 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;
+ }
+ 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;
- const prev = el.value;
- el.innerHTML = '';
+ 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";
+ }
+ }
+ 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 + ")";
- el.appendChild(opt);
+ if (bm.id === selectedId) opt.selected = true;
+ sel.appendChild(opt);
});
- if (prev) el.value = prev;
- });
- }
- let pendingExtraBmIds = [];
- function renderExtraBmList() {
- var container = document.getElementById("scheduler-ts-extra-bm-list");
- if (!container) return;
- container.innerHTML = "";
- pendingExtraBmIds.forEach(function(id, idx) {
- var bm = bookmarkList.find(function(b) {
- return b.id === id;
- });
- var chip = document.createElement("span");
- chip.className = "sch-extra-bm-chip";
- var 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);
- var label = document.createTextNode(" " + (bm ? bm.name : id));
- chip.appendChild(label);
- container.appendChild(chip);
- });
- var pick = document.getElementById("scheduler-ts-extra-bm-pick");
- if (pick) {
- Array.from(pick.options).forEach(function(opt) {
- if (opt.value) {
- opt.disabled = pendingExtraBmIds.includes(opt.value);
+ }
+ 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");
+ 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();
- }
- };
- if (window.satScheduler) window.satScheduler.wireEvents();
- }
- function isInputFocused() {
- var el = document.activeElement;
- if (!el) return false;
- var tag = el.tagName;
- return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el.isContentEditable;
- }
- document.addEventListener("keydown", function(e) {
- if (isInputFocused()) return;
- if (e.shiftKey && e.key === "R") {
- e.preventDefault();
- var releaseBtn = document.getElementById("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);
+ });
+ tr.querySelector(".sch-inline-cancel")?.addEventListener("click", function() {
+ renderTimespanEntries();
+ });
}
- });
- (function() {
- var details = document.querySelector(".sch-ts-details");
- if (!details) return;
- var key = "sch-details-open";
- var saved = localStorage.getItem(key);
- if (saved !== null) details.open = saved === "1";
- details.addEventListener("toggle", function() {
- localStorage.setItem(key, details.open ? "1" : "0");
+ 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);
+ }
+ });
+ }
+ });
+ 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);
+ }
+ });
+ }
+ }
+ 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();
+ }
})();
- window.initScheduler = initScheduler;
- window.destroyScheduler = destroyScheduler;
- window.wireSchedulerEvents = wireSchedulerEvents;
- window.setSchedulerRig = setSchedulerRig;
- if (typeof authRole !== "undefined" && authRole !== null) {
- initScheduler(typeof lastActiveRigId !== "undefined" ? lastActiveRigId : null, authRole);
- wireSchedulerEvents();
- }
})();
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 54ae2058..3d56c4d1 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
@@ -23,7 +23,6 @@ await build({
"plugin-runtime": path.join(sourceDir, "plugin-runtime.ts"),
screenshot: path.join(sourceDir, "screenshot.ts"),
"webgl-renderer": path.join(sourceDir, "webgl-renderer.ts"),
- scheduler: path.join(sourceDir, "plugins", "scheduler.js"),
},
outdir: outputDir,
bundle: false,
@@ -52,6 +51,7 @@ await build({
"sat-scheduler": path.join(sourceDir, "plugins", "sat-scheduler.ts"),
vchan: path.join(sourceDir, "plugins", "vchan.ts"),
bookmarks: path.join(sourceDir, "plugins", "bookmarks.ts"),
+ scheduler: path.join(sourceDir, "plugins", "scheduler.ts"),
},
outdir: outputDir,
bundle: true,
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js
index 16d5691e..61efe784 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/app.js
@@ -1269,7 +1269,7 @@ function applyRigList(activeRigId, rigIds, displayNames) {
updateRigIdentitySummary(lastActiveRigId);
window.trxUi?.setActiveRig(lastActiveRigId);
if (rigListChanged) {
- if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId);
+ window.trx.modules.scheduler?.setRig(lastActiveRigId);
if (typeof setBackgroundDecodeRig === "function") setBackgroundDecodeRig(lastActiveRigId);
if (typeof bmPopulateScopePicker === "function") bmPopulateScopePicker();
if (typeof bmFetch === "function") bmFetch(document.getElementById("bm-category-filter")?.value || "");
@@ -3935,7 +3935,7 @@ async function switchRigFromSelect(selectEl) {
updateRigSubtitle(lastActiveRigId);
updateRigIdentitySummary(lastActiveRigId);
window.trxUi?.setActiveRig(lastActiveRigId);
- if (typeof setSchedulerRig === "function") setSchedulerRig(lastActiveRigId);
+ window.trx.modules.scheduler?.setRig(lastActiveRigId);
if (typeof setBackgroundDecodeRig === "function") setBackgroundDecodeRig(lastActiveRigId);
if (typeof bmFetch === "function") bmFetch(document.getElementById("bm-category-filter")?.value || "");
window.trx.modules.map?.syncAprsReceiverMarker();
@@ -4759,10 +4759,8 @@ async function initializeApp() {
}
function initSettingsUI() {
- if (typeof initScheduler === "function") {
- initScheduler(lastActiveRigId, authRole);
- wireSchedulerEvents();
- }
+ window.trx.modules.scheduler?.initialize(lastActiveRigId, authRole);
+ window.trx.modules.scheduler?.wireEvents();
if (typeof initBackgroundDecode === "function") {
initBackgroundDecode(lastActiveRigId, authRole);
wireBackgroundDecodeEvents();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts
index 166655c9..ba58791b 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugin-loader.ts
@@ -16,7 +16,7 @@ const pluginGroups: Readonly> = {
const loaded = new Set();
const loading = new Map>();
-const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js", "/sat.js", "/sat-scheduler.js", "/vchan.js", "/bookmarks.js"]);
+const modulePlugins = new Set(["/ft8.js", "/ft4.js", "/ft2.js", "/wspr.js", "/cw.js", "/vdes.js", "/wefax.js", "/background-decode.js", "/ais.js", "/aprs.js", "/hf-aprs.js", "/sat.js", "/sat-scheduler.js", "/vchan.js", "/bookmarks.js", "/scheduler.js"]);
function loadLegacyScript(path: string): Promise {
return new Promise((resolve, reject) => {
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/sat-scheduler.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/sat-scheduler.ts
index 61ba946e..eda807dd 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/sat-scheduler.ts
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/sat-scheduler.ts
@@ -7,13 +7,12 @@ import type {
SatelliteScheduleConfig,
SatelliteScheduleEntry,
SatelliteSchedulerApi,
- SatelliteSchedulerBridge,
SatelliteSchedulerStatus,
- SchedulerConfigWithSatellites,
} from "./satellite-types";
+import type { SchedulerConfig, SchedulerService } from "./scheduler-types.js";
interface SatelliteSchedulerWindow {
- schedulerBridge?: SatelliteSchedulerBridge;
+ trx?: { modules?: { scheduler?: SchedulerService } };
satScheduler?: SatelliteSchedulerApi;
trxUi?: { notify(message: string, options: { kind: "error" }): void };
}
@@ -49,15 +48,15 @@ const satSchedulerWindow = window as unknown as SatelliteSchedulerWindow;
// ── Local state ───────────────────────────────────────────────────
let editIdx: number | null = null;
+ let eventsWired = false;
// ── Scheduler bridge ──────────────────────────────────────────────
- // These accessors call into scheduler.js via window.schedulerBridge,
- // which is set up by scheduler.js after it initializes.
- function getBridge(): SatelliteSchedulerBridge | null {
- return satSchedulerWindow.schedulerBridge ?? null;
+ // These accessors use the shared typed scheduler service when available.
+ function getBridge(): SchedulerService | null {
+ return satSchedulerWindow.trx?.modules?.scheduler ?? null;
}
- function getConfig(): SchedulerConfigWithSatellites | null {
+ function getConfig(): SchedulerConfig | null {
const b = getBridge();
return b?.getConfig() ?? null;
}
@@ -309,6 +308,8 @@ const satSchedulerWindow = window as unknown as SatelliteSchedulerWindow;
// ── Wire all events ───────────────────────────────────────────────
function wireEvents() {
+ if (eventsWired) return;
+ eventsWired = true;
if (dom.enabled) {
const enabledInput = dom.enabled;
dom.enabled.addEventListener("change", function () {
@@ -334,4 +335,8 @@ const satSchedulerWindow = window as unknown as SatelliteSchedulerWindow;
renderPassStatus: renderPassStatus,
collectSatelliteConfig: collectSatelliteConfig,
};
+ if (getBridge()) {
+ wireEvents();
+ renderSection();
+ }
})();
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/scheduler-types.ts b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/scheduler-types.ts
new file mode 100644
index 00000000..bb6caa76
--- /dev/null
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/scheduler-types.ts
@@ -0,0 +1,86 @@
+// SPDX-FileCopyrightText: 2026 Stan Grams
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+import type { SatelliteScheduleConfig, SatelliteSchedulerApi } from "./satellite-types.js";
+
+export type SchedulerMode = "disabled" | "grayline" | "time_span";
+
+export interface SchedulerBookmark {
+ id: string;
+ name: string;
+ freq_hz: number;
+ mode: string;
+}
+
+export interface GraylineConfig {
+ lat: number;
+ lon: number;
+ transition_window_min: number;
+ day_bookmark_id: string | null;
+ night_bookmark_id: string | null;
+ dawn_bookmark_id: string | null;
+ dusk_bookmark_id: string | null;
+}
+
+export interface ScheduleEntry {
+ id?: string;
+ start_min: number;
+ end_min: number;
+ bookmark_id: string;
+ label: string | null;
+ interleave_min: number | null;
+ center_hz: number | null;
+ bookmark_ids: string[];
+ record: boolean;
+ exclusive?: boolean;
+}
+
+export interface SchedulerConfig {
+ remote: string | null;
+ mode: SchedulerMode;
+ grayline: GraylineConfig | null;
+ entries: ScheduleEntry[];
+ interleave_min?: number | null;
+ satellites?: SatelliteScheduleConfig | null;
+}
+
+export interface SchedulerStatus {
+ active: boolean;
+ last_entry_id?: string | null;
+ last_bookmark_id?: string | null;
+ last_bookmark_name?: string | null;
+ last_applied_utc?: number | null;
+ last_center_hz?: number | null;
+ last_bookmark_ids?: string[];
+ active_satellite?: string | null;
+ freq_hz?: number | null;
+ mode?: string | null;
+ active_decoders?: string[];
+}
+
+export interface SchedulerService {
+ initialize(rigId: string | null, role: string | null): void;
+ destroy(): void;
+ setRig(rigId: string | null): void;
+ wireEvents(): void;
+ getConfig(): SchedulerConfig | null;
+ getStatus(): SchedulerStatus | null;
+ getBookmarks(): SchedulerBookmark[];
+ markDirty(): void;
+}
+
+export interface SchedulerWindow extends Window {
+ trx: { modules: { scheduler?: SchedulerService } };
+ trxUi: {
+ confirm(options: { title: string; message: string; confirmLabel: string; danger?: boolean }): Promise;
+ notify?(message: string, options: { kind: "error" }): void;
+ };
+ authRole?: string | null;
+ lastActiveRigId?: string | null;
+ serverLat?: number | null;
+ serverLon?: number | null;
+ vchanTakeSchedulerControl?(): Promise;
+ vchanToggleSchedulerRelease?(): Promise;
+ satScheduler?: SatelliteSchedulerApi;
+}
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/scheduler.js b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/scheduler.ts
similarity index 65%
rename from src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/scheduler.js
rename to src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/scheduler.ts
index bb95d5e1..a97789bc 100644
--- a/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/scheduler.js
+++ b/src/trx-client/trx-frontend/trx-frontend-http/frontend/src/plugins/scheduler.ts
@@ -2,6 +2,33 @@
//
// SPDX-License-Identifier: GPL-2.0-or-later
+import type {
+ ScheduleEntry,
+ SchedulerBookmark,
+ SchedulerConfig,
+ SchedulerService,
+ SchedulerStatus,
+ SchedulerWindow,
+} from "./scheduler-types.js";
+
+export {};
+
+/* Scheduler DOM IDs are part of the server-rendered page contract. */
+/* eslint-disable @typescript-eslint/no-unnecessary-condition */
+/* Timeline SVG and table templates intentionally concatenate typed numeric
+ * coordinates with markup fragments. */
+/* eslint-disable @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-unnecessary-type-conversion */
+
+type SchedulerElement = HTMLElement & HTMLInputElement & HTMLSelectElement;
+interface SchedulerLogEntry { utc: number; action?: string; entry_label?: string; bookmark_name?: string }
+const schedulerWindow = window as unknown as SchedulerWindow;
+const wiredElements = new WeakSet();
+function schedulerEl(id: string): SchedulerElement {
+ const element = document.getElementById(id);
+ if (!element) throw new Error(`Missing scheduler element #${id}`);
+ return element as SchedulerElement;
+}
+
// Background Decoding Scheduler UI
(function () {
@@ -10,22 +37,22 @@
// -------------------------------------------------------------------------
// State
// -------------------------------------------------------------------------
- let schedulerRole = null; // "control" | "rx" | null
- let currentRigId = null;
- let currentConfig = null;
- let currentSchedulerStatus = null;
- let bookmarkList = []; // [{id, name, freq_hz, mode}, ...]
- let statusInterval = null;
- let interleaveTicker = null;
+ let schedulerRole: string | null = null;
+ let currentRigId: string | null = null;
+ let currentConfig: SchedulerConfig | null = null;
+ let currentSchedulerStatus: SchedulerStatus | null = null;
+ let bookmarkList: SchedulerBookmark[] = [];
+ let statusInterval: number | null = null;
+ let interleaveTicker: number | null = null;
let schedulerStepPending = false;
- let schEntryEditIdx = null; // null = adding, number = editing that index
+ let schEntryEditIdx: number | null = null;
let schedulerDirty = false; // true when unsaved changes exist
// Satellite entry editing state moved to sat-scheduler.js
// -------------------------------------------------------------------------
// Init
// -------------------------------------------------------------------------
- function initScheduler(rigId, role) {
+ function initScheduler(rigId: string | null, role: string | null): void {
schedulerRole = role;
currentRigId = rigId || null;
if (currentRigId) loadScheduler();
@@ -47,7 +74,7 @@
// -------------------------------------------------------------------------
// Active rig (mirrors top-bar rig picker in app.js)
// -------------------------------------------------------------------------
- function setSchedulerRig(rigId) {
+ function setSchedulerRig(rigId: string | null): void {
const nextRigId = rigId || null;
if (nextRigId === currentRigId) return;
currentRigId = nextRigId;
@@ -60,25 +87,25 @@
// -------------------------------------------------------------------------
// API helpers
// -------------------------------------------------------------------------
- function apiGetScheduler(rigId) {
+ function apiGetScheduler(rigId: string): Promise {
return fetch("/scheduler/" + encodeURIComponent(rigId)).then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status);
- return r.json();
+ return r.json() as Promise;
});
}
- function apiPutScheduler(rigId, config) {
+ function apiPutScheduler(rigId: string, config: SchedulerConfig): Promise {
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();
+ return r.json() as Promise;
});
}
- function apiDeleteScheduler(rigId) {
+ function apiDeleteScheduler(rigId: string): Promise {
return fetch("/scheduler/" + encodeURIComponent(rigId), {
method: "DELETE",
}).then(function (r) {
@@ -87,32 +114,32 @@
});
}
- function apiGetStatus(rigId) {
+ function apiGetStatus(rigId: string): Promise {
return fetch("/scheduler/" + encodeURIComponent(rigId) + "/status").then(
function (r) {
if (!r.ok) throw new Error("HTTP " + r.status);
- return r.json();
+ return r.json() as Promise;
}
);
}
- function apiActivateSchedulerEntry(rigId, entryId) {
+ function apiActivateSchedulerEntry(rigId: string, entryId: string): Promise {
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();
+ return r.json() as Promise;
});
}
- function apiGetBookmarks() {
+ function apiGetBookmarks(): Promise {
// Fetch merged general + rig-specific bookmarks in a single request.
- var url = currentRigId
+ const url = currentRigId
? "/bookmarks?scope=" + encodeURIComponent(currentRigId)
: "/bookmarks";
- return fetch(url).then(function (r) { return r.ok ? r.json() : []; });
+ return fetch(url).then(function (r) { return r.ok ? r.json() as Promise : []; });
}
// -------------------------------------------------------------------------
@@ -131,8 +158,8 @@
clearSchedulerDirty();
renderSchedulerInterleaveStatus();
})
- .catch(function (e) {
- console.error("scheduler load failed", e);
+ .catch(function (error: unknown) {
+ console.error("scheduler load failed", error);
renderSchedulerInterleaveStatus();
});
}
@@ -165,7 +192,7 @@
};
}
- function schedulerEntryIsActive(entry, nowMin) {
+ function schedulerEntryIsActive(entry: ScheduleEntry, nowMin: number): boolean {
const start = Number(entry && entry.start_min);
const end = Number(entry && entry.end_min);
if (!Number.isFinite(start) || !Number.isFinite(end)) return false;
@@ -174,7 +201,7 @@
return nowMin >= start || nowMin < end;
}
- function schedulerEntryCurrentWindowStart(entry, nowMin) {
+ function schedulerEntryCurrentWindowStart(entry: ScheduleEntry, nowMin: number): number {
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;
@@ -183,14 +210,14 @@
return nowMin >= start ? start : (start - 1440);
}
- function schedulerEntryDisplayName(entry) {
+ function schedulerEntryDisplayName(entry: ScheduleEntry | undefined): string {
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) {
+ function schedulerInterleaveState(config: SchedulerConfig | null): { activeEntries: ScheduleEntry[]; currentIndex: number; remainingSec: number; cycleMin: number } {
if (!config || config.mode !== "time_span") {
return { activeEntries: [], currentIndex: -1, remainingSec: 0, cycleMin: 0 };
}
@@ -207,9 +234,10 @@
return { activeEntries: active, currentIndex: 0, remainingSec: 0, cycleMin: 0 };
}
// Exclusive entry wins outright — no interleaving.
- var exclIdx = active.findIndex(function (e) { return e.exclusive; });
+ const exclIdx = active.findIndex(function (e) { return e.exclusive; });
if (exclIdx >= 0) {
- return { activeEntries: [active[exclIdx]], currentIndex: 0, remainingSec: 0, cycleMin: 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) {
@@ -218,7 +246,7 @@
if (Number.isFinite(defaultInterleave) && defaultInterleave > 0) return Math.floor(defaultInterleave);
return 0;
});
- const cycleMin = durations.reduce(function (sum, value) { return sum + value; }, 0);
+ const cycleMin = durations.reduce(function (sum: number, value: number) { return sum + value; }, 0);
if (!(cycleMin > 0)) {
return { activeEntries: active, currentIndex: 0, remainingSec: 0, cycleMin: 0 };
}
@@ -232,7 +260,7 @@
? Number(currentSchedulerStatus.last_applied_utc)
: null;
if (statusIndex >= 0 && statusAppliedUtc != null) {
- const manualDurationMin = durations[statusIndex];
+ const manualDurationMin = durations[statusIndex] ?? 0;
const elapsedSec = Math.max(0, schedulerUtcSeconds() - statusAppliedUtc);
const remainingSec = (manualDurationMin > 0)
? Math.max(1, (manualDurationMin * 60) - elapsedSec)
@@ -246,7 +274,7 @@
};
}
}
- const overlapStart = active.reduce(function (maxStart, entry) {
+ const overlapStart = active.reduce(function (maxStart: number, entry: ScheduleEntry) {
return Math.max(maxStart, schedulerEntryCurrentWindowStart(entry, nowMin));
}, Number.NEGATIVE_INFINITY);
if (!Number.isFinite(overlapStart)) {
@@ -259,12 +287,13 @@
let currentIndex = 0;
let currentDuration = 0;
for (let i = 0; i < durations.length; i += 1) {
- const nextCumulative = cumulative + durations[i];
+ const duration = durations[i] ?? 0;
+ const nextCumulative = cumulative + duration;
if (posMin < nextCumulative) {
slotStart = cumulative;
cumulative = nextCumulative;
currentIndex = i;
- currentDuration = durations[i];
+ currentDuration = duration;
break;
}
cumulative = nextCumulative;
@@ -283,7 +312,7 @@
}
function renderSchedulerInterleaveStatus() {
- const wrap = document.getElementById("scheduler-cycle-status");
+ const wrap = schedulerEl("scheduler-cycle-status");
if (!wrap) return;
const state = schedulerInterleaveState(currentConfig);
@@ -292,21 +321,21 @@
wrap.style.display = isActive ? "" : "none";
if (isActive) {
- var activeName = schedulerEntryDisplayName(state.activeEntries[state.currentIndex]);
- var totalSlotSec = state.cycleMin > 0
+ const activeName = schedulerEntryDisplayName(state.activeEntries[state.currentIndex]);
+ const totalSlotSec = state.cycleMin > 0
? (state.cycleMin * 60) / state.activeEntries.length
: 0;
- var elapsedPct = totalSlotSec > 0
+ const elapsedPct = totalSlotSec > 0
? Math.min(100, Math.max(0, ((totalSlotSec - state.remainingSec) / totalSlotSec) * 100))
: 0;
- var ringFill = document.getElementById("interleave-ring-fill");
+ const ringFill = schedulerEl("interleave-ring-fill");
if (ringFill) ringFill.setAttribute("stroke-dashoffset", String(100 - elapsedPct));
- var nameEl = document.getElementById("interleave-active-name");
+ const nameEl = schedulerEl("interleave-active-name");
if (nameEl) nameEl.textContent = activeName;
- var countdownEl = document.getElementById("interleave-countdown");
+ const countdownEl = schedulerEl("interleave-countdown");
if (countdownEl) countdownEl.textContent = "next in " + state.remainingSec + "s · " + state.cycleMin + "m cycle";
}
@@ -316,8 +345,8 @@
}
function renderSchedulerStepControls() {
- const prevBtn = document.getElementById("scheduler-prev-btn");
- const nextBtn = document.getElementById("scheduler-next-btn");
+ const prevBtn = schedulerEl("scheduler-prev-btn");
+ const nextBtn = schedulerEl("scheduler-next-btn");
if (!prevBtn || !nextBtn) return;
const state = schedulerInterleaveState(currentConfig);
const enabled =
@@ -348,8 +377,8 @@
.catch(function () {});
}
- function renderStatus(st) {
- const el = document.getElementById("scheduler-status-card");
+ function renderStatus(st: SchedulerStatus | null): void {
+ const el = schedulerEl("scheduler-status-card");
if (!el) return;
if (!st || (!st.active && !st.last_bookmark_id)) {
el.textContent = "No activity yet.";
@@ -370,7 +399,7 @@
const satLabel = st.active_satellite
? " [SAT: " + st.active_satellite + "]"
: "";
- var details = "";
+ let details = "";
if (st.freq_hz) {
details += formatFreq(st.freq_hz);
if (st.mode) details += " \u00B7 " + st.mode;
@@ -389,15 +418,15 @@
// -------------------------------------------------------------------------
// Activity log
// -------------------------------------------------------------------------
- function apiGetSchedulerLog(rigId) {
+ function apiGetSchedulerLog(rigId: string): Promise {
return fetch("/scheduler/" + encodeURIComponent(rigId) + "/log").then(function (r) {
- return r.ok ? r.json() : [];
+ return r.ok ? r.json() as Promise : [];
});
}
function renderActivityLog() {
- var wrap = document.getElementById("scheduler-activity-log-wrap");
- var container = document.getElementById("scheduler-activity-log");
+ const wrap = schedulerEl("scheduler-activity-log-wrap");
+ const container = schedulerEl("scheduler-activity-log");
if (!wrap || !container || !currentRigId) return;
apiGetSchedulerLog(currentRigId).then(function (entries) {
@@ -406,12 +435,12 @@
return;
}
wrap.style.display = "";
- var html = entries.slice().reverse().map(function (e) {
- var d = new Date(e.utc * 1000);
- var ts = d.toUTCString();
- var action = e.action || "unknown";
- var label = e.entry_label || "";
- var bm = e.bookmark_name || "";
+ const html = entries.slice().reverse().map(function (e) {
+ const d = new Date(e.utc * 1000);
+ const ts = d.toUTCString();
+ const action = e.action || "unknown";
+ const label = e.entry_label || "";
+ const bm = e.bookmark_name || "";
return '' +
'
' + escHtml(ts) + ' ' +
'
' + escHtml(action) + ' ' +
@@ -427,7 +456,7 @@
// Render the full scheduler panel
// -------------------------------------------------------------------------
function renderScheduler() {
- const panel = document.getElementById("scheduler-panel");
+ const panel = schedulerEl("scheduler-panel");
if (!panel) return;
const mode = (currentConfig && currentConfig.mode) || "disabled";
@@ -438,12 +467,12 @@
// Show/hide main-view scheduler controls (visible when base mode active OR satellites enabled)
const satEnabled = currentConfig && currentConfig.satellites && currentConfig.satellites.enabled;
- const controlRow = document.querySelector(".scheduler-control-row");
+ const controlRow = document.querySelector
(".scheduler-control-row");
if (controlRow) controlRow.style.display = (mode !== "disabled" || satEnabled) ? "" : "none";
// Show/hide sections
- const glSection = document.getElementById("scheduler-grayline-section");
- const tsSection = document.getElementById("scheduler-timespan-section");
+ 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";
@@ -454,12 +483,12 @@
if (mode === "grayline" && currentConfig && currentConfig.grayline) {
const gl = currentConfig.grayline;
// Prefer saved value; fall back to server coordinates from app.js globals.
- const lat = gl.lat != null ? gl.lat : (typeof serverLat !== "undefined" ? serverLat : "");
- const lon = gl.lon != null ? gl.lon : (typeof serverLon !== "undefined" ? serverLon : "");
+ 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 : "");
- var gridEl = document.getElementById("scheduler-gl-grid");
- if (gridEl && lat !== "" && 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);
@@ -469,11 +498,11 @@
renderBookmarkSelect("scheduler-gl-night", gl.night_bookmark_id);
} else if (mode === "grayline") {
// No saved grayline config yet — pre-fill coords from server if available.
- const lat = typeof serverLat !== "undefined" ? serverLat : "";
- const lon = typeof serverLon !== "undefined" ? serverLon : "";
+ const lat = schedulerWindow.serverLat ?? "";
+ const lon = schedulerWindow.serverLon ?? "";
setInputValue("scheduler-gl-lat", lat != null ? lat : "");
setInputValue("scheduler-gl-lon", lon != null ? lon : "");
- var gridEl2 = document.getElementById("scheduler-gl-grid");
+ const gridEl2 = schedulerEl("scheduler-gl-grid");
if (gridEl2 && lat !== "" && lon !== "") {
gridEl2.value = latLonToGrid(lat, lon);
}
@@ -490,42 +519,42 @@
}
// Interleave input
- const ilEl = document.getElementById("scheduler-ts-interleave");
+ const ilEl = schedulerEl("scheduler-ts-interleave");
if (ilEl) {
const il = currentConfig && currentConfig.interleave_min;
- ilEl.value = il ? il : "";
+ ilEl.value = il ? String(il) : "";
}
// TimeSpan entries
renderTimespanEntries();
// Enable/disable controls
- const formEls = panel.querySelectorAll("input, select, button.sch-write");
+ const formEls = panel.querySelectorAll("input, select, button.sch-write");
formEls.forEach(function (el) {
el.disabled = !isControl;
});
- const saveBtn = document.getElementById("scheduler-save-btn");
+ const saveBtn = schedulerEl("scheduler-save-btn");
if (saveBtn) {
saveBtn.style.display = isControl ? "" : "none";
}
- const resetBtn = document.getElementById("scheduler-reset-btn");
+ const resetBtn = schedulerEl("scheduler-reset-btn");
if (resetBtn) {
resetBtn.style.display = isControl ? "" : "none";
}
}
- function setSelected(id, value) {
- const el = document.getElementById(id);
+ function setSelected(id: string, value: string): void {
+ const el = schedulerEl(id);
if (el) el.value = value;
}
- function setInputValue(id, value) {
- const el = document.getElementById(id);
- if (el) el.value = value;
+ function setInputValue(id: string, value: string | number): void {
+ const el = schedulerEl(id);
+ if (el) el.value = String(value);
}
- function renderBookmarkSelect(id, selectedId) {
- const sel = document.getElementById(id);
+ function renderBookmarkSelect(id: string, selectedId: string | null): void {
+ const sel = schedulerEl(id);
if (!sel) return;
sel.innerHTML = '';
bookmarkList.forEach(function (bm) {
@@ -537,7 +566,7 @@
});
}
- function formatFreq(hz) {
+ function formatFreq(hz: number): string {
if (hz >= 1e6) return (hz / 1e6).toFixed(3) + " MHz";
if (hz >= 1e3) return (hz / 1e3).toFixed(1) + " kHz";
return hz + " Hz";
@@ -546,33 +575,33 @@
// -------------------------------------------------------------------------
// Entry form (inline card below Add Entry button)
// -------------------------------------------------------------------------
- function schOpenEntryForm(entry, idx) {
+ function schOpenEntryForm(entry: ScheduleEntry | null, idx?: number): void {
schEntryEditIdx = (idx != null) ? idx : null;
- const titleEl = document.getElementById("sch-entry-form-title");
+ const titleEl = schedulerEl("sch-entry-form-title");
if (titleEl) titleEl.textContent = entry ? "Edit Entry" : "Add Entry";
- const startEl = document.getElementById("scheduler-ts-start");
- const endEl = document.getElementById("scheduler-ts-end");
- const bmEl = document.getElementById("scheduler-ts-bookmark");
- const labelEl = document.getElementById("scheduler-ts-label");
- const ilEl = document.getElementById("scheduler-ts-entry-interleave");
- const centerHzEl = document.getElementById("scheduler-ts-center-hz");
+ 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 && entry.interleave_min ? entry.interleave_min : "";
- if (centerHzEl) centerHzEl.value = entry && entry.center_hz ? entry.center_hz : "";
+ if (ilEl) ilEl.value = entry?.interleave_min ? String(entry.interleave_min) : "";
+ if (centerHzEl) centerHzEl.value = entry?.center_hz ? String(entry.center_hz) : "";
- const recordEl = document.getElementById("scheduler-ts-entry-record");
+ 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 = document.getElementById("sch-entry-form-wrap");
+ const wrap = schedulerEl("sch-entry-form-wrap");
if (wrap) {
wrap.style.display = "block";
if (startEl) startEl.focus();
@@ -580,26 +609,26 @@
}
function schCloseEntryForm() {
- const wrap = document.getElementById("sch-entry-form-wrap");
+ const wrap = schedulerEl("sch-entry-form-wrap");
if (wrap) wrap.style.display = "none";
schEntryEditIdx = null;
pendingExtraBmIds = [];
}
- function schEntryFormSubmit(e) {
+ function schEntryFormSubmit(e: Event): void {
e.preventDefault();
- const startEl = document.getElementById("scheduler-ts-start");
- const endEl = document.getElementById("scheduler-ts-end");
- const bmEl = document.getElementById("scheduler-ts-bookmark");
- const labelEl = document.getElementById("scheduler-ts-label");
- const ilEl = document.getElementById("scheduler-ts-entry-interleave");
- const centerHzEl = document.getElementById("scheduler-ts-center-hz");
+ 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) {
- window.trxUi?.notify("Select a primary bookmark before saving.", { kind: "error" });
+ schedulerWindow.trxUi.notify?.("Select a primary bookmark before saving.", { kind: "error" });
return;
}
@@ -612,15 +641,14 @@
const centerHz = !isNaN(centerHzRaw) && centerHzRaw > 0 ? centerHzRaw : null;
const extraBmIds = pendingExtraBmIds.slice();
- if (!currentConfig) {
- currentConfig = { remote: currentRigId, mode: "time_span", entries: [] };
- }
- if (!currentConfig.entries) currentConfig.entries = [];
+ currentConfig ??= { remote: currentRigId, mode: "time_span", grayline: null, entries: [] };
+ const config = currentConfig;
- const recordCb = document.getElementById("scheduler-ts-entry-record");
+ const recordCb = schedulerEl("scheduler-ts-entry-record");
const entryRecord = recordCb ? recordCb.checked : false;
- const entryData = {
+ const entryData: ScheduleEntry = {
+ id: "ts_" + Date.now().toString(36),
start_min: startMin,
end_min: endMin,
bookmark_id: bmId,
@@ -629,15 +657,15 @@
center_hz: centerHz,
bookmark_ids: extraBmIds,
record: entryRecord,
+ exclusive: false,
};
if (schEntryEditIdx !== null) {
- const existing = currentConfig.entries[schEntryEditIdx];
- entryData.id = existing ? existing.id : ("ts_" + Date.now().toString(36));
- currentConfig.entries[schEntryEditIdx] = entryData;
+ const existing = config.entries[schEntryEditIdx];
+ if (existing?.id) entryData.id = existing.id;
+ config.entries[schEntryEditIdx] = entryData;
} else {
- entryData.id = "ts_" + Date.now().toString(36);
- currentConfig.entries.push(entryData);
+ config.entries.push(entryData);
}
schCloseEntryForm();
@@ -648,75 +676,75 @@
// -------------------------------------------------------------------------
// 24h Timeline Bar
// -------------------------------------------------------------------------
- var TIMELINE_COLORS = ["#38bdf8", "#f59e0b", "#a78bfa", "#34d399", "#fb7185", "#60a5fa"];
+ const TIMELINE_COLORS = ["#38bdf8", "#f59e0b", "#a78bfa", "#34d399", "#fb7185", "#60a5fa"];
function renderTimeline() {
- var container = document.getElementById("scheduler-ts-timeline");
+ const container = schedulerEl("scheduler-ts-timeline");
if (!container) return;
- var entries = currentConfig && Array.isArray(currentConfig.entries) ? currentConfig.entries : [];
+ const entries = currentConfig && Array.isArray(currentConfig.entries) ? currentConfig.entries : [];
if (entries.length === 0) {
container.innerHTML = "";
return;
}
- var W = 1000;
- var H = 80;
- var BAR_Y = 6;
- var BAR_H = 30;
- var TICK_Y = BAR_Y + BAR_H + 2;
+ const W = 1000;
+ const H = 80;
+ const BAR_Y = 6;
+ const BAR_H = 30;
+ const TICK_Y = BAR_Y + BAR_H + 2;
- var svg = '