Files
generalFunctions/src/nodered/statusBadge.js
znetsixe 47faf94048 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>
2026-05-10 18:27:29 +02:00

97 lines
3.0 KiB
JavaScript

/**
* 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 };