use crate::cli::DashboardCommands;
use crate::error::{IntentError, Result};
use crate::project::ProjectContext;
pub const DASHBOARD_PORT: u16 = 11391;
async fn send_shutdown_request(port: u16) -> Result<()> {
let url = format!("http://127.0.0.1:{}/api/internal/shutdown", port);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(5))
.build()
.map_err(|e| {
IntentError::OtherError(anyhow::anyhow!("Failed to create HTTP client: {}", e))
})?;
let response = client.post(&url).send().await.map_err(|e| {
IntentError::OtherError(anyhow::anyhow!("Failed to send shutdown request: {}", e))
})?;
if response.status().is_success() {
Ok(())
} else {
Err(IntentError::OtherError(anyhow::anyhow!(
"Shutdown request failed with status: {}",
response.status()
)))
}
}
pub async fn check_dashboard_health(port: u16) -> bool {
let health_url = format!("http://127.0.0.1:{}/api/health", port);
match reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(2))
.build()
{
Ok(client) => match client.get(&health_url).send().await {
Ok(resp) if resp.status().is_success() => {
tracing::debug!("Dashboard health check passed for port {}", port);
true
},
Ok(resp) => {
tracing::debug!("Dashboard health check failed: status {}", resp.status());
false
},
Err(e) => {
tracing::debug!("Dashboard health check failed: {}", e);
false
},
},
Err(e) => {
tracing::error!("Failed to create HTTP client: {}", e);
false
},
}
}
pub async fn check_dashboard_status() -> serde_json::Value {
use serde_json::json;
let dashboard_url = format!("http://127.0.0.1:{}", DASHBOARD_PORT);
if check_dashboard_health(DASHBOARD_PORT).await {
json!({
"check": "Dashboard",
"status": "✓ PASS",
"details": {
"url": dashboard_url,
"status": "running",
"access": format!("Visit {} in your browser", dashboard_url)
}
})
} else {
json!({
"check": "Dashboard",
"status": "⚠ WARNING",
"details": {
"status": "not running",
"message": "Dashboard is not running. Start it with 'ie dashboard start'",
"command": "ie dashboard start"
}
})
}
}
pub async fn check_mcp_connections() -> serde_json::Value {
use serde_json::json;
if !check_dashboard_health(DASHBOARD_PORT).await {
return json!({
"check": "MCP Connections",
"status": "⚠ WARNING",
"details": {
"count": 0,
"message": "Dashboard not running - cannot query connections",
"command": "ie dashboard start"
}
});
}
let url = format!("http://127.0.0.1:{}/api/projects", DASHBOARD_PORT);
let client = match reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(2))
.build()
{
Ok(c) => c,
Err(e) => {
return json!({
"check": "MCP Connections",
"status": "✗ FAIL",
"details": {
"error": format!("Failed to create HTTP client: {}", e)
}
});
},
};
match client.get(&url).send().await {
Ok(resp) if resp.status().is_success() => {
if let Ok(data) = resp.json::<serde_json::Value>().await {
let empty_vec = vec![];
let projects = data["projects"].as_array().unwrap_or(&empty_vec);
let mcp_count = projects
.iter()
.filter(|p| p["mcp_connected"].as_bool().unwrap_or(false))
.count();
json!({
"check": "MCP Connections",
"status": if mcp_count > 0 { "✓ PASS" } else { "⚠ WARNING" },
"details": {
"count": mcp_count,
"message": if mcp_count > 0 {
format!("{} MCP client(s) connected", mcp_count)
} else {
"No MCP clients connected".to_string()
}
}
})
} else {
json!({
"check": "MCP Connections",
"status": "✗ FAIL",
"details": {"error": "Failed to parse response"}
})
}
},
_ => json!({
"check": "MCP Connections",
"status": "⚠ WARNING",
"details": {"count": 0, "message": "Dashboard not responding"}
}),
}
}
async fn start_foreground_mode(
port: u16,
project_path: std::path::PathBuf,
db_path: std::path::PathBuf,
project_name: String,
browser: bool,
) -> Result<()> {
use crate::dashboard::server::DashboardServer;
let server = DashboardServer::new(port, project_path, db_path).await?;
println!("Dashboard starting for project: {}", project_name);
println!(" Port: {}", port);
println!(" URL: http://127.0.0.1:{}", port);
println!("\n🚀 Dashboard server running at http://127.0.0.1:{}", port);
println!(" Press Ctrl+C to stop\n");
if browser {
let dashboard_url = format!("http://127.0.0.1:{}", port);
tokio::time::sleep(tokio::time::Duration::from_millis(800)).await;
println!("🌐 Opening dashboard in browser...");
if let Err(e) = open::that(&dashboard_url) {
eprintln!("⚠️ Could not open browser automatically: {}", e);
eprintln!(" Please manually visit: {}", dashboard_url);
}
println!();
}
server.run().await.map_err(IntentError::OtherError)?;
Ok(())
}
#[cfg(unix)]
async fn start_daemon_mode(
port: u16,
project_path: std::path::PathBuf,
db_path: std::path::PathBuf,
project_name: String,
browser: bool,
) -> Result<()> {
use nix::unistd::{fork, ForkResult};
use std::fs::OpenOptions;
use std::os::unix::io::AsRawFd;
println!("Starting Dashboard in daemon mode...");
println!(" Project: {}", project_name);
println!(" Port: {}", port);
let log_file_path = dirs::home_dir()
.ok_or_else(|| IntentError::InvalidInput("Could not determine home directory".to_string()))?
.join(".intent-engine")
.join("dashboard.log");
match unsafe { fork() } {
Ok(ForkResult::Parent { child }) => {
let child_pid = child.as_raw() as u32;
println!("✓ Dashboard started in background");
println!(" PID: {}", child_pid);
println!(" URL: http://127.0.0.1:{}", port);
println!(" Logs: {}", log_file_path.display());
if browser {
tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await;
let dashboard_url = format!("http://127.0.0.1:{}", port);
println!("🌐 Opening dashboard in browser...");
if let Err(e) = open::that(&dashboard_url) {
eprintln!("⚠️ Could not open browser automatically: {}", e);
}
}
Ok(())
},
Ok(ForkResult::Child) => {
let log_file = OpenOptions::new()
.create(true)
.append(true)
.open(&log_file_path)?;
let log_fd = log_file.as_raw_fd();
if let Err(e) = nix::unistd::dup2(log_fd, std::io::stdout().as_raw_fd()) {
eprintln!("Failed to redirect stdout: {}", e);
}
if let Err(e) = nix::unistd::dup2(log_fd, std::io::stderr().as_raw_fd()) {
eprintln!("Failed to redirect stderr: {}", e);
}
use crate::dashboard::server::DashboardServer;
let server = DashboardServer::new(port, project_path, db_path).await?;
tracing::info!("Dashboard daemon started (PID: {})", std::process::id());
tracing::info!("Port: {}", port);
tracing::info!("Log file: {}", log_file_path.display());
server.run().await.map_err(IntentError::OtherError)?;
Ok(())
},
Err(e) => Err(IntentError::OtherError(anyhow::anyhow!(
"Failed to fork process: {}",
e
))),
}
}
#[cfg(windows)]
async fn start_daemon_mode(
port: u16,
_project_path: std::path::PathBuf,
_db_path: std::path::PathBuf,
project_name: String,
browser: bool,
) -> Result<()> {
use std::os::windows::process::CommandExt;
use std::process::Command;
println!("Starting Dashboard in daemon mode...");
println!(" Project: {}", project_name);
println!(" Port: {}", port);
let log_file_path = dirs::home_dir()
.ok_or_else(|| IntentError::InvalidInput("Could not determine home directory".to_string()))?
.join(".intent-engine")
.join("dashboard.log");
let exe_path = std::env::current_exe().map_err(|e| {
IntentError::IoError(std::io::Error::other(format!(
"Failed to get executable path: {}",
e
)))
})?;
const CREATE_NO_WINDOW: u32 = 0x08000000;
const DETACHED_PROCESS: u32 = 0x00000008;
let child = Command::new(exe_path)
.args([
"dashboard",
"start",
"--port",
&port.to_string(),
])
.creation_flags(CREATE_NO_WINDOW | DETACHED_PROCESS)
.spawn()
.map_err(|e| {
IntentError::IoError(std::io::Error::other(format!(
"Failed to spawn daemon process: {}",
e
)))
})?;
let child_pid = child.id();
println!("✓ Dashboard started in background");
println!(" PID: {}", child_pid);
println!(" URL: http://127.0.0.1:{}", port);
println!(" Logs: {}", log_file_path.display());
if browser {
tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await;
let dashboard_url = format!("http://127.0.0.1:{}", port);
println!("🌐 Opening dashboard in browser...");
if let Err(e) = open::that(&dashboard_url) {
eprintln!("⚠️ Could not open browser automatically: {}", e);
}
}
Ok(())
}
pub async fn handle_dashboard_command(dashboard_cmd: DashboardCommands) -> Result<()> {
match dashboard_cmd {
DashboardCommands::Start {
port,
browser,
daemon,
} => {
let project_ctx = ProjectContext::load_or_init().await?;
let project_path = project_ctx.root.clone();
let db_path = project_ctx.db_path.clone();
let project_name = project_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let allocated_port = port.unwrap_or(11391);
if check_dashboard_health(allocated_port).await {
println!("Dashboard already running:");
println!(" Port: {}", allocated_port);
println!(" URL: http://127.0.0.1:{}", allocated_port);
return Ok(());
}
if std::net::TcpListener::bind(("0.0.0.0", allocated_port)).is_err() {
return Err(IntentError::InvalidInput(format!(
"Port {} is already in use",
allocated_port
)));
}
if daemon {
start_daemon_mode(allocated_port, project_path, db_path, project_name, browser)
.await?;
} else {
start_foreground_mode(allocated_port, project_path, db_path, project_name, browser)
.await?;
}
Ok(())
},
DashboardCommands::Stop { all } => {
let port = 11391;
if all {
println!("Note: Single Dashboard mode - checking port {}", port);
}
if !check_dashboard_health(port).await {
println!("Dashboard not running");
return Ok(());
}
println!("Stopping Dashboard...");
match send_shutdown_request(port).await {
Ok(_) => {
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
if !check_dashboard_health(port).await {
println!("✓ Dashboard stopped successfully");
} else {
eprintln!("⚠ Dashboard may still be running");
}
},
Err(e) => {
eprintln!("Failed to stop Dashboard: {}", e);
eprintln!("\nManual stop instructions:");
#[cfg(unix)]
eprintln!(" Unix: lsof -ti:{} | xargs kill", port);
#[cfg(windows)]
eprintln!(" Windows: netstat -ano | findstr :{}", port);
},
}
Ok(())
},
DashboardCommands::Status { all } => {
let port = 11391;
if all {
println!("Note: Single Dashboard mode - checking port {}", port);
}
if check_dashboard_health(port).await {
let url = format!("http://127.0.0.1:{}/api/info", port);
println!("Dashboard status:");
println!(" Status: ✓ Running");
println!(" Port: {}", port);
println!(" URL: http://127.0.0.1:{}", port);
if let Ok(response) = reqwest::get(&url).await {
if response.status().is_success() {
#[derive(serde::Deserialize)]
struct InfoResponse {
data: serde_json::Value,
}
if let Ok(info) = response.json::<InfoResponse>().await {
if let Some(project_name) = info.data.get("project_name") {
println!(" Project: {}", project_name);
}
if let Some(project_path) = info.data.get("project_path") {
println!(" Path: {}", project_path);
}
}
}
}
} else {
println!("Dashboard status:");
println!(" Status: ✗ Not running");
println!(" Port: {}", port);
}
Ok(())
},
DashboardCommands::List => {
let port = 11391;
if !check_dashboard_health(port).await {
println!("Dashboard not running");
println!("\nUse 'ie dashboard start' to start the Dashboard");
return Ok(());
}
let url = format!("http://127.0.0.1:{}/api/projects", port);
match reqwest::get(&url).await {
Ok(response) if response.status().is_success() => {
#[derive(serde::Deserialize)]
struct ApiResponse {
data: Vec<serde_json::Value>,
}
match response.json::<ApiResponse>().await {
Ok(api_response) => {
if api_response.data.is_empty() {
println!("Dashboard running but no projects registered");
println!(" Port: {}", port);
println!(" URL: http://127.0.0.1:{}", port);
return Ok(());
}
println!("Dashboard projects:");
println!("{:<30} {:<8} {:<15} MCP", "PROJECT", "PORT", "STATUS");
println!("{}", "-".repeat(80));
for project in api_response.data {
let name = project
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let mcp_connected = project
.get("mcp_connected")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let mcp_status = if mcp_connected {
"✓ Connected"
} else {
"✗ Disconnected"
};
println!(
"{:<30} {:<8} {:<15} {}",
name, port, "Running", mcp_status
);
if let Some(path) = project.get("path").and_then(|v| v.as_str()) {
println!(" Path: {}", path);
}
}
},
Err(e) => {
eprintln!("Failed to parse projects list: {}", e);
println!("Dashboard running on port {}", port);
},
}
},
Ok(response) => {
eprintln!("Failed to get projects list: HTTP {}", response.status());
println!("Dashboard running on port {}", port);
},
Err(e) => {
eprintln!("Failed to connect to Dashboard API: {}", e);
println!("Dashboard may not be running properly on port {}", port);
},
}
Ok(())
},
DashboardCommands::Open => {
let port = 11391;
if !check_dashboard_health(port).await {
eprintln!("Dashboard is not running");
eprintln!("Start it with: ie dashboard start");
return Err(IntentError::InvalidInput(
"Dashboard not running".to_string(),
));
}
let url = format!("http://127.0.0.1:{}", port);
println!("Opening dashboard: {}", url);
if let Err(e) = open::that(&url) {
eprintln!("Failed to open browser: {}", e);
eprintln!("Please manually visit: {}", url);
}
Ok(())
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore = "Depends on dashboard not running"]
async fn test_check_dashboard_status_not_running() {
let status = check_dashboard_status().await;
assert_eq!(status["check"], "Dashboard");
assert_eq!(status["status"], "⚠ WARNING");
assert_eq!(status["details"]["status"], "not running");
assert!(status["details"]["message"]
.as_str()
.unwrap()
.contains("not running"));
assert_eq!(status["details"]["command"], "ie dashboard start");
}
#[tokio::test]
#[ignore = "Depends on dashboard not running"]
async fn test_check_mcp_connections_dashboard_not_running() {
let result = check_mcp_connections().await;
assert_eq!(result["check"], "MCP Connections");
assert_eq!(result["status"], "⚠ WARNING");
assert_eq!(result["details"]["count"], 0);
assert!(result["details"]["message"]
.as_str()
.unwrap()
.contains("not running"));
assert_eq!(result["details"]["command"], "ie dashboard start");
}
#[test]
fn test_dashboard_port_constant() {
assert_eq!(DASHBOARD_PORT, 11391);
}
#[tokio::test]
async fn test_check_dashboard_health_invalid_port() {
let is_healthy = check_dashboard_health(65000).await;
assert!(!is_healthy);
}
#[tokio::test]
async fn test_check_dashboard_health_default_port_not_running() {
let is_healthy = check_dashboard_health(DASHBOARD_PORT).await;
if !is_healthy {
assert!(!is_healthy); }
}
}