// The 'app' module is shared between the server and the client. mod app; // --- SERVER-SIDE CODE --- // #[cfg(feature = "server")] mod server { use axum::{ extract::{Path, State}, http::{Method, StatusCode}, routing::get, Json, Router, }; use deadpool_redis::{Config, Pool, Runtime}; use redis::{from_redis_value, AsyncCommands, FromRedisValue, Value}; use std::collections::HashMap; use std::env; use std::net::SocketAddr; use tower_http::cors::{Any, CorsLayer}; use tower_http::services::ServeDir; // Import the shared application state and data structures use crate::app::{QueueStats, TaskDetails, TaskSummary, WorkerDataResponse}; const REDIS_TASK_DETAILS_PREFIX: &str = "rhai_task_details:"; const REDIS_QUEUE_PREFIX: &str = "rhai_tasks:"; // The main function to run the server pub async fn run() { let redis_url = env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1/".to_string()); let cfg = Config::from_url(redis_url); let pool = cfg .create_pool(Some(Runtime::Tokio1)) .expect("Failed to create Redis pool"); let cors = CorsLayer::new() .allow_methods([Method::GET]) .allow_origin(Any); let app = Router::new() .route( "/api/worker/:worker_name/tasks_and_stats", get(get_worker_data), ) .route("/api/worker/:worker_name/queue_stats", get(get_queue_stats)) .route("/api/task/:hash", get(get_task_details)) .nest_service("/", ServeDir::new("dist")) .with_state(pool) .layer(cors); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); println!("Backend server listening on http://{}", addr); println!("Serving static files from './dist' directory."); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, app).await.unwrap(); } // --- API Handlers (Live Redis Data) --- async fn get_worker_data( State(pool): State, Path(worker_name): Path, ) -> Result, (StatusCode, String)> { let mut conn = pool.get().await.map_err(internal_error)?; let queue_key = format!("{}{}", REDIS_QUEUE_PREFIX, worker_name); let task_ids: Vec = conn .lrange(&queue_key, 0, -1) .await .map_err(internal_error)?; let mut tasks = Vec::new(); for task_id in task_ids { let task_key = format!("{}{}", REDIS_TASK_DETAILS_PREFIX, task_id); let task_details: redis::Value = conn.hgetall(&task_key).await.map_err(internal_error)?; if let Ok(summary) = task_summary_from_redis_value(&task_details) { tasks.push(summary); } } let queue_stats = get_queue_stats_internal(&mut conn, &worker_name).await?; Ok(Json(WorkerDataResponse { tasks, queue_stats: Some(queue_stats), })) } async fn get_queue_stats( State(pool): State, Path(worker_name): Path, ) -> Result, (StatusCode, String)> { let mut conn = pool.get().await.map_err(internal_error)?; let stats = get_queue_stats_internal(&mut conn, &worker_name).await?; Ok(Json(stats)) } async fn get_task_details( State(pool): State, Path(hash): Path, ) -> Result, (StatusCode, String)> { let mut conn = pool.get().await.map_err(internal_error)?; let task_key = format!("{}{}", REDIS_TASK_DETAILS_PREFIX, hash); let task_details: redis::Value = conn.hgetall(&task_key).await.map_err(internal_error)?; let details = task_details_from_redis_value(&task_details).map_err(internal_error)?; Ok(Json(details)) } // --- Internal Helper Functions --- async fn get_queue_stats_internal( conn: &mut deadpool_redis::Connection, worker_name: &str, ) -> Result { let queue_key = format!("{}{}", REDIS_QUEUE_PREFIX, worker_name); let size: u32 = conn.llen(&queue_key).await.map_err(internal_error)?; let color_code = match size { 0..=10 => "green", 11..=50 => "yellow", _ => "red", } .to_string(); Ok(QueueStats { current_size: size, color_code, }) } fn internal_error(err: E) -> (StatusCode, String) { (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) } fn task_summary_from_redis_value(v: &Value) -> redis::RedisResult { let map: HashMap = from_redis_value(v)?; Ok(TaskSummary { hash: map.get("hash").cloned().unwrap_or_default(), created_at: map .get("createdAt") .and_then(|s| s.parse().ok()) .unwrap_or_default(), status: map .get("status") .cloned() .unwrap_or_else(|| "Unknown".to_string()), }) } fn task_details_from_redis_value(v: &Value) -> redis::RedisResult { let map: HashMap = from_redis_value(v)?; Ok(TaskDetails { hash: map.get("hash").cloned().unwrap_or_default(), created_at: map .get("createdAt") .and_then(|s| s.parse().ok()) .unwrap_or_default(), status: map .get("status") .cloned() .unwrap_or_else(|| "Unknown".to_string()), script_content: map.get("script").cloned().unwrap_or_default(), result: map.get("output").cloned(), error: map.get("error").cloned(), }) } } // --- MAIN ENTRY POINTS --- // // Main function for the server binary #[cfg(feature = "server")] #[tokio::main] async fn main() { server::run().await; } // Main function for the WASM client (compiles when 'server' feature is not enabled) #[cfg(not(feature = "server"))] fn main() { wasm_logger::init(wasm_logger::Config::default()); log::info!("Rhai Worker UI starting..."); yew::Renderer::::new().render(); }