Merge claude/2
This commit is contained in:
commit
5241d352a6
@ -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));
|
||||
|
||||
@ -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 ?? '–'}` }))}`;
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user