From 6fbc808bb169968e139e9046b166656794228d6a Mon Sep 17 00:00:00 2001 From: George Lambert Date: Fri, 11 Sep 2026 13:15:52 -0400 Subject: [PATCH] Color fleet monitor rows: green operational, yellow degraded, red down Paused or unhealthy replicas mark the service degraded even if the tree-node floor is still met. Unmanaged NATS with no process is down. --- packages/verae-fleet/public/index.html | 17 +++++++--- packages/verae-fleet/src/classify.js | 29 +++++++++++++++++ packages/verae-fleet/src/supervisor.js | 2 ++ packages/verae-fleet/test/classify.test.js | 36 ++++++++++++++++++++++ 4 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 packages/verae-fleet/src/classify.js create mode 100644 packages/verae-fleet/test/classify.test.js diff --git a/packages/verae-fleet/public/index.html b/packages/verae-fleet/public/index.html index 460731f..263cd1d 100644 --- a/packages/verae-fleet/public/index.html +++ b/packages/verae-fleet/public/index.html @@ -15,10 +15,13 @@ table { width:100%; border-collapse:collapse; background:#fff; } th, td { text-align:left; padding:.4rem .5rem; border-bottom:1px solid var(--line); font-size:13px; } th { font-size:11px; letter-spacing:.06em; text-transform:uppercase; color:var(--muted); } + tr.state-operational { background:#e3f6e8; } + tr.state-degraded { background:#fff3bf; } + tr.state-down { background:#f9d4d4; } .pill { font:700 10px system-ui; letter-spacing:.06em; text-transform:uppercase; padding:.12rem .35rem; border-radius:4px; } - .good { background:#d4efe6; color:var(--ok); } - .bad { background:#f8d4d4; color:var(--err); } - .warn { background:#f5e6c8; color:var(--warn); } + .good { background:#c8efd4; color:var(--ok); } + .bad { background:#f3c0c0; color:var(--err); } + .warn { background:#ffe08a; color:var(--warn); } button { margin-right:.25rem; font:650 12px system-ui; border:1px solid var(--line); background:#fff; border-radius:5px; padding:.25rem .45rem; cursor:pointer; } code { font:12px ui-monospace,Menlo,monospace; } .inst { font:12px ui-monospace,Menlo,monospace; margin:.15rem 0; } @@ -32,7 +35,7 @@

- +
ServiceConfigmin/maxavailableinstancesactions
ServiceStateConfigmin/maxavailableinstancesactions
@@ -44,6 +47,9 @@ async function draw() { document.getElementById('meta').textContent = 'probe ' + (s.monitor?.t || '—') + ' · NATS ' + (s.nats?.url || ''); const tb = document.getElementById('rows'); tb.innerHTML = Object.values(s.services).map((sv) => { + const health = sv.health || 'down'; + const healthLabel = health === 'operational' ? 'operational' : health === 'degraded' ? 'degraded' : 'not operational'; + const healthCls = health === 'operational' ? 'good' : health === 'degraded' ? 'warn' : 'bad'; const floor = sv.belowFloor ? 'below floor' : (sv.keepFloor ? 'floor ok' : ''); const inst = (sv.instances || []).map((i) => { const st = !i.pid ? 'dead' : i.paused ? 'paused' : i.healthy === false ? 'unhealthy' : 'up'; @@ -54,8 +60,9 @@ async function draw() { `; }).join('') || 'none'; - return ` + return ` ${sv.id}
${sv.title || ''} + ${healthLabel} ${sv.configPath || ''} ${sv.min} / ${sv.max} ${floor} ${sv.available} running=${sv.running} paused=${sv.paused} diff --git a/packages/verae-fleet/src/classify.js b/packages/verae-fleet/src/classify.js new file mode 100644 index 0000000..3313b7a --- /dev/null +++ b/packages/verae-fleet/src/classify.js @@ -0,0 +1,29 @@ +/** + * Row health: operational | degraded | down + * @module classify + */ + +/** + * @param {{ + * min?: number, + * available?: number, + * running?: number, + * paused?: number, + * managed?: boolean, + * instances?: Array<{ pid?: number|null, paused?: boolean, healthy?: boolean }> + * }} sv + * @returns {'operational'|'degraded'|'down'} + */ +export function classifyService(sv) { + const min = sv.min ?? 0; + const available = sv.available ?? 0; + const running = sv.running ?? 0; + const paused = sv.paused ?? 0; + const inst = sv.instances || []; + const unhealthy = inst.filter((i) => i.pid && i.healthy === false && !i.paused).length; + + if (min === 0 && running === 0) return 'operational'; + if (available === 0 && min > 0) return 'down'; + if (available >= min && paused === 0 && unhealthy === 0) return 'operational'; + return 'degraded'; +} diff --git a/packages/verae-fleet/src/supervisor.js b/packages/verae-fleet/src/supervisor.js index 94ec6c1..81f6d00 100644 --- a/packages/verae-fleet/src/supervisor.js +++ b/packages/verae-fleet/src/supervisor.js @@ -9,6 +9,7 @@ import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { loadFleet } from './load.js'; import { httpProbe, postControl } from './health.js'; +import { classifyService } from './classify.js'; const WORKER = path.join(path.dirname(fileURLToPath(import.meta.url)), 'worker.js'); @@ -284,6 +285,7 @@ export class Supervisor { restarts: i.restarts, })), }; + services[spec.id].health = classifyService(services[spec.id]); } return { control: this.loaded.control, diff --git a/packages/verae-fleet/test/classify.test.js b/packages/verae-fleet/test/classify.test.js new file mode 100644 index 0000000..7b5b083 --- /dev/null +++ b/packages/verae-fleet/test/classify.test.js @@ -0,0 +1,36 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { classifyService } from '../src/classify.js'; + +describe('classifyService', () => { + it('operational when available meets min and none paused', () => { + assert.equal( + classifyService({ min: 3, available: 3, running: 3, paused: 0, instances: [{ pid: 1, healthy: true }] }), + 'operational', + ); + }); + + it('degraded when a replica is paused even if floor is still met', () => { + assert.equal( + classifyService({ + min: 3, + available: 3, + running: 4, + paused: 1, + instances: [ + { pid: 1, paused: true, healthy: false }, + { pid: 2, paused: false, healthy: true }, + ], + }), + 'degraded', + ); + }); + + it('not operational when min>0 and nothing available', () => { + assert.equal(classifyService({ min: 1, available: 0, running: 0, paused: 0, instances: [] }), 'down'); + }); + + it('optional idle (min 0, not running) is operational', () => { + assert.equal(classifyService({ min: 0, available: 0, running: 0, paused: 0 }), 'operational'); + }); +});