Merge claude/2
This commit is contained in:
commit
f5f3db900a
@ -22,8 +22,11 @@
|
||||
//
|
||||
// GET /api/system/storage
|
||||
// `docker system df` — total + reclaimable per category (images,
|
||||
// containers, volumes, build cache). Cached for STORAGE_TTL_MS because
|
||||
// this is one of the more expensive calls on a busy daemon.
|
||||
// containers, build cache). Cached for STORAGE_TTL_MS because this is one
|
||||
// of the more expensive calls on a busy daemon. Named volumes are omitted:
|
||||
// LibrePortal apps keep data in bind mounts, so volume accounting is always
|
||||
// ~empty here — per-app on-disk usage is generated separately (see
|
||||
// webuiSystemAppStorage / /data/system/app_storage.json).
|
||||
//
|
||||
// Mounted at /api/system in routes.js (so paths are /api/system/containers
|
||||
// etc.). Uses the shared docker util (utils/docker.js) which talks to the
|
||||
@ -336,9 +339,9 @@ router.get('/storage', async (req, res) => {
|
||||
// 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.
|
||||
// unused images and stopped containers 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>'));
|
||||
@ -361,14 +364,6 @@ router.get('/storage', async (req, res) => {
|
||||
},
|
||||
{ count: 0, size: 0, reclaimable: 0 }
|
||||
);
|
||||
const sumVolumes = (df.Volumes || []).reduce(
|
||||
(a, v) => {
|
||||
a.count++;
|
||||
a.size += (v.UsageData && v.UsageData.Size) || 0;
|
||||
return a;
|
||||
},
|
||||
{ count: 0, size: 0, reclaimable: 0 }
|
||||
);
|
||||
const sumBuild = (df.BuildCache || []).reduce(
|
||||
(a, b) => {
|
||||
a.count++;
|
||||
@ -392,29 +387,15 @@ router.get('/storage', async (req, res) => {
|
||||
containers: im.Containers || 0,
|
||||
created: im.Created,
|
||||
}));
|
||||
// Largest volumes — reclaimable or otherwise. Useful when /docker
|
||||
// is hitting 90%+.
|
||||
const topVolumes = (df.Volumes || [])
|
||||
.slice()
|
||||
.sort((a, b) => ((b.UsageData?.Size) || 0) - ((a.UsageData?.Size) || 0))
|
||||
.slice(0, 10)
|
||||
.map(v => ({
|
||||
name: v.Name,
|
||||
driver: v.Driver,
|
||||
size: (v.UsageData && v.UsageData.Size) || 0,
|
||||
ref_count: (v.UsageData && v.UsageData.RefCount) || 0,
|
||||
}));
|
||||
const total = sumImages.size + sumContainers.size + sumVolumes.size + sumBuild.size;
|
||||
const reclaimable = sumImages.reclaimable + sumContainers.reclaimable + sumVolumes.reclaimable + sumBuild.reclaimable;
|
||||
const total = sumImages.size + sumContainers.size + sumBuild.size;
|
||||
const reclaimable = sumImages.reclaimable + sumContainers.reclaimable + sumBuild.reclaimable;
|
||||
res.set('Cache-Control', 'no-store');
|
||||
res.json({
|
||||
total, reclaimable,
|
||||
images: sumImages,
|
||||
containers: sumContainers,
|
||||
volumes: sumVolumes,
|
||||
build_cache: sumBuild,
|
||||
top_images: topImages,
|
||||
top_volumes: topVolumes,
|
||||
updated: new Date().toISOString(),
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@ -335,7 +335,7 @@ class AdminSystem {
|
||||
const head = `
|
||||
<div class="sys-section-head">
|
||||
<h2>Storage</h2>
|
||||
<span class="sys-chart-meta">${dk.containers_running ?? 0}/${dk.containers_total ?? 0} running · ${dk.images ?? 0} images · ${dk.volumes ?? 0} volumes</span>
|
||||
<span class="sys-chart-meta">${dk.containers_running ?? 0}/${dk.containers_total ?? 0} running · ${dk.images ?? 0} images</span>
|
||||
</div>`;
|
||||
if (!s || !s.total || !SP) {
|
||||
const msg = (s && !s.total) ? 'No Docker storage in use yet.' : 'Storage usage unavailable.';
|
||||
@ -368,7 +368,7 @@ class AdminSystem {
|
||||
<div class="sys-storage-srows">${rows}</div>
|
||||
<div class="sys-storage-summary-foot">
|
||||
<span class="sys-storage-recl-pill">${this.bytes(recl)} reclaimable${reclPct ? ` · ${reclPct}% of total` : ''}</span>
|
||||
<button type="button" class="sys-storage-more" data-sys-storage>View largest images & volumes →</button>
|
||||
<button type="button" class="sys-storage-more" data-sys-storage>View storage breakdown →</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
// Admin → System → Storage — Docker disk-usage breakdown.
|
||||
// Admin → System → Storage — where the disk is going.
|
||||
//
|
||||
// Mounted at /admin/config/system/storage. Visualises `docker system df`:
|
||||
// Mounted at /admin/config/system/storage. Two data sources:
|
||||
//
|
||||
// - Headline total + reclaimable
|
||||
// - Donut chart split by category (images / containers / volumes / build cache)
|
||||
// - Per-category cards (count + size + reclaimable)
|
||||
// - Top 10 images by size
|
||||
// - Top 10 volumes by size
|
||||
// - Storage by app — on-disk size of each app's bind-mounted data, from the
|
||||
// generated /data/system/app_storage.json (du, not docker — see the
|
||||
// generator). The headline view for LibrePortal, whose data lives in bind
|
||||
// mounts, not named volumes.
|
||||
// - Docker engine `docker system df` — headline total + reclaimable, a
|
||||
// per-category donut (images / containers / build cache), per-category cards,
|
||||
// and the top images by size.
|
||||
//
|
||||
// One backend call (GET /api/system/storage), cached server-side for 5s.
|
||||
// A "Reclaim space" button runs the safe prune (build cache + dangling
|
||||
// images, never volumes) via the system_reclaim task, then re-reads usage.
|
||||
// images, never app data) via the system_reclaim task, then re-reads usage.
|
||||
|
||||
class SystemStoragePage {
|
||||
constructor(rootId = 'config-section') {
|
||||
@ -81,7 +83,7 @@ class SystemStoragePage {
|
||||
if (window.showConfirmation) {
|
||||
window.showConfirmation(
|
||||
'Reclaim space',
|
||||
'Clear the build cache and dangling images? Volumes and images in use are left untouched.',
|
||||
'Clear the build cache and dangling images? Images in use and your app data are left untouched.',
|
||||
run,
|
||||
'Reclaim space',
|
||||
'Cancel',
|
||||
@ -115,9 +117,9 @@ class SystemStoragePage {
|
||||
<a href="/admin/config/system" data-back>Admin · System</a>
|
||||
</div>
|
||||
<h1>Storage</h1>
|
||||
<p class="sys-storage-sub">Docker disk usage — images, containers, volumes, and build cache.</p>
|
||||
<p class="sys-storage-sub">On-disk space by app, plus Docker's images, containers, 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)">
|
||||
<button type="button" class="sys-storage-reclaim" data-storage-reclaim title="Clear build cache and dangling images (images in use and app data 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>
|
||||
@ -131,7 +133,6 @@ class SystemStoragePage {
|
||||
static segmentsFrom(d) {
|
||||
return [
|
||||
{ key: 'images', label: 'Images', color: 'accent', data: (d && d.images) || {} },
|
||||
{ key: 'volumes', label: 'Volumes', color: 'status-success', data: (d && d.volumes) || {} },
|
||||
{ key: 'containers', label: 'Containers', color: 'status-info', data: (d && d.containers) || {} },
|
||||
{ key: 'build_cache', label: 'Build cache', color: 'status-warning', data: (d && d.build_cache) || {} },
|
||||
];
|
||||
@ -242,7 +243,6 @@ class SystemStoragePage {
|
||||
</div>`;
|
||||
|
||||
const topImages = Array.isArray(d.top_images) ? d.top_images : [];
|
||||
const topVolumes = Array.isArray(d.top_volumes) ? d.top_volumes : [];
|
||||
|
||||
const imagesTable = topImages.length ? `
|
||||
<div class="sys-section-head"><h2>Largest images</h2><span class="sys-chart-meta">top ${topImages.length} by size</span></div>
|
||||
@ -262,23 +262,6 @@ class SystemStoragePage {
|
||||
</table>
|
||||
</div>` : '';
|
||||
|
||||
const volumesTable = topVolumes.length ? `
|
||||
<div class="sys-section-head"><h2>Largest volumes</h2><span class="sys-chart-meta">top ${topVolumes.length} by size</span></div>
|
||||
<div class="sys-apps-wrap">
|
||||
<table class="sys-apps">
|
||||
<thead><tr><th>Name</th><th>Driver</th><th>Size</th><th>Refs</th></tr></thead>
|
||||
<tbody>
|
||||
${topVolumes.map(v => `
|
||||
<tr>
|
||||
<td class="sys-app-name">${fmt.escape(v.name)}</td>
|
||||
<td>${fmt.escape(v.driver || '—')}</td>
|
||||
<td>${fmt.bytes(v.size)}</td>
|
||||
<td>${v.ref_count}${v.ref_count === 0 ? ' <span class="sys-storage-orphan">orphan</span>' : ''}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>` : '';
|
||||
|
||||
// Storage by app — the number Docker can't give us: the on-disk size of
|
||||
// each app's bind-mounted data, measured by the generator. This is the
|
||||
// useful "what's eating my disk" view for LibrePortal, where app data
|
||||
@ -315,7 +298,7 @@ class SystemStoragePage {
|
||||
</div>
|
||||
${appBody}`;
|
||||
|
||||
body.innerHTML = `${headline}${appsSection}${catCards}${imagesTable}${volumesTable}`;
|
||||
body.innerHTML = `${headline}${appsSection}${catCards}${imagesTable}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user