intent_engine/cli_handlers/
dashboard.rs1use crate::cli::DashboardCommands;
2use crate::error::{IntentError, Result};
3use crate::project::ProjectContext;
4
5const DASHBOARD_PORT: u16 = 11391;
7
8async fn check_dashboard_health(port: u16) -> bool {
9 let health_url = format!("http://127.0.0.1:{}/api/health", port);
10
11 match reqwest::Client::builder()
12 .timeout(std::time::Duration::from_secs(2))
13 .build()
14 {
15 Ok(client) => match client.get(&health_url).send().await {
16 Ok(resp) if resp.status().is_success() => {
17 tracing::debug!("Dashboard health check passed for port {}", port);
18 true
19 },
20 Ok(resp) => {
21 tracing::debug!("Dashboard health check failed: status {}", resp.status());
22 false
23 },
24 Err(e) => {
25 tracing::debug!("Dashboard health check failed: {}", e);
26 false
27 },
28 },
29 Err(e) => {
30 tracing::error!("Failed to create HTTP client: {}", e);
31 false
32 },
33 }
34}
35
36pub async fn check_dashboard_status() -> serde_json::Value {
38 use serde_json::json;
39
40 let dashboard_url = format!("http://127.0.0.1:{}", DASHBOARD_PORT);
41
42 if check_dashboard_health(DASHBOARD_PORT).await {
43 json!({
44 "check": "Dashboard",
45 "status": "✓ PASS",
46 "details": {
47 "url": dashboard_url,
48 "status": "running",
49 "access": format!("Visit {} in your browser", dashboard_url)
50 }
51 })
52 } else {
53 json!({
54 "check": "Dashboard",
55 "status": "⚠ WARNING",
56 "details": {
57 "status": "not running",
58 "message": "Dashboard is not running. Start it with 'ie dashboard start'",
59 "command": "ie dashboard start"
60 }
61 })
62 }
63}
64
65pub async fn check_mcp_connections() -> serde_json::Value {
67 use serde_json::json;
68
69 if !check_dashboard_health(DASHBOARD_PORT).await {
70 return json!({
71 "check": "MCP Connections",
72 "status": "⚠ WARNING",
73 "details": {
74 "count": 0,
75 "message": "Dashboard not running - cannot query connections",
76 "command": "ie dashboard start"
77 }
78 });
79 }
80
81 let url = format!("http://127.0.0.1:{}/api/projects", DASHBOARD_PORT);
83 let client = match reqwest::Client::builder()
84 .timeout(std::time::Duration::from_secs(2))
85 .build()
86 {
87 Ok(c) => c,
88 Err(e) => {
89 return json!({
90 "check": "MCP Connections",
91 "status": "✗ FAIL",
92 "details": {
93 "error": format!("Failed to create HTTP client: {}", e)
94 }
95 });
96 },
97 };
98
99 match client.get(&url).send().await {
100 Ok(resp) if resp.status().is_success() => {
101 if let Ok(data) = resp.json::<serde_json::Value>().await {
102 let empty_vec = vec![];
103 let projects = data["projects"].as_array().unwrap_or(&empty_vec);
104 let mcp_count = projects
105 .iter()
106 .filter(|p| p["mcp_connected"].as_bool().unwrap_or(false))
107 .count();
108
109 json!({
110 "check": "MCP Connections",
111 "status": if mcp_count > 0 { "✓ PASS" } else { "⚠ WARNING" },
112 "details": {
113 "count": mcp_count,
114 "message": if mcp_count > 0 {
115 format!("{} MCP client(s) connected", mcp_count)
116 } else {
117 "No MCP clients connected".to_string()
118 }
119 }
120 })
121 } else {
122 json!({
123 "check": "MCP Connections",
124 "status": "✗ FAIL",
125 "details": {"error": "Failed to parse response"}
126 })
127 }
128 },
129 _ => json!({
130 "check": "MCP Connections",
131 "status": "⚠ WARNING",
132 "details": {"count": 0, "message": "Dashboard not responding"}
133 }),
134 }
135}
136
137pub async fn handle_dashboard_command(dashboard_cmd: DashboardCommands) -> Result<()> {
138 match dashboard_cmd {
139 DashboardCommands::Start { port, browser } => {
140 let project_ctx = ProjectContext::load_or_init().await?;
142 let project_path = project_ctx.root.clone();
143 let db_path = project_ctx.db_path.clone();
144 let project_name = project_path
145 .file_name()
146 .and_then(|n| n.to_str())
147 .unwrap_or("unknown")
148 .to_string();
149
150 let allocated_port = port.unwrap_or(11391);
152
153 if check_dashboard_health(allocated_port).await {
155 println!("Dashboard already running:");
156 println!(" Port: {}", allocated_port);
157 println!(" URL: http://127.0.0.1:{}", allocated_port);
158 return Ok(());
159 }
160
161 if std::net::TcpListener::bind(("0.0.0.0", allocated_port)).is_err() {
163 return Err(IntentError::InvalidInput(format!(
164 "Port {} is already in use",
165 allocated_port
166 )));
167 }
168
169 use crate::dashboard::server::DashboardServer;
171
172 let server =
173 DashboardServer::new(allocated_port, project_path.clone(), db_path.clone()).await?;
174
175 println!("Dashboard starting for project: {}", project_name);
176 println!(" Port: {}", allocated_port);
177 println!(" URL: http://127.0.0.1:{}", allocated_port);
178 println!(
179 "\n🚀 Dashboard server running at http://127.0.0.1:{}",
180 allocated_port
181 );
182 println!(" Press Ctrl+C to stop\n");
183
184 if browser {
186 let dashboard_url = format!("http://127.0.0.1:{}", allocated_port);
187 tokio::time::sleep(tokio::time::Duration::from_millis(800)).await;
188 println!("🌐 Opening dashboard in browser...");
189 if let Err(e) = open::that(&dashboard_url) {
190 eprintln!("⚠️ Could not open browser automatically: {}", e);
191 eprintln!(" Please manually visit: {}", dashboard_url);
192 }
193 println!();
194 }
195
196 server.run().await.map_err(IntentError::OtherError)?;
198
199 Ok(())
200 },
201
202 DashboardCommands::Stop { all } => {
203 let port = 11391;
204
205 if all {
206 println!("Note: Single Dashboard mode - checking port {}", port);
207 }
208
209 if check_dashboard_health(port).await {
211 println!("Dashboard is running on port {}", port);
212 println!();
213 println!("To stop the Dashboard:");
214 println!(" - If running in foreground: Press Ctrl+C in the terminal");
215 println!(" - If started by MCP Server: Stop the AI tool (Claude Code, etc.)");
216 #[cfg(unix)]
217 println!(" - Or run: lsof -ti:{} | xargs kill", port);
218 #[cfg(windows)]
219 println!(" - Or find the process in Task Manager");
220 } else {
221 println!("Dashboard not running");
222 }
223
224 Ok(())
225 },
226
227 DashboardCommands::Status { all } => {
228 let port = 11391;
229
230 if all {
231 println!("Note: Single Dashboard mode - checking port {}", port);
232 }
233
234 if check_dashboard_health(port).await {
236 let url = format!("http://127.0.0.1:{}/api/info", port);
238 println!("Dashboard status:");
239 println!(" Status: ✓ Running");
240 println!(" Port: {}", port);
241 println!(" URL: http://127.0.0.1:{}", port);
242
243 if let Ok(response) = reqwest::get(&url).await {
244 if response.status().is_success() {
245 #[derive(serde::Deserialize)]
246 struct InfoResponse {
247 data: serde_json::Value,
248 }
249 if let Ok(info) = response.json::<InfoResponse>().await {
250 if let Some(project_name) = info.data.get("project_name") {
251 println!(" Project: {}", project_name);
252 }
253 if let Some(project_path) = info.data.get("project_path") {
254 println!(" Path: {}", project_path);
255 }
256 }
257 }
258 }
259 } else {
260 println!("Dashboard status:");
261 println!(" Status: ✗ Not running");
262 println!(" Port: {}", port);
263 }
264
265 Ok(())
266 },
267
268 DashboardCommands::List => {
269 let port = 11391;
270
271 if !check_dashboard_health(port).await {
273 println!("Dashboard not running");
274 println!("\nUse 'ie dashboard start' to start the Dashboard");
275 return Ok(());
276 }
277
278 let url = format!("http://127.0.0.1:{}/api/projects", port);
280 match reqwest::get(&url).await {
281 Ok(response) if response.status().is_success() => {
282 #[derive(serde::Deserialize)]
283 struct ApiResponse {
284 data: Vec<serde_json::Value>,
285 }
286 match response.json::<ApiResponse>().await {
287 Ok(api_response) => {
288 if api_response.data.is_empty() {
289 println!("Dashboard running but no projects registered");
290 println!(" Port: {}", port);
291 println!(" URL: http://127.0.0.1:{}", port);
292 return Ok(());
293 }
294
295 println!("Dashboard projects:");
296 println!("{:<30} {:<8} {:<15} MCP", "PROJECT", "PORT", "STATUS");
297 println!("{}", "-".repeat(80));
298
299 for project in api_response.data {
300 let name = project
301 .get("name")
302 .and_then(|v| v.as_str())
303 .unwrap_or("unknown");
304 let mcp_connected = project
305 .get("mcp_connected")
306 .and_then(|v| v.as_bool())
307 .unwrap_or(false);
308 let mcp_status = if mcp_connected {
309 "✓ Connected"
310 } else {
311 "✗ Disconnected"
312 };
313
314 println!(
315 "{:<30} {:<8} {:<15} {}",
316 name, port, "Running", mcp_status
317 );
318
319 if let Some(path) = project.get("path").and_then(|v| v.as_str()) {
320 println!(" Path: {}", path);
321 }
322 }
323 },
324 Err(e) => {
325 eprintln!("Failed to parse projects list: {}", e);
326 println!("Dashboard running on port {}", port);
327 },
328 }
329 },
330 Ok(response) => {
331 eprintln!("Failed to get projects list: HTTP {}", response.status());
332 println!("Dashboard running on port {}", port);
333 },
334 Err(e) => {
335 eprintln!("Failed to connect to Dashboard API: {}", e);
336 println!("Dashboard may not be running properly on port {}", port);
337 },
338 }
339
340 Ok(())
341 },
342
343 DashboardCommands::Open => {
344 let port = 11391;
345
346 if !check_dashboard_health(port).await {
348 eprintln!("Dashboard is not running");
349 eprintln!("Start it with: ie dashboard start");
350 return Err(IntentError::InvalidInput(
351 "Dashboard not running".to_string(),
352 ));
353 }
354
355 let url = format!("http://127.0.0.1:{}", port);
356 println!("Opening dashboard: {}", url);
357
358 if let Err(e) = open::that(&url) {
359 eprintln!("Failed to open browser: {}", e);
360 eprintln!("Please manually visit: {}", url);
361 }
362
363 Ok(())
364 },
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[tokio::test]
375 #[ignore = "Depends on dashboard not running"]
376 async fn test_check_dashboard_status_not_running() {
377 let status = check_dashboard_status().await;
380
381 assert_eq!(status["check"], "Dashboard");
383 assert_eq!(status["status"], "⚠ WARNING");
384
385 assert_eq!(status["details"]["status"], "not running");
387 assert!(status["details"]["message"]
388 .as_str()
389 .unwrap()
390 .contains("not running"));
391 assert_eq!(status["details"]["command"], "ie dashboard start");
392 }
393
394 #[tokio::test]
397 #[ignore = "Depends on dashboard not running"]
398 async fn test_check_mcp_connections_dashboard_not_running() {
399 let result = check_mcp_connections().await;
400
401 assert_eq!(result["check"], "MCP Connections");
403 assert_eq!(result["status"], "⚠ WARNING");
404
405 assert_eq!(result["details"]["count"], 0);
407 assert!(result["details"]["message"]
408 .as_str()
409 .unwrap()
410 .contains("not running"));
411 assert_eq!(result["details"]["command"], "ie dashboard start");
412 }
413
414 #[test]
416 fn test_dashboard_port_constant() {
417 assert_eq!(DASHBOARD_PORT, 11391);
418 }
419
420 #[tokio::test]
423 async fn test_check_dashboard_health_invalid_port() {
424 let is_healthy = check_dashboard_health(65000).await;
426 assert!(!is_healthy);
427 }
428
429 #[tokio::test]
432 async fn test_check_dashboard_health_default_port_not_running() {
433 let is_healthy = check_dashboard_health(DASHBOARD_PORT).await;
436
437 if !is_healthy {
441 assert!(!is_healthy); }
443 }
444}