1use crate::cli::DashboardCommands;
2use crate::error::{IntentError, Result};
3use crate::project::ProjectContext;
4
5pub const DASHBOARD_PORT: u16 = 11391;
7
8async fn send_shutdown_request(port: u16) -> Result<()> {
10 let url = format!("http://127.0.0.1:{}/api/internal/shutdown", port);
11
12 let client = reqwest::Client::builder()
13 .timeout(std::time::Duration::from_secs(5))
14 .build()
15 .map_err(|e| {
16 IntentError::OtherError(anyhow::anyhow!("Failed to create HTTP client: {}", e))
17 })?;
18
19 let response = client.post(&url).send().await.map_err(|e| {
20 IntentError::OtherError(anyhow::anyhow!("Failed to send shutdown request: {}", e))
21 })?;
22
23 if response.status().is_success() {
24 Ok(())
25 } else {
26 Err(IntentError::OtherError(anyhow::anyhow!(
27 "Shutdown request failed with status: {}",
28 response.status()
29 )))
30 }
31}
32
33pub async fn check_dashboard_health(port: u16) -> bool {
34 let health_url = format!("http://127.0.0.1:{}/api/health", port);
35
36 match reqwest::Client::builder()
37 .timeout(std::time::Duration::from_secs(2))
38 .build()
39 {
40 Ok(client) => match client.get(&health_url).send().await {
41 Ok(resp) if resp.status().is_success() => {
42 tracing::debug!("Dashboard health check passed for port {}", port);
43 true
44 },
45 Ok(resp) => {
46 tracing::debug!("Dashboard health check failed: status {}", resp.status());
47 false
48 },
49 Err(e) => {
50 tracing::debug!("Dashboard health check failed: {}", e);
51 false
52 },
53 },
54 Err(e) => {
55 tracing::error!("Failed to create HTTP client: {}", e);
56 false
57 },
58 }
59}
60
61pub async fn check_dashboard_status() -> serde_json::Value {
63 use serde_json::json;
64
65 let dashboard_url = format!("http://127.0.0.1:{}", DASHBOARD_PORT);
66
67 if check_dashboard_health(DASHBOARD_PORT).await {
68 json!({
69 "check": "Dashboard",
70 "status": "✓ PASS",
71 "details": {
72 "url": dashboard_url,
73 "status": "running",
74 "access": format!("Visit {} in your browser", dashboard_url)
75 }
76 })
77 } else {
78 json!({
79 "check": "Dashboard",
80 "status": "⚠ WARNING",
81 "details": {
82 "status": "not running",
83 "message": "Dashboard is not running. Start it with 'ie dashboard start'",
84 "command": "ie dashboard start"
85 }
86 })
87 }
88}
89
90pub async fn check_mcp_connections() -> serde_json::Value {
92 use serde_json::json;
93
94 if !check_dashboard_health(DASHBOARD_PORT).await {
95 return json!({
96 "check": "MCP Connections",
97 "status": "⚠ WARNING",
98 "details": {
99 "count": 0,
100 "message": "Dashboard not running - cannot query connections",
101 "command": "ie dashboard start"
102 }
103 });
104 }
105
106 let url = format!("http://127.0.0.1:{}/api/projects", DASHBOARD_PORT);
108 let client = match reqwest::Client::builder()
109 .timeout(std::time::Duration::from_secs(2))
110 .build()
111 {
112 Ok(c) => c,
113 Err(e) => {
114 return json!({
115 "check": "MCP Connections",
116 "status": "✗ FAIL",
117 "details": {
118 "error": format!("Failed to create HTTP client: {}", e)
119 }
120 });
121 },
122 };
123
124 match client.get(&url).send().await {
125 Ok(resp) if resp.status().is_success() => {
126 if let Ok(data) = resp.json::<serde_json::Value>().await {
127 let empty_vec = vec![];
128 let projects = data["projects"].as_array().unwrap_or(&empty_vec);
129 let mcp_count = projects
130 .iter()
131 .filter(|p| p["mcp_connected"].as_bool().unwrap_or(false))
132 .count();
133
134 json!({
135 "check": "MCP Connections",
136 "status": if mcp_count > 0 { "✓ PASS" } else { "⚠ WARNING" },
137 "details": {
138 "count": mcp_count,
139 "message": if mcp_count > 0 {
140 format!("{} MCP client(s) connected", mcp_count)
141 } else {
142 "No MCP clients connected".to_string()
143 }
144 }
145 })
146 } else {
147 json!({
148 "check": "MCP Connections",
149 "status": "✗ FAIL",
150 "details": {"error": "Failed to parse response"}
151 })
152 }
153 },
154 _ => json!({
155 "check": "MCP Connections",
156 "status": "⚠ WARNING",
157 "details": {"count": 0, "message": "Dashboard not responding"}
158 }),
159 }
160}
161
162async fn start_foreground_mode(
164 port: u16,
165 project_path: std::path::PathBuf,
166 db_path: std::path::PathBuf,
167 project_name: String,
168 browser: bool,
169) -> Result<()> {
170 use crate::dashboard::server::DashboardServer;
171
172 let server = DashboardServer::new(port, project_path, db_path).await?;
173
174 println!("Dashboard starting for project: {}", project_name);
175 println!(" Port: {}", port);
176 println!(" URL: http://127.0.0.1:{}", port);
177 println!("\n🚀 Dashboard server running at http://127.0.0.1:{}", port);
178 println!(" Press Ctrl+C to stop\n");
179
180 if browser {
182 let dashboard_url = format!("http://127.0.0.1:{}", port);
183 tokio::time::sleep(tokio::time::Duration::from_millis(800)).await;
184 println!("🌐 Opening dashboard in browser...");
185 if let Err(e) = open::that(&dashboard_url) {
186 eprintln!("⚠️ Could not open browser automatically: {}", e);
187 eprintln!(" Please manually visit: {}", dashboard_url);
188 }
189 println!();
190 }
191
192 server.run().await.map_err(IntentError::OtherError)?;
194
195 Ok(())
196}
197
198#[cfg(unix)]
200async fn start_daemon_mode(
201 port: u16,
202 project_path: std::path::PathBuf,
203 db_path: std::path::PathBuf,
204 project_name: String,
205 browser: bool,
206) -> Result<()> {
207 use nix::unistd::{fork, ForkResult};
208 use std::fs::OpenOptions;
209 use std::os::unix::io::AsRawFd;
210
211 println!("Starting Dashboard in daemon mode...");
212 println!(" Project: {}", project_name);
213 println!(" Port: {}", port);
214
215 let log_file_path = dirs::home_dir()
217 .ok_or_else(|| IntentError::InvalidInput("Could not determine home directory".to_string()))?
218 .join(".intent-engine")
219 .join("dashboard.log");
220
221 match unsafe { fork() } {
223 Ok(ForkResult::Parent { child }) => {
224 let child_pid = child.as_raw() as u32;
226
227 println!("✓ Dashboard started in background");
228 println!(" PID: {}", child_pid);
229 println!(" URL: http://127.0.0.1:{}", port);
230 println!(" Logs: {}", log_file_path.display());
231
232 if browser {
234 tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await;
235 let dashboard_url = format!("http://127.0.0.1:{}", port);
236 println!("🌐 Opening dashboard in browser...");
237 if let Err(e) = open::that(&dashboard_url) {
238 eprintln!("⚠️ Could not open browser automatically: {}", e);
239 }
240 }
241
242 Ok(())
243 },
244 Ok(ForkResult::Child) => {
245 let log_file = OpenOptions::new()
247 .create(true)
248 .append(true)
249 .open(&log_file_path)?;
250
251 let log_fd = log_file.as_raw_fd();
252
253 if let Err(e) = nix::unistd::dup2(log_fd, std::io::stdout().as_raw_fd()) {
255 eprintln!("Failed to redirect stdout: {}", e);
256 }
257 if let Err(e) = nix::unistd::dup2(log_fd, std::io::stderr().as_raw_fd()) {
258 eprintln!("Failed to redirect stderr: {}", e);
259 }
260
261 use crate::dashboard::server::DashboardServer;
263 let server = DashboardServer::new(port, project_path, db_path).await?;
264
265 tracing::info!("Dashboard daemon started (PID: {})", std::process::id());
266 tracing::info!("Port: {}", port);
267 tracing::info!("Log file: {}", log_file_path.display());
268
269 server.run().await.map_err(IntentError::OtherError)?;
271
272 Ok(())
273 },
274 Err(e) => Err(IntentError::OtherError(anyhow::anyhow!(
275 "Failed to fork process: {}",
276 e
277 ))),
278 }
279}
280
281#[cfg(windows)]
283async fn start_daemon_mode(
284 port: u16,
285 _project_path: std::path::PathBuf,
286 _db_path: std::path::PathBuf,
287 project_name: String,
288 browser: bool,
289) -> Result<()> {
290 use std::os::windows::process::CommandExt;
291 use std::process::Command;
292
293 println!("Starting Dashboard in daemon mode...");
294 println!(" Project: {}", project_name);
295 println!(" Port: {}", port);
296
297 let log_file_path = dirs::home_dir()
299 .ok_or_else(|| IntentError::InvalidInput("Could not determine home directory".to_string()))?
300 .join(".intent-engine")
301 .join("dashboard.log");
302
303 let exe_path = std::env::current_exe().map_err(|e| {
305 IntentError::IoError(std::io::Error::other(format!(
306 "Failed to get executable path: {}",
307 e
308 )))
309 })?;
310
311 const CREATE_NO_WINDOW: u32 = 0x08000000;
313 const DETACHED_PROCESS: u32 = 0x00000008;
314
315 let child = Command::new(exe_path)
316 .args([
317 "dashboard",
318 "start",
319 "--port",
320 &port.to_string(),
321 ])
323 .creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS)
324 .spawn()
325 .map_err(|e| {
326 IntentError::IoError(std::io::Error::other(format!(
327 "Failed to spawn daemon process: {}",
328 e
329 )))
330 })?;
331
332 let child_pid = child.id();
333
334 println!("✓ Dashboard started in background");
335 println!(" PID: {}", child_pid);
336 println!(" URL: http://127.0.0.1:{}", port);
337 println!(" Logs: {}", log_file_path.display());
338
339 if browser {
341 tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await;
342 let dashboard_url = format!("http://127.0.0.1:{}", port);
343 println!("🌐 Opening dashboard in browser...");
344 if let Err(e) = open::that(&dashboard_url) {
345 eprintln!("⚠️ Could not open browser automatically: {}", e);
346 }
347 }
348
349 Ok(())
350}
351
352pub async fn handle_dashboard_command(dashboard_cmd: DashboardCommands) -> Result<()> {
353 match dashboard_cmd {
354 DashboardCommands::Start {
355 port,
356 browser,
357 daemon,
358 } => {
359 let project_ctx = ProjectContext::load_or_init().await?;
361 let project_path = project_ctx.root.clone();
362 let db_path = project_ctx.db_path.clone();
363 let project_name = project_path
364 .file_name()
365 .and_then(|n| n.to_str())
366 .unwrap_or("unknown")
367 .to_string();
368
369 let allocated_port = port.unwrap_or(11391);
371
372 if check_dashboard_health(allocated_port).await {
374 println!("Dashboard already running:");
375 println!(" Port: {}", allocated_port);
376 println!(" URL: http://127.0.0.1:{}", allocated_port);
377 return Ok(());
378 }
379
380 if std::net::TcpListener::bind(("0.0.0.0", allocated_port)).is_err() {
382 return Err(IntentError::InvalidInput(format!(
383 "Port {} is already in use",
384 allocated_port
385 )));
386 }
387
388 if daemon {
390 start_daemon_mode(allocated_port, project_path, db_path, project_name, browser)
392 .await?;
393 } else {
394 start_foreground_mode(allocated_port, project_path, db_path, project_name, browser)
396 .await?;
397 }
398
399 Ok(())
400 },
401
402 DashboardCommands::Stop { all } => {
403 let port = 11391;
404
405 if all {
406 println!("Note: Single Dashboard mode - checking port {}", port);
407 }
408
409 if !check_dashboard_health(port).await {
411 println!("Dashboard not running");
412 return Ok(());
413 }
414
415 println!("Stopping Dashboard...");
417 match send_shutdown_request(port).await {
418 Ok(_) => {
419 tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
421
422 if !check_dashboard_health(port).await {
424 println!("✓ Dashboard stopped successfully");
425 } else {
426 eprintln!("⚠ Dashboard may still be running");
427 }
428 },
429 Err(e) => {
430 eprintln!("Failed to stop Dashboard: {}", e);
431 eprintln!("\nManual stop instructions:");
432 #[cfg(unix)]
433 eprintln!(" Unix: lsof -ti:{} | xargs kill", port);
434 #[cfg(windows)]
435 eprintln!(" Windows: netstat -ano | findstr :{}", port);
436 },
437 }
438
439 Ok(())
440 },
441
442 DashboardCommands::Status { all } => {
443 let port = 11391;
444
445 if all {
446 println!("Note: Single Dashboard mode - checking port {}", port);
447 }
448
449 if check_dashboard_health(port).await {
451 let url = format!("http://127.0.0.1:{}/api/info", port);
453 println!("Dashboard status:");
454 println!(" Status: ✓ Running");
455 println!(" Port: {}", port);
456 println!(" URL: http://127.0.0.1:{}", port);
457
458 if let Ok(response) = reqwest::get(&url).await {
459 if response.status().is_success() {
460 #[derive(serde::Deserialize)]
461 struct InfoResponse {
462 data: serde_json::Value,
463 }
464 if let Ok(info) = response.json::<InfoResponse>().await {
465 if let Some(project_name) = info.data.get("project_name") {
466 println!(" Project: {}", project_name);
467 }
468 if let Some(project_path) = info.data.get("project_path") {
469 println!(" Path: {}", project_path);
470 }
471 }
472 }
473 }
474 } else {
475 println!("Dashboard status:");
476 println!(" Status: ✗ Not running");
477 println!(" Port: {}", port);
478 }
479
480 Ok(())
481 },
482
483 DashboardCommands::List => {
484 let port = 11391;
485
486 if !check_dashboard_health(port).await {
488 println!("Dashboard not running");
489 println!("\nUse 'ie dashboard start' to start the Dashboard");
490 return Ok(());
491 }
492
493 let url = format!("http://127.0.0.1:{}/api/projects", port);
495 match reqwest::get(&url).await {
496 Ok(response) if response.status().is_success() => {
497 #[derive(serde::Deserialize)]
498 struct ApiResponse {
499 data: Vec<serde_json::Value>,
500 }
501 match response.json::<ApiResponse>().await {
502 Ok(api_response) => {
503 if api_response.data.is_empty() {
504 println!("Dashboard running but no projects registered");
505 println!(" Port: {}", port);
506 println!(" URL: http://127.0.0.1:{}", port);
507 return Ok(());
508 }
509
510 println!("Dashboard projects:");
511 println!("{:<30} {:<8} {:<15} MCP", "PROJECT", "PORT", "STATUS");
512 println!("{}", "-".repeat(80));
513
514 for project in api_response.data {
515 let name = project
516 .get("name")
517 .and_then(|v| v.as_str())
518 .unwrap_or("unknown");
519 let mcp_connected = project
520 .get("mcp_connected")
521 .and_then(|v| v.as_bool())
522 .unwrap_or(false);
523 let mcp_status = if mcp_connected {
524 "✓ Connected"
525 } else {
526 "✗ Disconnected"
527 };
528
529 println!(
530 "{:<30} {:<8} {:<15} {}",
531 name, port, "Running", mcp_status
532 );
533
534 if let Some(path) = project.get("path").and_then(|v| v.as_str()) {
535 println!(" Path: {}", path);
536 }
537 }
538 },
539 Err(e) => {
540 eprintln!("Failed to parse projects list: {}", e);
541 println!("Dashboard running on port {}", port);
542 },
543 }
544 },
545 Ok(response) => {
546 eprintln!("Failed to get projects list: HTTP {}", response.status());
547 println!("Dashboard running on port {}", port);
548 },
549 Err(e) => {
550 eprintln!("Failed to connect to Dashboard API: {}", e);
551 println!("Dashboard may not be running properly on port {}", port);
552 },
553 }
554
555 Ok(())
556 },
557
558 DashboardCommands::Open => {
559 let port = 11391;
560
561 if !check_dashboard_health(port).await {
563 eprintln!("Dashboard is not running");
564 eprintln!("Start it with: ie dashboard start");
565 return Err(IntentError::InvalidInput(
566 "Dashboard not running".to_string(),
567 ));
568 }
569
570 let url = format!("http://127.0.0.1:{}", port);
571 println!("Opening dashboard: {}", url);
572
573 if let Err(e) = open::that(&url) {
574 eprintln!("Failed to open browser: {}", e);
575 eprintln!("Please manually visit: {}", url);
576 }
577
578 Ok(())
579 },
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586
587 #[tokio::test]
590 #[ignore = "Depends on dashboard not running"]
591 async fn test_check_dashboard_status_not_running() {
592 let status = check_dashboard_status().await;
595
596 assert_eq!(status["check"], "Dashboard");
598 assert_eq!(status["status"], "⚠ WARNING");
599
600 assert_eq!(status["details"]["status"], "not running");
602 assert!(status["details"]["message"]
603 .as_str()
604 .unwrap()
605 .contains("not running"));
606 assert_eq!(status["details"]["command"], "ie dashboard start");
607 }
608
609 #[tokio::test]
612 #[ignore = "Depends on dashboard not running"]
613 async fn test_check_mcp_connections_dashboard_not_running() {
614 let result = check_mcp_connections().await;
615
616 assert_eq!(result["check"], "MCP Connections");
618 assert_eq!(result["status"], "⚠ WARNING");
619
620 assert_eq!(result["details"]["count"], 0);
622 assert!(result["details"]["message"]
623 .as_str()
624 .unwrap()
625 .contains("not running"));
626 assert_eq!(result["details"]["command"], "ie dashboard start");
627 }
628
629 #[test]
631 fn test_dashboard_port_constant() {
632 assert_eq!(DASHBOARD_PORT, 11391);
633 }
634
635 #[tokio::test]
638 async fn test_check_dashboard_health_invalid_port() {
639 let is_healthy = check_dashboard_health(65000).await;
641 assert!(!is_healthy);
642 }
643
644 #[tokio::test]
647 async fn test_check_dashboard_health_default_port_not_running() {
648 let is_healthy = check_dashboard_health(DASHBOARD_PORT).await;
651
652 if !is_healthy {
656 assert!(!is_healthy); }
658 }
659}