rhailib/research/rhai_engine_ui/src/main.rs

185 lines
6.3 KiB
Rust

// 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<Pool>,
Path(worker_name): Path<String>,
) -> Result<Json<WorkerDataResponse>, (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<String> = 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<Pool>,
Path(worker_name): Path<String>,
) -> Result<Json<QueueStats>, (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<Pool>,
Path(hash): Path<String>,
) -> Result<Json<TaskDetails>, (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<QueueStats, (StatusCode, String)> {
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<E: std::error::Error>(err: E) -> (StatusCode, String) {
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string())
}
fn task_summary_from_redis_value(v: &Value) -> redis::RedisResult<TaskSummary> {
let map: HashMap<String, String> = 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<TaskDetails> {
let map: HashMap<String, String> = 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::<app::App>::new().render();
}