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
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { useShallow } from "zustand/react/shallow";
import type { ProjectRemovalReport } from "../../../lib/types";
import { useAppState } from "../../../store/appState";
import { useProjectActions } from "../../../hooks/useProjectActions";
import { useProjects } from "../../../hooks/useProjects";
@@ -30,6 +31,16 @@ const TABS = [
export type ProjectHomeTabId = (typeof TABS)[number]["id"];
/** Names what a `ProjectRemovalReport` says survived, for the leftover toast. */
function describeLeftovers(report: ProjectRemovalReport): string {
const parts: string[] = [];
if (report.container) parts.push("its container");
if (report.image) parts.push("its saved image");
if (report.volumes.length === 1) parts.push("a volume");
else if (report.volumes.length > 1) parts.push(`${report.volumes.length} volumes`);
return parts.join(", ");
}
interface Props {
projectId: string;
active: boolean;
@@ -282,7 +293,14 @@ export default function ProjectHome({ projectId, active }: Props) {
onConfirm={async () => {
setConfirmRemove(false);
try {
await remove(project.id);
const report = await remove(project.id);
if (report.container || report.image || report.volumes.length > 0) {
useAppState.getState().pushToast({
kind: "info",
message: `${project.name}” was removed, but some Docker resources are still on disk`,
detail: `Triple-C will retry removing ${describeLeftovers(report)} the next time it starts.`,
});
}
} catch (e) {
useAppState.getState().pushToast({
kind: "error",