[refactor](trx-frontend-http): extract satellite scheduling UI into dedicated module

Move ~230 lines of satellite pass scheduling code from scheduler.js
into a new sat-scheduler.js plugin with cached DOM refs, createElement-
based rendering, and a clean bridge API. Refactor sat.js predictions
view to deduplicate row builders, extract countdown timer lifecycle
management, and cache all DOM references.

https://claude.ai/code/session_0144nUfHAKs7yRnYTsozNagw
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-28 19:38:12 +00:00
committed by Stan Grams
parent 3e3fdbcb30
commit 891141489c
6 changed files with 539 additions and 432 deletions
@@ -1393,6 +1393,7 @@
<script src="/sat.js"></script> <script src="/sat.js"></script>
<script src="/bookmarks.js"></script> <script src="/bookmarks.js"></script>
<script src="/scheduler.js"></script> <script src="/scheduler.js"></script>
<script src="/sat-scheduler.js"></script>
<script src="/background-decode.js"></script> <script src="/background-decode.js"></script>
<script src="/vchan.js"></script> <script src="/vchan.js"></script>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
@@ -0,0 +1,308 @@
// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: BSD-2-Clause
// Satellite Pass Scheduling UI
// Manages the satellite overlay section within the background decoding scheduler.
// Communicates with scheduler.js via a thin window API for shared state access.
(function () {
"use strict";
// ── DOM references (cached once) ──────────────────────────────────
const dom = {
enabled: document.getElementById("scheduler-sat-enabled"),
pretune: document.getElementById("scheduler-sat-pretune"),
body: document.getElementById("scheduler-sat-body"),
tbody: document.getElementById("scheduler-sat-tbody"),
addBtn: document.getElementById("scheduler-sat-add-btn"),
passStatus: document.getElementById("scheduler-sat-pass-status"),
formWrap: document.getElementById("sch-sat-form-wrap"),
formTitle: document.getElementById("sch-sat-form-title"),
form: document.getElementById("sch-sat-form"),
formCancel: document.getElementById("sch-sat-form-cancel"),
preset: document.getElementById("scheduler-sat-preset"),
name: document.getElementById("scheduler-sat-name"),
norad: document.getElementById("scheduler-sat-norad"),
bookmark: document.getElementById("scheduler-sat-bookmark"),
minEl: document.getElementById("scheduler-sat-min-el"),
priority: document.getElementById("scheduler-sat-priority"),
centerHz: document.getElementById("scheduler-sat-center-hz"),
};
// ── Local state ───────────────────────────────────────────────────
let editIdx = null; // null = adding, number = editing
// ── Scheduler bridge ──────────────────────────────────────────────
// These accessors call into scheduler.js via window.schedulerBridge,
// which is set up by scheduler.js after it initializes.
function getBridge() {
return window.schedulerBridge || {};
}
function getConfig() {
const b = getBridge();
return typeof b.getConfig === "function" ? b.getConfig() : null;
}
function getStatus() {
const b = getBridge();
return typeof b.getStatus === "function" ? b.getStatus() : null;
}
function getBookmarks() {
const b = getBridge();
return typeof b.getBookmarks === "function" ? b.getBookmarks() : [];
}
function bmName(id) {
const bm = getBookmarks().find(function (b) { return b.id === id; });
return bm ? bm.name : String(id || "");
}
function escHtml(s) {
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function formatFreq(hz) {
if (hz >= 1e6) return (hz / 1e6).toFixed(3) + " MHz";
if (hz >= 1e3) return (hz / 1e3).toFixed(1) + " kHz";
return hz + " Hz";
}
// ── Satellite config helpers ──────────────────────────────────────
function getSatelliteEntries() {
var config = getConfig();
return (config && config.satellites && Array.isArray(config.satellites.entries))
? config.satellites.entries
: [];
}
function ensureSatelliteConfig() {
var config = getConfig();
if (!config) return { enabled: false, pretune_secs: 60, entries: [] };
if (!config.satellites) config.satellites = { enabled: false, pretune_secs: 60, entries: [] };
if (!config.satellites.entries) config.satellites.entries = [];
return config.satellites;
}
function collectSatelliteConfig() {
var enabled = dom.enabled ? dom.enabled.checked : false;
var pretune = dom.pretune ? parseInt(dom.pretune.value, 10) : 60;
return {
enabled: enabled,
pretune_secs: isNaN(pretune) || pretune < 0 ? 60 : pretune,
entries: getSatelliteEntries(),
};
}
// ── Render: section ───────────────────────────────────────────────
function renderSection() {
var config = getConfig();
var satCfg = (config && config.satellites) || {};
var enabled = !!satCfg.enabled;
if (dom.enabled) dom.enabled.checked = enabled;
if (dom.pretune) dom.pretune.value = satCfg.pretune_secs != null ? satCfg.pretune_secs : 60;
if (dom.body) dom.body.style.display = enabled ? "" : "none";
renderEntries();
renderPassStatus();
}
// ── Render: entries table ─────────────────────────────────────────
function renderEntries() {
if (!dom.tbody) return;
var entries = getSatelliteEntries();
var frag = document.createDocumentFragment();
entries.forEach(function (entry, idx) {
var tr = document.createElement("tr");
var tdSat = document.createElement("td");
tdSat.textContent = entry.satellite || "";
tr.appendChild(tdSat);
var tdNorad = document.createElement("td");
tdNorad.textContent = entry.norad_id || "";
tr.appendChild(tdNorad);
var tdBm = document.createElement("td");
tdBm.textContent = bmName(entry.bookmark_id);
tr.appendChild(tdBm);
var tdEl = document.createElement("td");
tdEl.textContent = (entry.min_elevation_deg != null ? entry.min_elevation_deg + "\u00B0" : "5\u00B0");
tr.appendChild(tdEl);
var tdPrio = document.createElement("td");
tdPrio.textContent = entry.priority || 0;
tr.appendChild(tdPrio);
var tdActions = document.createElement("td");
var editBtn = document.createElement("button");
editBtn.className = "sch-write";
editBtn.type = "button";
editBtn.textContent = "Edit";
editBtn.addEventListener("click", function () {
openForm(entry, idx);
});
tdActions.appendChild(editBtn);
var removeBtn = document.createElement("button");
removeBtn.className = "sch-write";
removeBtn.type = "button";
removeBtn.textContent = "Remove";
removeBtn.addEventListener("click", function () {
removeEntry(idx);
});
tdActions.appendChild(removeBtn);
tr.appendChild(tdActions);
frag.appendChild(tr);
});
dom.tbody.replaceChildren(frag);
}
// ── Render: pass status ───────────────────────────────────────────
function renderPassStatus() {
if (!dom.passStatus) return;
var entries = getSatelliteEntries();
if (entries.length === 0) {
dom.passStatus.innerHTML = "";
return;
}
var status = getStatus();
if (status && status.active_satellite) {
dom.passStatus.innerHTML =
'<span class="sch-sat-active-badge">PASS ACTIVE: ' +
escHtml(status.active_satellite) +
'</span>';
} else {
dom.passStatus.innerHTML =
'<span style="color:var(--text-muted);font-size:0.8rem;">No satellite pass active. Predictions available in the SAT tab.</span>';
}
}
// ── Render: bookmark dropdown ─────────────────────────────────────
function renderBookmarkSelect(selectedId) {
if (!dom.bookmark) return;
dom.bookmark.innerHTML = '<option value="">— none —</option>';
getBookmarks().forEach(function (bm) {
var 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;
dom.bookmark.appendChild(opt);
});
}
// ── Entry management ──────────────────────────────────────────────
function removeEntry(idx) {
var sat = ensureSatelliteConfig();
sat.entries.splice(idx, 1);
renderEntries();
}
// ── Form: open ────────────────────────────────────────────────────
function openForm(entry, idx) {
editIdx = (idx != null) ? idx : null;
if (dom.formTitle) dom.formTitle.textContent = entry ? "Edit Satellite" : "Add Satellite";
if (dom.preset) dom.preset.value = "";
if (dom.name) dom.name.value = entry ? (entry.satellite || "") : "";
if (dom.norad) dom.norad.value = entry ? (entry.norad_id || "") : "";
if (dom.minEl) dom.minEl.value = entry && entry.min_elevation_deg != null ? entry.min_elevation_deg : 5;
if (dom.priority) dom.priority.value = entry && entry.priority != null ? entry.priority : 0;
if (dom.centerHz) dom.centerHz.value = entry && entry.center_hz ? entry.center_hz : "";
renderBookmarkSelect(entry ? entry.bookmark_id : null);
if (dom.formWrap) {
dom.formWrap.style.display = "flex";
if (dom.name) dom.name.focus();
}
}
// ── Form: close ───────────────────────────────────────────────────
function closeForm() {
if (dom.formWrap) dom.formWrap.style.display = "none";
editIdx = null;
}
// ── Form: submit ──────────────────────────────────────────────────
function onFormSubmit(e) {
e.preventDefault();
var satellite = dom.name ? dom.name.value.trim() : "";
var noradId = dom.norad ? parseInt(dom.norad.value, 10) : NaN;
var bmId = dom.bookmark ? dom.bookmark.value : "";
if (!satellite) { alert("Please enter a satellite name."); return; }
if (isNaN(noradId) || noradId <= 0) { alert("Please enter a valid NORAD catalog number."); return; }
if (!bmId) { alert("Please select a bookmark."); return; }
var minEl = dom.minEl ? parseFloat(dom.minEl.value) : 5;
var prio = dom.priority ? parseInt(dom.priority.value, 10) : 0;
var centerHzRaw = dom.centerHz ? parseInt(dom.centerHz.value, 10) : NaN;
var sat = ensureSatelliteConfig();
var entryData = {
satellite: satellite,
norad_id: noradId,
bookmark_id: bmId,
min_elevation_deg: isNaN(minEl) ? 5 : minEl,
priority: isNaN(prio) ? 0 : prio,
center_hz: !isNaN(centerHzRaw) && centerHzRaw > 0 ? centerHzRaw : null,
bookmark_ids: [],
};
if (editIdx !== null) {
var existing = sat.entries[editIdx];
entryData.id = existing ? existing.id : ("sat_" + Date.now().toString(36));
sat.entries[editIdx] = entryData;
} else {
entryData.id = "sat_" + Date.now().toString(36);
sat.entries.push(entryData);
}
closeForm();
renderEntries();
}
// ── Preset change handler ─────────────────────────────────────────
function onPresetChange() {
if (!dom.preset || !dom.preset.value) return;
var parts = dom.preset.value.split("|");
if (dom.name) dom.name.value = parts[0] || "";
if (dom.norad) dom.norad.value = parts[1] || "";
}
// ── Wire all events ───────────────────────────────────────────────
function wireEvents() {
if (dom.enabled) {
dom.enabled.addEventListener("change", function () {
if (dom.body) dom.body.style.display = dom.enabled.checked ? "" : "none";
});
}
if (dom.addBtn) dom.addBtn.addEventListener("click", function () { openForm(null, null); });
if (dom.form) dom.form.addEventListener("submit", onFormSubmit);
if (dom.formCancel) dom.formCancel.addEventListener("click", closeForm);
if (dom.preset) dom.preset.addEventListener("change", onPresetChange);
}
// ── Public API ────────────────────────────────────────────────────
window.satScheduler = {
wireEvents: wireEvents,
renderSection: renderSection,
renderPassStatus: renderPassStatus,
collectSatelliteConfig: collectSatelliteConfig,
};
})();
@@ -3,27 +3,46 @@
// History view: filterable table of all decoded images // History view: filterable table of all decoded images
// Predictions view: next 24 h passes for ham satellites // Predictions view: next 24 h passes for ham satellites
// ── DOM references ────────────────────────────────────────────────── // ── DOM references (cached once) ───────────────────────────────────
const satStatus = document.getElementById("sat-status"); const satDom = {
const satLiveView = document.getElementById("sat-live-view"); status: document.getElementById("sat-status"),
const satHistoryView = document.getElementById("sat-history-view"); liveView: document.getElementById("sat-live-view"),
const satPredictionsView = document.getElementById("sat-predictions-view"); historyView: document.getElementById("sat-history-view"),
const satLiveLatest = document.getElementById("sat-live-latest"); predictionsView: document.getElementById("sat-predictions-view"),
const satHistoryList = document.getElementById("sat-history-list"); liveLatest: document.getElementById("sat-live-latest"),
const satHistoryCount = document.getElementById("sat-history-count"); historyList: document.getElementById("sat-history-list"),
const satFilterInput = document.getElementById("sat-filter"); historyCount: document.getElementById("sat-history-count"),
const satSortSelect = document.getElementById("sat-sort"); filterInput: document.getElementById("sat-filter"),
const satTypeFilter = document.getElementById("sat-type-filter"); sortSelect: document.getElementById("sat-sort"),
const satAptState = document.getElementById("sat-apt-state"); typeFilter: document.getElementById("sat-type-filter"),
const satLrptState = document.getElementById("sat-lrpt-state"); aptState: document.getElementById("sat-apt-state"),
lrptState: document.getElementById("sat-lrpt-state"),
viewLiveBtn: document.getElementById("sat-view-live"),
viewHistoryBtn: document.getElementById("sat-view-history"),
viewPredBtn: document.getElementById("sat-view-predictions"),
predFilter: document.getElementById("sat-pred-filter"),
predMinEl: document.getElementById("sat-pred-min-el"),
predCategory: document.getElementById("sat-pred-category"),
predCurrentList: document.getElementById("sat-pred-current-list"),
predUpcomingList: document.getElementById("sat-pred-list"),
predCurrentSec: document.getElementById("sat-pred-current-section"),
predUpcomingSec: document.getElementById("sat-pred-upcoming-section"),
predStatus: document.getElementById("sat-pred-status"),
};
// ── State ─────────────────────────────────────────────────────────── // ── State ───────────────────────────────────────────────────────────
let satImageHistory = []; let satImageHistory = [];
const SAT_MAX_IMAGES = 100; const SAT_MAX_IMAGES = 100;
const SAT_PRED_PAGE_SIZE = 50; // max rows before "Show more" const SAT_PRED_PAGE_SIZE = 50;
let satPredShowAll = false; let satPredShowAll = false;
let satFilterText = ""; let satFilterText = "";
let satActiveView = "live"; // "live" | "history" | "predictions" let satActiveView = "live"; // "live" | "history" | "predictions"
let satPredData = [];
let satPredFilterText = "";
let satPredMinEl = 0;
let satPredCategory = "all";
let satPredSatCount = 0;
let satPredCountdownTimer = null;
// ── UI scheduler helper ───────────────────────────────────────────── // ── UI scheduler helper ─────────────────────────────────────────────
function scheduleSatUi(key, job) { function scheduleSatUi(key, job) {
@@ -35,23 +54,16 @@ function scheduleSatUi(key, job) {
} }
// ── View switching ────────────────────────────────────────────────── // ── View switching ──────────────────────────────────────────────────
const satViewLiveBtn = document.getElementById("sat-view-live");
const satViewHistoryBtn = document.getElementById("sat-view-history");
const satViewPredictionsBtn = document.getElementById("sat-view-predictions");
function switchSatView(view) { function switchSatView(view) {
const leavingPredictions = satActiveView === "predictions" && view !== "predictions"; const leavingPredictions = satActiveView === "predictions" && view !== "predictions";
satActiveView = view; satActiveView = view;
if (satLiveView) satLiveView.style.display = view === "live" ? "" : "none"; if (satDom.liveView) satDom.liveView.style.display = view === "live" ? "" : "none";
if (satHistoryView) satHistoryView.style.display = view === "history" ? "" : "none"; if (satDom.historyView) satDom.historyView.style.display = view === "history" ? "" : "none";
if (satPredictionsView) satPredictionsView.style.display = view === "predictions" ? "" : "none"; if (satDom.predictionsView) satDom.predictionsView.style.display = view === "predictions" ? "" : "none";
if (satViewLiveBtn) satViewLiveBtn.classList.toggle("sat-view-active", view === "live"); if (satDom.viewLiveBtn) satDom.viewLiveBtn.classList.toggle("sat-view-active", view === "live");
if (satViewHistoryBtn) satViewHistoryBtn.classList.toggle("sat-view-active", view === "history"); if (satDom.viewHistoryBtn) satDom.viewHistoryBtn.classList.toggle("sat-view-active", view === "history");
if (satViewPredictionsBtn) satViewPredictionsBtn.classList.toggle("sat-view-active", view === "predictions"); if (satDom.viewPredBtn) satDom.viewPredBtn.classList.toggle("sat-view-active", view === "predictions");
// Clear prediction DOM when leaving to reduce node count. if (leavingPredictions) clearPredictionDom();
if (leavingPredictions) {
clearPredictionDom();
}
if (view === "history") { if (view === "history") {
renderSatHistoryTable(); renderSatHistoryTable();
} else if (view === "predictions") { } else if (view === "predictions") {
@@ -61,39 +73,38 @@ function switchSatView(view) {
} }
function clearPredictionDom() { function clearPredictionDom() {
if (satPredCountdownTimer) { clearInterval(satPredCountdownTimer); satPredCountdownTimer = null; } stopCountdownTimer();
if (satPredCurrentList) satPredCurrentList.innerHTML = ""; if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
if (satPredUpcomingList) satPredUpcomingList.innerHTML = ""; if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
} }
window.clearSatPredictionDom = clearPredictionDom; window.clearSatPredictionDom = clearPredictionDom;
satViewLiveBtn?.addEventListener("click", () => switchSatView("live")); satDom.viewLiveBtn?.addEventListener("click", () => switchSatView("live"));
satViewHistoryBtn?.addEventListener("click", () => switchSatView("history")); satDom.viewHistoryBtn?.addEventListener("click", () => switchSatView("history"));
satViewPredictionsBtn?.addEventListener("click", () => switchSatView("predictions")); satDom.viewPredBtn?.addEventListener("click", () => switchSatView("predictions"));
// ── Live view: decoder state ──────────────────────────────────────── // ── Live view: decoder state ────────────────────────────────────────
// Updated from app.js render() via window.updateSatLiveState
let _lastSatAptOn = null, _lastSatLrptOn = null; let _lastSatAptOn = null, _lastSatLrptOn = null;
window.updateSatLiveState = function (update) { window.updateSatLiveState = function (update) {
if (!satAptState || !satLrptState) return; if (!satDom.aptState || !satDom.lrptState) return;
const aptOn = !!update.wxsat_decode_enabled; const aptOn = !!update.wxsat_decode_enabled;
const lrptOn = !!update.lrpt_decode_enabled; const lrptOn = !!update.lrpt_decode_enabled;
if (aptOn !== _lastSatAptOn) { if (aptOn !== _lastSatAptOn) {
_lastSatAptOn = aptOn; _lastSatAptOn = aptOn;
satAptState.textContent = aptOn ? "Listening" : "Idle"; satDom.aptState.textContent = aptOn ? "Listening" : "Idle";
satAptState.className = "sat-live-value " + (aptOn ? "sat-state-listening" : "sat-state-idle"); satDom.aptState.className = "sat-live-value " + (aptOn ? "sat-state-listening" : "sat-state-idle");
} }
if (lrptOn !== _lastSatLrptOn) { if (lrptOn !== _lastSatLrptOn) {
_lastSatLrptOn = lrptOn; _lastSatLrptOn = lrptOn;
satLrptState.textContent = lrptOn ? "Listening" : "Idle"; satDom.lrptState.textContent = lrptOn ? "Listening" : "Idle";
satLrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle"); satDom.lrptState.className = "sat-live-value " + (lrptOn ? "sat-state-listening" : "sat-state-idle");
} }
}; };
function renderSatLatestCard() { function renderSatLatestCard() {
if (!satLiveLatest) return; if (!satDom.liveLatest) return;
if (satImageHistory.length === 0) { if (satImageHistory.length === 0) {
satLiveLatest.innerHTML = satDom.liveLatest.innerHTML =
'<div style="color:var(--text-muted);font-size:0.82rem;">No images decoded yet. Enable a decoder and wait for a satellite pass.</div>'; '<div style="color:var(--text-muted);font-size:0.82rem;">No images decoded yet. Enable a decoder and wait for a satellite pass.</div>';
return; return;
} }
@@ -124,19 +135,17 @@ function renderSatLatestCard() {
html += ` <button type="button" class="sat-map-btn" onclick="window.satShowOnMap(${img.geo_bounds[0]},${img.geo_bounds[1]},${img.geo_bounds[2]},${img.geo_bounds[3]})" style="font-size:0.8rem;margin-top:0.25rem;margin-left:0.5rem;cursor:pointer;background:none;border:1px solid var(--accent);color:var(--accent);border-radius:3px;padding:1px 6px;">Show on Map</button>`; html += ` <button type="button" class="sat-map-btn" onclick="window.satShowOnMap(${img.geo_bounds[0]},${img.geo_bounds[1]},${img.geo_bounds[2]},${img.geo_bounds[3]})" style="font-size:0.8rem;margin-top:0.25rem;margin-left:0.5rem;cursor:pointer;background:none;border:1px solid var(--accent);color:var(--accent);border-radius:3px;padding:1px 6px;">Show on Map</button>`;
} }
html += `</div>`; html += `</div>`;
satLiveLatest.innerHTML = html; satDom.liveLatest.innerHTML = html;
} }
// ── History view: table ───────────────────────────────────────────── // ── History view: table ─────────────────────────────────────────────
function getSatFilteredHistory() { function getSatFilteredHistory() {
let items = satImageHistory; let items = satImageHistory;
// Type filter const typeVal = satDom.typeFilter ? satDom.typeFilter.value : "all";
const typeVal = satTypeFilter ? satTypeFilter.value : "all";
if (typeVal === "apt") items = items.filter((i) => i._decoder === "apt"); if (typeVal === "apt") items = items.filter((i) => i._decoder === "apt");
else if (typeVal === "lrpt") items = items.filter((i) => i._decoder === "lrpt"); else if (typeVal === "lrpt") items = items.filter((i) => i._decoder === "lrpt");
// Text filter
if (satFilterText) { if (satFilterText) {
items = items.filter((i) => { items = items.filter((i) => {
const haystack = [ const haystack = [
@@ -145,18 +154,13 @@ function getSatFilteredHistory() {
i.channels || "", i.channels || "",
i.channel_a || "", i.channel_a || "",
i.channel_b || "", i.channel_b || "",
] ].join(" ").toUpperCase();
.join(" ")
.toUpperCase();
return haystack.includes(satFilterText); return haystack.includes(satFilterText);
}); });
} }
// Sort const sortVal = satDom.sortSelect ? satDom.sortSelect.value : "newest";
const sortVal = satSortSelect ? satSortSelect.value : "newest"; if (sortVal === "oldest") items = items.slice().reverse();
if (sortVal === "oldest") {
items = items.slice().reverse();
}
return items; return items;
} }
@@ -194,18 +198,18 @@ function renderSatHistoryRow(img) {
} }
function renderSatHistoryTable() { function renderSatHistoryTable() {
if (!satHistoryList) return; if (!satDom.historyList) return;
const items = getSatFilteredHistory(); const items = getSatFilteredHistory();
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
for (let i = 0; i < items.length; i += 1) { for (let i = 0; i < items.length; i += 1) {
fragment.appendChild(renderSatHistoryRow(items[i])); fragment.appendChild(renderSatHistoryRow(items[i]));
} }
satHistoryList.replaceChildren(fragment); satDom.historyList.replaceChildren(fragment);
if (satHistoryCount) { if (satDom.historyCount) {
const total = satImageHistory.length; const total = satImageHistory.length;
const shown = items.length; const shown = items.length;
satHistoryCount.textContent = satDom.historyCount.textContent =
total === 0 total === 0
? "No images yet" ? "No images yet"
: shown === total : shown === total
@@ -238,7 +242,7 @@ function addSatImage(img, decoder) {
// ── Server callbacks ──────────────────────────────────────────────── // ── Server callbacks ────────────────────────────────────────────────
window.onServerSatImage = function (msg) { window.onServerSatImage = function (msg) {
if (satStatus) satStatus.textContent = "Image received (NOAA APT)"; if (satDom.status) satDom.status.textContent = "Image received (NOAA APT)";
addSatImage(msg, "apt"); addSatImage(msg, "apt");
if (msg.geo_bounds && msg.path && window.addSatMapOverlay) { if (msg.geo_bounds && msg.path && window.addSatMapOverlay) {
window.addSatMapOverlay(msg); window.addSatMapOverlay(msg);
@@ -246,7 +250,7 @@ window.onServerSatImage = function (msg) {
}; };
window.onServerLrptImage = function (msg) { window.onServerLrptImage = function (msg) {
if (satStatus) satStatus.textContent = "Image received (Meteor LRPT)"; if (satDom.status) satDom.status.textContent = "Image received (Meteor LRPT)";
addSatImage(msg, "lrpt"); addSatImage(msg, "lrpt");
if (msg.geo_bounds && msg.path && window.addSatMapOverlay) { if (msg.geo_bounds && msg.path && window.addSatMapOverlay) {
window.addSatMapOverlay(msg); window.addSatMapOverlay(msg);
@@ -255,7 +259,7 @@ window.onServerLrptImage = function (msg) {
window.resetSatHistoryView = function () { window.resetSatHistoryView = function () {
satImageHistory = []; satImageHistory = [];
if (satHistoryList) satHistoryList.innerHTML = ""; if (satDom.historyList) satDom.historyList.innerHTML = "";
renderSatLatestCard(); renderSatLatestCard();
renderSatHistoryTable(); renderSatHistoryTable();
if (window.clearSatMapOverlays) window.clearSatMapOverlays(); if (window.clearSatMapOverlays) window.clearSatMapOverlays();
@@ -288,13 +292,13 @@ lrptDecodeToggleBtn?.addEventListener("click", async () => {
}); });
// ── Filter / sort event listeners ─────────────────────────────────── // ── Filter / sort event listeners ───────────────────────────────────
satFilterInput?.addEventListener("input", () => { satDom.filterInput?.addEventListener("input", () => {
satFilterText = satFilterInput.value.trim().toUpperCase(); satFilterText = satDom.filterInput.value.trim().toUpperCase();
renderSatHistoryTable(); renderSatHistoryTable();
}); });
satSortSelect?.addEventListener("change", () => renderSatHistoryTable()); satDom.sortSelect?.addEventListener("change", () => renderSatHistoryTable());
satTypeFilter?.addEventListener("change", () => renderSatHistoryTable()); satDom.typeFilter?.addEventListener("change", () => renderSatHistoryTable());
// ── Settings: clear history ───────────────────────────────────────── // ── Settings: clear history ─────────────────────────────────────────
document document
@@ -309,55 +313,7 @@ document
} }
}); });
// ── Predictions view ──────────────────────────────────────────────── // ── Predictions: helpers ────────────────────────────────────────────
let satPredData = [];
let satPredFilterText = "";
let satPredMinEl = 0;
let satPredCategory = "all";
let satPredSatCount = 0;
let satPredCountdownTimer = null;
const satPredFilterInput = document.getElementById("sat-pred-filter");
const satPredMinElSelect = document.getElementById("sat-pred-min-el");
const satPredCategorySelect = document.getElementById("sat-pred-category");
const satPredCurrentList = document.getElementById("sat-pred-current-list");
const satPredUpcomingList = document.getElementById("sat-pred-list");
const satPredCurrentSection = document.getElementById("sat-pred-current-section");
const satPredUpcomingSection = document.getElementById("sat-pred-upcoming-section");
const satPredStatus = document.getElementById("sat-pred-status");
function getFilteredPredictions() {
let items = satPredData;
if (satPredCategory !== "all") {
items = items.filter((p) => p.category === satPredCategory);
}
if (satPredMinEl > 0) {
items = items.filter((p) => p.max_elevation_deg >= satPredMinEl);
}
if (satPredFilterText) {
items = items.filter((p) => p.satellite.toUpperCase().includes(satPredFilterText));
}
return items;
}
function applyPredFilters() {
renderSatPredictions(getFilteredPredictions());
}
satPredFilterInput?.addEventListener("input", () => {
satPredFilterText = satPredFilterInput.value.trim().toUpperCase();
applyPredFilters();
});
satPredMinElSelect?.addEventListener("change", () => {
satPredMinEl = parseInt(satPredMinElSelect.value, 10) || 0;
applyPredFilters();
});
satPredCategorySelect?.addEventListener("change", () => {
satPredCategory = satPredCategorySelect.value;
applyPredFilters();
});
function azToCardinal(deg) { function azToCardinal(deg) {
const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]; const dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
return dirs[Math.round(deg / 45) % 8]; return dirs[Math.round(deg / 45) % 8];
@@ -367,9 +323,7 @@ function formatPredTime(ms) {
const d = new Date(ms); const d = new Date(ms);
const now = new Date(); const now = new Date();
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const day = d.getUTCDay() !== now.getUTCDay() const day = d.getUTCDay() !== now.getUTCDay() ? dayNames[d.getUTCDay()] + " " : "";
? dayNames[d.getUTCDay()] + " "
: "";
const hh = String(d.getUTCHours()).padStart(2, "0"); const hh = String(d.getUTCHours()).padStart(2, "0");
const mm = String(d.getUTCMinutes()).padStart(2, "0"); const mm = String(d.getUTCMinutes()).padStart(2, "0");
return `${day}${hh}:${mm}`; return `${day}${hh}:${mm}`;
@@ -387,126 +341,27 @@ function formatCountdown(ms) {
return `${m}:${String(s).padStart(2, "0")}`; return `${m}:${String(s).padStart(2, "0")}`;
} }
function renderSatPredictions(passes, error) { function elevationClass(deg) {
const currentList = satPredCurrentList; if (deg >= 45) return "sat-pred-el-high";
const upcomingList = satPredUpcomingList; if (deg >= 10) return "sat-pred-el-mid";
const currentSection = satPredCurrentSection; return "sat-pred-el-low";
const upcomingSection = satPredUpcomingSection; }
const status = satPredStatus;
// Stop any previous countdown timer // ── Predictions: countdown timer management ─────────────────────────
if (satPredCountdownTimer) { clearInterval(satPredCountdownTimer); satPredCountdownTimer = null; } function stopCountdownTimer() {
if (satPredCountdownTimer) {
if (error) {
if (currentList) currentList.innerHTML = "";
if (upcomingList) upcomingList.innerHTML = "";
if (currentSection) currentSection.style.display = "none";
if (upcomingSection) upcomingSection.style.display = "none";
if (status) status.textContent = error;
return;
}
if (!Array.isArray(passes) || passes.length === 0) {
if (currentList) currentList.innerHTML = "";
if (upcomingList) upcomingList.innerHTML = "";
if (currentSection) currentSection.style.display = "none";
if (upcomingSection) upcomingSection.style.display = "none";
if (status) status.textContent = "No passes found in the next 24 hours.";
return;
}
const now = Date.now();
const current = passes.filter((p) => p.aos_ms <= now && p.los_ms > now);
const upcoming = passes.filter((p) => p.aos_ms > now);
// ── Current passes ──
if (currentSection) currentSection.style.display = current.length > 0 ? "" : "none";
if (currentList) {
if (current.length === 0) {
currentList.innerHTML = "";
} else {
const frag = document.createDocumentFragment();
for (const pass of current) {
const row = document.createElement("div");
row.className = "sat-pred-row-current";
const elClass = pass.max_elevation_deg >= 45
? "sat-pred-el-high"
: pass.max_elevation_deg >= 10
? "sat-pred-el-mid"
: "sat-pred-el-low";
const dir = `${azToCardinal(pass.azimuth_aos_deg)}${azToCardinal(pass.azimuth_los_deg)}`;
const remaining = Math.max(0, pass.los_ms - now);
row.innerHTML = [
`<span class="sat-pred-col-sat">${pass.satellite}</span>`,
`<span class="sat-pred-col-el ${elClass}">${pass.max_elevation_deg.toFixed(1)}°</span>`,
`<span class="sat-pred-col-time">${formatPredTime(pass.aos_ms)}</span>`,
`<span class="sat-pred-col-time">${formatPredTime(pass.los_ms)}</span>`,
`<span class="sat-pred-col-countdown" data-los="${pass.los_ms}">${formatCountdown(remaining)}</span>`,
`<span class="sat-pred-col-dir">${dir}</span>`,
].join("");
frag.appendChild(row);
}
currentList.replaceChildren(frag);
}
}
// ── Upcoming passes (capped to reduce DOM node count) ──
const upcomingLimit = satPredShowAll ? upcoming.length : SAT_PRED_PAGE_SIZE;
const visibleUpcoming = upcoming.slice(0, upcomingLimit);
const hiddenCount = upcoming.length - visibleUpcoming.length;
if (upcomingSection) upcomingSection.style.display = upcoming.length > 0 ? "" : "none";
if (upcomingList) {
const frag = document.createDocumentFragment();
for (const pass of visibleUpcoming) {
const row = document.createElement("div");
row.className = "sat-pred-row";
const elClass = pass.max_elevation_deg >= 45
? "sat-pred-el-high"
: pass.max_elevation_deg >= 10
? "sat-pred-el-mid"
: "sat-pred-el-low";
const dir = `${azToCardinal(pass.azimuth_aos_deg)}${azToCardinal(pass.azimuth_los_deg)}`;
row.innerHTML = [
`<span class="sat-pred-col-time">${formatPredTime(pass.aos_ms)}</span>`,
`<span class="sat-pred-col-sat">${pass.satellite}</span>`,
`<span class="sat-pred-col-el ${elClass}">${pass.max_elevation_deg.toFixed(1)}°</span>`,
`<span class="sat-pred-col-dur">${formatPredDuration(pass.duration_s)}</span>`,
`<span class="sat-pred-col-dir">${dir}</span>`,
].join("");
frag.appendChild(row);
}
if (hiddenCount > 0) {
const moreRow = document.createElement("div");
moreRow.className = "sat-pred-row";
moreRow.style.cursor = "pointer";
moreRow.style.textAlign = "center";
moreRow.innerHTML = `<span style="grid-column:1/-1;color:var(--accent);font-size:0.82rem;">Show ${hiddenCount} more passes\u2026</span>`;
moreRow.addEventListener("click", () => {
satPredShowAll = true;
renderSatPredictions(getFilteredPredictions());
});
frag.appendChild(moreRow);
}
upcomingList.replaceChildren(frag);
}
// ── Status ──
if (status) {
let text = `${current.length} active · ${upcoming.length} upcoming · times in UTC`;
if (satPredSatCount > 0) text += ` · ${satPredSatCount} satellites tracked`;
status.textContent = text;
}
// ── Countdown timer: update "time left" every second ──
// Only run when predictions view is actually visible.
if (current.length > 0 && satActiveView === "predictions") {
const countdownEls = currentList ? currentList.querySelectorAll(".sat-pred-col-countdown") : [];
if (countdownEls.length > 0) {
satPredCountdownTimer = setInterval(() => {
// Pause timer if predictions view was hidden (e.g. switched tabs).
if (satActiveView !== "predictions") {
clearInterval(satPredCountdownTimer); clearInterval(satPredCountdownTimer);
satPredCountdownTimer = null; satPredCountdownTimer = null;
}
}
function startCountdownTimer(container) {
const countdownEls = container ? container.querySelectorAll(".sat-pred-col-countdown") : [];
if (countdownEls.length === 0) return;
satPredCountdownTimer = setInterval(() => {
if (satActiveView !== "predictions") {
stopCountdownTimer();
return; return;
} }
const n = Date.now(); const n = Date.now();
@@ -522,19 +377,150 @@ function renderSatPredictions(passes, error) {
} }
} }
if (!anyActive) { if (!anyActive) {
clearInterval(satPredCountdownTimer); stopCountdownTimer();
satPredCountdownTimer = null;
renderSatPredictions(getFilteredPredictions()); renderSatPredictions(getFilteredPredictions());
} }
}, 1000); }, 1000);
}
// ── Predictions: row builders ───────────────────────────────────────
function buildCurrentPassRow(pass, now) {
const row = document.createElement("div");
row.className = "sat-pred-row-current";
const dir = `${azToCardinal(pass.azimuth_aos_deg)} \u2192 ${azToCardinal(pass.azimuth_los_deg)}`;
const remaining = Math.max(0, pass.los_ms - now);
row.innerHTML = [
`<span class="sat-pred-col-sat">${pass.satellite}</span>`,
`<span class="sat-pred-col-el ${elevationClass(pass.max_elevation_deg)}">${pass.max_elevation_deg.toFixed(1)}\u00B0</span>`,
`<span class="sat-pred-col-time">${formatPredTime(pass.aos_ms)}</span>`,
`<span class="sat-pred-col-time">${formatPredTime(pass.los_ms)}</span>`,
`<span class="sat-pred-col-countdown" data-los="${pass.los_ms}">${formatCountdown(remaining)}</span>`,
`<span class="sat-pred-col-dir">${dir}</span>`,
].join("");
return row;
}
function buildUpcomingPassRow(pass) {
const row = document.createElement("div");
row.className = "sat-pred-row";
const dir = `${azToCardinal(pass.azimuth_aos_deg)} \u2192 ${azToCardinal(pass.azimuth_los_deg)}`;
row.innerHTML = [
`<span class="sat-pred-col-time">${formatPredTime(pass.aos_ms)}</span>`,
`<span class="sat-pred-col-sat">${pass.satellite}</span>`,
`<span class="sat-pred-col-el ${elevationClass(pass.max_elevation_deg)}">${pass.max_elevation_deg.toFixed(1)}\u00B0</span>`,
`<span class="sat-pred-col-dur">${formatPredDuration(pass.duration_s)}</span>`,
`<span class="sat-pred-col-dir">${dir}</span>`,
].join("");
return row;
}
// ── Predictions: filter state ───────────────────────────────────────
function getFilteredPredictions() {
let items = satPredData;
if (satPredCategory !== "all") items = items.filter((p) => p.category === satPredCategory);
if (satPredMinEl > 0) items = items.filter((p) => p.max_elevation_deg >= satPredMinEl);
if (satPredFilterText) items = items.filter((p) => p.satellite.toUpperCase().includes(satPredFilterText));
return items;
}
function applyPredFilters() {
renderSatPredictions(getFilteredPredictions());
}
satDom.predFilter?.addEventListener("input", () => {
satPredFilterText = satDom.predFilter.value.trim().toUpperCase();
applyPredFilters();
});
satDom.predMinEl?.addEventListener("change", () => {
satPredMinEl = parseInt(satDom.predMinEl.value, 10) || 0;
applyPredFilters();
});
satDom.predCategory?.addEventListener("change", () => {
satPredCategory = satDom.predCategory.value;
applyPredFilters();
});
// ── Predictions: main render ────────────────────────────────────────
function renderSatPredictions(passes, error) {
stopCountdownTimer();
if (error) {
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
if (satDom.predStatus) satDom.predStatus.textContent = error;
return;
} }
if (!Array.isArray(passes) || passes.length === 0) {
if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = "none";
if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = "none";
if (satDom.predStatus) satDom.predStatus.textContent = "No passes found in the next 24 hours.";
return;
}
const now = Date.now();
const current = passes.filter((p) => p.aos_ms <= now && p.los_ms > now);
const upcoming = passes.filter((p) => p.aos_ms > now);
// ── Current passes ──
if (satDom.predCurrentSec) satDom.predCurrentSec.style.display = current.length > 0 ? "" : "none";
if (satDom.predCurrentList) {
if (current.length === 0) {
satDom.predCurrentList.innerHTML = "";
} else {
const frag = document.createDocumentFragment();
for (const pass of current) frag.appendChild(buildCurrentPassRow(pass, now));
satDom.predCurrentList.replaceChildren(frag);
}
}
// ── Upcoming passes ──
const upcomingLimit = satPredShowAll ? upcoming.length : SAT_PRED_PAGE_SIZE;
const visibleUpcoming = upcoming.slice(0, upcomingLimit);
const hiddenCount = upcoming.length - visibleUpcoming.length;
if (satDom.predUpcomingSec) satDom.predUpcomingSec.style.display = upcoming.length > 0 ? "" : "none";
if (satDom.predUpcomingList) {
const frag = document.createDocumentFragment();
for (const pass of visibleUpcoming) frag.appendChild(buildUpcomingPassRow(pass));
if (hiddenCount > 0) {
const moreRow = document.createElement("div");
moreRow.className = "sat-pred-row";
moreRow.style.cursor = "pointer";
moreRow.style.textAlign = "center";
moreRow.innerHTML = `<span style="grid-column:1/-1;color:var(--accent);font-size:0.82rem;">Show ${hiddenCount} more passes\u2026</span>`;
moreRow.addEventListener("click", () => {
satPredShowAll = true;
renderSatPredictions(getFilteredPredictions());
});
frag.appendChild(moreRow);
}
satDom.predUpcomingList.replaceChildren(frag);
}
// ── Status ──
if (satDom.predStatus) {
let text = `${current.length} active \u00B7 ${upcoming.length} upcoming \u00B7 times in UTC`;
if (satPredSatCount > 0) text += ` \u00B7 ${satPredSatCount} satellites tracked`;
satDom.predStatus.textContent = text;
}
// ── Countdown timer ──
if (current.length > 0 && satActiveView === "predictions") {
startCountdownTimer(satDom.predCurrentList);
} }
} }
// ── Predictions: data loading ───────────────────────────────────────
async function loadSatPredictions() { async function loadSatPredictions() {
if (satPredStatus) satPredStatus.textContent = "Loading predictions\u2026"; if (satDom.predStatus) satDom.predStatus.textContent = "Loading predictions\u2026";
if (satPredCurrentList) satPredCurrentList.innerHTML = ""; if (satDom.predCurrentList) satDom.predCurrentList.innerHTML = "";
if (satPredUpcomingList) satPredUpcomingList.innerHTML = ""; if (satDom.predUpcomingList) satDom.predUpcomingList.innerHTML = "";
try { try {
const resp = await fetch("/sat_passes"); const resp = await fetch("/sat_passes");
if (!resp.ok) throw new Error(`HTTP ${resp.status}`); if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
@@ -554,11 +540,9 @@ async function loadSatPredictions() {
// ── Navigate to map centered on satellite image bounds ────────────── // ── Navigate to map centered on satellite image bounds ──────────────
window.satShowOnMap = function (south, west, north, east) { window.satShowOnMap = function (south, west, north, east) {
// Enable sat filter if not active
if (typeof window.enableMapSourceFilter === "function") { if (typeof window.enableMapSourceFilter === "function") {
window.enableMapSourceFilter("sat"); window.enableMapSourceFilter("sat");
} }
// Navigate to the center of the image bounds
const lat = (south + north) / 2; const lat = (south + north) / 2;
const lon = (west + east) / 2; const lon = (west + east) / 2;
if (window.navigateToAprsMap) { if (window.navigateToAprsMap) {
@@ -19,7 +19,7 @@
let interleaveTicker = null; let interleaveTicker = null;
let schedulerStepPending = false; let schedulerStepPending = false;
let schEntryEditIdx = null; // null = adding, number = editing that index let schEntryEditIdx = null; // null = adding, number = editing that index
let schSatEditIdx = null; // null = adding, number = editing satellite entry // Satellite entry editing state moved to sat-scheduler.js
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Init // Init
@@ -866,225 +866,30 @@
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Satellite overlay // Satellite overlay (delegated to sat-scheduler.js)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
function getSatelliteEntries() {
return (currentConfig && currentConfig.satellites && Array.isArray(currentConfig.satellites.entries))
? currentConfig.satellites.entries
: [];
}
function ensureSatelliteConfig() {
if (!currentConfig) currentConfig = { remote: currentRigId, mode: "disabled", entries: [] };
if (!currentConfig.satellites) currentConfig.satellites = { enabled: false, pretune_secs: 60, entries: [] };
if (!currentConfig.satellites.entries) currentConfig.satellites.entries = [];
return currentConfig.satellites;
}
function collectSatelliteConfig() {
const enabledEl = document.getElementById("scheduler-sat-enabled");
const pretuneEl = document.getElementById("scheduler-sat-pretune");
const enabled = enabledEl ? enabledEl.checked : false;
const pretune = pretuneEl ? parseInt(pretuneEl.value, 10) : 60;
return {
enabled: enabled,
pretune_secs: isNaN(pretune) || pretune < 0 ? 60 : pretune,
entries: getSatelliteEntries(),
};
}
function renderSatelliteSection() { function renderSatelliteSection() {
const satCfg = (currentConfig && currentConfig.satellites) || {}; if (window.satScheduler) window.satScheduler.renderSection();
const enabled = !!satCfg.enabled;
const enabledEl = document.getElementById("scheduler-sat-enabled");
if (enabledEl) enabledEl.checked = enabled;
const pretuneEl = document.getElementById("scheduler-sat-pretune");
if (pretuneEl) pretuneEl.value = satCfg.pretune_secs != null ? satCfg.pretune_secs : 60;
const bodyEl = document.getElementById("scheduler-sat-body");
if (bodyEl) bodyEl.style.display = enabled ? "" : "none";
renderSatelliteEntries();
renderSatPassStatus();
}
function renderSatelliteEntries() {
const tbody = document.getElementById("scheduler-sat-tbody");
if (!tbody) return;
tbody.innerHTML = "";
const entries = getSatelliteEntries();
entries.forEach(function (entry, idx) {
const tr = document.createElement("tr");
tr.innerHTML =
"<td>" + escHtml(entry.satellite || "") + "</td>" +
"<td>" + (entry.norad_id || "") + "</td>" +
"<td>" + escHtml(bmName(entry.bookmark_id)) + "</td>" +
"<td>" + (entry.min_elevation_deg != null ? entry.min_elevation_deg + "\u00B0" : "5\u00B0") + "</td>" +
"<td>" + (entry.priority || 0) + "</td>" +
'<td>' +
'<button class="sch-write sch-sat-edit-btn" data-idx="' + idx + '" type="button">Edit</button>' +
'<button class="sch-write sch-sat-remove-btn" data-idx="' + idx + '" type="button">Remove</button>' +
'</td>';
tbody.appendChild(tr);
});
tbody.querySelectorAll(".sch-sat-edit-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
const i = parseInt(btn.dataset.idx, 10);
const entry = getSatelliteEntries()[i];
if (entry) schOpenSatForm(entry, i);
});
});
tbody.querySelectorAll(".sch-sat-remove-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
removeSatEntry(parseInt(btn.dataset.idx, 10));
});
});
}
function removeSatEntry(idx) {
const sat = ensureSatelliteConfig();
sat.entries.splice(idx, 1);
renderSatelliteEntries();
}
function schOpenSatForm(entry, idx) {
schSatEditIdx = (idx != null) ? idx : null;
const titleEl = document.getElementById("sch-sat-form-title");
if (titleEl) titleEl.textContent = entry ? "Edit Satellite" : "Add Satellite";
const presetEl = document.getElementById("scheduler-sat-preset");
const nameEl = document.getElementById("scheduler-sat-name");
const noradEl = document.getElementById("scheduler-sat-norad");
const bmEl = document.getElementById("scheduler-sat-bookmark");
const minElEl = document.getElementById("scheduler-sat-min-el");
const prioEl = document.getElementById("scheduler-sat-priority");
const centerHzEl = document.getElementById("scheduler-sat-center-hz");
if (presetEl) presetEl.value = "";
if (nameEl) nameEl.value = entry ? (entry.satellite || "") : "";
if (noradEl) noradEl.value = entry ? (entry.norad_id || "") : "";
if (bmEl) bmEl.value = entry ? (entry.bookmark_id || "") : "";
if (minElEl) minElEl.value = entry && entry.min_elevation_deg != null ? entry.min_elevation_deg : 5;
if (prioEl) prioEl.value = entry && entry.priority != null ? entry.priority : 0;
if (centerHzEl) centerHzEl.value = entry && entry.center_hz ? entry.center_hz : "";
// Populate bookmark dropdown
renderBookmarkSelect("scheduler-sat-bookmark", entry ? entry.bookmark_id : null);
const wrap = document.getElementById("sch-sat-form-wrap");
if (wrap) {
wrap.style.display = "flex";
if (nameEl) nameEl.focus();
}
}
function schCloseSatForm() {
const wrap = document.getElementById("sch-sat-form-wrap");
if (wrap) wrap.style.display = "none";
schSatEditIdx = null;
}
function schSatFormSubmit(e) {
e.preventDefault();
const nameEl = document.getElementById("scheduler-sat-name");
const noradEl = document.getElementById("scheduler-sat-norad");
const bmEl = document.getElementById("scheduler-sat-bookmark");
const minElEl = document.getElementById("scheduler-sat-min-el");
const prioEl = document.getElementById("scheduler-sat-priority");
const centerHzEl = document.getElementById("scheduler-sat-center-hz");
const satellite = nameEl ? nameEl.value.trim() : "";
const noradId = noradEl ? parseInt(noradEl.value, 10) : NaN;
const bmId = bmEl ? bmEl.value : "";
if (!satellite) { alert("Please enter a satellite name."); return; }
if (isNaN(noradId) || noradId <= 0) { alert("Please enter a valid NORAD catalog number."); return; }
if (!bmId) { alert("Please select a bookmark."); return; }
const minEl = minElEl ? parseFloat(minElEl.value) : 5;
const prio = prioEl ? parseInt(prioEl.value, 10) : 0;
const centerHzRaw = centerHzEl ? parseInt(centerHzEl.value, 10) : NaN;
const sat = ensureSatelliteConfig();
const entryData = {
satellite: satellite,
norad_id: noradId,
bookmark_id: bmId,
min_elevation_deg: isNaN(minEl) ? 5 : minEl,
priority: isNaN(prio) ? 0 : prio,
center_hz: !isNaN(centerHzRaw) && centerHzRaw > 0 ? centerHzRaw : null,
bookmark_ids: [],
};
if (schSatEditIdx !== null) {
const existing = sat.entries[schSatEditIdx];
entryData.id = existing ? existing.id : ("sat_" + Date.now().toString(36));
sat.entries[schSatEditIdx] = entryData;
} else {
entryData.id = "sat_" + Date.now().toString(36);
sat.entries.push(entryData);
}
schCloseSatForm();
renderSatelliteEntries();
}
function wireSatPresetChange() {
const presetEl = document.getElementById("scheduler-sat-preset");
if (!presetEl || presetEl._wired) return;
presetEl._wired = true;
presetEl.addEventListener("change", function () {
if (!presetEl.value) return;
const parts = presetEl.value.split("|");
const nameEl = document.getElementById("scheduler-sat-name");
const noradEl = document.getElementById("scheduler-sat-norad");
if (nameEl) nameEl.value = parts[0] || "";
if (noradEl) noradEl.value = parts[1] || "";
});
} }
function renderSatPassStatus() { function renderSatPassStatus() {
const el = document.getElementById("scheduler-sat-pass-status"); if (window.satScheduler) window.satScheduler.renderPassStatus();
if (!el) return;
const entries = getSatelliteEntries();
if (entries.length === 0) {
el.innerHTML = "";
return;
}
// Show active satellite from status if available.
if (currentSchedulerStatus && currentSchedulerStatus.active_satellite) {
el.innerHTML =
'<span class="sch-sat-active-badge">PASS ACTIVE: ' +
escHtml(currentSchedulerStatus.active_satellite) +
'</span>';
} else {
el.innerHTML = '<span style="color:var(--text-muted);font-size:0.8rem;">No satellite pass active. Predictions available in the SAT tab.</span>';
} }
function collectSatelliteConfig() {
return window.satScheduler
? window.satScheduler.collectSatelliteConfig()
: { enabled: false, pretune_secs: 60, entries: [] };
} }
function wireSatelliteEvents() { function wireSatelliteEvents() {
const enabledEl = document.getElementById("scheduler-sat-enabled"); // Expose bridge for sat-scheduler.js to access shared state.
if (enabledEl) { window.schedulerBridge = {
enabledEl.addEventListener("change", function () { getConfig: function () { return currentConfig; },
const bodyEl = document.getElementById("scheduler-sat-body"); getStatus: function () { return currentSchedulerStatus; },
if (bodyEl) bodyEl.style.display = enabledEl.checked ? "" : "none"; getBookmarks: function () { return bookmarkList; },
}); };
} if (window.satScheduler) window.satScheduler.wireEvents();
const addBtn = document.getElementById("scheduler-sat-add-btn");
if (addBtn) addBtn.addEventListener("click", function () { schOpenSatForm(null, null); });
const satForm = document.getElementById("sch-sat-form");
if (satForm) satForm.addEventListener("submit", schSatFormSubmit);
const cancelBtn = document.getElementById("sch-sat-form-cancel");
if (cancelBtn) cancelBtn.addEventListener("click", schCloseSatForm);
wireSatPresetChange();
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -1689,6 +1689,7 @@ define_gz_cache!(gz_cw_js, status::CW_JS, "cw.js");
define_gz_cache!(gz_sat_js, status::SAT_JS, "sat.js"); define_gz_cache!(gz_sat_js, status::SAT_JS, "sat.js");
define_gz_cache!(gz_bookmarks_js, status::BOOKMARKS_JS, "bookmarks.js"); define_gz_cache!(gz_bookmarks_js, status::BOOKMARKS_JS, "bookmarks.js");
define_gz_cache!(gz_scheduler_js, status::SCHEDULER_JS, "scheduler.js"); define_gz_cache!(gz_scheduler_js, status::SCHEDULER_JS, "scheduler.js");
define_gz_cache!(gz_sat_scheduler_js, status::SAT_SCHEDULER_JS, "sat-scheduler.js");
define_gz_cache!(gz_background_decode_js, status::BACKGROUND_DECODE_JS, "background-decode.js"); define_gz_cache!(gz_background_decode_js, status::BACKGROUND_DECODE_JS, "background-decode.js");
define_gz_cache!(gz_vchan_js, status::VCHAN_JS, "vchan.js"); define_gz_cache!(gz_vchan_js, status::VCHAN_JS, "vchan.js");
@@ -2249,6 +2250,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(sat_js) .service(sat_js)
.service(bookmarks_js) .service(bookmarks_js)
.service(scheduler_js) .service(scheduler_js)
.service(sat_scheduler_js)
.service(background_decode_js) .service(background_decode_js)
.service(vchan_js) .service(vchan_js)
// Virtual channels // Virtual channels
@@ -2421,6 +2423,12 @@ async fn scheduler_js(req: HttpRequest) -> impl Responder {
static_asset_response(&req, "application/javascript; charset=utf-8", &c.gz, &c.etag) static_asset_response(&req, "application/javascript; charset=utf-8", &c.gz, &c.etag)
} }
#[get("/sat-scheduler.js")]
async fn sat_scheduler_js(req: HttpRequest) -> impl Responder {
let c = gz_sat_scheduler_js();
static_asset_response(&req, "application/javascript; charset=utf-8", &c.gz, &c.etag)
}
#[get("/background-decode.js")] #[get("/background-decode.js")]
async fn background_decode_js(req: HttpRequest) -> impl Responder { async fn background_decode_js(req: HttpRequest) -> impl Responder {
let c = gz_background_decode_js(); let c = gz_background_decode_js();
@@ -27,6 +27,7 @@ pub const CW_JS: &str = include_str!("../assets/web/plugins/cw.js");
pub const SAT_JS: &str = include_str!("../assets/web/plugins/sat.js"); pub const SAT_JS: &str = include_str!("../assets/web/plugins/sat.js");
pub const BOOKMARKS_JS: &str = include_str!("../assets/web/plugins/bookmarks.js"); pub const BOOKMARKS_JS: &str = include_str!("../assets/web/plugins/bookmarks.js");
pub const SCHEDULER_JS: &str = include_str!("../assets/web/plugins/scheduler.js"); pub const SCHEDULER_JS: &str = include_str!("../assets/web/plugins/scheduler.js");
pub const SAT_SCHEDULER_JS: &str = include_str!("../assets/web/plugins/sat-scheduler.js");
pub const BACKGROUND_DECODE_JS: &str = include_str!("../assets/web/plugins/background-decode.js"); pub const BACKGROUND_DECODE_JS: &str = include_str!("../assets/web/plugins/background-decode.js");
pub const VCHAN_JS: &str = include_str!("../assets/web/plugins/vchan.js"); pub const VCHAN_JS: &str = include_str!("../assets/web/plugins/vchan.js");