pulseengine_mcp_server/
dashboard_endpoint.rs

1//! Dashboard endpoints for metrics visualization
2
3use axum::{
4    extract::{Path, State},
5    http::StatusCode,
6    response::{Html, IntoResponse, Json},
7    routing::get,
8    Router,
9};
10use pulseengine_mcp_logging::DashboardManager;
11use serde::{Deserialize, Serialize};
12use std::sync::Arc;
13
14/// Dashboard state
15pub struct DashboardState {
16    pub dashboard_manager: Arc<DashboardManager>,
17}
18
19/// Dashboard data response
20#[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
26/// Get dashboard HTML
27pub 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
32/// Get dashboard configuration
33pub 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
38/// Get dashboard data (for AJAX updates)
39pub 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    // Get data for each chart
44    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
60/// Get specific chart data
61pub 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    // Find the chart configuration
68    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
87/// Get dashboard health check
88pub 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
113/// Create dashboard router
114pub 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()); // Will be empty without metrics
178    }
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        // Test with existing chart
189        let response = server.get("/dashboard/charts/requests_overview").await;
190        assert_eq!(response.status_code(), StatusCode::OK);
191
192        // Test with non-existent chart
193        let response = server.get("/dashboard/charts/nonexistent").await;
194        assert_eq!(response.status_code(), StatusCode::NOT_FOUND);
195    }
196}