From 5c28ed1269a412bb9864dafb3db020e1d539563e Mon Sep 17 00:00:00 2001 From: Stan Grams Date: Fri, 13 Mar 2026 00:22:18 +0100 Subject: [PATCH] [feat](trx-frontend-http): add scheduler control handoff Co-authored-by: OpenAI Codex Signed-off-by: Stan Grams --- .../trx-frontend-http/assets/web/index.html | 8 +- .../assets/web/plugins/background-decode.js | 1 + .../assets/web/plugins/vchan.js | 107 +++++++++++ .../trx-frontend-http/assets/web/style.css | 34 ++++ .../trx-frontend/trx-frontend-http/src/api.rs | 6 + .../trx-frontend-http/src/auth.rs | 1 + .../src/background_decode.rs | 18 +- .../trx-frontend-http/src/scheduler.rs | 170 ++++++++++++++++-- .../trx-frontend-http/src/server.rs | 9 +- 9 files changed, 338 insertions(+), 16 deletions(-) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index 30fc3b9..173263b 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -261,7 +261,13 @@
Signal
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/background-decode.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/background-decode.js index 1d1872a..80b1d98 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/background-decode.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/background-decode.js @@ -308,6 +308,7 @@ case "no_supported_decoders": return "Unsupported"; case "disabled": return "Disabled"; case "handled_by_scheduler": return "Scheduler"; + case "scheduler_has_control": return "Scheduler"; case "handled_by_virtual_channel": return "VChan"; default: return "Inactive"; } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vchan.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vchan.js index 3e64708..7fe2538 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vchan.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vchan.js @@ -11,6 +11,8 @@ let vchanSessionId = null; let vchanRigId = null; let vchanChannels = []; let vchanActiveId = null; +let schedulerReleaseState = null; +let schedulerReleasePollTimer = null; function vchanFmtFreq(hz) { if (!Number.isFinite(hz) || hz <= 0) return "--"; @@ -20,11 +22,101 @@ function vchanFmtFreq(hz) { return hz + "\u202fHz"; } +function schedulerReleaseSummaryText(state) { + if (!state) return "Scheduler is controlling the rig."; + const connected = Number(state.connected_sessions) || 0; + const released = Number(state.released_sessions) || 0; + if (connected === 0) return "Scheduler can control the rig."; + if (state.all_released) { + return connected === 1 + ? "Scheduler is controlling the rig." + : `Scheduler is controlling the rig for all ${connected} users.`; + } + if (!state.current_session_released) { + const othersReleased = Math.max(released, 0); + return othersReleased > 0 + ? `You are holding control. ${othersReleased} other user${othersReleased === 1 ? "" : "s"} already released it.` + : "You are holding control. Release it to return control to the scheduler."; + } + const blocking = Math.max(connected - released, 0); + return blocking > 0 + ? `Scheduler is waiting for ${blocking} user${blocking === 1 ? "" : "s"} to stop manual tuning.` + : "Scheduler can control the rig."; +} + +function vchanRenderSchedulerRelease() { + const btn = document.getElementById("scheduler-release-btn"); + const status = document.getElementById("scheduler-release-status"); + if (!btn || !status) return; + const currentReleased = !!(schedulerReleaseState && schedulerReleaseState.current_session_released); + btn.disabled = !vchanSessionId || currentReleased; + btn.classList.toggle("active", !currentReleased); + btn.textContent = "Release to Scheduler"; + status.textContent = schedulerReleaseSummaryText(schedulerReleaseState); +} + +async function vchanPollSchedulerRelease() { + if (!vchanSessionId) { + schedulerReleaseState = null; + vchanRenderSchedulerRelease(); + return; + } + try { + const resp = await fetch(`/scheduler-control?session_id=${encodeURIComponent(vchanSessionId)}`); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + schedulerReleaseState = await resp.json(); + vchanRenderSchedulerRelease(); + } catch (e) { + console.error("scheduler release status failed", e); + } +} + +function vchanStartSchedulerReleasePolling() { + if (schedulerReleasePollTimer) { + clearInterval(schedulerReleasePollTimer); + } + schedulerReleasePollTimer = setInterval(vchanPollSchedulerRelease, 10000); +} + +async function vchanToggleSchedulerRelease() { + if (!vchanSessionId) return; + try { + const resp = await fetch("/scheduler-control", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ session_id: vchanSessionId, released: true }), + }); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + schedulerReleaseState = await resp.json(); + vchanRenderSchedulerRelease(); + } catch (e) { + console.error("scheduler release toggle failed", e); + } +} + +async function vchanTakeSchedulerControl() { + if (!vchanSessionId) return; + if (schedulerReleaseState && !schedulerReleaseState.current_session_released) return; + try { + const resp = await fetch("/scheduler-control", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ session_id: vchanSessionId, released: false }), + }); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + schedulerReleaseState = await resp.json(); + vchanRenderSchedulerRelease(); + } catch (e) { + console.error("scheduler control takeover failed", e); + } +} + // Called by app.js when the SSE `session` event arrives. function vchanHandleSession(data) { try { const d = JSON.parse(data); vchanSessionId = d.session_id || null; + vchanPollSchedulerRelease(); } catch (e) { console.warn("vchan: bad session event", e); } @@ -43,6 +135,7 @@ function vchanHandleChannels(data) { vchanReconnectAudio(); } vchanRender(); + vchanRenderSchedulerRelease(); if (typeof renderRdsOverlays === "function") renderRdsOverlays(); } catch (e) { console.warn("vchan: bad channels event", e); @@ -94,6 +187,7 @@ function vchanRender() { picker.appendChild(addBtn); vchanSyncAccentUI(); + vchanRenderSchedulerRelease(); } async function vchanAllocate() { @@ -196,6 +290,7 @@ function vchanApplyCapabilities(caps) { const row = document.getElementById("vchan-row"); if (!row) return; row.style.display = (caps && caps.filter_controls) ? "" : "none"; + vchanRenderSchedulerRelease(); } // --------------------------------------------------------------------------- @@ -391,10 +486,22 @@ window.vchanInterceptBandwidth = async function(bwHz) { await vchanSetChannelFreq(freqHz); return; } + await vchanTakeSchedulerControl(); if (typeof _orig === "function") return _orig(freqHz); }; })(); +(function initSchedulerReleaseControl() { + const btn = document.getElementById("scheduler-release-btn"); + if (btn) { + btn.addEventListener("click", () => { + vchanToggleSchedulerRelease(); + }); + } + vchanStartSchedulerReleasePolling(); + vchanRenderSchedulerRelease(); +})(); + // Wrap refreshFreqDisplay so the main freq field stays in sync with the // active virtual channel's frequency (SSE rig-state updates would otherwise // constantly overwrite it with channel 0's freq). diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 84daa81..f65af9a 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -371,6 +371,28 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem; flex-wrap: wrap; gap: 4px; } +.vchan-row-controls { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.8rem; +} +.scheduler-release-wrap { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.35rem; + min-width: 14rem; +} +.scheduler-release-wrap button.active { + border-color: var(--accent-yellow); + color: var(--accent-yellow); +} +.scheduler-release-status { + color: var(--text-muted); + font-size: 0.78rem; + text-align: right; +} .vchan-picker button { display: inline-flex; align-items: center; @@ -3582,6 +3604,7 @@ button:focus-visible, input:focus-visible, select:focus-visible { .bgd-status-state[data-state="out_of_span"], .bgd-status-state[data-state="waiting_for_spectrum"], .bgd-status-state[data-state="waiting_for_user"], +.bgd-status-state[data-state="scheduler_has_control"], .bgd-status-state[data-state="inactive"], .bgd-status-state[data-state="handled_by_scheduler"], .bgd-status-state[data-state="handled_by_virtual_channel"] { @@ -3592,6 +3615,17 @@ button:focus-visible, input:focus-visible, select:focus-visible { color: var(--accent-red); } @media (max-width: 600px) { + .vchan-row-controls { + flex-direction: column; + align-items: stretch; + } + .scheduler-release-wrap { + align-items: stretch; + min-width: 0; + } + .scheduler-release-status { + text-align: left; + } .sch-row { flex-direction: column; } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs index 5dfce3b..ffa9902 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api.rs @@ -223,6 +223,7 @@ pub async fn events( clients: web::Data>, context: web::Data>, vchan_mgr: web::Data>, + scheduler_control: web::Data, ) -> Result { let rx = state.get_ref().clone(); let initial = wait_for_view(rx.clone()).await?; @@ -232,6 +233,7 @@ pub async fn events( // Assign a stable UUID to this SSE session for channel binding. let session_id = Uuid::new_v4(); + scheduler_control.register_session(session_id); // Seed the primary channel for the currently-selected rig (no-op if // already initialised or if no rig is selected yet). @@ -336,11 +338,13 @@ pub async fn events( let vchan_drop = vchan_mgr.get_ref().clone(); let counter_drop = counter.clone(); + let scheduler_control_drop = scheduler_control.get_ref().clone(); let live = select(select(pings, updates), chan_updates); let stream = prefix_stream.chain(live); let stream = DropStream::new(Box::pin(stream), move || { counter_drop.fetch_sub(1, Ordering::Relaxed); vchan_drop.release_session(session_id); + scheduler_control_drop.unregister_session(session_id); }); Ok(HttpResponse::Ok() @@ -1318,6 +1322,8 @@ pub fn configure(cfg: &mut web::ServiceConfig) { .service(crate::server::scheduler::put_scheduler) .service(crate::server::scheduler::delete_scheduler) .service(crate::server::scheduler::get_scheduler_status) + .service(crate::server::scheduler::get_scheduler_control) + .service(crate::server::scheduler::put_scheduler_control) .service(crate::server::background_decode::get_background_decode) .service(crate::server::background_decode::put_background_decode) .service(crate::server::background_decode::delete_background_decode) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs index bf79a23..f7012df 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/auth.rs @@ -437,6 +437,7 @@ impl RouteAccess { || path.starts_with("/bookmarks?") || path.starts_with("/bookmarks/") || path.starts_with("/scheduler/") + || path.starts_with("/scheduler-control") || path.starts_with("/channels/") { return Self::Read; diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/background_decode.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/background_decode.rs index 6e9f5e1..c13ed10 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/background_decode.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/background_decode.rs @@ -18,7 +18,7 @@ use trx_frontend::{FrontendRuntimeContext, SharedSpectrum, VChanAudioCmd}; use uuid::Uuid; use crate::server::bookmarks::{Bookmark, BookmarkStore}; -use crate::server::scheduler::SchedulerStatusMap; +use crate::server::scheduler::{SchedulerStatusMap, SharedSchedulerControlManager}; use crate::server::vchan::{ClientChannel, ClientChannelManager}; const SUPPORTED_DECODER_KINDS: &[&str] = &["aprs", "ais", "ft8", "wspr", "hf-aprs"]; @@ -124,6 +124,7 @@ pub struct BackgroundDecodeManager { bookmarks: Arc, context: Arc, scheduler_status: SchedulerStatusMap, + scheduler_control: SharedSchedulerControlManager, vchan_mgr: Arc, status: Arc>>, notify_tx: broadcast::Sender<()>, @@ -135,6 +136,7 @@ impl BackgroundDecodeManager { bookmarks: Arc, context: Arc, scheduler_status: SchedulerStatusMap, + scheduler_control: SharedSchedulerControlManager, vchan_mgr: Arc, ) -> Arc { let (notify_tx, _) = broadcast::channel(16); @@ -143,6 +145,7 @@ impl BackgroundDecodeManager { bookmarks, context, scheduler_status, + scheduler_control, vchan_mgr, status: Arc::new(RwLock::new(HashMap::new())), notify_tx, @@ -314,10 +317,11 @@ impl BackgroundDecodeManager { let config = self.get_config(&rig_id); let selected = dedup_ids(&config.bookmark_ids); let users_connected = self.context.sse_clients.load(Ordering::Relaxed) > 0; - let scheduled_bookmark_ids = if users_connected { - Vec::new() - } else { + let scheduler_has_control = self.scheduler_control.scheduler_allowed() && users_connected; + let scheduled_bookmark_ids = if scheduler_has_control || !users_connected { self.scheduler_bookmark_ids(&rig_id) + } else { + Vec::new() }; let selected_bookmarks: HashMap = self .bookmarks @@ -373,6 +377,12 @@ impl BackgroundDecodeManager { continue; } + if scheduler_has_control { + status.state = "scheduler_has_control".to_string(); + statuses.push(status); + continue; + } + if scheduled_bookmark_ids.iter().any(|id| id == &bookmark.id) { status.state = "handled_by_scheduler".to_string(); statuses.push(status); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/scheduler.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/scheduler.rs index af36416..3c4a077 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/scheduler.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/scheduler.rs @@ -4,9 +4,10 @@ //! Background Decoding Scheduler. //! -//! When no SSE clients are connected the scheduler periodically inspects the -//! current UTC time, selects the matching bookmark from the per-rig config, -//! and issues `SetFreq` + `SetMode` commands to retune the rig automatically. +//! When no SSE clients are connected, or when every connected user explicitly +//! releases control, the scheduler periodically inspects the current UTC time, +//! selects the matching bookmark from the per-rig config, and issues rig +//! commands to retune and activate the scheduled decoder set automatically. use std::collections::HashMap; use std::path::{Path, PathBuf}; @@ -19,6 +20,7 @@ use serde::{Deserialize, Serialize}; use tokio::sync::{mpsc, oneshot}; use tokio::time; use tracing::{info, warn}; +use uuid::Uuid; use trx_core::radio::freq::Freq; use trx_core::rig::command::RigCommand; @@ -370,27 +372,94 @@ pub struct SchedulerStatus { /// Shared mutable state for scheduler status (one entry per rig). pub type SchedulerStatusMap = Arc>>; +#[derive(Debug, Clone, Serialize, Default)] +pub struct SchedulerControlSummary { + pub connected_sessions: usize, + pub released_sessions: usize, + pub all_released: bool, + pub current_session_released: bool, +} + +#[derive(Default)] +pub struct SchedulerControlManager { + sessions: RwLock>, +} + +impl SchedulerControlManager { + pub fn register_session(&self, session_id: Uuid) { + if let Ok(mut sessions) = self.sessions.write() { + sessions.insert(session_id, true); + } + } + + pub fn unregister_session(&self, session_id: Uuid) { + if let Ok(mut sessions) = self.sessions.write() { + sessions.remove(&session_id); + } + } + + pub fn set_released(&self, session_id: Uuid, released: bool) -> SchedulerControlSummary { + if let Ok(mut sessions) = self.sessions.write() { + sessions.insert(session_id, released); + } + self.summary(Some(session_id)) + } + + pub fn summary(&self, session_id: Option) -> SchedulerControlSummary { + let Ok(sessions) = self.sessions.read() else { + return SchedulerControlSummary::default(); + }; + let connected_sessions = sessions.len(); + let released_sessions = sessions.values().filter(|released| **released).count(); + let all_released = connected_sessions > 0 && released_sessions == connected_sessions; + let current_session_released = session_id + .and_then(|id| sessions.get(&id).copied()) + .unwrap_or(false); + SchedulerControlSummary { + connected_sessions, + released_sessions, + all_released, + current_session_released, + } + } + + pub fn scheduler_allowed(&self) -> bool { + let summary = self.summary(None); + summary.connected_sessions == 0 || summary.all_released + } + + pub fn has_active_user_control(&self) -> bool { + let summary = self.summary(None); + summary.connected_sessions > 0 && !summary.all_released + } +} + +pub type SharedSchedulerControlManager = Arc; + pub fn spawn_scheduler_task( - context: Arc, + _context: Arc, rig_tx: mpsc::Sender, store: Arc, bookmarks: Arc, status_map: SchedulerStatusMap, + control: SharedSchedulerControlManager, ) { tokio::spawn(async move { let mut interval = time::interval(Duration::from_secs(30)); // Track last applied bookmark per rig to avoid redundant retunes. let mut last_applied: HashMap = HashMap::new(); + let mut last_control_allowed = false; loop { interval.tick().await; - // Skip if any user is currently connected. - if context - .sse_clients - .load(std::sync::atomic::Ordering::Relaxed) - > 0 - { + // Skip while at least one connected user is still holding control. + let scheduler_allowed = control.scheduler_allowed(); + if scheduler_allowed && !last_control_allowed { + last_applied.clear(); + } + last_control_allowed = scheduler_allowed; + if !scheduler_allowed { continue; } @@ -434,6 +503,10 @@ pub fn spawn_scheduler_task( let extra_bm_ids: Vec = active_entry .map(|e| e.bookmark_ids.clone()) .unwrap_or_default(); + let extra_bookmarks: Vec<_> = extra_bm_ids + .iter() + .filter_map(|id| bookmarks.get(id)) + .collect(); info!( "scheduler: rig '{}' → bookmark '{}' ({} Hz {})", @@ -485,6 +558,14 @@ pub fn spawn_scheduler_task( } } + apply_scheduler_decoders( + &rig_tx, + &config.rig_id, + &bm, + &extra_bookmarks, + ) + .await; + last_applied.insert(config.rig_id.clone(), bm_id.clone()); // Update status map (includes center_hz + extra bookmark_ids @@ -512,6 +593,48 @@ pub fn spawn_scheduler_task( }); } +async fn apply_scheduler_decoders( + rig_tx: &mpsc::Sender, + rig_id: &str, + bookmark: &crate::server::bookmarks::Bookmark, + extra_bookmarks: &[crate::server::bookmarks::Bookmark], +) { + let mut want_aprs = bookmark.mode.trim().eq_ignore_ascii_case("PKT"); + let mut want_hf_aprs = false; + let mut want_ft8 = false; + let mut want_wspr = false; + + let mut update_from = |bm: &crate::server::bookmarks::Bookmark| { + for decoder in bm.decoders.iter().map(|item| item.trim().to_ascii_lowercase()) { + match decoder.as_str() { + "aprs" => want_aprs = true, + "hf-aprs" => want_hf_aprs = true, + "ft8" => want_ft8 = true, + "wspr" => want_wspr = true, + _ => {} + } + } + }; + + update_from(bookmark); + for bm in extra_bookmarks { + update_from(bm); + } + + let desired = [ + ("APRS", RigCommand::SetAprsDecodeEnabled(want_aprs)), + ("HF APRS", RigCommand::SetHfAprsDecodeEnabled(want_hf_aprs)), + ("FT8", RigCommand::SetFt8DecodeEnabled(want_ft8)), + ("WSPR", RigCommand::SetWsprDecodeEnabled(want_wspr)), + ]; + + for (label, cmd) in desired { + if let Err(e) = scheduler_send(rig_tx, cmd, rig_id.to_string()).await { + warn!("scheduler: Set{label}DecodeEnabled failed for '{}': {:?}", rig_id, e); + } + } +} + /// Send a single RigCommand from the scheduler context (fire-and-forget style). async fn scheduler_send( rig_tx: &mpsc::Sender, @@ -592,3 +715,30 @@ pub async fn get_scheduler_status( let status = map.get(&rig_id).cloned().unwrap_or_default(); HttpResponse::Ok().json(status) } + +#[derive(Deserialize)] +pub struct SchedulerControlQuery { + pub session_id: Option, +} + +#[derive(Deserialize)] +pub struct SchedulerControlUpdate { + pub session_id: Uuid, + pub released: bool, +} + +#[get("/scheduler-control")] +pub async fn get_scheduler_control( + query: web::Query, + control: web::Data, +) -> impl Responder { + HttpResponse::Ok().json(control.summary(query.session_id)) +} + +#[put("/scheduler-control")] +pub async fn put_scheduler_control( + body: web::Json, + control: web::Data, +) -> impl Responder { + HttpResponse::Ok().json(control.set_released(body.session_id, body.released)) +} diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/server.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/server.rs index 97b3c37..222af9c 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/server.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/server.rs @@ -40,7 +40,7 @@ use trx_frontend::{FrontendRuntimeContext, FrontendSpawner}; use auth::{AuthConfig, AuthState, SameSite}; use background_decode::{BackgroundDecodeManager, BackgroundDecodeStore}; -use scheduler::{SchedulerStatusMap, SchedulerStore}; +use scheduler::{SchedulerControlManager, SchedulerStatusMap, SchedulerStore}; use vchan::ClientChannelManager; /// HTTP frontend implementation. @@ -76,6 +76,7 @@ async fn serve( let bookmark_path = bookmarks::BookmarkStore::default_path(); let bookmark_store = Arc::new(bookmarks::BookmarkStore::open(&bookmark_path)); let scheduler_status: SchedulerStatusMap = Arc::new(RwLock::new(HashMap::new())); + let scheduler_control = Arc::new(SchedulerControlManager::default()); scheduler::spawn_scheduler_task( context.clone(), @@ -83,6 +84,7 @@ async fn serve( scheduler_store.clone(), bookmark_store.clone(), scheduler_status.clone(), + scheduler_control.clone(), ); let background_decode_path = BackgroundDecodeStore::default_path(); @@ -94,6 +96,7 @@ async fn serve( bookmark_store.clone(), context.clone(), scheduler_status.clone(), + scheduler_control.clone(), vchan_mgr.clone(), ); background_decode_mgr.spawn(); @@ -133,6 +136,7 @@ async fn serve( bookmark_store, scheduler_store, scheduler_status, + scheduler_control, vchan_mgr, background_decode_mgr, )?; @@ -157,6 +161,7 @@ fn build_server( bookmark_store: Arc, scheduler_store: Arc, scheduler_status: SchedulerStatusMap, + scheduler_control: Arc, vchan_mgr: Arc, background_decode_mgr: Arc, ) -> Result { @@ -170,6 +175,7 @@ fn build_server( let scheduler_store = web::Data::new(scheduler_store); let scheduler_status = web::Data::new(scheduler_status); + let scheduler_control = web::Data::new(scheduler_control); let vchan_mgr = web::Data::new(vchan_mgr); let background_decode_mgr = web::Data::new(background_decode_mgr); @@ -214,6 +220,7 @@ fn build_server( .app_data(bookmark_store.clone()) .app_data(scheduler_store.clone()) .app_data(scheduler_status.clone()) + .app_data(scheduler_control.clone()) .app_data(vchan_mgr.clone()) .app_data(background_decode_mgr.clone()) .wrap(Compress::default())