pulseengine_mcp_server/
dashboard_endpoint.rs1use axum::{
4 Router,
5 extract::{Path, State},
6 http::StatusCode,
7 response::{Html, IntoResponse, Json},
8 routing::get,
9};
10use pulseengine_mcp_logging::DashboardManager;
11use serde::{Deserialize, Serialize};
12use std::sync::Arc;
13
14pub struct DashboardState {
16 pub dashboard_manager: Arc<DashboardManager>,
17}
18
19#[derive(Debug, Serialize, Deserialize)]
21pub struct DashboardDataResponse {
22 pub charts: std::collections::HashMap<String, pulseengine_mcp_logging::ChartData>,
23 pub last_updated: chrono::DateTime<chrono::Utc>,
24}
25
26pub async fn get_dashboard_html(State(state): State<Arc<DashboardState>>) -> impl IntoResponse {
28 let html = state.dashboard_manager.generate_html().await;
29 (StatusCode::OK, Html(html)).into_response()
30}
31
32pub async fn get_dashboard_config(State(state): State<Arc<DashboardState>>) -> impl IntoResponse {
34 let config = state.dashboard_manager.get_config();
35 (StatusCode::OK, Json(config)).into_response()
36}
37
38pub async fn get_dashboard_data(State(state): State<Arc<DashboardState>>) -> impl IntoResponse {
40 let config = state.dashboard_manager.get_config();
41 let mut charts = std::collections::HashMap::new();
42
43 for chart in &config.charts {
45 let chart_data = state
46 .dashboard_manager
47 .get_chart_data(&chart.id, chart.options.time_range_secs)
48 .await;
49 charts.insert(chart.id.clone(), chart_data);
50 }
51
52 let response = DashboardDataResponse {
53 charts,
54 last_updated: chrono::Utc::now(),
55 };
56
57 (StatusCode::OK, Json(response)).into_response()
58}
59
60pub async fn get_chart_data(
62 Path(chart_id): Path<String>,
63 State(state): State<Arc<DashboardState>>,
64) -> impl IntoResponse {
65 let config = state.dashboard_manager.get_config();
66
67 if let Some(chart) = config.charts.iter().find(|c| c.id == chart_id) {
69 let chart_data = state
70 .dashboard_manager
71 .get_chart_data(&chart_id, chart.options.time_range_secs)
72 .await;
73
74 (StatusCode::OK, Json(chart_data)).into_response()
75 } else {
76 (
77 StatusCode::NOT_FOUND,
78 Json(serde_json::json!({
79 "error": "Chart not found",
80 "chart_id": chart_id
81 })),
82 )
83 .into_response()
84 }
85}
86
87pub async fn get_dashboard_health(State(state): State<Arc<DashboardState>>) -> impl IntoResponse {
89 let current_metrics = state.dashboard_manager.get_current_metrics().await;
90
91 let health_status = if current_metrics.is_some() {
92 "healthy"
93 } else {
94 "no_data"
95 };
96
97 (
98 StatusCode::OK,
99 Json(serde_json::json!({
100 "status": health_status,
101 "timestamp": chrono::Utc::now(),
102 "has_current_metrics": current_metrics.is_some(),
103 "dashboard_config": {
104 "enabled": state.dashboard_manager.get_config().enabled,
105 "charts_count": state.dashboard_manager.get_config().charts.len(),
106 "refresh_interval_secs": state.dashboard_manager.get_config().refresh_interval_secs,
107 }
108 })),
109 )
110 .into_response()
111}
112
113pub fn create_dashboard_router(dashboard_manager: Arc<DashboardManager>) -> Router {
115 let state = Arc::new(DashboardState { dashboard_manager });
116
117 Router::new()
118 .route("/dashboard", get(get_dashboard_html))
119 .route("/dashboard/config", get(get_dashboard_config))
120 .route("/dashboard/data", get(get_dashboard_data))
121 .route("/dashboard/health", get(get_dashboard_health))
122 .route("/dashboard/charts/:chart_id", get(get_chart_data))
123 .with_state(state)
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use axum::http::StatusCode;
130 use axum_test::TestServer;
131 use pulseengine_mcp_logging::DashboardConfig;
132
133 #[tokio::test]
134 async fn test_dashboard_config_endpoint() {
135 let config = DashboardConfig::default();
136 let manager = Arc::new(DashboardManager::new(config));
137 let router = create_dashboard_router(manager);
138
139 let server = TestServer::new(router).unwrap();
140 let response = server.get("/dashboard/config").await;
141
142 assert_eq!(response.status_code(), StatusCode::OK);
143
144 let config: DashboardConfig = response.json();
145 assert!(config.enabled);
146 assert_eq!(config.title, "MCP Server Dashboard");
147 }
148
149 #[tokio::test]
150 async fn test_dashboard_health_endpoint() {
151 let config = DashboardConfig::default();
152 let manager = Arc::new(DashboardManager::new(config));
153 let router = create_dashboard_router(manager);
154
155 let server = TestServer::new(router).unwrap();
156 let response = server.get("/dashboard/health").await;
157
158 assert_eq!(response.status_code(), StatusCode::OK);
159
160 let health: serde_json::Value = response.json();
161 assert!(health.get("status").is_some());
162 assert!(health.get("timestamp").is_some());
163 }
164
165 #[tokio::test]
166 async fn test_dashboard_data_endpoint() {
167 let config = DashboardConfig::default();
168 let manager = Arc::new(DashboardManager::new(config));
169 let router = create_dashboard_router(manager);
170
171 let server = TestServer::new(router).unwrap();
172 let response = server.get("/dashboard/data").await;
173
174 assert_eq!(response.status_code(), StatusCode::OK);
175
176 let data: DashboardDataResponse = response.json();
177 assert!(data.charts.is_empty() || !data.charts.is_empty()); }
179
180 #[tokio::test]
181 async fn test_chart_data_endpoint() {
182 let config = DashboardConfig::default();
183 let manager = Arc::new(DashboardManager::new(config));
184 let router = create_dashboard_router(manager);
185
186 let server = TestServer::new(router).unwrap();
187
188 let response = server.get("/dashboard/charts/requests_overview").await;
190 assert_eq!(response.status_code(), StatusCode::OK);
191
192 let response = server.get("/dashboard/charts/nonexistent").await;
194 assert_eq!(response.status_code(), StatusCode::NOT_FOUND);
195 }
196}