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

68 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.