import {
hostState
} from "./chunk-KL66PICH.js";
// 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 schedulerOptionalEl(id) {
return document.getElementById(id);
}
(function() {
"use strict";
let schedulerRole = null;
let currentRigId = null;
let currentConfig = null;
let currentSchedulerStatus = null;
let bookmarkList = [];
let statusInterval = null;
let interleaveTicker = null;
let schedulerStepPending = false;
let schEntryEditIdx = null;
let schedulerDirty = false;
function initScheduler(rigId, role) {
schedulerRole = role;
currentRigId = rigId || null;
if (currentRigId) loadScheduler();
startStatusPolling();
startInterleaveTicker();
}
function destroyScheduler() {
if (statusInterval) {
clearInterval(statusInterval);
statusInterval = null;
}
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) {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
}
);
}
function apiActivateSchedulerEntry(rigId, entryId) {
return fetch("/scheduler/" + encodeURIComponent(rigId) + "/activate", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ entry_id: entryId })
}).then(function(r) {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
});
}
function apiGetBookmarks() {
const url = currentRigId ? "/bookmarks?scope=" + encodeURIComponent(currentRigId) : "/bookmarks";
return fetch(url).then(function(r) {
return r.ok ? r.json() : [];
});
}
function loadScheduler() {
const rig = currentRigId;
if (!rig) return;
Promise.all([apiGetScheduler(rig), apiGetBookmarks()]).then(function([config, bms]) {
currentConfig = config;
bookmarkList = Array.isArray(bms) ? bms : [];
populateTsBookmarkSelect();
renderScheduler();
clearSchedulerDirty();
renderSchedulerInterleaveStatus();
}).catch(function(error) {
console.error("scheduler load failed", error);
renderSchedulerInterleaveStatus();
});
}
function startStatusPolling() {
if (statusInterval) clearInterval(statusInterval);
statusInterval = setInterval(pollStatus, 15e3);
pollStatus();
}
function startInterleaveTicker() {
if (interleaveTicker) clearInterval(interleaveTicker);
interleaveTicker = setInterval(renderSchedulerInterleaveStatus, 1e3);
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 };
}
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;
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 ?? hostState.serverLat ?? "";
const lon = gl.lon ?? hostState.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 = hostState.serverLat ?? "";
const lon = hostState.serverLon ?? "";
setInputValue("scheduler-gl-lat", lat != null ? lat : "");
setInputValue("scheduler-gl-lon", lon != null ? lon : "");
const gridEl2 = schedulerEl("scheduler-gl-grid");
if (gridEl2 && lat !== "" && lon !== "") {
gridEl2.value = latLonToGrid(lat, lon);
}
setInputValue("scheduler-gl-window", 20);
renderBookmarkSelect("scheduler-gl-dawn", null);
renderBookmarkSelect("scheduler-gl-day", null);
renderBookmarkSelect("scheduler-gl-dusk", null);
renderBookmarkSelect("scheduler-gl-night", null);
} else {
renderBookmarkSelect("scheduler-gl-dawn", null);
renderBookmarkSelect("scheduler-gl-day", null);
renderBookmarkSelect("scheduler-gl-dusk", null);
renderBookmarkSelect("scheduler-gl-night", null);
}
const ilEl = schedulerEl("scheduler-ts-interleave");
if (ilEl) {
const il = currentConfig && currentConfig.interleave_min;
ilEl.value = il ? String(il) : "";
}
renderTimespanEntries();
const formEls = panel.querySelectorAll("input, select, button.sch-write");
formEls.forEach(function(el) {
el.disabled = !isControl;
});
const saveBtn = schedulerEl("scheduler-save-btn");
if (saveBtn) {
saveBtn.style.display = isControl ? "" : "none";
}
const resetBtn = schedulerEl("scheduler-reset-btn");
if (resetBtn) {
resetBtn.style.display = isControl ? "" : "none";
}
}
function setSelected(id, value) {
const el = schedulerEl(id);
if (el) el.value = value;
}
function setInputValue(id, value) {
const el = schedulerEl(id);
if (el) el.value = String(value);
}
function renderBookmarkSelect(id, selectedId) {
const sel = schedulerEl(id);
if (!sel) return;
sel.innerHTML = '';
bookmarkList.forEach(function(bm) {
const opt = document.createElement("option");
opt.value = bm.id;
opt.textContent = bm.name + " (" + formatFreq(bm.freq_hz) + " " + bm.mode + ")";
if (bm.id === selectedId) opt.selected = true;
sel.appendChild(opt);
});
}
function formatFreq(hz) {
if (hz >= 1e6) return (hz / 1e6).toFixed(3) + " MHz";
if (hz >= 1e3) return (hz / 1e3).toFixed(1) + " kHz";
return hz + " Hz";
}
function schOpenEntryForm(entry, idx) {
schEntryEditIdx = idx != null ? idx : null;
const titleEl = schedulerEl("sch-entry-form-title");
if (titleEl) titleEl.textContent = entry ? "Edit Entry" : "Add Entry";
const startEl = schedulerEl("scheduler-ts-start");
const endEl = schedulerEl("scheduler-ts-end");
const bmEl = schedulerEl("scheduler-ts-bookmark");
const labelEl = schedulerEl("scheduler-ts-label");
const ilEl = schedulerEl("scheduler-ts-entry-interleave");
const centerHzEl = schedulerEl("scheduler-ts-center-hz");
if (startEl) startEl.value = entry ? minToHHMM(entry.start_min) : "";
if (endEl) endEl.value = entry ? minToHHMM(entry.end_min) : "";
if (bmEl) bmEl.value = entry ? entry.bookmark_id || "" : "";
if (labelEl) labelEl.value = entry ? entry.label || "" : "";
if (ilEl) ilEl.value = entry?.interleave_min ? String(entry.interleave_min) : "";
if (centerHzEl) centerHzEl.value = entry?.center_hz ? String(entry.center_hz) : "";
const recordEl = schedulerEl("scheduler-ts-entry-record");
if (recordEl) recordEl.checked = !!(entry && entry.record);
pendingExtraBmIds = entry && Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids.slice() : [];
renderExtraBmList();
const wrap = schedulerEl("sch-entry-form-wrap");
if (wrap) {
wrap.style.display = "block";
if (startEl) startEl.focus();
}
}
function schCloseEntryForm() {
const wrap = schedulerEl("sch-entry-form-wrap");
if (wrap) wrap.style.display = "none";
schEntryEditIdx = null;
pendingExtraBmIds = [];
}
function schEntryFormSubmit(e) {
e.preventDefault();
const startEl = schedulerEl("scheduler-ts-start");
const endEl = schedulerEl("scheduler-ts-end");
const bmEl = schedulerEl("scheduler-ts-bookmark");
const labelEl = schedulerEl("scheduler-ts-label");
const ilEl = schedulerEl("scheduler-ts-entry-interleave");
const centerHzEl = schedulerEl("scheduler-ts-center-hz");
if (!startEl || !endEl || !bmEl) return;
const bmId = bmEl.value;
if (!bmId) {
schedulerWindow.trxUi.notify?.("Select a primary bookmark before saving.", { kind: "error" });
return;
}
const startMin = hhmmToMin(startEl.value);
const endMin = hhmmToMin(endEl.value);
const label = labelEl ? labelEl.value.trim() : "";
const ilVal = ilEl ? parseInt(ilEl.value, 10) : NaN;
const entryInterleave = !isNaN(ilVal) && ilVal > 0 ? ilVal : null;
const centerHzRaw = centerHzEl ? parseInt(centerHzEl.value, 10) : NaN;
const centerHz = !isNaN(centerHzRaw) && centerHzRaw > 0 ? centerHzRaw : null;
const extraBmIds = pendingExtraBmIds.slice();
currentConfig ??= { remote: currentRigId, mode: "time_span", grayline: null, entries: [] };
const config = currentConfig;
const recordCb = schedulerEl("scheduler-ts-entry-record");
const entryRecord = recordCb ? recordCb.checked : false;
const entryData = {
id: "ts_" + Date.now().toString(36),
start_min: startMin,
end_min: endMin,
bookmark_id: bmId,
label: label || null,
interleave_min: entryInterleave,
center_hz: centerHz,
bookmark_ids: extraBmIds,
record: entryRecord,
exclusive: false
};
if (schEntryEditIdx !== null) {
const existing = config.entries[schEntryEditIdx];
if (existing?.id) entryData.id = existing.id;
config.entries[schEntryEditIdx] = entryData;
} else {
config.entries.push(entryData);
}
schCloseEntryForm();
renderTimespanEntries();
markSchedulerDirty();
}
const TIMELINE_COLORS = ["#38bdf8", "#f59e0b", "#a78bfa", "#34d399", "#fb7185", "#60a5fa"];
function renderTimeline() {
const container = schedulerEl("scheduler-ts-timeline");
if (!container) return;
const entries = currentConfig && Array.isArray(currentConfig.entries) ? currentConfig.entries : [];
if (entries.length === 0) {
container.innerHTML = "";
return;
}
const W = 1e3;
const H = 80;
const BAR_Y = 6;
const BAR_H = 30;
const TICK_Y = BAR_Y + BAR_H + 2;
let svg = '";
container.innerHTML = svg;
container.querySelectorAll(".sch-timeline-seg").forEach(function(seg) {
seg.addEventListener("click", function() {
const i = parseInt(seg.getAttribute("data-idx") ?? "", 10);
const entry = currentConfig && currentConfig.entries ? currentConfig.entries[i] : null;
if (entry) schOpenEntryForm(entry, i);
});
});
const svgEl = container.querySelector("svg");
if (svgEl) {
const timelineSvg = svgEl;
svgEl.addEventListener("click", function(e) {
if (e.target instanceof Element && e.target.classList.contains("sch-timeline-seg")) return;
const rect = timelineSvg.getBoundingClientRect();
const xPct = (e.clientX - rect.left) / rect.width;
const clickMin = Math.floor(xPct * 1440);
const startHour = Math.floor(clickMin / 60);
const startMin = startHour * 60;
const endMin = (startHour + 1) % 24 * 60;
schOpenEntryForm(null);
const startEl = schedulerEl("scheduler-ts-start");
const endEl = schedulerEl("scheduler-ts-end");
if (startEl) startEl.value = minToHHMM(startMin);
if (endEl) endEl.value = minToHHMM(endMin);
});
svgEl.style.cursor = "crosshair";
}
}
function timelineNeedleSvg() {
const info = schedulerUtcMinuteInfo();
const nowMin = info.minuteOfDay + info.secondOfMinute / 60;
const x = nowMin / 1440 * 1e3;
return '';
}
function renderTimelineNeedle() {
const g = schedulerOptionalEl("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();
});
tr.querySelector(".sch-inline-cancel")?.addEventListener("click", function() {
renderTimespanEntries();
});
}
function renderTimespanEntries() {
const tbody = schedulerEl("scheduler-ts-tbody");
if (!tbody) return;
tbody.innerHTML = "";
const entries = currentConfig && Array.isArray(currentConfig.entries) ? currentConfig.entries : [];
entries.forEach(function(entry, idx) {
const tr = document.createElement("tr");
if (currentSchedulerStatus && currentSchedulerStatus.last_entry_id && entry.id && String(entry.id) === String(currentSchedulerStatus.last_entry_id)) {
tr.classList.add("sch-active");
}
const il = entry.exclusive ? "Exclusive" : entry.interleave_min ? String(entry.interleave_min) + " min" : "—";
const allDay = entry.start_min === entry.end_min;
const centerCell = entry.center_hz ? formatFreq(entry.center_hz) : "—";
const extraIds = Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids : [];
const extraCell = extraIds.length ? extraIds.map(function(id) {
return escHtml(bmName(id));
}).join(", ") : "—";
tr.innerHTML = '⠇ | ' + (allDay ? "All day" : minToHHMM(entry.start_min) + ' (' + minToLocal(entry.start_min) + ")") + " | " + (allDay ? "—" : minToHHMM(entry.end_min) + ' (' + minToLocal(entry.end_min) + ")") + " | " + centerCell + " | " + escHtml(bmName(entry.bookmark_id)) + " | " + extraCell + " | " + escHtml(entry.label || "") + " | " + il + " | " + (entry.record ? "Yes" : "") + ' | | ';
tbody.appendChild(tr);
});
tbody.querySelectorAll(".sch-edit-btn").forEach(function(btn) {
btn.addEventListener("click", function() {
const i = parseInt(btn.dataset.idx ?? "", 10);
const entry = currentConfig && currentConfig.entries ? currentConfig.entries[i] : null;
const row = btn.closest("tr");
if (entry && row) schInlineEdit(row, entry, i);
});
});
tbody.querySelectorAll(".sch-remove-btn").forEach(function(btn) {
btn.addEventListener("click", function() {
removeEntry(parseInt(btn.dataset.idx ?? "", 10));
});
});
(function() {
const handles = tbody.querySelectorAll(".sch-drag-handle");
let dragIdx = null;
handles.forEach(function(handle, idx) {
const row = handle.parentElement;
if (!row) return;
const dragRow = row;
handle.addEventListener("dragstart", function(event) {
const e = event;
dragIdx = idx;
dragRow.classList.add("sch-dragging");
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", String(idx));
}
});
dragRow.addEventListener("dragover", function(event) {
const e = event;
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
dragRow.classList.add("sch-drag-over");
});
dragRow.addEventListener("dragleave", function() {
dragRow.classList.remove("sch-drag-over");
});
dragRow.addEventListener("drop", function(event) {
const e = event;
e.preventDefault();
dragRow.classList.remove("sch-drag-over");
if (dragIdx === null || dragIdx === idx) return;
if (!currentConfig) return;
const entries2 = currentConfig.entries;
const moved = entries2.splice(dragIdx, 1)[0];
if (moved) entries2.splice(idx, 0, moved);
renderTimespanEntries();
markSchedulerDirty();
});
handle.addEventListener("dragend", function() {
dragRow.classList.remove("sch-dragging");
dragIdx = null;
});
});
})();
renderTimeline();
}
function bmName(id) {
const bm = bookmarkList.find(function(b) {
return b.id === id;
});
return bm ? bm.name : String(id || "");
}
function minToLocal(min) {
const now = /* @__PURE__ */ new Date();
const utcMidnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
const utcMs = utcMidnight.getTime() + min * 6e4;
const local = new Date(utcMs);
return String(local.getHours()).padStart(2, "0") + ":" + String(local.getMinutes()).padStart(2, "0");
}
function minToHHMM(min) {
const h = Math.floor(min / 60) % 24;
const m = min % 60;
return String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0");
}
function hhmmToMin(str) {
const parts = str.split(":");
return parseInt(parts[0] || "0", 10) * 60 + parseInt(parts[1] || "0", 10);
}
function gridToLatLon(grid) {
grid = String(grid).toUpperCase().trim();
if (grid.length < 4) return null;
const lonField = grid.charCodeAt(0) - 65;
const latField = grid.charCodeAt(1) - 65;
const lonSquare = parseInt(grid.charAt(2), 10);
const latSquare = parseInt(grid.charAt(3), 10);
if (isNaN(lonSquare) || isNaN(latSquare) || lonField < 0 || lonField > 17 || latField < 0 || latField > 17) return null;
let lon = lonField * 20 + lonSquare * 2 - 180;
let lat = latField * 10 + latSquare * 1 - 90;
if (grid.length >= 6) {
const lonSub = grid.charCodeAt(4) - 65;
const latSub = grid.charCodeAt(5) - 65;
if (lonSub >= 0 && lonSub < 24 && latSub >= 0 && latSub < 24) {
lon += lonSub * (2 / 24) + 1 / 24;
lat += latSub * (1 / 24) + 0.5 / 24;
}
} else {
lon += 1;
lat += 0.5;
}
return { lat, lon };
}
function latLonToGrid(lat, lon) {
lon += 180;
lat += 90;
if (isNaN(lon) || isNaN(lat)) return "";
const lonField = String.fromCharCode(65 + Math.floor(lon / 20));
const latField = String.fromCharCode(65 + Math.floor(lat / 10));
const lonSquare = Math.floor(lon % 20 / 2);
const latSquare = Math.floor(lat % 10);
const lonSub = String.fromCharCode(97 + Math.floor(lon % 2 / 2 * 24));
const latSub = String.fromCharCode(97 + Math.floor(lat % 1 * 24));
return lonField + latField + lonSquare + latSquare + lonSub + latSub;
}
function escHtml(s) {
return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
}
function schedulerSelectRelativeEntry(delta) {
const state = schedulerInterleaveState(currentConfig);
const rigId = currentRigId;
if (!rigId || schedulerStepPending || state.activeEntries.length <= 1) return;
const count = state.activeEntries.length;
const currentIndex = state.currentIndex >= 0 ? state.currentIndex : 0;
const targetIndex = (currentIndex + delta + count) % count;
const target = state.activeEntries[targetIndex];
if (!target || !target.id) return;
const targetId = target.id;
schedulerStepPending = true;
renderSchedulerStepControls();
Promise.resolve(schedulerWindow.trx.modules.vchan?.takeSchedulerControl() ?? null).then(function() {
return apiActivateSchedulerEntry(rigId, targetId);
}).then(function(status) {
currentSchedulerStatus = status || null;
return Promise.resolve(
schedulerWindow.trx.modules.vchan?.releaseToScheduler() ?? 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 (hostState.authRole != null) {
initScheduler(hostState.lastActiveRigId, hostState.authRole);
wireSchedulerEvents();
}
})();