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. // Last N lines of combined stdout/stderr, multiplex-decoded.
// //
// GET /api/system/storage // GET /api/system/storage
// `docker system df` — total + reclaimable for the engine overhead worth // `docker system df` — total + reclaimable per category (images,
// acting on (images, build cache). Cached for STORAGE_TTL_MS because this is // containers, build cache). Cached for STORAGE_TTL_MS because this is one
// one of the more expensive calls on a busy daemon. Named volumes and // of the more expensive calls on a busy daemon. Named volumes are omitted:
// container writable layers are omitted: LibrePortal apps keep data in bind // LibrePortal apps keep data in bind mounts, so volume accounting is always
// mounts, so both read ~empty here — per-app on-disk usage is generated // ~empty here — per-app on-disk usage is generated separately (see
// separately (see webuiSystemAppStorage / /data/system/app_storage.json). // webuiSystemAppStorage / /data/system/app_storage.json).
// //
// Mounted at /api/system in routes.js (so paths are /api/system/containers // 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 // etc.). Uses the shared docker util (utils/docker.js) which talks to the
@ -336,15 +336,12 @@ router.get('/storage', async (req, res) => {
try { try {
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. We // Roll the verbose response up into headline numbers per category.
// surface only the engine overhead worth acting on — images and build // "Reclaimable" across this page reflects exactly what the Reclaim
// cache — and skip container writable layers: for LibrePortal that's a // button frees: dangling images + the whole build cache. Tagged-but-
// near-zero scratch number (app data lives in bind mounts, shown per-app // unused images and stopped containers are deliberately NOT counted —
// elsewhere) that just confuses the picture. // the safe prune leaves them alone — so the headline number matches the
// "Reclaimable" reflects exactly what the Reclaim button frees: dangling // button's effect instead of overstating it.
// 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.
const isDangling = (im) => { const isDangling = (im) => {
const tags = im.RepoTags || []; const tags = im.RepoTags || [];
return tags.length === 0 || tags.every(t => t.includes('<none>')); 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 } { 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( const sumBuild = (df.BuildCache || []).reduce(
(a, b) => { (a, b) => {
a.count++; a.count++;
@ -382,12 +387,13 @@ router.get('/storage', async (req, res) => {
containers: im.Containers || 0, containers: im.Containers || 0,
created: im.Created, created: im.Created,
})); }));
const total = sumImages.size + sumBuild.size; const total = sumImages.size + sumContainers.size + sumBuild.size;
const reclaimable = sumImages.reclaimable + sumBuild.reclaimable; const reclaimable = sumImages.reclaimable + sumContainers.reclaimable + sumBuild.reclaimable;
res.set('Cache-Control', 'no-store'); res.set('Cache-Control', 'no-store');
res.json({ res.json({
total, reclaimable, total, reclaimable,
images: sumImages, images: sumImages,
containers: sumContainers,
build_cache: sumBuild, build_cache: sumBuild,
top_images: topImages, top_images: topImages,
updated: new Date().toISOString(), updated: new Date().toISOString(),

View File

@ -7,8 +7,8 @@
// generator). The headline view for LibrePortal, whose data lives in bind // generator). The headline view for LibrePortal, whose data lives in bind
// mounts, not named volumes. // mounts, not named volumes.
// - Docker engine `docker system df` — headline total + reclaimable, a // - Docker engine `docker system df` — headline total + reclaimable, a
// donut (images / build cache), per-category cards, and the top images by // per-category donut (images / containers / build cache), per-category cards,
// size. The daemon's own overhead, separate from the per-app data. // and the top images by size.
// //
// One backend call (GET /api/system/storage), cached server-side for 5s. // One backend call (GET /api/system/storage), cached server-side for 5s.
// A "Reclaim space" button runs the safe prune (build cache + dangling // 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> <a href="/admin/config/system" data-back>Admin · System</a>
</div> </div>
<h1>Storage</h1> <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> </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)"> <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> <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) { static segmentsFrom(d) {
return [ return [
{ key: 'images', label: 'Images', color: 'accent', data: (d && d.images) || {} }, { 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) || {} }, { key: 'build_cache', label: 'Build cache', color: 'status-warning', data: (d && d.build_cache) || {} },
]; ];
} }
@ -230,7 +231,7 @@ class SystemStoragePage {
</div>`; </div>`;
const catCards = ` 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"> <div class="sys-storage-cards">
${segments.map(s => { ${segments.map(s => {
const v = s.data || {}; const v = s.data || {};