Phase 1 wave 1: domain + nodered + stats infra (additive)
Adds platform infrastructure used by the upcoming refactor of
nodeClass / specificClass across all 12 nodes:
- src/domain/UnitPolicy.js — extracted from rotatingMachine/MGC
- src/domain/ChildRouter.js — declarative event routing on top of childRegistrationUtils
- src/domain/LatestWinsGate.js — extracted from MGC dispatch gate
- src/domain/HealthStatus.js — standardised {level, flags, message, source}
- src/nodered/statusBadge.js — compose / error / idle / byState / text helpers
- src/stats/index.js — mean / stdDev / median / mad / lerp
All additive — no existing exports change shape.
56 unit tests pass under node:test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
96
src/nodered/statusBadge.js
Normal file
96
src/nodered/statusBadge.js
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* statusBadge — small helpers that build Node-RED status objects
|
||||
* ({ fill, shape, text }) consistently across every node.
|
||||
*
|
||||
* See CONTRACTS.md §7. Domains compose badges via these helpers so the
|
||||
* editor look-and-feel converges instead of every node rolling its own
|
||||
* emoji + colour rules.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const MAX_TEXT = 60;
|
||||
const SEPARATOR = ' | ';
|
||||
|
||||
const DEFAULT_BADGE = { fill: 'green', shape: 'dot' };
|
||||
const ERROR_BADGE = { fill: 'red', shape: 'ring' };
|
||||
const IDLE_BADGE = { fill: 'blue', shape: 'dot' };
|
||||
const UNKNOWN_BADGE = { fill: 'grey', shape: 'ring' };
|
||||
|
||||
// Truncate to MAX_TEXT keeping room for the ellipsis. Editor clips the
|
||||
// rest visually anyway, but we want the cut to be deterministic so
|
||||
// snapshot tests don't drift across Node-RED versions.
|
||||
function _clip(text) {
|
||||
if (text == null) return '';
|
||||
const s = String(text);
|
||||
if (s.length <= MAX_TEXT) return s;
|
||||
return s.slice(0, MAX_TEXT - 1) + '…';
|
||||
}
|
||||
|
||||
function _joinParts(parts) {
|
||||
if (!Array.isArray(parts) || parts.length === 0) return '';
|
||||
const kept = parts.filter((p) => p != null && p !== false && p !== '');
|
||||
if (kept.length === 0) return '';
|
||||
return kept.map(String).join(SEPARATOR);
|
||||
}
|
||||
|
||||
function compose(parts, opts) {
|
||||
const text = _clip(_joinParts(parts));
|
||||
return {
|
||||
fill: (opts && opts.fill) || DEFAULT_BADGE.fill,
|
||||
shape: (opts && opts.shape) || DEFAULT_BADGE.shape,
|
||||
text,
|
||||
};
|
||||
}
|
||||
|
||||
function error(message) {
|
||||
return {
|
||||
fill: ERROR_BADGE.fill,
|
||||
shape: ERROR_BADGE.shape,
|
||||
text: _clip(`⚠ ${message == null ? '' : message}`),
|
||||
};
|
||||
}
|
||||
|
||||
function idle(label) {
|
||||
return {
|
||||
fill: IDLE_BADGE.fill,
|
||||
shape: IDLE_BADGE.shape,
|
||||
text: _clip(`⏸️ ${label == null ? '' : label}`),
|
||||
};
|
||||
}
|
||||
|
||||
// Look up a state-template badge and optionally compose extra parts
|
||||
// into its text. Missing template falls back to a grey "unknown state"
|
||||
// badge — silent so caller can still surface the bad state through logs.
|
||||
function byState(stateMap, currentState, opts) {
|
||||
const template = stateMap && stateMap[currentState];
|
||||
if (!template) {
|
||||
return {
|
||||
fill: UNKNOWN_BADGE.fill,
|
||||
shape: UNKNOWN_BADGE.shape,
|
||||
text: _clip(`unknown state: ${currentState == null ? '' : currentState}`),
|
||||
};
|
||||
}
|
||||
const baseText = template.text == null ? '' : String(template.text);
|
||||
const extras = opts && Array.isArray(opts.compose) ? opts.compose : [];
|
||||
const merged = extras.length > 0
|
||||
? _joinParts([baseText, ...extras])
|
||||
: baseText;
|
||||
return {
|
||||
fill: template.fill || DEFAULT_BADGE.fill,
|
||||
shape: template.shape || DEFAULT_BADGE.shape,
|
||||
text: _clip(merged),
|
||||
};
|
||||
}
|
||||
|
||||
function text(string, opts) {
|
||||
return {
|
||||
fill: (opts && opts.fill) || DEFAULT_BADGE.fill,
|
||||
shape: (opts && opts.shape) || DEFAULT_BADGE.shape,
|
||||
text: _clip(string == null ? '' : string),
|
||||
};
|
||||
}
|
||||
|
||||
const statusBadge = { compose, error, idle, byState, text };
|
||||
|
||||
module.exports = { statusBadge, MAX_TEXT };
|
||||
Reference in New Issue
Block a user