Merge claude/2

This commit is contained in:
librelad 2026-05-28 22:59:49 +01:00
commit 5241d352a6
4 changed files with 64 additions and 17 deletions

View File

@ -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));

View File

@ -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 ?? ''}` }))}`;
}

View File

@ -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 = `
<circle cx="60" cy="60" r="${ir}" fill="none" stroke="rgba(var(--text-rgb),0.10)" stroke-width="7"/>
<circle cx="60" cy="60" r="${ir}" fill="none" stroke="${icolor.line}" stroke-width="7"
stroke-linecap="round" stroke-dasharray="${IC.toFixed(1)}" stroke-dashoffset="${ioff.toFixed(1)}"
const cap = opts.segment ? 'butt' : 'round';
let segArc = '';
if (opts.segment) {
const seg = Math.max(0, Math.min(pct, (opts.segment.value || 0) / max));
if (seg > 0) {
const sc = palette(opts.segment.color || 'accent');
segArc = `<circle cx="60" cy="60" r="${r}" fill="none" stroke="${sc.line}" stroke-width="10"
stroke-linecap="butt" stroke-dasharray="${C.toFixed(1)}" stroke-dashoffset="${(C * (1 - seg)).toFixed(1)}"
transform="rotate(-90 60 60)" style="transition:stroke-dashoffset .5s ease"/>`;
}
}
return `
<div class="lp-gauge">
<svg viewBox="0 0 120 120" class="lp-gauge-svg">
<circle cx="60" cy="60" r="${r}" fill="none" stroke="rgba(var(--text-rgb),0.10)" stroke-width="10"/>
<circle cx="60" cy="60" r="${r}" fill="none" stroke="${color.line}" stroke-width="10"
stroke-linecap="round" stroke-dasharray="${C.toFixed(1)}" stroke-dashoffset="${off.toFixed(1)}"
stroke-linecap="${cap}" stroke-dasharray="${C.toFixed(1)}" stroke-dashoffset="${off.toFixed(1)}"
transform="rotate(-90 60 60)" style="transition:stroke-dashoffset .5s ease"/>
${innerSvg}
${segArc}
</svg>
<div class="lp-gauge-center">
<div class="lp-gauge-value">${opts.display !== undefined ? opts.display : Math.round(value)}<span>${opts.suffix || '%'}</span></div>

View File

@ -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 {
? `<circle cx="${xAt(stats.minIdx).toFixed(1)}" cy="${yAt(stats.min).toFixed(1)}" r="3" class="sys-detail-min"/>` : '';
const nowIdx = n - 1;
const nowDot = `<circle cx="${xAt(nowIdx).toFixed(1)}" cy="${yAt(vals[nowIdx]).toFixed(1)}" r="5" class="sys-detail-now"/>`;
// 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 = `
<line x1="${padL}" y1="${ly.toFixed(1)}" x2="${(padL + innerW).toFixed(1)}" y2="${ly.toFixed(1)}" class="sys-detail-lpline"/>
<text x="${(padL + innerW - 6).toFixed(0)}" y="${(ly - 7).toFixed(0)}" text-anchor="end" class="sys-detail-lplabel">${label}</text>`;
}
const crosshair = `<line class="sys-detail-cross" x1="0" y1="${padT}" x2="0" y2="${(padT + innerH).toFixed(1)}" visibility="hidden"/>
<circle class="sys-detail-cross-dot" cx="0" cy="0" r="5" visibility="hidden"/>`;
const gradId = 'sys-detail-grad';
@ -378,6 +411,7 @@ class SystemMetricPage {
<path d="${area}" fill="url(#${gradId})" stroke="none"/>
<path d="${line}" fill="none" stroke="url(#${gradId}-line)" stroke-width="2.25"
vector-effect="non-scaling-stroke" stroke-linejoin="round" stroke-linecap="round"/>
${lpLine}
${minDot}
${peakDot}
${nowDot}