ux(system): move Reclaim button top-right, make it actually free space
Three fixes from testing the storage page: - Placement: the "Reclaim space" button moves into the page header, top-right (matching the metric page), instead of sitting in the body. - It now actually reclaims: build cache needs -a to drop (docker reports 0 B "reclaimable" without it, but it's pure cache — safe to clear), so the CLI uses `docker builder prune -af`. Previously the safe scope freed ~nothing on a box whose reclaimable was mostly cache. - Honest "Reclaimable" number: /api/system/storage was counting the whole build cache AND unused tagged images, overstating what the safe prune frees (e.g. 340 MB shown, ~96 MB per docker, button cleared 0). Reclaimable now = dangling images + build cache only; stopped containers and volumes are never counted (the safe prune never touches them). Headline now matches the button's effect. Also simplify the CLI output (drop the jargony scope notice and the reclaimed-total greps) and re-enable the now-persistent header button after the post-reclaim refreshes. Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
816b96fe97
commit
49cf7e8bec
@ -334,12 +334,21 @@ router.get('/storage', async (req, res) => {
|
|||||||
const df = await storageCache.get('df', () => dockerRequest('GET', '/system/df'));
|
const df = await storageCache.get('df', () => dockerRequest('GET', '/system/df'));
|
||||||
if (!df) return res.status(500).json({ error: 'no_data' });
|
if (!df) return res.status(500).json({ error: 'no_data' });
|
||||||
// Roll the verbose response up into headline numbers per category.
|
// 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('<none>'));
|
||||||
|
};
|
||||||
const sumImages = (df.Images || []).reduce(
|
const sumImages = (df.Images || []).reduce(
|
||||||
(a, im) => {
|
(a, im) => {
|
||||||
a.count++;
|
a.count++;
|
||||||
a.size += im.Size || 0;
|
a.size += im.Size || 0;
|
||||||
a.shared += im.SharedSize || 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;
|
return a;
|
||||||
},
|
},
|
||||||
{ count: 0, size: 0, shared: 0, reclaimable: 0 }
|
{ count: 0, size: 0, shared: 0, reclaimable: 0 }
|
||||||
@ -348,18 +357,14 @@ router.get('/storage', async (req, res) => {
|
|||||||
(a, c) => {
|
(a, c) => {
|
||||||
a.count++;
|
a.count++;
|
||||||
a.size += c.SizeRw || 0;
|
a.size += c.SizeRw || 0;
|
||||||
if (c.State && c.State !== 'running') a.reclaimable += c.SizeRw || 0;
|
|
||||||
return a;
|
return a;
|
||||||
},
|
},
|
||||||
{ count: 0, size: 0, reclaimable: 0 }
|
{ count: 0, size: 0, reclaimable: 0 }
|
||||||
);
|
);
|
||||||
const sumVolumes = (df.Volumes || []).reduce(
|
const sumVolumes = (df.Volumes || []).reduce(
|
||||||
(a, v) => {
|
(a, v) => {
|
||||||
const sz = (v.UsageData && v.UsageData.Size) || 0;
|
|
||||||
const refs = (v.UsageData && v.UsageData.RefCount) || 0;
|
|
||||||
a.count++;
|
a.count++;
|
||||||
a.size += sz;
|
a.size += (v.UsageData && v.UsageData.Size) || 0;
|
||||||
if (refs <= 0) a.reclaimable += sz;
|
|
||||||
return a;
|
return a;
|
||||||
},
|
},
|
||||||
{ count: 0, size: 0, reclaimable: 0 }
|
{ count: 0, size: 0, reclaimable: 0 }
|
||||||
|
|||||||
@ -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); }
|
.sys-storage-stat-recl .sys-storage-stat-v { color: var(--status-warning); }
|
||||||
|
|
||||||
/* "Reclaim space" action — orange to match the Reclaimable stat it sits
|
/* Storage header: title left, "Reclaim space" action top-right (matches the
|
||||||
under. Safe prune only (build cache + dangling images). */
|
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 {
|
.sys-storage-reclaim {
|
||||||
align-self: flex-start;
|
|
||||||
margin-top: 14px;
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 9px 16px;
|
padding: 9px 16px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
color: var(--status-warning);
|
color: var(--status-warning);
|
||||||
background: rgba(var(--status-warning-rgb), 0.12);
|
background: rgba(var(--status-warning-rgb), 0.12);
|
||||||
border: 1px solid rgba(var(--status-warning-rgb), 0.4);
|
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 svg { flex: 0 0 auto; }
|
||||||
.sys-storage-reclaim.is-running svg { animation: sysReclaimSpin 0.8s linear infinite; }
|
.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); } }
|
@keyframes sysReclaimSpin { to { transform: rotate(360deg); } }
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.sys-storage-reclaim.is-running svg { animation: none; }
|
.sys-storage-reclaim.is-running svg { animation: none; }
|
||||||
|
|||||||
@ -55,7 +55,10 @@ class SystemStoragePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Confirm, then run the safe reclaim task and re-read usage as it lands.
|
// 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) {
|
_reclaim(btn) {
|
||||||
|
const done = () => { btn.disabled = false; btn.classList.remove('is-running'); };
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.classList.add('is-running');
|
btn.classList.add('is-running');
|
||||||
@ -64,22 +67,21 @@ class SystemStoragePage {
|
|||||||
throw new Error('Task system not ready');
|
throw new Error('Task system not ready');
|
||||||
}
|
}
|
||||||
await window.tasksManager.router.routeAction('system_reclaim');
|
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
|
// The prune runs in the background task processor; re-read
|
||||||
// usage a couple of times so the numbers settle without a
|
// 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()), 3000);
|
||||||
setTimeout(() => this._load().then(() => this._render()), 8000);
|
setTimeout(() => { this._load().then(() => this._render()); done(); }, 8000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
window.notificationSystem?.show?.(`Reclaim failed: ${err.message || err}`, 'error');
|
window.notificationSystem?.show?.(`Reclaim failed: ${err.message || err}`, 'error');
|
||||||
btn.disabled = false;
|
done();
|
||||||
btn.classList.remove('is-running');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (window.showConfirmation) {
|
if (window.showConfirmation) {
|
||||||
window.showConfirmation(
|
window.showConfirmation(
|
||||||
'Reclaim Docker space',
|
'Reclaim space',
|
||||||
'Remove the build cache and dangling (untagged) images? Volumes, app data, and images currently in use are left untouched.',
|
'Clear the build cache and dangling images? Volumes and images in use are left untouched.',
|
||||||
run,
|
run,
|
||||||
'Reclaim space',
|
'Reclaim space',
|
||||||
'Cancel',
|
'Cancel',
|
||||||
@ -113,6 +115,10 @@ class SystemStoragePage {
|
|||||||
<h1>Storage</h1>
|
<h1>Storage</h1>
|
||||||
<p class="sys-storage-sub">Docker disk usage — images, containers, volumes, and build cache.</p>
|
<p class="sys-storage-sub">Docker disk usage — images, containers, volumes, and build cache.</p>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="sys-storage-reclaim" data-storage-reclaim title="Clear build cache and dangling images (volumes and images in use are kept)">
|
||||||
|
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||||
|
Reclaim space
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sys-storage-body" data-storage-body>
|
<div class="sys-storage-body" data-storage-body>
|
||||||
<div class="sys-storage-loading">Reading Docker daemon…</div>
|
<div class="sys-storage-loading">Reading Docker daemon…</div>
|
||||||
@ -203,13 +209,8 @@ class SystemStoragePage {
|
|||||||
<div class="sys-storage-stat sys-storage-stat-recl">
|
<div class="sys-storage-stat sys-storage-stat-recl">
|
||||||
<span class="sys-storage-stat-k">Reclaimable</span>
|
<span class="sys-storage-stat-k">Reclaimable</span>
|
||||||
<strong class="sys-storage-stat-v">${fmt.bytes(recl)}</strong>
|
<strong class="sys-storage-stat-v">${fmt.bytes(recl)}</strong>
|
||||||
<span class="sys-storage-stat-sub">${reclPct.toFixed(0)}% of total · stopped containers, dangling images, unused volumes & cache</span>
|
<span class="sys-storage-stat-sub">${reclPct.toFixed(0)}% of total · build cache & dangling images</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="sys-storage-reclaim" data-storage-reclaim title="Remove build cache and dangling images (volumes and in-use images are kept)">
|
|
||||||
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
|
||||||
Reclaim space
|
|
||||||
</button>
|
|
||||||
<span class="sys-storage-reclaim-note">Frees build cache & dangling images · volumes and in-use images are kept</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
|
|||||||
@ -3,25 +3,20 @@
|
|||||||
# System Commands Handler
|
# System Commands Handler
|
||||||
# Handles all system subcommands by calling core functions
|
# Handles all system subcommands by calling core functions
|
||||||
|
|
||||||
# Reclaim Docker disk space — SAFE scope only: build cache + dangling
|
# Safe disk reclaim: clear the whole build cache (-a; it's pure cache, always
|
||||||
# (untagged) images. Never prunes volumes (app data) or tagged/in-use images,
|
# safe to drop) and remove dangling images. Never touches volumes or in-use
|
||||||
# so nothing an app relies on is removed. runFileOp targets the correct daemon
|
# images. runFileOp hits the right daemon (rootless: as the install user).
|
||||||
# (rootless: as the install user with DOCKER_HOST set).
|
|
||||||
reclaimDockerSpace()
|
reclaimDockerSpace()
|
||||||
{
|
{
|
||||||
isHeader "Reclaiming Docker Space"
|
isHeader "Reclaiming Space"
|
||||||
isNotice "Safe scope: build cache + dangling images only (no volumes, no in-use images)."
|
|
||||||
|
|
||||||
local cache_out image_out
|
runFileOp docker builder prune -af >/dev/null 2>&1
|
||||||
cache_out=$(runFileOp docker builder prune -f 2>&1)
|
checkSuccess "Cleared build cache"
|
||||||
checkSuccess "Pruned build cache"
|
|
||||||
echo "$cache_out" | grep -i "Total:" | sed 's/^/ /'
|
|
||||||
|
|
||||||
image_out=$(runFileOp docker image prune -f 2>&1)
|
runFileOp docker image prune -f >/dev/null 2>&1
|
||||||
checkSuccess "Pruned dangling images"
|
checkSuccess "Removed dangling images"
|
||||||
echo "$image_out" | grep -i "Total reclaimed space" | sed 's/^/ /'
|
|
||||||
|
|
||||||
isSuccessful "Reclaim complete"
|
isSuccessful "Done"
|
||||||
}
|
}
|
||||||
|
|
||||||
cliHandleSystemCommands()
|
cliHandleSystemCommands()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user