From 67a841299cb5cf6a69e2bcdae8f3b57459286051 Mon Sep 17 00:00:00 2001 From: librelad Date: Thu, 28 May 2026 22:59:49 +0100 Subject: [PATCH] ux(system): disk ring shows LibrePortal as a portion + LP line on disk page Replace the disk gauge's concentric inner ring with a single ring whose leading portion is coloured to mark the LibrePortal share of the disk (one ring: total disk used overall, LibrePortal highlighted within it). On the full-screen Disk metric page, add a flat reference line marking LibrePortal's current share alongside the disk-usage trend. The gauge gains a `segment` option; the chart line is a "now" value (no historical LP series yet), so it's flat across the range. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- containers/libreportal/frontend/css/admin.css | 12 +++++++ .../js/components/admin/admin-system.js | 8 ++--- .../frontend/js/components/admin/charts.js | 27 ++++++++------- .../js/components/admin/system-metric-page.js | 34 +++++++++++++++++++ 4 files changed, 64 insertions(+), 17 deletions(-) diff --git a/containers/libreportal/frontend/css/admin.css b/containers/libreportal/frontend/css/admin.css index 77dd4b4..8cd6c59 100644 --- a/containers/libreportal/frontend/css/admin.css +++ b/containers/libreportal/frontend/css/admin.css @@ -525,6 +525,18 @@ table.sys-apps tr:hover td { background: rgba(var(--text-rgb), 0.03); } .sys-detail-axis-x { text-anchor: middle; } .sys-detail-peak { fill: var(--status-warning); filter: drop-shadow(0 0 6px rgba(var(--status-warning-rgb, 255 193 7), 0.6)); } .sys-detail-min { fill: rgba(var(--text-rgb), 0.55); } +/* LibrePortal share reference line on the Disk page. */ +.sys-detail-lpline { + stroke: var(--accent); + stroke-width: 1.5; + stroke-dasharray: 5 4; + opacity: 0.85; +} +.sys-detail-lplabel { + fill: var(--accent); + font-size: 12px; + font-weight: 600; +} .sys-detail-now { fill: rgb(var(--metric-rgb)); filter: drop-shadow(0 0 8px rgba(var(--metric-rgb), 0.7)); diff --git a/containers/libreportal/frontend/js/components/admin/admin-system.js b/containers/libreportal/frontend/js/components/admin/admin-system.js index 82ef868..5056231 100644 --- a/containers/libreportal/frontend/js/components/admin/admin-system.js +++ b/containers/libreportal/frontend/js/components/admin/admin-system.js @@ -218,9 +218,9 @@ class AdminSystem { : loadRatio >= 1.0 ? 'status-warning' : 'status-success'; - // Disk gauge gets a second inner ring for the slice of the disk that's - // LibrePortal (app data on disk + Docker images/cache), so it shows both - // total disk used (outer) and how much of that is us (inner). + // The disk ring colours its leading portion to show the slice that's + // LibrePortal (app data on disk + Docker images/cache) — one ring, total + // disk used overall, the LibrePortal part highlighted within it. const lpBytes = ((this.d.appStorage && this.d.appStorage.total_local) || 0) + ((this.d.storage && this.d.storage.total) || 0); const diskTotal = Number(rootDisk.total) || 0; @@ -230,7 +230,7 @@ class AdminSystem { return ` ${wrap('cpu', C.gauge(cpu.percent || 0, { label: 'CPU', sublabel: `${cpu.cores || '?'} cores` }))} ${wrap('mem', C.gauge(mem.percent || 0, { label: 'Memory', sublabel: `${this.bytes(mem.used)} / ${this.bytes(mem.total)}` }))} - ${wrap('disk', C.gauge(rootDisk.percent || 0, { label: 'Disk', sublabel: diskSub, inner: { value: lpPct, color: 'accent' } }))} + ${wrap('disk', C.gauge(rootDisk.percent || 0, { label: 'Disk', sublabel: diskSub, segment: { value: lpPct, color: 'accent' } }))} ${wrap('load1', C.gauge(load1, { label: 'Load', display: load1.toFixed(2), suffix: '', max: cores * 2, color: loadColor, sublabel: `1m · ${cpu.load5 ?? '–'}/${cpu.load15 ?? '–'}` }))}`; } diff --git a/containers/libreportal/frontend/js/components/admin/charts.js b/containers/libreportal/frontend/js/components/admin/charts.js index 49bb0ae..2f52167 100644 --- a/containers/libreportal/frontend/js/components/admin/charts.js +++ b/containers/libreportal/frontend/js/components/admin/charts.js @@ -92,32 +92,33 @@ const LPCharts = (() => { } // Circular ring gauge. value 0..max. opts: { label, color, sublabel, max, - // inner }. inner = { value, color } draws a second, smaller ring inside the - // main one (e.g. "of the disk used, this much is LibrePortal"). + // segment }. segment = { value, color } colours the LEADING part of the used + // arc differently — a sub-share within the same ring (e.g. the slice of the + // disk that's LibrePortal). When present, caps are butt so the split is crisp. function gauge(value, opts = {}) { const max = opts.max || 100; const pct = Math.max(0, Math.min(1, (value || 0) / max)); const color = palette(opts.color || pickColor(pct)); const r = 52, C = 2 * Math.PI * r, off = C * (1 - pct); - let innerSvg = ''; - if (opts.inner) { - const ip = Math.max(0, Math.min(1, (opts.inner.value || 0) / max)); - const icolor = palette(opts.inner.color || 'accent'); - const ir = 38, IC = 2 * Math.PI * ir, ioff = IC * (1 - ip); - innerSvg = ` - - 0) { + const sc = palette(opts.segment.color || 'accent'); + segArc = ``; + } } return `
- ${innerSvg} + ${segArc}
${opts.display !== undefined ? opts.display : Math.round(value)}${opts.suffix || '%'}
diff --git a/containers/libreportal/frontend/js/components/admin/system-metric-page.js b/containers/libreportal/frontend/js/components/admin/system-metric-page.js index 33d706b..2b60037 100644 --- a/containers/libreportal/frontend/js/components/admin/system-metric-page.js +++ b/containers/libreportal/frontend/js/components/admin/system-metric-page.js @@ -23,6 +23,7 @@ class SystemMetricPage { this.tier = '1m'; this.hoverIdx = -1; this._rafHover = null; + this._lpPct = null; // disk only: LibrePortal's share of the disk (%) this._unsubLive = null; this._onKey = this._onKey.bind(this); this._onResize = this._onResize.bind(this); @@ -49,6 +50,29 @@ class SystemMetricPage { this._bind(); await this._loadRange(); this._attachLive(); + this._loadLibrePortalShare(); + } + + // Disk page only: compute LibrePortal's current share of the disk so the + // chart can mark it with a reference line beside the disk-usage trend. + // It's a "now" value (app_storage + Docker df ÷ disk total) — there's no + // historical LP series, so the line is flat across the range. + async _loadLibrePortalShare() { + if (this.metricKey !== 'disk') return; + try { + const [app, dock, metrics] = await Promise.all([ + fetch(`/data/system/app_storage.json?t=${Date.now()}`).then(r => r.ok ? r.json() : null).catch(() => null), + fetch('/api/system/storage').then(r => r.ok ? r.json() : null).catch(() => null), + fetch(`/data/system/metrics.json?t=${Date.now()}`).then(r => r.ok ? r.json() : null).catch(() => null), + ]); + const lpBytes = ((app && app.total_local) || 0) + ((dock && dock.total) || 0); + const disks = (metrics && Array.isArray(metrics.disks)) ? metrics.disks : []; + const root = disks.find(d => d.mount === '/') || disks[0]; + const diskTotal = root ? Number(root.total) || 0 : 0; + this._lpPct = (diskTotal > 0 && lpBytes > 0) ? (lpBytes / diskTotal) * 100 : null; + this._lpBytes = lpBytes; + this._renderChart(); + } catch (_) { this._lpPct = null; } } dispose() { @@ -357,6 +381,15 @@ class SystemMetricPage { ? `` : ''; const nowIdx = n - 1; const nowDot = ``; + // Disk page: a flat reference line marking LibrePortal's share of the disk. + let lpLine = ''; + if (this.metricKey === 'disk' && Number.isFinite(this._lpPct)) { + const ly = yAt(this._lpPct); + const label = `LibrePortal ${this._lpPct.toFixed(1)}%${this._lpBytes ? ` · ${window.SystemFmt.bytes(this._lpBytes)}` : ''}`; + lpLine = ` + + ${label}`; + } const crosshair = ` `; const gradId = 'sys-detail-grad'; @@ -378,6 +411,7 @@ class SystemMetricPage { + ${lpLine} ${minDot} ${peakDot} ${nowDot}