Skip to main content

cortexai_dashboard/
server.rs

1//! Dashboard HTTP server
2
3use axum::{
4    routing::{get, post},
5    Router,
6};
7use cortexai_monitoring::CostTracker;
8use std::sync::Arc;
9use std::time::Duration;
10use tokio::net::TcpListener;
11use tower_http::cors::{Any, CorsLayer};
12use tower_http::services::ServeDir;
13use tracing::info;
14
15use crate::handlers::{
16    agent_handler, agents_handler, health_handler, index_handler, metrics_handler,
17    prometheus_metrics_handler, restart_agent_handler, session_handler, session_messages_handler,
18    session_traces_handler, sessions_handler, start_agent_handler, stats_handler,
19    stop_agent_handler, studio_handler, traces_handler, ws_handler,
20};
21use crate::state::DashboardState;
22
23/// Dashboard web server
24pub struct DashboardServer {
25    state: Arc<DashboardState>,
26}
27
28impl DashboardServer {
29    /// Create a new dashboard server with Prometheus metrics
30    pub fn new(cost_tracker: Arc<CostTracker>) -> Self {
31        // Initialize Prometheus metrics recorder
32        let prometheus_handle = cortexai_monitoring::init_prometheus();
33
34        Self {
35            state: Arc::new(DashboardState::new(cost_tracker, prometheus_handle)),
36        }
37    }
38
39    /// Get the dashboard state for external updates
40    pub fn state(&self) -> Arc<DashboardState> {
41        self.state.clone()
42    }
43
44    /// Build the Axum router
45    fn build_router(&self) -> Router {
46        let cors = CorsLayer::new()
47            .allow_origin(Any)
48            .allow_methods(Any)
49            .allow_headers(Any);
50
51        Router::new()
52            // Pages
53            .route("/", get(index_handler))
54            .route("/studio", get(studio_handler))
55            // WebSocket
56            .route("/ws", get(ws_handler))
57            // API - Metrics
58            .route("/api/metrics", get(metrics_handler))
59            .route("/api/stats", get(stats_handler))
60            // API - Traces
61            .route("/api/traces", get(traces_handler))
62            // API - Sessions
63            .route("/api/sessions", get(sessions_handler))
64            .route("/api/sessions/:id", get(session_handler))
65            .route("/api/sessions/:id/traces", get(session_traces_handler))
66            .route("/api/sessions/:id/messages", get(session_messages_handler))
67            // API - Agents
68            .route("/api/agents", get(agents_handler))
69            .route("/api/agents/:id", get(agent_handler))
70            .route("/api/agents/:id/start", post(start_agent_handler))
71            .route("/api/agents/:id/stop", post(stop_agent_handler))
72            .route("/api/agents/:id/restart", post(restart_agent_handler))
73            // Health
74            .route("/health", get(health_handler))
75            // Prometheus metrics endpoint
76            .route("/metrics", get(prometheus_metrics_handler))
77            // Serve static files for studio WASM
78            .nest_service(
79                "/studio/pkg",
80                ServeDir::new("crates/dashboard/static/studio/pkg"),
81            )
82            .layer(cors)
83            .with_state(self.state.clone())
84    }
85
86    /// Run the dashboard server
87    pub async fn run(self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
88        let router = self.build_router();
89        let listener = TcpListener::bind(addr).await?;
90
91        info!("Dashboard running at http://{}", addr);
92        info!("  Legacy dashboard: http://{}/", addr);
93        info!("  Agent Studio:     http://{}/studio", addr);
94
95        // Spawn periodic metrics broadcast
96        let state = self.state.clone();
97        tokio::spawn(async move {
98            let mut interval = tokio::time::interval(Duration::from_secs(1));
99            loop {
100                interval.tick().await;
101                state.broadcast_metrics();
102            }
103        });
104
105        axum::serve(listener, router).await?;
106
107        Ok(())
108    }
109
110    /// Run the dashboard server in a background task
111    pub fn spawn(self, addr: String) -> tokio::task::JoinHandle<()> {
112        let state = self.state.clone();
113
114        tokio::spawn(async move {
115            // Spawn periodic metrics broadcast
116            let broadcast_state = state.clone();
117            tokio::spawn(async move {
118                let mut interval = tokio::time::interval(Duration::from_secs(1));
119                loop {
120                    interval.tick().await;
121                    broadcast_state.broadcast_metrics();
122                }
123            });
124
125            let cors = CorsLayer::new()
126                .allow_origin(Any)
127                .allow_methods(Any)
128                .allow_headers(Any);
129
130            let router = Router::new()
131                // Pages
132                .route("/", get(index_handler))
133                .route("/studio", get(studio_handler))
134                // WebSocket
135                .route("/ws", get(ws_handler))
136                // API - Metrics
137                .route("/api/metrics", get(metrics_handler))
138                .route("/api/stats", get(stats_handler))
139                // API - Traces
140                .route("/api/traces", get(traces_handler))
141                // API - Sessions
142                .route("/api/sessions", get(sessions_handler))
143                .route("/api/sessions/:id", get(session_handler))
144                .route("/api/sessions/:id/traces", get(session_traces_handler))
145                .route("/api/sessions/:id/messages", get(session_messages_handler))
146                // API - Agents
147                .route("/api/agents", get(agents_handler))
148                .route("/api/agents/:id", get(agent_handler))
149                .route("/api/agents/:id/start", post(start_agent_handler))
150                .route("/api/agents/:id/stop", post(stop_agent_handler))
151                .route("/api/agents/:id/restart", post(restart_agent_handler))
152                // Health
153                .route("/health", get(health_handler))
154                // Serve static files for studio WASM
155                .nest_service(
156                    "/studio/pkg",
157                    ServeDir::new("crates/dashboard/static/studio/pkg"),
158                )
159                .layer(cors)
160                .with_state(state);
161
162            let listener = TcpListener::bind(&addr).await.unwrap();
163            info!("Dashboard running at http://{}", addr);
164            info!("  Legacy dashboard: http://{}/", addr);
165            info!("  Agent Studio:     http://{}/studio", addr);
166            axum::serve(listener, router).await.unwrap();
167        })
168    }
169}