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