Per .claude/rules/output-coverage.md every node ships test/_output-manifest.md enumerating every output across every state. This manifest covers all the outputs added by slices #34-#42 in this PRD: - Port 0 upsert message: every key (topic, url, method, headers, payload, meta) with type and tested states. - Port 1: explicit "not used" with rationale. - Port 2: explicit "not used" with rationale. - Structured log outputs: 5 events (regen-emitted, regen-skipped, manual-regen-requested, parent-panels-deduped, flows:started) with fields and corresponding test. - specificClass return shapes: 6 methods with populated + degraded states. - Anti-patterns enforced: no payload:null, absent vs null discipline, tab id avoidance in predicate. - test/_output-manifest.md: the manifest. - test/basic/slice43-output-manifest.basic.test.js: 6 cross-cutting tests exercising populated AND degraded states (token absent, folderUid absent, template missing, diff-skip, regen logging, manual regen). Backfill manifests for other nodes tracked in IMPROVEMENTS_BACKLOG. Closes #43
68 lines
6.3 KiB
Markdown
68 lines
6.3 KiB
Markdown
# 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` | success | `null` when no template for softwareType | `test/basic/slice43-output-manifest.basic.test.js` |
|
||
| `generateDashboardsForGraph(root)` | array of `buildDashboard` results, root first, children after | 0..N children | empty array when root config missing | `test/basic/slice35-graph-perf-and-uid-uniqueness.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+childId+grandchildren | myId only when child has no grandchildren | `test/basic/slice36-diff-predicate.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`.
|