pub use magic_dashboard_macros::Dashboard;
pub use serde_json;
use axum::{
body::Bytes,
response::{Html, IntoResponse},
routing::get,
Router,
};
use std::net::SocketAddr;
use std::sync::{Arc, RwLock};
use tokio::net::TcpListener;
pub trait DashboardState: Send + Sync {
fn to_json(&self) -> String;
fn metadata(&self) -> String {
"{}".to_string()
}
fn update_field(&mut self, _field: &str, _value: &str) -> Result<(), String> {
Err("Update not supported".to_string())
}
}
#[derive(Clone)]
pub struct DashboardRunner<T: DashboardState + 'static> {
state: Arc<RwLock<T>>,
}
impl<T: DashboardState + 'static> DashboardRunner<T> {
pub fn new(initial_state: T) -> Self {
Self {
state: Arc::new(RwLock::new(initial_state)),
}
}
pub fn update<F>(&self, f: F)
where
F: FnOnce(&mut T),
{
if let Ok(mut state) = self.state.write() {
f(&mut state);
}
}
pub fn read<F, R>(&self, f: F) -> R
where
F: FnOnce(&T) -> R,
{
let state = self.state.read().unwrap();
f(&state)
}
pub fn start_server(&self, port: u16) {
let state_clone = self.state.clone();
std::thread::spawn(move || {
if let Ok(rt) = tokio::runtime::Runtime::new() {
rt.block_on(async move {
let app = Router::new()
.route("/", get(index_handler))
.route(
"/api/metadata",
get({
let state = state_clone.clone();
move || metadata_handler(state)
}),
)
.route(
"/api/state",
get({
let state = state_clone.clone();
move || state_handler(state)
})
.post({
let state = state_clone.clone();
move |body| update_state_handler(state, body)
}),
);
let addr = SocketAddr::from(([127, 0, 0, 1], port));
if let Ok(listener) = TcpListener::bind(addr).await {
println!("Magic Dashboard running at http://127.0.0.1:{}", port);
let _ = axum::serve(listener, app).await;
} else {
eprintln!("Failed to start Magic Dashboard on port {}", port);
}
});
} else {
eprintln!("Failed to create Tokio runtime for Magic Dashboard");
}
});
}
}
async fn index_handler() -> Html<&'static str> {
Html(include_str!("index.html"))
}
async fn metadata_handler<T: DashboardState>(state: Arc<RwLock<T>>) -> impl IntoResponse {
if let Ok(guard) = state.read() {
guard.metadata()
} else {
r#"{"error": "state locked"}"#.to_string()
}
}
async fn state_handler<T: DashboardState>(state: Arc<RwLock<T>>) -> impl IntoResponse {
if let Ok(guard) = state.read() {
guard.to_json()
} else {
r#"{"error": "state locked"}"#.to_string()
}
}
async fn update_state_handler<T: DashboardState>(
state: Arc<RwLock<T>>,
body: Bytes,
) -> impl IntoResponse {
if let Ok(json) = serde_json::from_slice::<serde_json::Value>(&body) {
if let (Some(field), Some(value)) = (
json.get("field").and_then(|v| v.as_str()),
json.get("value"),
) {
if let Ok(mut guard) = state.write() {
let value_str = value.to_string();
if let Err(e) = guard.update_field(field, &value_str) {
return (axum::http::StatusCode::BAD_REQUEST, e).into_response();
}
return "OK".into_response();
} else {
return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Lock error")
.into_response();
}
}
}
(
axum::http::StatusCode::BAD_REQUEST,
"Invalid request format",
)
.into_response()
}