diff --git a/backend/src/routes/stacks.ts b/backend/src/routes/stacks.ts index 8e3e1953..c8098a2f 100644 --- a/backend/src/routes/stacks.ts +++ b/backend/src/routes/stacks.ts @@ -526,6 +526,7 @@ stacksRouter.delete('/:stackName', async (req: Request, res: Response) => { if (!isValidStackName(stackName)) { return res.status(400).json({ error: 'Invalid stack name' }); } + const pruneVolumes = req.query.pruneVolumes === 'true'; try { try { await ComposeService.getInstance(req.nodeId).downStack(stackName); @@ -533,6 +534,15 @@ stacksRouter.delete('/:stackName', async (req: Request, res: Response) => { console.warn(`[Teardown] Docker down failed or nothing to clean up for ${stackName}`); } + if (pruneVolumes) { + try { + const result = await DockerController.getInstance().pruneManagedOnly('volumes', [stackName]); + console.log(`[Stacks] Pruned volumes for ${stackName}: ${result.reclaimedBytes} bytes reclaimed`); + } catch (pruneErr) { + console.warn(`[Stacks] Volume prune failed for ${stackName}, continuing delete:`, pruneErr); + } + } + let fsErr: unknown = null; try { await FileSystemService.getInstance(req.nodeId).deleteStack(stackName); diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 2832ad36..7585c1c9 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -272,6 +272,7 @@ export default function EditorLayout() { const [gitDeployNow, setGitDeployNow] = useState(false); const [creatingFromGit, setCreatingFromGit] = useState(false); const [stackToDelete, setStackToDelete] = useState(null); + const [pruneVolumesOnDelete, setPruneVolumesOnDelete] = useState(false); const [pendingUnsavedLoad, setPendingUnsavedLoad] = useState(null); const [pendingUnsavedNode, setPendingUnsavedNode] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -1557,7 +1558,10 @@ export default function EditorLayout() { if (isStackBusy(deleteKey)) return; setStackAction(deleteKey, 'delete'); try { - const response = await apiFetch(`/stacks/${stackToDelete}`, { + const url = pruneVolumesOnDelete + ? `/stacks/${stackToDelete}?pruneVolumes=true` + : `/stacks/${stackToDelete}`; + const response = await apiFetch(url, { method: 'DELETE', }); if (!response.ok) { @@ -1567,6 +1571,7 @@ export default function EditorLayout() { toast.success('Stack deleted successfully!'); setDeleteDialogOpen(false); setStackToDelete(null); + setPruneVolumesOnDelete(false); if (selectedFile === stackToDelete) { setSelectedFile(null); setContent(''); @@ -1981,7 +1986,7 @@ export default function EditorLayout() { stop: () => executeStackActionByFile(file, 'stop', 'stop'), restart: () => executeStackActionByFile(file, 'restart', 'restart'), update: () => executeStackActionByFile(file, 'update', 'update'), - remove: () => { setStackToDelete(stackName); setDeleteDialogOpen(true); }, + remove: () => { setStackToDelete(stackName); setPruneVolumesOnDelete(false); setDeleteDialogOpen(true); }, pin: () => pin(file), unpin: () => unpin(file), setAutoUpdateEnabled: async (enabled: boolean) => { @@ -2488,6 +2493,7 @@ export default function EditorLayout() { disabled={loadingAction !== null} onClick={() => { setStackToDelete(selectedFile); + setPruneVolumesOnDelete(false); setDeleteDialogOpen(true); }} > @@ -2938,6 +2944,16 @@ export default function EditorLayout() { Are you sure you want to delete {stackToDelete}? This action cannot be undone. +
+ setPruneVolumesOnDelete(v === true)} + /> + +
setDeleteDialogOpen(false)}>Cancel Delete