forge_runtime/dashboard/
mod.rs

1mod api;
2mod assets;
3mod pages;
4
5pub use api::DashboardApi;
6pub use assets::DashboardAssets;
7pub use pages::DashboardPages;
8
9use std::sync::Arc;
10
11use axum::{
12    Router,
13    routing::{get, post},
14};
15use sqlx::PgPool;
16use tower_http::cors::{Any, CorsLayer};
17
18use crate::cron::CronRegistry;
19use crate::jobs::{JobDispatcher, JobRegistry};
20use crate::workflow::{WorkflowExecutor, WorkflowRegistry};
21
22/// Dashboard configuration.
23#[derive(Debug, Clone)]
24pub struct DashboardConfig {
25    /// Whether the dashboard is enabled.
26    pub enabled: bool,
27
28    /// Dashboard path prefix (default: "/_dashboard").
29    pub path_prefix: String,
30
31    /// API path prefix (default: "/_api").
32    pub api_prefix: String,
33
34    /// Require authentication for dashboard access.
35    pub require_auth: bool,
36
37    /// Allowed admin user IDs (if require_auth is true).
38    pub admin_users: Vec<String>,
39}
40
41impl Default for DashboardConfig {
42    fn default() -> Self {
43        Self {
44            enabled: true,
45            path_prefix: "/_dashboard".to_string(),
46            api_prefix: "/_api".to_string(),
47            require_auth: false,
48            admin_users: Vec::new(),
49        }
50    }
51}
52
53/// Dashboard state shared across handlers.
54#[derive(Clone)]
55pub struct DashboardState {
56    pub pool: PgPool,
57    pub config: DashboardConfig,
58    pub job_registry: JobRegistry,
59    pub cron_registry: Arc<CronRegistry>,
60    pub workflow_registry: WorkflowRegistry,
61    /// Optional job dispatcher for dispatching jobs from dashboard.
62    pub job_dispatcher: Option<Arc<JobDispatcher>>,
63    /// Optional workflow executor for starting workflows from dashboard.
64    pub workflow_executor: Option<Arc<WorkflowExecutor>>,
65}
66
67/// Create the dashboard router.
68pub fn create_dashboard_router(state: DashboardState) -> Router {
69    Router::new()
70        // Dashboard pages
71        .route("/", get(pages::index))
72        .route("/metrics", get(pages::metrics))
73        .route("/logs", get(pages::logs))
74        .route("/traces", get(pages::traces))
75        .route("/traces/{trace_id}", get(pages::trace_detail))
76        .route("/alerts", get(pages::alerts))
77        .route("/jobs", get(pages::jobs))
78        .route("/workflows", get(pages::workflows))
79        .route("/crons", get(pages::crons))
80        .route("/cluster", get(pages::cluster))
81        // Static assets
82        .route("/assets/styles.css", get(assets::styles_css))
83        .route("/assets/main.js", get(assets::main_js))
84        .route("/assets/chart.js", get(assets::chart_js))
85        .with_state(state)
86}
87
88/// Create the API router for observability data.
89pub fn create_api_router(state: DashboardState) -> Router {
90    let cors = CorsLayer::new()
91        .allow_origin(Any)
92        .allow_methods(Any)
93        .allow_headers(Any);
94
95    Router::new()
96        // Metrics API
97        .route("/metrics", get(api::list_metrics))
98        .route("/metrics/{name}", get(api::get_metric))
99        .route("/metrics/series", get(api::get_metric_series))
100        // Logs API
101        .route("/logs", get(api::list_logs))
102        .route("/logs/search", get(api::search_logs))
103        // Traces API
104        .route("/traces", get(api::list_traces))
105        .route("/traces/{trace_id}", get(api::get_trace))
106        // Alerts API
107        .route("/alerts", get(api::list_alerts))
108        .route("/alerts/active", get(api::get_active_alerts))
109        .route("/alerts/{id}/acknowledge", post(api::acknowledge_alert))
110        .route("/alerts/{id}/resolve", post(api::resolve_alert))
111        // Alert Rules API
112        .route(
113            "/alerts/rules",
114            get(api::list_alert_rules).post(api::create_alert_rule),
115        )
116        .route(
117            "/alerts/rules/{id}",
118            get(api::get_alert_rule)
119                .put(api::update_alert_rule)
120                .delete(api::delete_alert_rule),
121        )
122        // Jobs API
123        .route("/jobs", get(api::list_jobs))
124        .route("/jobs/stats", get(api::get_job_stats))
125        .route("/jobs/registered", get(api::list_registered_jobs))
126        .route("/jobs/{id}", get(api::get_job))
127        .route("/jobs/{job_type}/dispatch", post(api::dispatch_job))
128        // Workflows API
129        .route("/workflows", get(api::list_workflows))
130        .route("/workflows/stats", get(api::get_workflow_stats))
131        .route("/workflows/registered", get(api::list_registered_workflows))
132        .route("/workflows/{id}", get(api::get_workflow))
133        .route(
134            "/workflows/{workflow_name}/start",
135            post(api::start_workflow),
136        )
137        // Crons API
138        .route("/crons", get(api::list_crons))
139        .route("/crons/stats", get(api::get_cron_stats))
140        .route("/crons/history", get(api::get_cron_history))
141        .route("/crons/registered", get(api::list_registered_crons))
142        .route("/crons/{name}/trigger", post(api::trigger_cron))
143        .route("/crons/{name}/pause", post(api::pause_cron))
144        .route("/crons/{name}/resume", post(api::resume_cron))
145        // Cluster API
146        .route("/cluster/nodes", get(api::list_nodes))
147        .route("/cluster/health", get(api::get_cluster_health))
148        // System API
149        .route("/system/info", get(api::get_system_info))
150        .route("/system/stats", get(api::get_system_stats))
151        .layer(cors)
152        .with_state(state)
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_dashboard_config_default() {
161        let config = DashboardConfig::default();
162        assert!(config.enabled);
163        assert_eq!(config.path_prefix, "/_dashboard");
164        assert_eq!(config.api_prefix, "/_api");
165        assert!(!config.require_auth);
166    }
167
168    #[test]
169    fn test_dashboard_state() {
170        // Just verify the types compile - the new state requires registries
171        let _ = DashboardConfig::default();
172    }
173}