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 = ''; svg += ''; entries.forEach(function(entry, idx) { const start = Number(entry.start_min); const end = Number(entry.end_min); if (!Number.isFinite(start) || !Number.isFinite(end)) return; const color = TIMELINE_COLORS[idx % TIMELINE_COLORS.length]; if (start === end) { svg += ''; } else if (start < end) { const x = start / 1440 * W; const w = (end - start) / 1440 * W; svg += ''; } else { const x1 = start / 1440 * W; const w1 = W - x1; svg += ''; const w2 = end / 1440 * W; svg += ''; } }); const interleaveMin = currentConfig && currentConfig.interleave_min ? Number(currentConfig.interleave_min) : 0; if (interleaveMin > 0 && entries.length > 1) { for (let m = 0; m < 1440; m += interleaveMin) { const overlapping = []; entries.forEach(function(entry, idx) { if (schedulerEntryIsActive(entry, m)) { overlapping.push(idx); } }); if (overlapping.length > 1) { const stripeX = m / 1440 * W; const stripeW = Math.max(1, interleaveMin / 1440 * W); const cyclePos = m % (interleaveMin * overlapping.length); const ownerSlot = Math.floor(cyclePos / interleaveMin); const ownerIdx = overlapping[ownerSlot % overlapping.length] ?? 0; const stripeColor = TIMELINE_COLORS[ownerIdx % TIMELINE_COLORS.length]; svg += ''; } } } for (let h = 0; h <= 24; h += 3) { const tx = h / 24 * W; svg += ''; if (h < 24) { svg += '' + String(h).padStart(2, "0") + ""; } } const LOCAL_TICK_Y = TICK_Y + 18; for (let h = 0; h < 24; h += 3) { const localMin = h * 60; const utcOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset(); const utcMin = (localMin + utcOffset + 1440) % 1440; const tx = utcMin / 1440 * W; svg += '' + String(h).padStart(2, "0") + "L"; } svg += '' + timelineNeedleSvg() + ""; 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(); } })();