Compare commits

..

No commits in common. "f792cf55f63d6036461a585d6a78041cbd96b9f0" and "816b96fe97c0c121254c3145ef299021241450a4" have entirely different histories.

4 changed files with 42 additions and 43 deletions

View File

@ -334,21 +334,12 @@ 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('<none>'));
};
const sumImages = (df.Images || []).reduce(
(a, im) => {
a.count++;
a.size += im.Size || 0;
a.shared += im.SharedSize || 0;
if (isDangling(im)) a.reclaimable += im.Size || 0;
if (!im.Containers || im.Containers <= 0) a.reclaimable += im.Size || 0;
return a;
},
{ count: 0, size: 0, shared: 0, reclaimable: 0 }
@ -357,14 +348,18 @@ 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 += (v.UsageData && v.UsageData.Size) || 0;
a.size += sz;
if (refs <= 0) a.reclaimable += sz;
return a;
},
{ count: 0, size: 0, reclaimable: 0 }

View File

@ -1010,22 +1010,17 @@ 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); }
/* 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. */
/* "Reclaim space" action orange to match the Reclaimable stat it sits
under. Safe prune only (build cache + dangling 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);
@ -1046,6 +1041,11 @@ 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; }

View File

@ -55,10 +55,7 @@ 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');
@ -67,21 +64,22 @@ class SystemStoragePage {
throw new Error('Task system not ready');
}
await window.tasksManager.router.routeAction('system_reclaim');
window.notificationSystem?.show?.('Reclaiming space…', 'info');
window.notificationSystem?.show?.('Reclaiming Docker 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.
// manual refresh. Each _render rebuilds the button fresh.
setTimeout(() => this._load().then(() => this._render()), 3000);
setTimeout(() => { this._load().then(() => this._render()); done(); }, 8000);
setTimeout(() => this._load().then(() => this._render()), 8000);
} catch (err) {
window.notificationSystem?.show?.(`Reclaim failed: ${err.message || err}`, 'error');
done();
btn.disabled = false;
btn.classList.remove('is-running');
}
};
if (window.showConfirmation) {
window.showConfirmation(
'Reclaim space',
'Clear the build cache and dangling images? Volumes and images in use are left untouched.',
'Reclaim Docker space',
'Remove the build cache and dangling (untagged) images? Volumes, app data, and images currently in use are left untouched.',
run,
'Reclaim space',
'Cancel',
@ -115,10 +113,6 @@ class SystemStoragePage {
<h1>Storage</h1>
<p class="sys-storage-sub">Docker disk usage images, containers, volumes, and build cache.</p>
</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 class="sys-storage-body" data-storage-body>
<div class="sys-storage-loading">Reading Docker daemon</div>
@ -209,8 +203,13 @@ class SystemStoragePage {
<div class="sys-storage-stat sys-storage-stat-recl">
<span class="sys-storage-stat-k">Reclaimable</span>
<strong class="sys-storage-stat-v">${fmt.bytes(recl)}</strong>
<span class="sys-storage-stat-sub">${reclPct.toFixed(0)}% of total · build cache &amp; dangling images</span>
<span class="sys-storage-stat-sub">${reclPct.toFixed(0)}% of total · stopped containers, dangling images, unused volumes &amp; cache</span>
</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 &amp; dangling images · volumes and in-use images are kept</span>
</div>
</div>`;

View File

@ -3,20 +3,25 @@
# System Commands Handler
# Handles all system subcommands by calling core functions
# 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).
# 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).
reclaimDockerSpace()
{
isHeader "Reclaiming Space"
isHeader "Reclaiming Docker Space"
isNotice "Safe scope: build cache + dangling images only (no volumes, no in-use images)."
runFileOp docker builder prune -af >/dev/null 2>&1
checkSuccess "Cleared build cache"
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 image prune -f >/dev/null 2>&1
checkSuccess "Removed dangling images"
image_out=$(runFileOp docker image prune -f 2>&1)
checkSuccess "Pruned dangling images"
echo "$image_out" | grep -i "Total reclaimed space" | sed 's/^/ /'
isSuccessful "Done"
isSuccessful "Reclaim complete"
}
cliHandleSystemCommands()