1use 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 pub design: crate::design::model::DesignAssessment,
36 pub context_path: Option<PathBuf>,
37}
38
39pub 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
74pub 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}