Files
trx-rs/src/trx-client/trx-frontend/trx-frontend-http/assets/web/generated/scheduler.js
T

1221 lines
53 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// src/plugins/scheduler.ts
var schedulerWindow = window;
var wiredElements = /* @__PURE__ */ new WeakSet();
function schedulerEl(id) {
const element = document.getElementById(id);
if (!element) throw new Error(`Missing scheduler element #${id}`);
return element;
}
(function() {
"use strict";
let schedulerRole = null;
let currentRigId = null;
let currentConfig = null;
let currentSchedulerStatus = null;
let bookmarkList = [];
let statusInterval = null;
let interleaveTicker = null;
let schedulerStepPending = false;
let schEntryEditIdx = null;
let schedulerDirty = false;
function initScheduler(rigId, role) {
schedulerRole = role;
currentRigId = rigId || null;
if (currentRigId) loadScheduler();
startStatusPolling();
startInterleaveTicker();
}
function 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 + '<br><span class="sch-status-detail">' + escHtml(details) + "</span>";
} 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 '<div class="sch-log-entry"><span class="sch-log-time">' + escHtml(ts) + '</span> <span class="sch-log-action">' + escHtml(action) + "</span> " + (bm ? '<span class="sch-log-bm">' + escHtml(bm) + "</span>" : "") + (label ? ' <span class="sch-log-label">(' + escHtml(label) + ")</span>" : "") + "</div>";
}).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 = '<option value="">— none —</option>';
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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + W + " " + H + '" preserveAspectRatio="none">';
svg += '<rect x="0" y="' + BAR_Y + '" width="' + W + '" height="' + BAR_H + '" rx="3" fill="var(--btn-bg)" />';
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 += '<rect class="sch-timeline-seg" x="0" y="' + BAR_Y + '" width="' + W + '" height="' + BAR_H + '" rx="3" fill="' + color + '" data-idx="' + idx + '" />';
} else if (start < end) {
const x = start / 1440 * W;
const w = (end - start) / 1440 * W;
svg += '<rect class="sch-timeline-seg" x="' + x.toFixed(1) + '" y="' + BAR_Y + '" width="' + w.toFixed(1) + '" height="' + BAR_H + '" fill="' + color + '" data-idx="' + idx + '" />';
} else {
const x1 = start / 1440 * W;
const w1 = W - x1;
svg += '<rect class="sch-timeline-seg" x="' + x1.toFixed(1) + '" y="' + BAR_Y + '" width="' + w1.toFixed(1) + '" height="' + BAR_H + '" fill="' + color + '" data-idx="' + idx + '" />';
const w2 = end / 1440 * W;
svg += '<rect class="sch-timeline-seg" x="0" y="' + BAR_Y + '" width="' + w2.toFixed(1) + '" height="' + BAR_H + '" fill="' + color + '" data-idx="' + idx + '" />';
}
});
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 += '<rect x="' + stripeX.toFixed(1) + '" y="' + (BAR_Y + BAR_H - 5) + '" width="' + stripeW.toFixed(1) + '" height="5" fill="' + stripeColor + '" opacity="0.9" />';
}
}
}
for (let h = 0; h <= 24; h += 3) {
const tx = h / 24 * W;
svg += '<line x1="' + tx.toFixed(1) + '" y1="' + TICK_Y + '" x2="' + tx.toFixed(1) + '" y2="' + (TICK_Y + 5) + '" stroke="var(--border-light)" stroke-width="1" />';
if (h < 24) {
svg += '<text class="sch-timeline-tick-label" x="' + (tx + 3).toFixed(1) + '" y="' + (TICK_Y + 16) + '">' + String(h).padStart(2, "0") + "</text>";
}
}
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 += '<text class="sch-timeline-tick-label sch-timeline-local-tick" x="' + (tx + 3).toFixed(1) + '" y="' + (LOCAL_TICK_Y + 10) + '">' + String(h).padStart(2, "0") + "L</text>";
}
svg += '<g id="sch-timeline-needle-g">' + timelineNeedleSvg() + "</g>";
svg += "</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 '<line class="sch-timeline-needle" x1="' + x.toFixed(1) + '" y1="2" x2="' + x.toFixed(1) + '" y2="38" /><polygon class="sch-timeline-needle-head" points="' + (x - 3).toFixed(1) + ",2 " + (x + 3).toFixed(1) + ",2 " + x.toFixed(1) + ',6" />';
}
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 '<option value="' + escHtml(bm.id) + '"' + sel + ">" + escHtml(bm.name) + "</option>";
}).join("");
const extraBmOptions = '<option value="">— add channel —</option>' + bookmarkList.map(function(bm) {
return '<option value="' + escHtml(bm.id) + '">' + escHtml(bm.name) + "</option>";
}).join("");
const inlineExtraIds = Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids.slice() : [];
tr.innerHTML = '<td class="sch-drag-handle" draggable="true" title="Drag to reorder">⠇</td><td><input type="time" class="status-input sch-inline-input" value="' + minToHHMM(entry.start_min) + '" data-field="start" /></td><td><input type="time" class="status-input sch-inline-input" value="' + minToHHMM(entry.end_min) + '" data-field="end" /></td><td>' + (entry.center_hz ? formatFreq(entry.center_hz) : "—") + '</td><td><select class="status-input sch-inline-input" data-field="bookmark">' + bmOptions + '</select></td><td><div class="sch-inline-extra-chips"></div><div style="display:flex;gap:0.4rem;margin-top:0.3rem;"><select class="status-input sch-inline-extra-pick">' + extraBmOptions + '</select><button class="sch-write sch-inline-extra-add" type="button" style="padding:0 0.7rem;">+</button></div></td><td><input type="text" class="status-input sch-inline-input" value="' + escHtml(entry.label || "") + '" data-field="label" /></td><td><input type="number" class="status-input sch-inline-input" value="' + (entry.interleave_min || "") + '" min="1" max="60" placeholder="—" data-field="interleave" style="width:4rem;" ' + (entry.exclusive ? "disabled" : "") + ' /><label style="display:block;font-size:0.85em;margin-top:0.2rem;"><input type="checkbox" data-field="exclusive" ' + (entry.exclusive ? "checked" : "") + ' /> Excl.</label></td><td><input type="checkbox" ' + (entry.record ? "checked" : "") + ' data-field="record" /></td><td><button class="sch-write sch-inline-save" type="button">Save</button><button class="sch-write sch-inline-cancel" type="button">Cancel</button></td>';
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 = '<td class="sch-drag-handle" draggable="true" title="Drag to reorder">⠇</td><td>' + (allDay ? "All day" : minToHHMM(entry.start_min) + ' <span class="sch-local-time">(' + minToLocal(entry.start_min) + ")</span>") + "</td><td>" + (allDay ? "—" : minToHHMM(entry.end_min) + ' <span class="sch-local-time">(' + minToLocal(entry.end_min) + ")</span>") + "</td><td>" + centerCell + "</td><td>" + escHtml(bmName(entry.bookmark_id)) + "</td><td>" + extraCell + "</td><td>" + escHtml(entry.label || "") + "</td><td>" + il + "</td><td>" + (entry.record ? "Yes" : "") + '</td><td><button class="sch-write sch-edit-btn" data-idx="' + idx + '" type="button">Edit</button><button class="sch-write sch-remove-btn" data-idx="' + idx + '" type="button">Remove</button></td>';
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
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 = '<option value="">— select bookmark —</option>';
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();
}
})();