This commit is contained in:
@@ -723,3 +723,34 @@
|
||||
color: var(--text-muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.resource-row + .resource-row {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.resource-line {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.resource-line span:last-child {
|
||||
color: var(--text);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.resource-meter {
|
||||
height: 0.35rem;
|
||||
margin-top: 0.25rem;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.resource-meter span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,12 @@
|
||||
|
||||
"id": "authoring-queue",
|
||||
|
||||
"type": "queue",
|
||||
|
||||
"title": "Authoring builds",
|
||||
|
||||
"integration": "authoring"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@ const DEFAULT_WIDGETS = [
|
||||
{ id: 'authoring-queue', type: 'queue', title: 'Authoring builds', integration: 'authoring' },
|
||||
{ id: 'authoring-detail', type: 'authoringDetail', title: 'Authoring detail', integration: 'authoring' },
|
||||
{ id: 'watcher', type: 'badge', title: 'Lima watcher', integration: 'watcher' },
|
||||
{ id: 'system-resources', type: 'systemResources', title: 'System resources', integration: 'resources' },
|
||||
{ id: 'timesheet-summary', type: 'timesheet', title: 'Hours this week', integration: 'timesheet' },
|
||||
{ id: 'scripts-health', type: 'log', title: 'Script logs', integration: 'scripts', logKey: 'orgWebBuild' },
|
||||
{ id: 'calibre-sync', type: 'log', title: 'Calibre export', integration: 'scripts', logKey: 'calibreSync' },
|
||||
@@ -217,6 +218,19 @@ function updateWidgetCard(widget, status) {
|
||||
: (t.message || 'No data');
|
||||
break;
|
||||
}
|
||||
case 'systemResources': {
|
||||
const resources = status.systemResources || {};
|
||||
const cpuPct = numberOrNull(resources.cpu?.usagePct);
|
||||
const memoryPct = numberOrNull(resources.memory?.usedPct);
|
||||
const storagePct = numberOrNull(resources.storage?.usedPct);
|
||||
const maxPct = Math.max(cpuPct ?? 0, memoryPct ?? 0, storagePct ?? 0);
|
||||
setBadge(badge, maxPct >= 90 ? 'High' : 'OK', maxPct >= 90 ? 'error' : maxPct >= 75 ? 'warn' : 'ok');
|
||||
body.innerHTML = `
|
||||
${resourceRow('CPU', cpuPct, resources.cpu?.cores ? `${resources.cpu.cores} cores` : '')}
|
||||
${resourceRow('RAM', memoryPct, formatBytes(resources.memory?.usedBytes, resources.memory?.totalBytes))}
|
||||
${resourceRow('Disk', storagePct, formatBytes(resources.storage?.usedBytes, resources.storage?.totalBytes))}`;
|
||||
break;
|
||||
}
|
||||
case 'log': {
|
||||
const info = (status.scripts || {})[widget.logKey] || {};
|
||||
const label = info.label || widget.logKey;
|
||||
@@ -253,6 +267,38 @@ function setBadge(el, text, tone) {
|
||||
el.className = `badge ${tone} widget-badge`;
|
||||
}
|
||||
|
||||
function numberOrNull(value) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function formatPct(value) {
|
||||
return value == null ? '—' : `${Math.round(value)}%`;
|
||||
}
|
||||
|
||||
function formatBytes(used, total) {
|
||||
const usedNum = Number(used);
|
||||
const totalNum = Number(total);
|
||||
if (!Number.isFinite(usedNum) || !Number.isFinite(totalNum) || totalNum <= 0) return '';
|
||||
return `${bytesToGiB(usedNum)} / ${bytesToGiB(totalNum)}`;
|
||||
}
|
||||
|
||||
function bytesToGiB(bytes) {
|
||||
return `${(bytes / 1073741824).toFixed(1)} GiB`;
|
||||
}
|
||||
|
||||
function resourceRow(label, pct, detail) {
|
||||
const width = Math.max(0, Math.min(100, pct ?? 0));
|
||||
return `
|
||||
<div class="resource-row">
|
||||
<div class="resource-line">
|
||||
<span>${label}</span>
|
||||
<span>${formatPct(pct)}${detail ? ` · ${detail}` : ''}</span>
|
||||
</div>
|
||||
<div class="resource-meter"><span style="width:${width}%"></span></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function safeDomId(value) {
|
||||
return String(value).replace(/[^a-zA-Z0-9_-]/g, '-');
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ test('manifest v2 defines integrations and widgets', () => {
|
||||
assert.ok(manifest.integrations?.authoring);
|
||||
assert.ok(Array.isArray(manifest.widgets) && manifest.widgets.length >= 3);
|
||||
assert.ok(manifest.widgets.some(w => w.id === 'scripts-health'));
|
||||
assert.ok(manifest.widgets.some(w => w.id === 'system-resources' && w.type === 'systemResources'));
|
||||
});
|
||||
|
||||
test('manifest defines unique link and build ids', () => {
|
||||
@@ -155,6 +156,8 @@ test('dashboard module references manifest-driven API endpoints', () => {
|
||||
assert.match(dashboardJs, /data\/manifest\.json/);
|
||||
assert.match(dashboardJs, /\/api\/zone\/status/);
|
||||
assert.match(dashboardJs, /renderWidgets/);
|
||||
assert.match(dashboardJs, /systemResources/);
|
||||
assert.match(dashboardJs, /resourceRow/);
|
||||
assert.match(dashboardJs, /buildDomIds/);
|
||||
assert.doesNotMatch(dashboardJs, /build-roam/);
|
||||
assert.doesNotMatch(dashboardJs, /run-combined/);
|
||||
|
||||
Reference in New Issue
Block a user