1pub use magic_dashboard_macros::Dashboard;
2pub use serde_json;
3
4use axum::{
5 body::Bytes,
6 response::{Html, IntoResponse},
7 routing::get,
8 Router,
9};
10use std::net::SocketAddr;
11use std::sync::{Arc, RwLock};
12use tokio::net::TcpListener;
13
14pub trait DashboardState: Send + Sync {
16 fn to_json(&self) -> String;
18
19 fn metadata(&self) -> String {
21 "{}".to_string()
22 }
23
24 fn update_field(&mut self, _field: &str, _value: &str) -> Result<(), String> {
26 Err("Update not supported".to_string())
27 }
28}
29
30#[derive(Clone)]
32pub struct DashboardRunner<T: DashboardState + 'static> {
33 state: Arc<RwLock<T>>,
34}
35
36impl<T: DashboardState + 'static> DashboardRunner<T> {
37 pub fn new(initial_state: T) -> Self {
38 Self {
39 state: Arc::new(RwLock::new(initial_state)),
40 }
41 }
42
43 pub fn update<F>(&self, f: F)
45 where
46 F: FnOnce(&mut T),
47 {
48 if let Ok(mut state) = self.state.write() {
49 f(&mut state);
50 }
51 }
52
53 pub fn read<F, R>(&self, f: F) -> R
55 where
56 F: FnOnce(&T) -> R,
57 {
58 let state = self.state.read().unwrap();
59 f(&state)
60 }
61
62 pub fn start_server(&self, port: u16) {
64 let state_clone = self.state.clone();
65
66 std::thread::spawn(move || {
67 if let Ok(rt) = tokio::runtime::Runtime::new() {
68 rt.block_on(async move {
69 let app = Router::new()
70 .route("/", get(index_handler))
71 .route(
72 "/api/metadata",
73 get({
74 let state = state_clone.clone();
75 move || metadata_handler(state)
76 }),
77 )
78 .route(
79 "/api/state",
80 get({
81 let state = state_clone.clone();
82 move || state_handler(state)
83 })
84 .post({
85 let state = state_clone.clone();
86 move |body| update_state_handler(state, body)
87 }),
88 );
89
90 let addr = SocketAddr::from(([127, 0, 0, 1], port));
91 if let Ok(listener) = TcpListener::bind(addr).await {
92 println!("Magic Dashboard running at http://127.0.0.1:{}", port);
93 let _ = axum::serve(listener, app).await;
94 } else {
95 eprintln!("Failed to start Magic Dashboard on port {}", port);
96 }
97 });
98 } else {
99 eprintln!("Failed to create Tokio runtime for Magic Dashboard");
100 }
101 });
102 }
103}
104
105async fn index_handler() -> Html<&'static str> {
106 Html(include_str!("index.html"))
107}
108
109async fn metadata_handler<T: DashboardState>(state: Arc<RwLock<T>>) -> impl IntoResponse {
110 if let Ok(guard) = state.read() {
111 guard.metadata()
112 } else {
113 r#"{"error": "state locked"}"#.to_string()
114 }
115}
116
117async fn state_handler<T: DashboardState>(state: Arc<RwLock<T>>) -> impl IntoResponse {
118 if let Ok(guard) = state.read() {
119 guard.to_json()
120 } else {
121 r#"{"error": "state locked"}"#.to_string()
122 }
123}
124
125async fn update_state_handler<T: DashboardState>(
126 state: Arc<RwLock<T>>,
127 body: Bytes,
128) -> impl IntoResponse {
129 if let Ok(json) = serde_json::from_slice::<serde_json::Value>(&body) {
130 if let (Some(field), Some(value)) = (
131 json.get("field").and_then(|v| v.as_str()),
132 json.get("value"),
133 ) {
134 if let Ok(mut guard) = state.write() {
135 let value_str = value.to_string();
136 if let Err(e) = guard.update_field(field, &value_str) {
137 return (axum::http::StatusCode::BAD_REQUEST, e).into_response();
138 }
139 return "OK".into_response();
140 } else {
141 return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "Lock error")
142 .into_response();
143 }
144 }
145 }
146 (
147 axum::http::StatusCode::BAD_REQUEST,
148 "Invalid request format",
149 )
150 .into_response()
151}