[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
@@ -3330,3 +3330,130 @@ button:focus-visible, input:focus-visible, select:focus-visible {
--wavelength-fg: #3a7a3a;
--spectrum-bg: #e0f0e0;
}
/* =========================================================================
Scheduler tab
========================================================================= */
.sch-panel {
padding: 1rem;
max-width: 900px;
}
.sch-toast {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%);
padding: 0.5rem 1.25rem;
border-radius: 0.4rem;
color: #fff;
font-weight: 600;
font-size: 0.9rem;
z-index: 999;
pointer-events: none;
}
.sch-row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1rem;
align-items: flex-end;
}
.sch-label {
display: flex;
flex-direction: column;
gap: 0.3rem;
font-size: 0.85rem;
color: var(--text-muted);
font-weight: 600;
min-width: 9rem;
}
.sch-rig-select {
min-width: 10rem;
}
.sch-section {
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--bg-secondary);
border-radius: 0.5rem;
border: 1px solid var(--border-light);
}
.sch-section-title {
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin-bottom: 0.75rem;
}
.sch-ts-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
margin-bottom: 1rem;
}
.sch-ts-table th,
.sch-ts-table td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid var(--border-light);
}
.sch-ts-table th {
color: var(--text-muted);
font-weight: 600;
}
.sch-remove-btn {
padding: 0.2rem 0.6rem;
background: var(--btn-bg);
color: var(--text);
border: 1px solid var(--border-light);
border-radius: 0.3rem;
cursor: pointer;
font-size: 0.8rem;
}
.sch-remove-btn:hover {
background: var(--btn-bg-hover, var(--btn-bg));
}
.sch-add-row {
align-items: flex-end;
}
.sch-actions {
display: flex;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.sch-save-btn {
background: var(--accent-green);
color: #fff;
border: none;
border-radius: 0.4rem;
padding: 0.5rem 1.25rem;
font-weight: 700;
cursor: pointer;
font-size: 0.95rem;
}
.sch-save-btn:disabled {
opacity: 0.5;
cursor: default;
}
.sch-reset-btn {
background: var(--btn-bg);
color: var(--text);
border: 1px solid var(--border-light);
border-radius: 0.4rem;
padding: 0.5rem 1.25rem;
font-weight: 600;
cursor: pointer;
font-size: 0.95rem;
}
.sch-status-card {
font-size: 0.9rem;
color: var(--text-muted);
}
@media (max-width: 600px) {
.sch-row {
flex-direction: column;
}
.sch-label {
min-width: 100%;
}
}