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