cargo_coupling/web/
server.rs1use 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
24pub 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
37pub 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
64pub 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}