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