Merge claude/2
This commit is contained in:
commit
f792cf55f6
@ -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