diff --git a/app/src-tauri/src/commands/project_commands.rs b/app/src-tauri/src/commands/project_commands.rs index 46ecbf9..b30040d 100644 --- a/app/src-tauri/src/commands/project_commands.rs +++ b/app/src-tauri/src/commands/project_commands.rs @@ -259,16 +259,18 @@ pub async fn stop_project_container( .get(&project_id) .ok_or_else(|| format!("Project {} not found", project_id))?; - if let Some(ref container_id) = project.container_id { - state.projects_store.update_status(&project_id, ProjectStatus::Stopping)?; + state.projects_store.update_status(&project_id, ProjectStatus::Stopping)?; + if let Some(ref container_id) = project.container_id { // Close exec sessions for this project state.exec_manager.close_sessions_for_container(container_id).await; - docker::stop_container(container_id).await?; - state.projects_store.update_status(&project_id, ProjectStatus::Stopped)?; + if let Err(e) = docker::stop_container(container_id).await { + log::warn!("Docker stop failed for container {} (project {}): {} — resetting to Stopped anyway", container_id, project_id, e); + } } + state.projects_store.update_status(&project_id, ProjectStatus::Stopped)?; Ok(()) } diff --git a/app/src-tauri/src/storage/projects_store.rs b/app/src-tauri/src/storage/projects_store.rs index de6f3d5..5f930e4 100644 --- a/app/src-tauri/src/storage/projects_store.rs +++ b/app/src-tauri/src/storage/projects_store.rs @@ -70,17 +70,38 @@ impl ProjectsStore { (Vec::new(), false) }; + // Reconcile stale transient statuses: on a cold app start no Docker + // operations can be in flight, so Starting/Stopping are always stale. + let mut projects = projects; + let mut needs_save = needs_save; + for p in projects.iter_mut() { + match p.status { + crate::models::ProjectStatus::Starting | crate::models::ProjectStatus::Stopping => { + log::warn!( + "Reconciling stale '{}' status for project '{}' ({}) → Stopped", + serde_json::to_string(&p.status).unwrap_or_default().trim_matches('"'), + p.name, + p.id + ); + p.status = crate::models::ProjectStatus::Stopped; + p.updated_at = chrono::Utc::now().to_rfc3339(); + needs_save = true; + } + _ => {} + } + } + let store = Self { projects: Mutex::new(projects), file_path, }; - // Persist migrated format back to disk + // Persist migrated/reconciled format back to disk if needs_save { - log::info!("Migrated projects.json from single-path to multi-path format"); + log::info!("Saving reconciled/migrated projects.json to disk"); let projects = store.lock(); if let Err(e) = store.save(&projects) { - log::error!("Failed to save migrated projects: {}", e); + log::error!("Failed to save projects: {}", e); } } diff --git a/app/src/components/projects/ProjectCard.tsx b/app/src/components/projects/ProjectCard.tsx index 4c93a1d..306d990 100644 --- a/app/src/components/projects/ProjectCard.tsx +++ b/app/src/components/projects/ProjectCard.tsx @@ -315,9 +315,12 @@ export default function ProjectCard({ project }: Props) { ) : ( - - {project.status}... - + <> + + {project.status}... + + + )} { e?.stopPropagation?.(); setShowConfig(!showConfig); }}