Skip to main content

cargo_coupling/web/
server.rs

1//! Web server for coupling visualization
2//!
3//! Provides an HTTP server using Axum to serve the visualization UI
4//! and JSON API endpoints.
5
6use std::net::SocketAddr;
7use std::path::PathBuf;
8use std::sync::Arc;
9
10use axum::Router;
11use tokio::net::TcpListener;
12
13use crate::analyze_history;
14use crate::balance::score::IssueThresholds;
15use crate::cli_output::{JsonHistory, history_report_to_json};
16use crate::config::CompiledConfig;
17use crate::metrics::project::ProjectMetrics;
18
19use super::routes;
20
21pub const DEFAULT_HISTORY_MAX_POINTS: usize = 30;
22
23/// Shared application state
24pub struct AppState {
25    pub metrics: ProjectMetrics,
26    pub thresholds: IssueThresholds,
27    pub api_endpoint: Option<String>,
28    pub history: JsonHistory,
29    pub analysis_path: PathBuf,
30    pub analysis_config: CompiledConfig,
31    pub git_months: usize,
32    pub no_git: bool,
33}
34
35/// Configuration for the web server
36pub struct ServerConfig {
37    pub port: u16,
38    pub open_browser: bool,
39    pub api_endpoint: Option<String>,
40    pub analysis_path: PathBuf,
41    pub analysis_config: CompiledConfig,
42    pub git_months: usize,
43    pub history_max_points: usize,
44    pub no_git: bool,
45}
46
47impl Default for ServerConfig {
48    fn default() -> Self {
49        Self {
50            port: 3000,
51            open_browser: true,
52            api_endpoint: None,
53            analysis_path: PathBuf::from("./src"),
54            analysis_config: CompiledConfig::empty(),
55            git_months: 6,
56            history_max_points: DEFAULT_HISTORY_MAX_POINTS,
57            no_git: false,
58        }
59    }
60}
61
62/// Start the web server and serve the visualization
63pub async fn start_server(
64    metrics: ProjectMetrics,
65    thresholds: IssueThresholds,
66    config: ServerConfig,
67) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
68    let history = load_history(&config, &thresholds);
69
70    let state = Arc::new(AppState {
71        metrics,
72        thresholds,
73        api_endpoint: config.api_endpoint.clone(),
74        history,
75        analysis_path: config.analysis_path.clone(),
76        analysis_config: config.analysis_config.clone(),
77        git_months: config.git_months,
78        no_git: config.no_git,
79    });
80
81    let app = Router::new()
82        .merge(routes::api_routes())
83        .merge(routes::static_routes())
84        .with_state(state);
85
86    let addr = SocketAddr::from(([127, 0, 0, 1], config.port));
87    let listener = TcpListener::bind(addr).await?;
88
89    let url = format!("http://localhost:{}", config.port);
90    eprintln!("Starting web server at {}", url);
91
92    if config.open_browser {
93        eprintln!("Opening browser...");
94        if let Err(e) = open::that(&url) {
95            eprintln!("Warning: Could not open browser: {}", e);
96            eprintln!("Please open {} manually", url);
97        }
98    }
99
100    eprintln!("Press Ctrl+C to stop the server");
101
102    axum::serve(listener, app).await?;
103
104    Ok(())
105}
106
107fn load_history(config: &ServerConfig, thresholds: &IssueThresholds) -> JsonHistory {
108    if config.no_git {
109        return JsonHistory {
110            months: config.git_months,
111            points: Vec::new(),
112            skipped: Vec::new(),
113        };
114    }
115
116    match analyze_history(
117        &config.analysis_path,
118        &config.analysis_config,
119        thresholds,
120        config.git_months,
121        config.history_max_points,
122    ) {
123        Ok(report) => history_report_to_json(&report),
124        Err(e) => {
125            eprintln!("Warning: History analysis failed: {}", e);
126            JsonHistory {
127                months: config.git_months,
128                points: Vec::new(),
129                skipped: Vec::new(),
130            }
131        }
132    }
133}