diff --git a/containers/libreportal/backend/routes/docker-info-routes.js b/containers/libreportal/backend/routes/docker-info-routes.js index 8e4791b..4f1057d 100644 --- a/containers/libreportal/backend/routes/docker-info-routes.js +++ b/containers/libreportal/backend/routes/docker-info-routes.js @@ -334,12 +334,21 @@ router.get('/storage', async (req, res) => { const df = await storageCache.get('df', () => dockerRequest('GET', '/system/df')); if (!df) return res.status(500).json({ error: 'no_data' }); // Roll the verbose response up into headline numbers per category. + // "Reclaimable" across this page reflects exactly what the Reclaim + // button frees: dangling images + the whole build cache. Tagged-but- + // unused images, stopped containers and volumes are deliberately NOT + // counted — the safe prune leaves them alone — so the headline number + // matches the button's effect instead of overstating it. + const isDangling = (im) => { + const tags = im.RepoTags || []; + return tags.length === 0 || tags.every(t => t.includes('')); + }; const sumImages = (df.Images || []).reduce( (a, im) => { a.count++; a.size += im.Size || 0; a.shared += im.SharedSize || 0; - if (!im.Containers || im.Containers <= 0) a.reclaimable += im.Size || 0; + if (isDangling(im)) a.reclaimable += im.Size || 0; return a; }, { count: 0, size: 0, shared: 0, reclaimable: 0 } @@ -348,18 +357,14 @@ router.get('/storage', async (req, res) => { (a, c) => { a.count++; a.size += c.SizeRw || 0; - if (c.State && c.State !== 'running') a.reclaimable += c.SizeRw || 0; return a; }, { count: 0, size: 0, reclaimable: 0 } ); const sumVolumes = (df.Volumes || []).reduce( (a, v) => { - const sz = (v.UsageData && v.UsageData.Size) || 0; - const refs = (v.UsageData && v.UsageData.RefCount) || 0; a.count++; - a.size += sz; - if (refs <= 0) a.reclaimable += sz; + a.size += (v.UsageData && v.UsageData.Size) || 0; return a; }, { count: 0, size: 0, reclaimable: 0 } diff --git a/containers/libreportal/frontend/css/admin.css b/containers/libreportal/frontend/css/admin.css index 67682d5..c843841 100644 --- a/containers/libreportal/frontend/css/admin.css +++ b/containers/libreportal/frontend/css/admin.css @@ -1010,17 +1010,22 @@ table.sys-apps tr:hover td { background: rgba(var(--text-rgb), 0.03); } } .sys-storage-stat-recl .sys-storage-stat-v { color: var(--status-warning); } -/* "Reclaim space" action — orange to match the Reclaimable stat it sits - under. Safe prune only (build cache + dangling images). */ +/* Storage header: title left, "Reclaim space" action top-right (matches the + metric page's header layout). */ +.sys-storage-page .page-header { + align-items: flex-start; + justify-content: space-between; +} +/* "Reclaim space" action — orange. Safe prune only (build cache + dangling + images); never volumes or in-use images. */ .sys-storage-reclaim { - align-self: flex-start; - margin-top: 14px; display: inline-flex; align-items: center; gap: 8px; padding: 9px 16px; font-size: 0.85rem; font-weight: 600; + white-space: nowrap; color: var(--status-warning); background: rgba(var(--status-warning-rgb), 0.12); border: 1px solid rgba(var(--status-warning-rgb), 0.4); @@ -1041,11 +1046,6 @@ table.sys-apps tr:hover td { background: rgba(var(--text-rgb), 0.03); } } .sys-storage-reclaim svg { flex: 0 0 auto; } .sys-storage-reclaim.is-running svg { animation: sysReclaimSpin 0.8s linear infinite; } -.sys-storage-reclaim-note { - margin-top: 6px; - font-size: 0.72rem; - color: rgba(var(--text-rgb), 0.5); -} @keyframes sysReclaimSpin { to { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { .sys-storage-reclaim.is-running svg { animation: none; } diff --git a/containers/libreportal/frontend/js/components/admin/system-storage-page.js b/containers/libreportal/frontend/js/components/admin/system-storage-page.js index 79fbfd8..eb51cda 100644 --- a/containers/libreportal/frontend/js/components/admin/system-storage-page.js +++ b/containers/libreportal/frontend/js/components/admin/system-storage-page.js @@ -55,7 +55,10 @@ class SystemStoragePage { } // Confirm, then run the safe reclaim task and re-read usage as it lands. + // The button lives in the header (not the body), so it survives the + // _render() refreshes below — re-enable it explicitly when done. _reclaim(btn) { + const done = () => { btn.disabled = false; btn.classList.remove('is-running'); }; const run = async () => { btn.disabled = true; btn.classList.add('is-running'); @@ -64,22 +67,21 @@ class SystemStoragePage { throw new Error('Task system not ready'); } await window.tasksManager.router.routeAction('system_reclaim'); - window.notificationSystem?.show?.('Reclaiming Docker space…', 'info'); + window.notificationSystem?.show?.('Reclaiming space…', 'info'); // The prune runs in the background task processor; re-read // usage a couple of times so the numbers settle without a - // manual refresh. Each _render rebuilds the button fresh. + // manual refresh. setTimeout(() => this._load().then(() => this._render()), 3000); - setTimeout(() => this._load().then(() => this._render()), 8000); + setTimeout(() => { this._load().then(() => this._render()); done(); }, 8000); } catch (err) { window.notificationSystem?.show?.(`Reclaim failed: ${err.message || err}`, 'error'); - btn.disabled = false; - btn.classList.remove('is-running'); + done(); } }; if (window.showConfirmation) { window.showConfirmation( - 'Reclaim Docker space', - 'Remove the build cache and dangling (untagged) images? Volumes, app data, and images currently in use are left untouched.', + 'Reclaim space', + 'Clear the build cache and dangling images? Volumes and images in use are left untouched.', run, 'Reclaim space', 'Cancel', @@ -113,6 +115,10 @@ class SystemStoragePage {

Storage

Docker disk usage — images, containers, volumes, and build cache.

+
Reading Docker daemon…
@@ -203,13 +209,8 @@ class SystemStoragePage {
Reclaimable ${fmt.bytes(recl)} - ${reclPct.toFixed(0)}% of total · stopped containers, dangling images, unused volumes & cache + ${reclPct.toFixed(0)}% of total · build cache & dangling images
- - Frees build cache & dangling images · volumes and in-use images are kept
`; diff --git a/scripts/cli/commands/system/cli_system_commands.sh b/scripts/cli/commands/system/cli_system_commands.sh index 3e5ddfc..2d99afa 100755 --- a/scripts/cli/commands/system/cli_system_commands.sh +++ b/scripts/cli/commands/system/cli_system_commands.sh @@ -3,25 +3,20 @@ # System Commands Handler # Handles all system subcommands by calling core functions -# Reclaim Docker disk space — SAFE scope only: build cache + dangling -# (untagged) images. Never prunes volumes (app data) or tagged/in-use images, -# so nothing an app relies on is removed. runFileOp targets the correct daemon -# (rootless: as the install user with DOCKER_HOST set). +# Safe disk reclaim: clear the whole build cache (-a; it's pure cache, always +# safe to drop) and remove dangling images. Never touches volumes or in-use +# images. runFileOp hits the right daemon (rootless: as the install user). reclaimDockerSpace() { - isHeader "Reclaiming Docker Space" - isNotice "Safe scope: build cache + dangling images only (no volumes, no in-use images)." + isHeader "Reclaiming Space" - local cache_out image_out - cache_out=$(runFileOp docker builder prune -f 2>&1) - checkSuccess "Pruned build cache" - echo "$cache_out" | grep -i "Total:" | sed 's/^/ /' + runFileOp docker builder prune -af >/dev/null 2>&1 + checkSuccess "Cleared build cache" - image_out=$(runFileOp docker image prune -f 2>&1) - checkSuccess "Pruned dangling images" - echo "$image_out" | grep -i "Total reclaimed space" | sed 's/^/ /' + runFileOp docker image prune -f >/dev/null 2>&1 + checkSuccess "Removed dangling images" - isSuccessful "Reclaim complete" + isSuccessful "Done" } cliHandleSystemCommands()