use std::sync::Arc; use anyhow::Result; use tokio::time::{interval, Duration}; use tracing::{info, error, warn}; use serde::Deserialize; use crate::command::CommandDispatcher; use crate::server::ServerManager; /// A scheduled task received from the panel API. #[derive(Debug, Clone, Deserialize)] pub struct ScheduledTask { pub id: String, pub server_uuid: String, pub action: String, // "command", "power", "backup" pub payload: String, // command string, power action, or "backup" pub schedule_type: String, pub is_active: bool, pub next_run_at: Option, // ISO 8601 } /// Scheduler that polls the panel API for due tasks and executes them. pub struct Scheduler { server_manager: Arc, command_dispatcher: Arc, api_url: String, node_token: String, poll_interval_secs: u64, } impl Scheduler { pub fn new( server_manager: Arc, command_dispatcher: Arc, api_url: String, node_token: String, ) -> Self { Self { server_manager, command_dispatcher, api_url, node_token, poll_interval_secs: 15, } } /// Run the scheduler loop. This should be spawned as a tokio task. pub async fn run(self: Arc) { info!("Scheduler started (poll interval: {}s)", self.poll_interval_secs); let mut tick = interval(Duration::from_secs(self.poll_interval_secs)); loop { tick.tick().await; if let Err(e) = self.poll_and_execute().await { error!(error = %e, "Scheduler poll failed"); } } } /// Poll the API for due tasks and execute them. async fn poll_and_execute(&self) -> Result<()> { let client = reqwest::Client::new(); let url = format!("{}/api/internal/schedules/due", self.api_url); let resp = client .get(&url) .bearer_auth(&self.node_token) .send() .await?; if !resp.status().is_success() { warn!(status = %resp.status(), "Failed to fetch due tasks"); return Ok(()); } #[derive(Deserialize)] struct DueResponse { tasks: Vec, } let due: DueResponse = resp.json().await?; if due.tasks.is_empty() { return Ok(()); } info!(count = due.tasks.len(), "Processing due scheduled tasks"); for task in &due.tasks { if let Err(e) = self.execute_task(task).await { error!( task_id = %task.id, server = %task.server_uuid, error = %e, "Failed to execute scheduled task" ); } // Notify API that task was executed let ack_url = format!( "{}/api/internal/schedules/{}/ack", self.api_url, task.id ); let _ = client .post(&ack_url) .bearer_auth(&self.node_token) .send() .await; } Ok(()) } /// Execute a single scheduled task. async fn execute_task(&self, task: &ScheduledTask) -> Result<()> { info!( task_id = %task.id, action = %task.action, server = %task.server_uuid, "Executing scheduled task" ); match task.action.as_str() { "command" => { self.command_dispatcher .send_command(&task.server_uuid, &task.payload) .await?; } "power" => { match task.payload.as_str() { "start" => self.server_manager.start_server(&task.server_uuid).await?, "stop" => self.server_manager.stop_server(&task.server_uuid, None, 0).await?, "restart" => { let _ = self.server_manager.stop_server(&task.server_uuid, None, 0).await; tokio::time::sleep(Duration::from_secs(3)).await; self.server_manager.start_server(&task.server_uuid).await?; } "kill" => self.server_manager.kill_server(&task.server_uuid).await?, _ => warn!(payload = %task.payload, "Unknown power action"), } } "backup" => { // Trigger backup via the backup module info!( server = %task.server_uuid, "Backup scheduled task — delegating to backup module" ); // Backup is handled by sending callback to API let client = reqwest::Client::new(); let url = format!( "{}/api/internal/servers/{}/backup", self.api_url, task.server_uuid ); let _ = client .post(&url) .bearer_auth(&self.node_token) .json(&serde_json::json!({ "name": format!("auto-{}", task.id) })) .send() .await; } _ => { warn!(action = %task.action, "Unknown scheduled action"); } } Ok(()) } }