From e6cc84271cfb71271d07b649d3b1866a4efc3816 Mon Sep 17 00:00:00 2001 From: librelad Date: Thu, 28 May 2026 21:28:19 +0100 Subject: [PATCH] =?UTF-8?q?ux(system):=20one=20unified=20Storage=20donut?= =?UTF-8?q?=20=E2=80=94=20apps=20+=20Docker=20together?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Put everything back in a single donut instead of an app-only one: a slice per app plus Docker's images and build cache, with one legend listing them all. Colours run continuously so app and Docker slices stay distinct, and the Docker cards below reuse the same colours. The per-folder app drill-down table stays. This is the "all the data, with app usage added" view that was asked for. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- .../components/admin/system-storage-page.js | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/containers/libreportal/frontend/js/components/admin/system-storage-page.js b/containers/libreportal/frontend/js/components/admin/system-storage-page.js index f3a4e88..c606c1f 100644 --- a/containers/libreportal/frontend/js/components/admin/system-storage-page.js +++ b/containers/libreportal/frontend/js/components/admin/system-storage-page.js @@ -202,22 +202,41 @@ class SystemStoragePage { const palette = SystemStoragePage.PALETTE; const colorFor = i => palette[i % palette.length]; - // ---- Headline: on-disk usage by app (the disk question that matters) ---- + // ---- Headline: ONE donut covering everything — a slice per app, then + // Docker's own categories (images, build cache). Colours run continuously + // across the whole list so app and Docker slices stay distinct, and the + // legend lists them all. This is the unified "where's my disk going" view. const appTotal = (as && as.total) || 0; - const appSegs = appRows.map((a, i) => ({ color: colorFor(i), data: { size: a.bytes || 0 } })); - const headline = appRows.length + const appSegs = appRows.map((a, i) => ({ label: a.app, color: colorFor(i), data: { size: a.bytes || 0 } })); + const dockerCats = d ? SystemStoragePage.segmentsFrom(d).map((s, j) => ({ ...s, color: colorFor(appSegs.length + j) })) : []; + const allSegs = [...appSegs, ...dockerCats]; + const grandTotal = allSegs.reduce((t, s) => t + ((s.data && s.data.size) || 0), 0); + const legend = ` +
    + ${allSegs.map(s => ` +
  • + + ${fmt.escape(s.label)} + + ${fmt.bytes((s.data && s.data.size) || 0)} +
  • `).join('')} +
`; + const headline = allSegs.length ? `
-
${SystemStoragePage.donutSvg(appSegs, appTotal, 'app data')}
+
+ ${SystemStoragePage.donutSvg(allSegs, grandTotal, 'in use')} + ${legend} +
App data on disk ${fmt.bytes(appTotal)} - ${appRows.length} app${appRows.length === 1 ? '' : 's'}${as && as.total_external ? ` · ${fmt.bytes(as.total_external)} on external drives` : ''} + ${appRows.length} app${appRows.length === 1 ? '' : 's'}${as && as.total_external ? ` · ${fmt.bytes(as.total_external)} external` : ''}
- ${d ? `
- Docker engine - ${fmt.bytes(d.total || 0)} - ${fmt.bytes(d.reclaimable || 0)} reclaimable + ${d ? `
+ Docker reclaimable + ${fmt.bytes(d.reclaimable || 0)} + of ${fmt.bytes(d.total || 0)} engine overhead
` : ''}
` @@ -262,10 +281,9 @@ class SystemStoragePage { // plus the largest images. The cleanup view, clearly separate from data. let dockerSection = ''; if (d) { - const segments = SystemStoragePage.segmentsFrom(d); const total = d.total || 0; const recl = d.reclaimable || 0; - const cards = segments.map(s => { + const cards = dockerCats.map(s => { const v = s.data || {}; const pct = v.size && total ? (v.size / total) * 100 : 0; const rPct = v.size ? (v.reclaimable / v.size) * 100 : 0;