Files
dashboardAPI/test/_output-manifest.md
znetsixe 990a8c09ea 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>
2026-05-27 09:45:37 +02:00

6.8 KiB
Raw Blame History

dashboardAPI output manifest

Per .claude/rules/output-coverage.md: every output on every layer, in every state.

Port 0 (process — Grafana upsert messages)

Emitted by the command handler(s) after a child.register or regenerate-dashboard message. Shape is the same for both; meta.trigger distinguishes them.

Key Source method Type States tested Test file
topic handlers.emitDashboardsFor 'create' (literal) populated test/basic/slice41-manual-regen.basic.test.js
url source.grafanaUpsertUrl() string (configured Grafana endpoint) populated, default-config test/basic/slice34-credentials-and-folder.basic.test.js
method handlers.emitDashboardsFor 'POST' (literal) populated test/basic/slice41-manual-regen.basic.test.js
headers.Accept handlers.emitDashboardsFor 'application/json' (literal) populated via output manifest test below
headers['Content-Type'] handlers.emitDashboardsFor 'application/json' (literal) populated via output manifest test below
headers.Authorization handlers.emitDashboardsFor 'Bearer <token>' when configured; absent when not populated, absent (degraded — no token) test/basic/slice43-output-manifest.basic.test.js
payload.dashboard source.buildDashboard() object (Grafana dashboard JSON) populated, byte-identical-on-repeat test/basic/slice35-graph-perf-and-uid-uniqueness.basic.test.js
payload.overwrite source.buildUpsertRequest() true (literal) populated test/basic/slice34-credentials-and-folder.basic.test.js
payload.folderUid source.buildUpsertRequest() string when configured; absent when empty populated, absent (degraded — empty config) test/basic/slice34-credentials-and-folder.basic.test.js
payload.folderId source.buildUpsertRequest() number when explicitly passed; absent otherwise absent (default), populated (explicit) test/basic/slice34-credentials-and-folder.basic.test.js
meta.nodeId handlers.emitDashboardsFor string (child node id) populated test/basic/slice43-output-manifest.basic.test.js
meta.softwareType handlers.emitDashboardsFor string (child softwareType) populated test/basic/slice43-output-manifest.basic.test.js
meta.uid handlers.emitDashboardsFor string (stableUid hash, deterministic) populated, byte-identical test/basic/slice35-graph-perf-and-uid-uniqueness.basic.test.js
meta.title handlers.emitDashboardsFor string (child name or id) populated test/basic/slice43-output-manifest.basic.test.js
meta.trigger handlers.emitDashboardsFor 'child.register' or 'manual' both states test/basic/slice41-manual-regen.basic.test.js

Degraded-state convention: missing keys are absent, never set to null. The http request consumer treats absent headers/payload fields as defaults.

Port 1 (InfluxDB telemetry)

dashboardAPI emits nothing on Port 1 by design — it has no measurements, no tick loop, no telemetry. Verified by absence: no formatForInflux import, no Port 1 wires in examples/.

Port 2 (registration / control plumbing)

dashboardAPI is a sink for child.register messages, not a source — it does not register itself with any parent. Nothing emitted on Port 2.

Structured log outputs

Event Level Triggered by Fields Test
regen-emitted info successful composition (auto or manual) event, trigger, dashboardApiId, childId, dashboardCount test/basic/slice43-output-manifest.basic.test.js
regen-skipped info diff predicate says subtree unchanged event, outcome: 'no-diff', trigger: 'child.register', dashboardApiId, childId, subtreeSize test/basic/slice43-output-manifest.basic.test.js
manual-regen-requested info regenerate-dashboard topic received event, trigger: 'manual', dashboardApiId, cachedChildCount test/basic/slice41-manual-regen.basic.test.js
parent-panels-deduped debug no-data-duplication filter removed root panels event, before, after, rootTitle covered by composition tests in slice39
flows:started debug Node-RED runtime emits flows:started event: 'flows:started', type, diff (count summary) covered by predicate tests in slice36

specificClass return shapes

Method Return shape Populated states Degraded states Test
buildDashboard(opts) { dashboard, uid, title, softwareType, nodeId, measurementName } or null; measurementName mirrors outputUtils.formatMsg (general.name || <softwareType>_<id>) so the dashboard _measurement var matches the telemetry series success (name set + name empty/fallback) null when no template for softwareType test/basic/slice43-output-manifest.basic.test.js, test/basic/slice46-measurement-name-parity.basic.test.js
generateDashboardsForGraph(root) flat pre-order array of buildDashboard results (root first, then full descendant subtree); per-parent dedup + links applied at every level 0..N children, 3-level tree, diamond, cycle empty array when root config missing test/basic/slice35-graph-perf-and-uid-uniqueness.basic.test.js, test/basic/slice44-recursive-discovery.basic.test.js
subtreeChanged(diff, ids) boolean id-in-diff, no-id-in-diff null diff → true (cold start) test/basic/slice36-diff-predicate.basic.test.js
subtreeIdsFor(myId, child) Set<string> myId + every id in the child's full subtree (recurses all levels, cycle-safe) myId only when child has no children test/basic/slice36-diff-predicate.basic.test.js, test/basic/slice44-recursive-discovery.basic.test.js
collectEmittedFields(dashboard) Set<string> populated dashboard empty set for null/{}/{panels:[]} test/basic/slice37-emitted-fields.basic.test.js
cachedChildSources() array of child sources 0..N cached empty after construction test/basic/slice41-manual-regen.basic.test.js

Anti-patterns enforced

  • Emitting {payload: null}handlers.emitDashboardsFor always builds payload: { dashboard, overwrite, ... }. Verified.
  • Mixing absent vs null for optional fields — folderUid / folderId are absent when unconfigured, never null. Verified.
  • Per-call token stamping — token is set on headers.Authorization when configured; absent when not. No empty-string sentinel.
  • Tab id over-triggering in diff predicate — predicate only matches against dashboardAPI's own id + child + grandchildren, never tab ids. Verified.

Migration plan applied

This manifest is created together with slice #43 — the new outputs added in slices #34#42 are documented here. Other EVOLV nodes still need their own manifests; tracked in IMPROVEMENTS_BACKLOG.md.