feat(dashboardapi): recursive subtree discovery + measurement-name/template parity

Generate dashboards for an entire parent-child subtree from a single root
registration (pre-order, cycle/diamond-safe), so wiring only the subtree root
(e.g. pumpingStation) to dashboardAPI yields dashboards for every descendant.

Fix two contract drifts that left generated panels blank against live telemetry:
- _measurement var now mirrors outputUtils.formatMsg (general.name ||
  <softwareType>_<id>); previously it always used the fallback form, so any
  named node's dashboard queried a non-existent series.
- pumpingStation template field keys realigned to emitted telemetry
  (flow.*.{upstream,out,overflow}, netFlowRate.measured, inflowLevel/
  outflowLevel/overflowLevel, maxVolAtOverflow/minVolAt{Inflow,Outflow}).

Adds template alias resolution (softwareType -> shared template file) and
locks parity with slice44/45/46 tests + output manifest. 67/67 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-27 09:45:37 +02:00
parent dc08c85409
commit 990a8c09ea
7 changed files with 357 additions and 80 deletions

View File

@@ -0,0 +1,50 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const DashboardApi = require('../../src/specificClass.js');
// softwareType (as reported at runtime, lowercased) -> the template that must resolve.
const CASES = [
['rotatingmachine', 'machine.json'],
['machinegroupcontrol', 'machineGroup.json'],
['pumpingstation', 'pumpingStation.json'],
['valvegroupcontrol', 'valveGroupControl.json'],
['diffuser', 'aeration.json'],
['measurement', 'measurement.json'],
['reactor', 'reactor.json'],
['settler', 'settler.json'],
['valve', 'valve.json'],
['monster', 'monster.json'],
];
for (const [softwareType, file] of CASES) {
test(`softwareType '${softwareType}' resolves to ${file}`, () => {
const api = new DashboardApi({});
const resolved = api._templateFileForSoftwareType(softwareType);
assert.ok(resolved, `expected a template path for ${softwareType}`);
assert.ok(resolved.endsWith(file), `expected ${file}, got ${resolved}`);
});
}
test('resolution is case-insensitive (camelCase softwareType still resolves)', () => {
const api = new DashboardApi({});
assert.ok(api._templateFileForSoftwareType('rotatingMachine').endsWith('machine.json'));
assert.ok(api._templateFileForSoftwareType('machineGroupControl').endsWith('machineGroup.json'));
});
test('rotatingmachine now builds a dashboard (was: no template found)', () => {
const api = new DashboardApi({});
const built = api.buildDashboard({
nodeConfig: { general: { id: 'rm-1', name: 'Pump A' }, functionality: { softwareType: 'rotatingmachine' } },
positionVsParent: 'downstream',
});
assert.ok(built, 'expected a built dashboard, not null');
assert.equal(built.softwareType, 'rotatingmachine');
});
test('unknown softwareType still returns null (no template)', () => {
const api = new DashboardApi({});
assert.equal(api._templateFileForSoftwareType('totally-unknown-type'), null);
});