Compare commits

..

No commits in common. "327fda8cd9eb3b5feb1ce95c961cbfc2d8a695f6" and "a31d77b7510c6d939f1dfc2aaaf37959cdd2a422" have entirely different histories.

2 changed files with 28 additions and 21 deletions

View File

@ -21,12 +21,12 @@
// Last N lines of combined stdout/stderr, multiplex-decoded.
//
// GET /api/system/storage
// `docker system df` — total + reclaimable for the engine overhead worth
// acting on (images, build cache). Cached for STORAGE_TTL_MS because this is
// one of the more expensive calls on a busy daemon. Named volumes and
// container writable layers are omitted: LibrePortal apps keep data in bind
// mounts, so both read ~empty here — per-app on-disk usage is generated
// separately (see webuiSystemAppStorage / /data/system/app_storage.json).
// `docker system df` — total + reclaimable per category (images,
// 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,15 +336,12 @@ router.get('/storage', async (req, res) => {
try {
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. We
// surface only the engine overhead worth acting on — images and build
// cache — and skip container writable layers: for LibrePortal that's a
// near-zero scratch number (app data lives in bind mounts, shown per-app
// elsewhere) that just confuses the picture.
// "Reclaimable" reflects exactly what the Reclaim button frees: dangling
// images + the whole build cache. Tagged-but-unused images are
// deliberately NOT counted — the safe prune leaves them alone — so the
// headline matches the button's effect instead of overstating it.
// 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 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>'));
@ -359,6 +356,14 @@ router.get('/storage', async (req, res) => {
},
{ count: 0, size: 0, shared: 0, reclaimable: 0 }
);
const sumContainers = (df.Containers || []).reduce(
(a, c) => {
a.count++;
a.size += c.SizeRw || 0;
return a;
},
{ count: 0, size: 0, reclaimable: 0 }
);
const sumBuild = (df.BuildCache || []).reduce(
(a, b) => {
a.count++;
@ -382,12 +387,13 @@ router.get('/storage', async (req, res) => {
containers: im.Containers || 0,
created: im.Created,
}));
const total = sumImages.size + sumBuild.size;
const reclaimable = sumImages.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,
build_cache: sumBuild,
top_images: topImages,
updated: new Date().toISOString(),

View File

@ -7,8 +7,8 @@
// generator). The headline view for LibrePortal, whose data lives in bind
// mounts, not named volumes.
// - Docker engine `docker system df` — headline total + reclaimable, a
// donut (images / build cache), per-category cards, and the top images by
// size. The daemon's own overhead, separate from the per-app data.
// 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
@ -130,7 +130,7 @@ class SystemStoragePage {
<a href="/admin/config/system" data-back>Admin · System</a>
</div>
<h1>Storage</h1>
<p class="sys-storage-sub">On-disk space by app, plus Docker's own engine usage.</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 (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>
@ -146,6 +146,7 @@ class SystemStoragePage {
static segmentsFrom(d) {
return [
{ key: 'images', label: 'Images', color: 'accent', data: (d && d.images) || {} },
{ 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) || {} },
];
}
@ -230,7 +231,7 @@ class SystemStoragePage {
</div>`;
const catCards = `
<div class="sys-section-head"><h2>Docker engine</h2><span class="sys-chart-meta">images &amp; build cache the daemon's own usage, separate from your app data</span></div>
<div class="sys-section-head"><h2>Categories</h2></div>
<div class="sys-storage-cards">
${segments.map(s => {
const v = s.data || {};