From 60695b377b1bfbb13a75af3891ea2e1402b02e88 Mon Sep 17 00:00:00 2001 From: Zaine Date: Fri, 26 Jun 2026 14:44:40 +0100 Subject: [PATCH] resource integration --- css/dashboard.css | 31 ++++++++++++++++ data/manifest.json | 22 +++++++----- js/dashboard.js | 80 ++++++++++++++++++++++++++++++++--------- test/dashboard.test.mjs | 3 ++ 4 files changed, 111 insertions(+), 25 deletions(-) diff --git a/css/dashboard.css b/css/dashboard.css index fcc73f3..9a30941 100755 --- a/css/dashboard.css +++ b/css/dashboard.css @@ -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); +} diff --git a/data/manifest.json b/data/manifest.json index 0481efb..b5558b7 100755 --- a/data/manifest.json +++ b/data/manifest.json @@ -24,14 +24,20 @@ "title": "Authoring detail", "integration": "authoring" }, - { - "id": "watcher", - "type": "badge", - "title": "Lima watcher", - "integration": "watcher" - }, - { - "id": "timesheet-summary", + { + "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" diff --git a/js/dashboard.js b/js/dashboard.js index 8720d53..c90be75 100755 --- a/js/dashboard.js +++ b/js/dashboard.js @@ -5,12 +5,13 @@ const logStreams = new Map(); let statusPollTimer = null; /** Fallback when manifest API fails and static file is an older v1 without widgets. */ -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: 'timesheet-summary', type: 'timesheet', title: 'Hours this week', integration: 'timesheet' }, - { id: 'scripts-health', type: 'log', title: 'Script logs', integration: 'scripts', logKey: 'orgWebBuild' }, +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' }, { id: 'org-web-build', type: 'buildStatus', title: 'Website build', integration: 'builds', lastRunKey: 'org_web' }, ]; @@ -209,15 +210,28 @@ function updateWidgetCard(widget, status) { body.textContent = w.SubState ? `SubState: ${w.SubState}` : (w.message || 'Watcher'); break; } - case 'timesheet': { - const t = status.timesheet || {}; - setBadge(badge, t.available ? 'OK' : 'โ€”', t.available ? 'ok' : 'neutral'); - body.textContent = t.available - ? `Week ${t.weekStart}: ${t.hoursWorked}h worked (${t.delta >= 0 ? '+' : ''}${t.delta}h vs contracted)` - : (t.message || 'No data'); - break; - } - case 'log': { + case 'timesheet': { + const t = status.timesheet || {}; + setBadge(badge, t.available ? 'OK' : 'โ€”', t.available ? 'ok' : 'neutral'); + body.textContent = t.available + ? `Week ${t.weekStart}: ${t.hoursWorked}h worked (${t.delta >= 0 ? '+' : ''}${t.delta}h vs contracted)` + : (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; const stale = info.stale; @@ -245,14 +259,46 @@ function updateWidgetCard(widget, status) { body.textContent = `Unknown widget type: ${type}`; setBadge(badge, '?', 'neutral'); } -} - +} + function setBadge(el, text, tone) { if (!el) return; el.textContent = text; 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 ` +
+
+ ${label} + ${formatPct(pct)}${detail ? ` ยท ${detail}` : ''} +
+
+
`; +} + function safeDomId(value) { return String(value).replace(/[^a-zA-Z0-9_-]/g, '-'); } diff --git a/test/dashboard.test.mjs b/test/dashboard.test.mjs index be44b5a..9faaea0 100755 --- a/test/dashboard.test.mjs +++ b/test/dashboard.test.mjs @@ -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/);