Report and retry Docker resources remove_project could not delete
Secret Scan / scan (push) Successful in 10s
Build App (Preview) / compute-version (pull_request) Successful in 7s
Secret Scan / scan (pull_request) Successful in 8s
Build App (Preview) / create-release (pull_request) Successful in 5s
Build App (Preview) / build-linux (pull_request) Successful in 6m5s
Build App (Preview) / build-macos (pull_request) Successful in 2m45s
Build App (Preview) / build-windows (pull_request) Successful in 5m42s
Build App (Preview) / prune-previews (pull_request) Successful in 3s

remove_project_volumes always returned Ok(()) regardless of what actually
happened, making the `if let Err(e)` guarding it at every call site dead
code. remove_project then dropped the project record unconditionally, so a
volume, image or container that failed to delete became permanently
unreachable — confirmed against a real orphaned volume pair found in the
wild (fixes #31).

remove_project_volumes/remove_snapshot_image/remove_container now report
what they could not remove (treating "already gone" as success rather than
a leftover), remove_project surfaces this to the user via a toast, and
before dropping the project record it writes a pending-cleanup record that
startup housekeeping retries automatically on the next launch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGjXq6fqtAFHdbhk4f3PfZ
This commit is contained in:
2026-08-27 08:18:41 -07:00
co-authored by Claude Sonnet 5
parent 1a79852f65
commit 4827170715
10 changed files with 520 additions and 32 deletions
+44
View File
@@ -422,6 +422,31 @@ pub enum ProjectStatus {
Error,
}
/// What `remove_project` could not delete, named so the UI can say so instead
/// of reporting a clean removal that was not one.
///
/// The project record is dropped from `projects.json` regardless — see the
/// long comment on `remove_project` for why refusing is not the answer — but
/// anything named here is also written to a pending-cleanup record that
/// startup housekeeping retries, so it stays reachable after the project it
/// belonged to no longer exists.
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ProjectRemovalReport {
/// The project's container, if it could not be removed.
pub container: Option<String>,
/// The `triple-c-snapshot-{id}` image, if it could not be removed.
pub image: Option<String>,
/// Named volumes (home, claude config) that could not be removed.
pub volumes: Vec<String>,
}
impl ProjectRemovalReport {
/// True when nothing was left behind.
pub fn is_clean(&self) -> bool {
self.container.is_none() && self.image.is_none() && self.volumes.is_empty()
}
}
/// Which AI model backend/provider the project uses.
/// - `Anthropic`: Direct Anthropic API (user runs `claude login` inside the container)
/// - `Bedrock`: AWS Bedrock with per-project AWS credentials
@@ -650,6 +675,25 @@ impl Project {
mod tests {
use super::*;
// ── ProjectRemovalReport ────────────────────────────────────────────────
#[test]
fn a_report_is_clean_only_with_nothing_left_behind() {
assert!(ProjectRemovalReport::default().is_clean());
let mut r = ProjectRemovalReport::default();
r.container = Some("abc123".to_string());
assert!(!r.is_clean(), "a leftover container must not read as clean");
let mut r = ProjectRemovalReport::default();
r.image = Some("triple-c-snapshot-x:latest".to_string());
assert!(!r.is_clean(), "a leftover image must not read as clean");
let mut r = ProjectRemovalReport::default();
r.volumes.push("triple-c-home-x".to_string());
assert!(!r.is_clean(), "a leftover volume must not read as clean");
}
// ── Custom environment variable names ─────────────────────────────────
#[test]