[feat](trx-frontend-http): add Background Decoding Scheduler

Implements a scheduler that retunes the rig automatically when no SSE
clients are connected.  Two modes are supported:

- Grayline: tunes to per-period bookmarks (dawn/day/dusk/night) based on
  an inline NOAA solar algorithm given station lat/lon.
- Time Span: tunes to bookmarks within user-defined UTC windows; midnight-
  spanning intervals supported.

Backend:
- SchedulerStore (PickleDB, sch:{rig_id} keys) in scheduler.rs
- spawn_scheduler_task polls every 30 s, checks context.sse_clients == 0,
  sends SetFreq + SetMode via RigRequest with rig_id_override
- HTTP API: GET/PUT/DELETE /scheduler/{rig_id}, GET …/status
- sse_clients Arc<AtomicUsize> added to FrontendRuntimeContext and shared
  with the SSE counter in build_server (single source of truth)
- /scheduler/ added to Read auth routes (write requires Control)

Frontend:
- Scheduler tab (clock icon, 6th position) with Grayline/TimeSpan UI
- scheduler.js plugin: loads config + bookmarks, live status polling
  every 15 s, write controls hidden for Rx-role users
- CSS .sch-* component styles added to style.css
- SCHEDULER.md design document at repo root

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-10 23:20:42 +01:00
parent 46c0f8d0bb
commit 6874055b1c
11 changed files with 1393 additions and 5 deletions
@@ -3347,7 +3347,7 @@ if (spectrumBwSweetBtn) {
}
// --- Tab navigation ---
const TAB_ORDER = ["main", "bookmarks", "decoders", "map", "about"];
const TAB_ORDER = ["main", "bookmarks", "decoders", "map", "scheduler", "about"];
function navigateToTab(name) {
if (authEnabled && !authRole && name !== "main") return;
@@ -3415,6 +3415,10 @@ document.querySelector(".tab-bar").addEventListener("click", (e) => {
window.addEventListener("resize", () => { scheduleSpectrumLayout(); });
// --- Auth startup sequence ---
function getAvailableRigIds() {
return lastRigIds || [];
}
async function initializeApp() {
showAuthGate(false);
const authStatus = await checkAuthStatus();
@@ -3426,6 +3430,7 @@ async function initializeApp() {
updateAuthUI();
connect();
connectDecode();
initSchedulerUI();
resizeHeaderSignalCanvas();
startHeaderSignalSampling();
return;
@@ -3439,6 +3444,7 @@ async function initializeApp() {
applyAuthRestrictions();
connect();
connectDecode();
initSchedulerUI();
resizeHeaderSignalCanvas();
startHeaderSignalSampling();
} else {
@@ -3449,6 +3455,13 @@ async function initializeApp() {
}
}
function initSchedulerUI() {
if (typeof initScheduler === "function") {
initScheduler(lastActiveRigId, authRole);
wireSchedulerEvents();
}
}
// Setup auth form
document.getElementById("auth-form").addEventListener("submit", async (e) => {
e.preventDefault();
@@ -3466,6 +3479,7 @@ document.getElementById("auth-form").addEventListener("submit", async (e) => {
applyAuthRestrictions();
connect();
connectDecode();
initSchedulerUI();
resizeHeaderSignalCanvas();
startHeaderSignalSampling();
} catch (err) {
@@ -3488,6 +3502,7 @@ if (guestBtn) {
applyAuthRestrictions();
connect();
connectDecode();
initSchedulerUI();
resizeHeaderSignalCanvas();
startHeaderSignalSampling();
});