resource integration
Some checks failed
Zone / build (push) Failing after 7s

This commit is contained in:
2026-06-26 14:44:40 +01:00
parent 9e182e01d6
commit 60695b377b
4 changed files with 111 additions and 25 deletions

View File

@@ -723,3 +723,34 @@
color: var(--text-muted); color: var(--text-muted);
line-height: 1.45; 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);
}

View File

@@ -24,14 +24,20 @@
}, },
"widgets": [ "widgets": [
{ {
"id": "authoring-queue", "id": "authoring-queue",
"type": "queue", "type": "queue",
"title": "Authoring builds",
"integration": "authoring"
},
{ {
"id": "authoring-detail", "id": "authoring-detail",

View File

@@ -5,12 +5,13 @@ const logStreams = new Map();
let statusPollTimer = null; let statusPollTimer = null;
/** Fallback when manifest API fails and static file is an older v1 without widgets. */ /** Fallback when manifest API fails and static file is an older v1 without widgets. */
const DEFAULT_WIDGETS = [ const DEFAULT_WIDGETS = [
{ id: 'authoring-queue', type: 'queue', title: 'Authoring builds', integration: 'authoring' }, { id: 'authoring-queue', type: 'queue', title: 'Authoring builds', integration: 'authoring' },
{ id: 'authoring-detail', type: 'authoringDetail', title: 'Authoring detail', integration: 'authoring' }, { id: 'authoring-detail', type: 'authoringDetail', title: 'Authoring detail', integration: 'authoring' },
{ id: 'watcher', type: 'badge', title: 'Lima watcher', integration: 'watcher' }, { id: 'watcher', type: 'badge', title: 'Lima watcher', integration: 'watcher' },
{ id: 'timesheet-summary', type: 'timesheet', title: 'Hours this week', integration: 'timesheet' }, { id: 'system-resources', type: 'systemResources', title: 'System resources', integration: 'resources' },
{ id: 'scripts-health', type: 'log', title: 'Script logs', integration: 'scripts', logKey: 'orgWebBuild' }, { 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: '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' }, { 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'); body.textContent = w.SubState ? `SubState: ${w.SubState}` : (w.message || 'Watcher');
break; break;
} }
case 'timesheet': { case 'timesheet': {
const t = status.timesheet || {}; const t = status.timesheet || {};
setBadge(badge, t.available ? 'OK' : '—', t.available ? 'ok' : 'neutral'); setBadge(badge, t.available ? 'OK' : '—', t.available ? 'ok' : 'neutral');
body.textContent = t.available body.textContent = t.available
? `Week ${t.weekStart}: ${t.hoursWorked}h worked (${t.delta >= 0 ? '+' : ''}${t.delta}h vs contracted)` ? `Week ${t.weekStart}: ${t.hoursWorked}h worked (${t.delta >= 0 ? '+' : ''}${t.delta}h vs contracted)`
: (t.message || 'No data'); : (t.message || 'No data');
break; break;
} }
case 'log': { 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 info = (status.scripts || {})[widget.logKey] || {};
const label = info.label || widget.logKey; const label = info.label || widget.logKey;
const stale = info.stale; const stale = info.stale;
@@ -245,14 +259,46 @@ function updateWidgetCard(widget, status) {
body.textContent = `Unknown widget type: ${type}`; body.textContent = `Unknown widget type: ${type}`;
setBadge(badge, '?', 'neutral'); setBadge(badge, '?', 'neutral');
} }
} }
function setBadge(el, text, tone) { function setBadge(el, text, tone) {
if (!el) return; if (!el) return;
el.textContent = text; el.textContent = text;
el.className = `badge ${tone} widget-badge`; 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) { function safeDomId(value) {
return String(value).replace(/[^a-zA-Z0-9_-]/g, '-'); return String(value).replace(/[^a-zA-Z0-9_-]/g, '-');
} }

View File

@@ -64,6 +64,7 @@ test('manifest v2 defines integrations and widgets', () => {
assert.ok(manifest.integrations?.authoring); assert.ok(manifest.integrations?.authoring);
assert.ok(Array.isArray(manifest.widgets) && manifest.widgets.length >= 3); 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 === 'scripts-health'));
assert.ok(manifest.widgets.some(w => w.id === 'system-resources' && w.type === 'systemResources'));
}); });
test('manifest defines unique link and build ids', () => { 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, /data\/manifest\.json/);
assert.match(dashboardJs, /\/api\/zone\/status/); assert.match(dashboardJs, /\/api\/zone\/status/);
assert.match(dashboardJs, /renderWidgets/); assert.match(dashboardJs, /renderWidgets/);
assert.match(dashboardJs, /systemResources/);
assert.match(dashboardJs, /resourceRow/);
assert.match(dashboardJs, /buildDomIds/); assert.match(dashboardJs, /buildDomIds/);
assert.doesNotMatch(dashboardJs, /build-roam/); assert.doesNotMatch(dashboardJs, /build-roam/);
assert.doesNotMatch(dashboardJs, /run-combined/); assert.doesNotMatch(dashboardJs, /run-combined/);