8 Commits

Author SHA1 Message Date
znetsixe
dac8576cab style: palette swatch → (domain-hue redesign 2026-05-21)
Sidebar swatch now follows function family rather than S88 level, so the
palette is visually identifiable instead of monochromatically blue. Editor-group
rectangles in flow.json still follow S88 — only the registerType color changed.
Full table + rationale: superproject .claude/rules/node-red-flow-layout.md §10.0
and .claude/refactor/OPEN_QUESTIONS.md (2026-05-21 entry).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:05:59 +02:00
znetsixe
e04c4a1132 fix: rename dashboardapi.{js,html} → dashboardAPI.{js,html}
Aligns the entry-file naming with the folder-name convention from
.claude/rules/node-architecture.md / superproject CLAUDE.md.

NON-BREAKING: the Node-RED type id stays lowercase
(`RED.nodes.registerType('dashboardapi', ...)`) so every deployed flow
that references this node continues to load. Only the file paths
change. Admin endpoints `/dashboardapi/menu.js` and
`/dashboardapi/configData.js` are unaffected — they follow the type
id, not the filename.

Updated:
- package.json `main` + `node-red.nodes` value
- test/basic/structure-module-load.basic.test.js require path
- CLAUDE.md: legacy-drift warning replaced with a note explaining the
  type-id preservation strategy

Same approach recommended for the remaining two legacy renames (mgc,
vgc); the superproject CLAUDE.md drift table now spells that out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:36:56 +02:00
znetsixe
0b857ef444 fix: align logging.enabled default to schema (true)
dashboardapiConfig.json declares `logging.enabled.default: true` but:
- dashboardapi.html defaulted `enableLog: false`
- src/specificClass.js `_buildConfig` used `Boolean(...)` which
  coerced undefined to false, overriding the schema default.

Aligned both to the schema: HTML default = true; _buildConfig now
uses `?? true` so an unspecified config follows the schema.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:59:19 +02:00
znetsixe
fb5a9ebff8 docs(wiki): regenerate topic-contract AUTOGEN block via wiki-gen
Replaces the agent-written placeholder inside Reference-Contracts.md with
the authoritative table generated from src/commands/index.js. Both the
BEGIN and END markers are normalized to the canonical form used by
`@evolv/wiki-gen`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:11:47 +02:00
znetsixe
a9fc51d6f0 docs(wiki): full 5-page wiki matching the rotatingMachine reference format
Replaces the prior stub/partial wiki with a Home + Reference-{Architecture,
Contracts,Examples,Limitations} + _Sidebar structure. Topic-contract and
data-model sections wrapped in AUTOGEN markers for the future wiki-gen tool.
Source-vs-spec contradictions surfaced and flagged inline (not silently
fixed). Pending-review notes mark sections that need a full node review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:42:14 +02:00
znetsixe
a6f09d821d docs: Folder & File Layout section + flag dashboardapi.{js,html} naming drift
Entry/HTML files should be dashboardAPI.{js,html} (case-sensitive match with
the folder name). Rename when the file is next touched. Full rule:
.claude/rules/node-architecture.md in the EVOLV superproject.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:30:46 +02:00
znetsixe
f0a7904985 P11.7 wiki: rewrite Home.md to full 14-section visual-first template
Adapts the canonical WIKI_TEMPLATE.md for dashboardAPI as a utility node
(no BaseDomain, no S88 level, no state chart). Key changes vs P9.3 draft:
- Banner hash bumped to 7b3da23
- Section 1: tightened to exactly describe topology→dashboard flow
- Section 2: adds FlowFuse/browser as downstream consumer of Grafana dashboards
- Section 3: expands capabilities (stable UID, bucket-per-position, alias alias)
- Section 4: adds dashboardapi.js entry node + real config/ template list
- Section 5: AUTOGEN markers regenerated via npm run wiki:all
- Section 6: rewrites diagram with resolveChildSource detail
- Section 7: full sequence including stableUid + links[] step
- Section 8: AUTOGEN marker regenerated; adds meta-field table
- Section 9: adds enableLog/logLevel fields; adds bucket-fallback table
- Section 10: explicit SKIPPED marker (stateless node)
- Section 11: adds inline wiring example
- Section 12: expands to 7 recipes (adds UID-change, machineGroupControl alias)
- Section 13: adds "not a BaseDomain node" + OPEN_QUESTIONS reference
- Section 14: adds OPEN_QUESTIONS.md link for BaseDomain decision; keeps 5 issues

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:06:42 +02:00
znetsixe
7b3da23fba P11.6 wiki regen + Phase 10 private-test rewrites where applicable
For all 11 nodes with auto-gen markers: wiki/Home.md sections 5 (topic
contract) and 9 (data model) regenerated via npm run wiki:all. New
Unit column shows '<measure> (default <unit>)' for declared topics,
'—' otherwise. Effect column now uses descriptor.description (P11.2
field) overriding the generic per-prefix fallback.

For rotatingMachine + reactor: Phase 10 test rewrites — 3 + 8 files
moved off private nodeClass internals (_attachInputHandler, _commands,
_pendingExtras, _registerChild, _tick, etc.) to the public
BaseNodeAdapter surface (node.handlers.input, node.source.*).
+6 / +7 net new tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:44:02 +02:00
12 changed files with 1004 additions and 179 deletions

View File

@@ -21,3 +21,21 @@ Key points for this node:
- Stack same-level siblings vertically. - Stack same-level siblings vertically.
- Parent/children sit on adjacent lanes (children one lane left, parent one lane right). - Parent/children sit on adjacent lanes (children one lane left, parent one lane right).
- Wrap in a Node-RED group box coloured `none` (Utility (no S88 level)). - Wrap in a Node-RED group box coloured `none` (Utility (no S88 level)).
## Folder & File Layout
Every per-node file MUST use the folder name (`dashboardAPI`) **exactly**, case-sensitive. Full rule: [`.claude/rules/node-architecture.md`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/rules/node-architecture.md) in the EVOLV superproject.
| Path | Required name |
|---|---|
| Entry file | `dashboardAPI.js` |
| Editor HTML | `dashboardAPI.html` |
| Node adapter | `src/nodeClass.js` |
| Domain logic | `src/specificClass.js` |
| Editor JS modules | `src/editor/*.js` (extract when inline editor JS exceeds ~50 lines) |
| Tests | `test/{basic,integration,edge}/*.test.js` |
| Example flows | `examples/*.flow.json` |
> **Note on the Node-RED type id.** The files are now `dashboardAPI.{js,html}` (folder-name convention satisfied 2026-05-19), but the registered type id stays lowercase: `RED.nodes.registerType('dashboardapi', …)`. Every deployed flow references the type id, not the file name, so this preserves backward compatibility. Admin endpoints (`/dashboardapi/menu.js`, `/dashboardapi/configData.js`) follow the type id and are also unchanged.
When adding new files, read the rule above first to avoid drift.

View File

@@ -4,10 +4,10 @@
<script> <script>
RED.nodes.registerType('dashboardapi', { RED.nodes.registerType('dashboardapi', {
category: 'EVOLV', category: 'EVOLV',
color: '#4f8582', color: '#7A8BA3',
defaults: { defaults: {
name: { value: '' }, name: { value: '' },
enableLog: { value: false }, enableLog: { value: true },
logLevel: { value: 'info' }, logLevel: { value: 'info' },
protocol: { value: 'http' }, protocol: { value: 'http' },

View File

@@ -2,7 +2,7 @@
"name": "dashboardAPI", "name": "dashboardAPI",
"version": "1.0.0", "version": "1.0.0",
"description": "EVOLV Grafana dashboard generator (Node-RED node).", "description": "EVOLV Grafana dashboard generator (Node-RED node).",
"main": "dashboardapi.js", "main": "dashboardAPI.js",
"scripts": { "scripts": {
"test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js", "test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js",
"wiki:contract": "node ../generalFunctions/scripts/wikiGen.js contract ./src/commands/index.js --write ./wiki/Home.md", "wiki:contract": "node ../generalFunctions/scripts/wikiGen.js contract ./src/commands/index.js --write ./wiki/Home.md",
@@ -22,7 +22,7 @@
}, },
"node-red": { "node-red": {
"nodes": { "nodes": {
"dashboardapi": "dashboardapi.js" "dashboardapi": "dashboardAPI.js"
} }
} }
} }

View File

@@ -55,7 +55,7 @@ class DashboardApi {
general: { general: {
name: config?.general?.name || 'dashboardapi', name: config?.general?.name || 'dashboardapi',
logging: { logging: {
enabled: Boolean(config?.general?.logging?.enabled), enabled: config?.general?.logging?.enabled ?? true,
logLevel: config?.general?.logging?.logLevel || 'info', logLevel: config?.general?.logging?.logLevel || 'info',
}, },
}, },

View File

@@ -3,6 +3,6 @@ const assert = require('node:assert/strict');
test('dashboardAPI module load smoke', () => { test('dashboardAPI module load smoke', () => {
assert.doesNotThrow(() => { assert.doesNotThrow(() => {
require('../../dashboardapi.js'); require('../../dashboardAPI.js');
}); });
}); });

View File

@@ -1,13 +1,28 @@
# dashboardAPI # dashboardAPI
> **Reflects code as of `92d7eba` · regenerated `2026-05-11` via `npm run wiki:all`** ![code-ref](https://img.shields.io/badge/code--ref-a6f09d8-blue) ![s88](https://img.shields.io/badge/S88-Utility-dddddd) ![status](https://img.shields.io/badge/status-pending--review-yellow)
> If this banner is stale, the page may be out of date. Treat as informative, not authoritative.
## 1. What this node is A `dashboardAPI` node converts EVOLV node topology into Grafana dashboards. On each inbound `child.register` event it resolves the child source, walks its direct children, loads per-`softwareType` Grafana JSON templates from `config/`, and emits one HTTP upsert request per dashboard on Port 0 to a downstream `http request` node. Sits adjacent to the S88 hierarchy as a passive HTTP emitter &mdash; **no measurements, no tick loop, no parent registration**.
**dashboardAPI** is an HTTP-emitter utility node — it listens for `child.register` events from other EVOLV nodes and emits one Grafana dashboard upsert HTTP request per node on Port 0. It is not a domain in the `BaseDomain` sense: no measurements, no tick loop, no children of its own, no parent registration. > [!NOTE]
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
## 2. Position in the platform ---
## At a glance
| Thing | Value |
|:---|:---|
| What it represents | Utility bridge between EVOLV topology and Grafana &mdash; auto-generates dashboards from `child.register` events |
| S88 level | **Utility** &mdash; not in the S88 hierarchy; sits adjacent to it |
| Use it when | You want Grafana dashboards to materialise automatically when an EVOLV node graph is deployed |
| Don't use it for | Maintaining hand-curated Grafana dashboards (will overwrite); arbitrary Grafana API calls; tick / measurement data plumbing |
| Children it accepts | Any EVOLV node whose `nodeSource.config` carries `functionality.softwareType` |
| Parents it talks to | None &mdash; dashboardAPI is a passive sink; it does not register with a parent |
---
## How it fits
```mermaid ```mermaid
flowchart LR flowchart LR
@@ -15,7 +30,9 @@ flowchart LR
mgc[machineGroupControl<br/>Unit]:::unit -.child.register.-> dash mgc[machineGroupControl<br/>Unit]:::unit -.child.register.-> dash
rm[rotatingMachine<br/>Equipment]:::equip -.child.register.-> dash rm[rotatingMachine<br/>Equipment]:::equip -.child.register.-> dash
meas[measurement<br/>Control Module]:::ctrl -.child.register.-> dash meas[measurement<br/>Control Module]:::ctrl -.child.register.-> dash
dash[dashboardAPI<br/>Utility]:::neutral -->|POST /api/dashboards/db| grafana[(Grafana<br/>HTTP API)] dash[dashboardAPI<br/>Utility]:::neutral -->|"POST /api/dashboards/db"| http[http request<br/>node-red core]:::neutral
http --> grafana[(Grafana<br/>HTTP API)]
grafana -.renders dashboards for.-> ff[FlowFuse / Browser]
classDef pc fill:#0c99d9,color:#fff classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000 classDef unit fill:#50a8d9,color:#000
classDef equip fill:#86bbdd,color:#000 classDef equip fill:#86bbdd,color:#000
@@ -23,204 +40,110 @@ flowchart LR
classDef neutral fill:#dddddd,color:#000 classDef neutral fill:#dddddd,color:#000
``` ```
dashboardAPI has **no S88 level** — it's a utility node. Dashed arrows = inbound `child.register` events; the solid arrow is the outbound HTTP upsert on Port 0 to a downstream `http request` node. Dashed arrows = inbound `child.register` events from any EVOLV process node. The solid arrow is the outbound HTTP upsert envelope on Port 0 &mdash; emitted **once per generated dashboard** in the walked graph. S88 colours and the utility-neutral `#dddddd` are anchored in `.claude/rules/node-red-flow-layout.md`.
## 3. Capability matrix ---
| Capability | Status | Notes | ## Try it &mdash; 3-minute demo
|---|---|---|
| Accept `child.register` from any EVOLV node | ✅ | Resolves via `RED.nodes.getNode``node._flow.getNode` → inline payload. |
| Emit Grafana dashboard upsert (Port 0) | ✅ | One msg per generated dashboard, shaped for `http request`. |
| Walk child graph + emit per-child dashboards | ✅ | `includeChildren: true` by default; opt-out per call. |
| Add root → child dashboard `links[]` | ✅ | Each direct child appears as a link on the root dashboard. |
| Template selection by `softwareType` | ✅ | Reads from `config/<softwareType>.json`; case-insensitive fallback. |
| Bearer-token auth | ✅ | Set via editor or `INFLUXDB_BUCKET` env (bucket only). |
| Domain output on Port 0 | ❌ | Never. Port 0 carries HTTP request envelopes, not measurements. |
| Port 1 telemetry / Port 2 registration | ❌ | Both unused — see Section 8. |
| Status badge / tick loop / FSM | ❌ | Stateless; no periodic emission. |
## 4. Code map Import the basic example flow, deploy, and watch a `child.register` payload turn into a Grafana dashboard upsert request.
```mermaid ```bash
flowchart TB curl -X POST -H 'Content-Type: application/json' \
subgraph nodeRED["nodeClass.js — passive adapter"] --data @nodes/dashboardAPI/examples/basic.flow.json \
nc["createRegistry(commands)<br/>dispatch on msg input<br/>NO BaseNodeAdapter"] http://localhost:1880/flow
end
subgraph domain["specificClass.js — DashboardApi service"]
sc["buildDashboard()<br/>generateDashboardsForGraph()<br/>extractChildren()"]
end
subgraph cmd["src/commands/"]
h["child.register handler<br/>+ registerChild alias"]
end
subgraph tpl["config/ (templates)"]
t["<softwareType>.json<br/>Grafana JSON skeletons"]
end
nc --> sc
nc --> cmd
sc --> tpl
``` ```
| Module | Owns | Read first if you're changing… | What to click after deploy:
|---|---|---|
| `src/nodeClass.js` | Input wiring, command dispatch, config build | Topic dispatching, configuration mapping. |
| `src/specificClass.js` | Template loading, dashboard composition, child graph walk | UID stability, links, templating-var substitution. |
| `src/commands/` | `child.register` handler + legacy alias | Adding / renaming inbound topics. |
| `config/` | Per-softwareType Grafana templates | Adding support for new node types. |
dashboardAPI deliberately does NOT split into `concerns/` — its surface is too narrow. See CONTRACT.md → "Why no BaseNodeAdapter / BaseDomain" for the rationale. 1. Open the inject node (`basic trigger`) and edit the payload to a `{source: {config: {...}}}` shape &mdash; see [Reference &mdash; Examples](Reference-Examples#wiring-pattern) for the minimal inline-payload shape.
2. Fire the inject. Watch the debug pane: one `topic: 'create'` HTTP envelope appears per dashboard in the walked graph (root + direct children).
3. Wire a downstream `http request` node (method `POST`) to the dashboardAPI output to actually POST the envelope to Grafana.
## 5. Topic contract > [!IMPORTANT]
> **GIF needed.** Demo recording of the inject &rarr; Port-0 envelope &rarr; Grafana dashboard upsert path. Save as `wiki/_partial-gifs/dashboardAPI/01-basic-demo.gif`, target &le; 1&nbsp;MB after `gifsicle -O3 --lossy=80`.
> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`. > [!WARNING]
> The shipped `basic.flow.json` / `integration.flow.json` / `edge.flow.json` are stubs &mdash; the inject payloads do not yet conform to the `child.register` resolver's expected shape. They will trigger `Missing or invalid child node` errors until updated. Tracked in [Limitations &mdash; Example flow stubs](Reference-Limitations#example-flow-stubs).
<!-- BEGIN AUTOGEN: topic-contract --> ---
| Canonical topic | Aliases | Payload | Effect | ## The one thing you'll send
|---|---|---|---|
| `child.register` | `registerChild` | `any` | Parent/child plumbing — registers or unregisters a child node. |
<!-- END AUTOGEN: topic-contract --> | Topic | Aliases | Payload | What it does |
|:---|:---|:---|:---|
| `child.register` | `registerChild` | `string` (child node id) **or** `{source: {...}}` **or** `{config: {...}}` (optionally `msg.includeChildren: boolean`, default `true`) | Resolves the child source (`RED.nodes.getNode` &rarr; `node._flow.getNode` &rarr; inline payload), calls `source.generateDashboardsForGraph(child, {includeChildren})`, then emits one `topic: 'create'` HTTP-upsert message on Port 0 per generated dashboard. |
The legacy `registerChild` alias logs a one-time deprecation warning on first use. The payload can be a string (child id), `{ source: {...} }`, or `{ config: {...} }`; `msg.includeChildren` (default `true`) controls graph-walk depth. That's it. There is no `set.*`, no `cmd.*`, no `query.*` &mdash; the registry has a single canonical topic (alias-with-deprecation). The legacy `registerChild` alias logs a one-time deprecation warning on first use.
## 6. Child registration ---
dashboardAPI does **not** maintain a child registry of its own. Instead, every inbound `child.register` triggers a one-shot resolution + dashboard emission. No state is held between calls. ## What you'll see come out
```mermaid Sample Port 0 message after a `child.register` for a `pumpingStation` node with two direct children:
flowchart LR
src["any EVOLV node<br/>(has functionality.softwareType)"]:::other -->|child.register| dash[dashboardAPI<br/>Utility]:::neutral
dash --> resolve[resolve child source<br/>RED.nodes.getNode → _flow → inline]
resolve --> walk["source.generateDashboardsForGraph(child)<br/>(includes children if flag set)"]
walk --> emit[emit one msg per dashboard<br/>topic='create']
emit --> http[(downstream<br/>http request node)]
classDef neutral fill:#dddddd,color:#000
classDef other fill:#ffffff,stroke:#666
```
| Inbound softwareType | Filter | Side effect | ```json
|---|---|---|
| any | child has `functionality.softwareType` | Loads `config/<softwareType>.json`; emits one upsert msg per dashboard in the graph walk. |
| (template missing) | no matching `config/*.json` | Warns and skips that dashboard. No error. |
## 7. Lifecycle — what one event does
```mermaid
sequenceDiagram
participant emitter as any EVOLV node
participant dash as dashboardAPI
participant api as DashboardApi
participant out as Port-0 output
participant grafana as Grafana HTTP API
emitter->>dash: child.register {source / config / id}
dash->>dash: commandRegistry dispatch
dash->>api: generateDashboardsForGraph(child, {includeChildren})
api->>api: loadTemplate(softwareType)
api->>api: stableUid + updateTemplatingVar
api->>api: walk children via childRegistrationUtils
api-->>dash: [{dashboard, uid, title}, ...]
loop per dashboard
dash->>out: msg{topic:'create', url, method, headers, payload}
out->>grafana: POST /api/dashboards/db
end
```
One inbound event yields N outbound HTTP messages (N = 1 + direct child count when `includeChildren=true`).
## 8. Data model — `getOutput()`
> **dashboardAPI has no domain output.** It does not extend `BaseDomain` and does not implement `getOutput()`. The `wiki:datamodel` script falls back to the placeholder below.
<!-- BEGIN AUTOGEN: data-model -->
No domain output. dashboardAPI emits **HTTP request envelopes on Port 0**, shaped for a downstream `http request` node:
```js
{ {
topic: 'create', "topic": "create",
url: 'http://<grafana>:<port>/api/dashboards/db', "url": "http://grafana:3000/api/dashboards/db",
method: 'POST', "method": "POST",
headers: { "headers": {
Accept: 'application/json', "Accept": "application/json",
'Content-Type': 'application/json', "Content-Type": "application/json",
Authorization: 'Bearer …' // only when bearerToken is set "Authorization": "Bearer eyJ..."
}, },
payload: { dashboard: {}, folderId: 0, overwrite: true }, "payload": {
meta: { nodeId, softwareType, uid, title } "dashboard": { "uid": "a1b2c3d4e5f6", "title": "Pumping Station Demo", "templating": {...} },
"folderId": 0,
"overwrite": true
},
"meta": {
"nodeId": "ps_demo",
"softwareType": "pumpingStation",
"uid": "a1b2c3d4e5f6",
"title": "Pumping Station Demo"
}
} }
``` ```
Port 1 (InfluxDB telemetry) and Port 2 (registration / control plumbing) are unused — dashboardAPI has no measurements and does not register with a parent. | Field | Meaning |
|:---|:---|
| `topic` | Always `'create'` &mdash; signals a dashboard-upsert HTTP envelope. |
| `url` | `grafanaUpsertUrl()` = `<protocol>://<host>:<port>/api/dashboards/db`. |
| `method` | Always `POST`. |
| `headers.Authorization` | Present only when `bearerToken` is configured; omitted otherwise. |
| `payload.dashboard` | The composed Grafana dashboard JSON (template + templating vars filled in). |
| `payload.dashboard.uid` | `sha1(softwareType:nodeId).slice(0, 12)` &mdash; stable across re-deploys. |
| `meta.*` | Correlation fields for the downstream consumer (nodeId, softwareType, uid, title). |
<!-- END AUTOGEN: data-model --> Inbound `msg` fields propagate via spread (`{...msg, ...envelope}`) so any caller-supplied correlation / trace fields survive.
See CONTRACT.md for the full envelope spec. > Port 1 (InfluxDB telemetry) and Port 2 (registration / control plumbing) are **unused** &mdash; dashboardAPI has no measurements and does not register with a parent. See [Reference &mdash; Architecture](Reference-Architecture#output-ports).
## 9. Configuration — editor form ↔ config keys ---
```mermaid ## The new bit &mdash; no BaseNodeAdapter / BaseDomain
flowchart TB
subgraph editor["Node-RED editor form"]
f1[Protocol]
f2[Host]
f3[Port]
f4[Bearer token]
f5[Default bucket]
end
subgraph config["Domain config slice"]
c1[grafanaConnector.protocol]
c2[grafanaConnector.host]
c3[grafanaConnector.port]
c4[grafanaConnector.bearerToken]
c5[defaultBucket]
end
f1 --> c1
f2 --> c2
f3 --> c3
f4 --> c4
f5 --> c5
```
| Form field | Config key | Default | Range | Where used | Most EVOLV nodes extend `BaseNodeAdapter` + `BaseDomain` from `generalFunctions/`. `dashboardAPI` does **not** &mdash; per `OPEN_QUESTIONS.md` (2026-05-10) the decision is to keep a bespoke adapter until `BaseNodeAdapter` grows passive / HTTP-only flags.
|---|---|---|---|---|
| Protocol | `grafanaConnector.protocol` | `http` | `http`\|`https` | `grafanaUpsertUrl()` |
| Host | `grafanaConnector.host` | `localhost` | hostname | `grafanaUpsertUrl()` |
| Port | `grafanaConnector.port` | `3000` | 165535 | `grafanaUpsertUrl()` |
| Bearer token | `grafanaConnector.bearerToken` | `''` | string | `Authorization` header |
| Default bucket | `defaultBucket` | `''` / `INFLUXDB_BUCKET` env | string | `updateTemplatingVar(bucket)` |
## 11. Examples Reasons:
| Tier | File | What it shows | Status | - No `generalFunctions/src/configs/dashboardapi.json` &mdash; `BaseDomain`'s constructor unconditionally calls `configManager.getConfig(ctor.name)` and would throw. The local `dependencies/dashboardapi/dashboardapiConfig.json` is for the editor menu endpoint, not the runtime config pipeline.
|---|---|---|---| - No periodic output &mdash; `BaseNodeAdapter._emitOutputs()` / `outputUtils.formatMsg` assumes a delta-compressed Port 0 / 1 stream; dashboardAPI emits HTTP-shaped messages instead.
| Basic | `examples/01-Basic.flow.json` | Inject `child.register` payload + a downstream `http request` → mock Grafana | ⏳ TBD | - No registration to a parent &mdash; `BaseNodeAdapter._scheduleRegistration` would emit a spurious `child.register` of its own.
| Integration | `examples/02-Integration.flow.json` | Real EVOLV node (e.g. pumpingStation) wired into dashboardAPI | ⏳ TBD | - No status badge / tick / measurements / children of its own.
| Dashboard | _n/a_ | dashboardAPI **is** the dashboard plumbing — no FlowFuse tier | — |
## 12. Debug recipes dashboardAPI uses the shared `commandRegistry` (canonical-topic naming + alias-with-deprecation) and stops there. See [Reference &mdash; Architecture](Reference-Architecture#why-no-basenodeadapter--basedomain) for the full rationale.
| Symptom | First thing to check | Where to look | ---
|---|---|---|
| No HTTP request emitted | Did the `child.register` resolve a source? `source.generateDashboardsForGraph` returns `[]` when child has no `config`. | `node_redlog` for "generateDashboardsForGraph skipped" warning. |
| `Skipping dashboard generation: no template` | `config/<softwareType>.json` missing for this node type. | `config/` directory; add a template. |
| Empty `Authorization` header | `bearerToken` not set in editor. | Editor form → Bearer token field. |
| Wrong bucket in Grafana | `defaultBucket` overrides position-based default. Check `INFLUXDB_BUCKET` env. | `_buildConfig` in nodeClass.js. |
| `registerChild` alias warns once | Expected — migrate callers to `child.register`. | Caller's `msg.topic`. |
> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors. ## Need more?
## 13. When you would NOT use this node | Page | What you'll find |
|:---|:---|
| [Reference &mdash; Contracts](Reference-Contracts) | Full topic contract, config schema, child resolution rules, template alias table |
| [Reference &mdash; Architecture](Reference-Architecture) | Code map, HTTP-endpoint lifecycle, template loader, UID stability, graph walk |
| [Reference &mdash; Examples](Reference-Examples) | Shipped example flows + debug recipes |
| [Reference &mdash; Limitations](Reference-Limitations) | Legacy filename drift, stub flows, missing template handling, open questions |
- Use dashboardAPI when you want **auto-generated Grafana dashboards** from EVOLV node topology. If you maintain dashboards by hand in Grafana, skip it. [EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home) &middot; [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) &middot; [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)
- Don't use dashboardAPI as a generic HTTP client — it only emits Grafana dashboard upserts. For arbitrary HTTP, use a plain `http request` node.
- Don't put dashboardAPI in a hot data path. It fires on registration events, not on each tick — wiring tick data to it is a misuse.
## 14. Known limitations / current issues
| # | Issue | Tracked in |
|---|---|---|
| 1 | No domain output — cannot be introspected through the standard `getOutput()` channel; debugging relies on Port 0 HTTP envelopes. | CONTRACT.md → "Why no BaseNodeAdapter / BaseDomain" |
| 2 | Template discovery is filename-based; renaming a node's `softwareType` requires renaming the template file (with the `machineGroupControl → machineGroup.json` alias being a one-off). | `_templateFileForSoftwareType` in specificClass.js |
| 3 | No retry / circuit-breaker on the downstream `http request` — Grafana outages drop dashboards silently. | TBD |
| 4 | Tier 1/2 example flows not yet written. | P9 wiki cleanup follow-up |

View File

@@ -0,0 +1,294 @@
# Reference &mdash; Architecture
![code-ref](https://img.shields.io/badge/code--ref-a6f09d8-blue)
> [!NOTE]
> Code structure for `dashboardAPI`: the (intentionally shallow) three-tier layout, the command registry, the dashboard composition pipeline, the HTTP-endpoint event lifecycle, and the output-port pipeline. For an intuitive overview, return to [Home](Home).
>
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
---
## Three-tier code layout
```
nodes/dashboardAPI/
|
+-- dashboardapi.js entry: RED.nodes.registerType('dashboardapi', NodeClass)
| (legacy lowercase filename — see Limitations)
|
+-- dashboardapi.html editor: form + oneditprepare / oneditsave
| (legacy lowercase filename — see Limitations)
|
+-- src/
| nodeClass.js passive adapter — buildConfig + createRegistry + input dispatch
| DOES NOT extend BaseNodeAdapter
| specificClass.js DashboardApi service — loadTemplate / buildDashboard /
| generateDashboardsForGraph / extractChildren
| DOES NOT extend BaseDomain
| |
| +-- commands/
| index.js topic descriptors (child.register only)
| handlers.js resolveChildSource + registerChild handler
|
+-- config/ Grafana JSON templates, one per softwareType
| aeration.json machineGroup.json pumpingStation.json
| dashboardapi.json measurement.json reactor.json
| machine.json monster.json settler.json
| valve.json valveGroupControl.json
|
+-- dependencies/
| dashboardapi/
| dashboardapiConfig.json editor menu config (NOT runtime config)
|
+-- examples/
| basic.flow.json currently stubs — see Examples & Limitations
| integration.flow.json
| edge.flow.json
|
+-- test/
basic/ structure-module-load test
integration/ structure-examples test
edge/ structure-examples-node-type test
helpers/
```
### Tier responsibilities
| Tier | File | What it owns | Touches `RED.*` |
|:---|:---|:---|:---:|
| entry | `dashboardapi.js` | `RED.nodes.registerType('dashboardapi', ...)`. Admin endpoints: `GET /dashboardapi/menu.js` (logger menu) + `GET /dashboardapi/configData.js` (editor metadata). | Yes |
| nodeClass | `src/nodeClass.js` | Builds runtime config via `configManager.buildConfig`. Creates command registry via `createRegistry(commands)`. Attaches `input` and `close` handlers. **No tick loop, no status badge, no Port 1 / 2 emissions.** Sets a one-shot red `dashboardapi error` status on dispatch failure. | Yes |
| specificClass | `src/specificClass.js` | Pure dashboard composition: template loading, UID derivation, templating-var fill, child graph walk, links generation, upsert request shaping. No `RED.*` calls. | No |
`specificClass` is small (~210 lines) and self-contained &mdash; no concern modules. The complexity surface is too narrow to warrant a `concerns/` split.
---
## Why no BaseNodeAdapter / BaseDomain
The decision is documented in `OPEN_QUESTIONS.md` (2026-05-10) and surfaced in `CONTRACT.md`. Four concrete blockers:
1. **No platform config JSON.** `BaseDomain`'s constructor unconditionally calls `configManager.getConfig(ctor.name)` against `generalFunctions/src/configs/<n>.json`. There is no `dashboardapi.json` in `generalFunctions` &mdash; the local `dependencies/dashboardapi/dashboardapiConfig.json` is for the editor menu endpoint only. Adding a platform config JUST to satisfy the base class would be a synthetic decision.
2. **No periodic output.** `BaseNodeAdapter._emitOutputs()` and `outputUtils.formatMsg` assume a delta-compressed Port 0 / 1 telemetry stream tied to a tick loop. dashboardAPI emits HTTP envelopes asynchronously on inbound events; the formatter pipeline would coerce these into the wrong shape.
3. **No parent registration.** `BaseNodeAdapter._scheduleRegistration` automatically emits a `child.register` on Port 2 at startup. dashboardAPI is a **sink** for `child.register`, not a source &mdash; emitting one of its own would feed into other dashboardAPI instances and cause loops.
4. **No status badge, no tick, no measurements, no children of its own.** Most of the base-class machinery would be inert or actively harmful.
What dashboardAPI **does** reuse from `generalFunctions/`:
- `configManager` (for `buildConfig`)
- `createRegistry` + the canonical-topic / alias-with-deprecation pattern
- `logger`
- `MenuManager` (for the editor menu endpoint)
That's enough common platform surface to keep the node aligned with EVOLV conventions without inheriting machinery it can't use.
---
## Command registry
`src/commands/index.js` declares one descriptor:
```js
module.exports = [
{
topic: 'child.register',
aliases: ['registerChild'],
payloadSchema: { type: 'any' },
handler: handlers.registerChild,
},
];
```
`createRegistry(commands, { logger })` returns a dispatcher with built-in alias-with-deprecation: the first time `msg.topic === 'registerChild'` fires, the logger emits a one-time deprecation warning; thereafter the alias is silently mapped to the canonical handler.
### `child.register` handler &mdash; resolution pipeline
`src/commands/handlers.js` `registerChild(source, msg, ctx)`:
1. **Resolve the child source** via `resolveChildSource(msg.payload, ctx)`:
- If `payload.source.config` exists &rarr; use `payload.source` directly (inline shape A).
- Else if `payload.config` exists &rarr; wrap as `{ config: payload.config }` (inline shape B).
- Else if `typeof payload === 'string'` &rarr; treat as a node id and resolve via `RED.nodes.getNode(id)` &rarr; fall back to `ctx.node._flow.getNode(id)`.
2. **Throw** `Missing or invalid child node` if neither path yields a `.config` &mdash; the nodeClass's catch sets the red `dashboardapi error` status badge and re-throws via `node.error`.
3. **Walk the graph** via `source.generateDashboardsForGraph(childSource, {includeChildren: msg.includeChildren ?? true})`.
4. **Emit one Port-0 envelope** per generated dashboard, with the `{...msg, topic: 'create', ...}` spread so caller fields propagate.
---
## Dashboard composition pipeline
```mermaid
flowchart TB
in[child.register payload]:::input --> res[resolveChildSource<br/>RED.nodes.getNode → _flow.getNode → inline]
res --> walk[generateDashboardsForGraph<br/>root + direct children if includeChildren]
walk --> bld[buildDashboard per node]
bld --> tpl[loadTemplate softwareType<br/>config/-st-.json with case-insensitive fallback<br/>+ machineGroupControl → machineGroup.json alias]
tpl --> uid[stableUid<br/>sha1 softwareType:nodeId .slice 0,12]
bld --> vars[updateTemplatingVar<br/>measurement = softwareType_nodeId<br/>bucket = position-based default or override]
walk --> links[Add root.links of child uid + slugify title]
links --> shape[buildUpsertRequest<br/>dashboard + folderId 0 + overwrite true]
shape --> emit[ctx.send one msg per dashboard<br/>topic 'create', url, method, headers, payload, meta]
emit --> out[Port 0]
classDef input fill:#dddddd,color:#000
```
### Template selection
`_templateFileForSoftwareType(softwareType)` tries these candidates in order:
1. `config/<softwareType>.json` (exact case)
2. `config/<softwareType.toLowerCase()>.json` (case-insensitive fallback)
3. `config/machineGroup.json` &mdash; only when `softwareType === 'machineGroupControl'` (one-off alias)
A missing template logs at `warn` level (`No dashboard template found for softwareType=<st>`) and the matching dashboard is skipped (no error thrown, the rest of the graph walk continues).
Currently shipped templates in `config/`:
| Template | Maps to softwareType |
|:---|:---|
| `aeration.json` | aeration |
| `dashboardapi.json` | dashboardapi (this node) |
| `machine.json` | (likely `rotatingmachine` / `machine` &mdash; verify when reviewing) |
| `machineGroup.json` | `machineGroupControl` (via alias) |
| `measurement.json` | measurement |
| `monster.json` | monster |
| `pumpingStation.json` | pumpingStation |
| `reactor.json` | reactor |
| `settler.json` | settler |
| `valve.json` | valve |
| `valveGroupControl.json` | valveGroupControl |
> [!NOTE]
> The exact softwareType &harr; template mapping (esp. `machine.json` vs the lowercase `rotatingmachine` softwareType emitted by `rotatingMachine`'s `functionality.softwareType`) needs verification during the full review &mdash; flagged.
### UID stability
`stableUid(input) = sha1(input).slice(0, 12)` &mdash; the same `softwareType:nodeId` always yields the same dashboard UID. Combined with `overwrite: true` in the upsert payload, this makes the operation idempotent: re-deploying the EVOLV flow re-runs the upsert with the same UID and Grafana replaces the existing dashboard rather than creating a duplicate.
### Position-based bucket fallback
When `defaultBucket` is empty AND `bucketMap[position]` has no entry:
| `positionVsParent` | Bucket used |
|:---|:---|
| `upstream` (case-insensitive) | `lvl1` |
| `downstream` (case-insensitive) | `lvl3` |
| any other / absent | `lvl2` |
Overridden by (in order): `config.defaultBucket` &rarr; `config.bucketMap[position]` &rarr; the table above. `INFLUXDB_BUCKET` env is read in `_buildConfig` and lands in `config.defaultBucket`.
### Root &rarr; child links
When `includeChildren=true` and the root has &ge; 1 direct child, the root dashboard's `links[]` is augmented with one entry per child:
```js
{
type: 'link',
title: childTitle,
url: `/d/${childUid}/${slugify(childTitle)}`,
tags: [],
targetBlank: false,
keepTime: true,
keepVariables: true,
}
```
`slugify` is lowercase-kebab-case, truncated to 60 chars. `keepTime` and `keepVariables` are Grafana's "preserve dashboard state across navigation" flags &mdash; clicking a link keeps the time range and templating selections.
---
## Lifecycle &mdash; what one event does
```mermaid
sequenceDiagram
autonumber
participant emitter as any EVOLV node
participant dash as dashboardAPI (nodeClass)
participant cr as commandRegistry
participant api as DashboardApi (specificClass)
participant out as Port 0
participant http as http request (downstream)
participant grafana as Grafana HTTP API
emitter->>dash: msg{topic: 'child.register', payload}
dash->>cr: dispatch(msg, source, ctx)
cr->>cr: canonicalise topic (alias→canonical, log deprecation once)
cr->>api: handlers.registerChild(source, msg, ctx)
api->>api: resolveChildSource(payload, ctx)
alt source missing
api-->>dash: throw 'Missing or invalid child node'
dash->>dash: node.status({fill:'red','dashboardapi error'})
dash->>dash: node.error(err, msg)
else source resolved
api->>api: generateDashboardsForGraph(childSource, {includeChildren})
api->>api: buildDashboard(root) → loadTemplate + stableUid + templating
api->>api: extractChildren → buildDashboard per child
api->>api: rootDash.links += child links
loop per dashboard in results
api->>out: ctx.send({...msg, topic:'create', url, method, headers, payload, meta})
out->>http: msg flows to downstream http request node
http->>grafana: POST /api/dashboards/db
end
end
```
One inbound event yields **N outbound HTTP envelopes**, where N = 1 (root) + count(direct children) when `includeChildren=true`, or 1 when `includeChildren=false`.
There is no FSM. There is no tick loop. There is no `state.emitter`. The node is event-driven and stateless &mdash; every `child.register` is handled independently and discarded.
---
## Output ports
| Port | Carries | Sample shape |
|:---|:---|:---|
| 0 (process) | One `topic: 'create'` HTTP envelope per generated dashboard | `{topic:'create', url, method:'POST', headers, payload:{dashboard,folderId:0,overwrite:true}, meta}` |
| 1 (telemetry) | **Unused.** No measurements; nothing emitted. | &mdash; |
| 2 (registration / control) | **Unused.** dashboardAPI is a sink for `child.register`, not a source. | &mdash; |
Port 0 deliberately diverges from the standard "process data + delta-compressed" convention: the envelope is a fully-formed HTTP request, shaped for a downstream `http request` core node. Caller-supplied `msg.*` fields propagate via the `{...msg, ...envelope}` spread so correlation / trace fields survive the hop.
> Per `.claude/rules/output-coverage.md`: this node has a small output surface (one Port-0 msg shape), and no tick / FSM states &mdash; the manifest is correspondingly small. The standard "every output, every state" sweep collapses to "every key in the envelope is present whenever a dashboard is generated; nothing is emitted when resolution fails."
---
## Event sources
| Source | Where it fires | What it triggers |
|:---|:---|:---|
| Inbound `msg.topic` | Node-RED input wire on Port 0 input | `commandRegistry.dispatch` &rarr; `handlers.registerChild` |
| Admin HTTP `GET /dashboardapi/menu.js` | Editor first-load | `MenuManager.createEndpoint('dashboardapi', ['logger'])` returns JS bootstrap |
| Admin HTTP `GET /dashboardapi/configData.js` | Editor first-load | Reads `dependencies/dashboardapi/dashboardapiConfig.json` and returns it as a JS-attached global on `window.EVOLV.nodes.dashboardapi.config` |
| `node.on('close')` | Node-RED redeploy / shutdown | No-op (handler exists but only calls `done()`) |
There is no `setInterval`, no `state.emitter`, no `child.measurements.emitter`. The node sleeps until `child.register` arrives.
---
## Where to start reading
| If you're changing... | Read first |
|:---|:---|
| Adding a new topic / changing the alias map | `src/commands/index.js` + `src/commands/handlers.js` |
| Payload resolution rules (string id / inline source / inline config) | `src/commands/handlers.js` `resolveChildSource` + `resolveChildNode` |
| Grafana URL composition / bearer token / headers | `src/specificClass.js` `grafanaUpsertUrl` + `handlers.registerChild` header logic |
| Template selection, alias rules, missing-template behaviour | `src/specificClass.js` `_templateFileForSoftwareType` + `loadTemplate` |
| UID derivation, dashboard composition, links | `src/specificClass.js` `buildDashboard` + `generateDashboardsForGraph` |
| Bucket fallback (position &rarr; lvl1/lvl2/lvl3) | `src/specificClass.js` `defaultBucketForPosition` |
| Editor form &harr; config keys | `dashboardapi.html` + `src/nodeClass.js` `_buildConfig` |
| Editor menu / config endpoints | `dashboardapi.js` (entry, admin endpoints) + `dependencies/dashboardapi/dashboardapiConfig.json` |
| Template content for a new EVOLV node type | `config/<softwareType>.json` &mdash; copy the closest existing one and adjust |
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Intuitive overview |
| [Reference &mdash; Contracts](Reference-Contracts) | Topic + config + template alias map |
| [Reference &mdash; Examples](Reference-Examples) | Shipped flows + debug recipes |
| [Reference &mdash; Limitations](Reference-Limitations) | Filename drift, stub flows, open questions |
| [EVOLV &mdash; Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Architecture) | Platform-wide three-tier pattern |
| [EVOLV &mdash; Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 InfluxDB layout (dashboardAPI is an exception &mdash; Port 0 carries HTTP envelopes) |

243
wiki/Reference-Contracts.md Normal file
View File

@@ -0,0 +1,243 @@
# Reference &mdash; Contracts
![code-ref](https://img.shields.io/badge/code--ref-a6f09d8-blue)
> [!NOTE]
> Full topic contract, configuration schema, child-resolution rules, and Port-0 envelope spec for `dashboardAPI`. Source of truth: `src/commands/index.js`, `src/commands/handlers.js`, `src/specificClass.js`, `src/nodeClass.js`, and `dependencies/dashboardapi/dashboardapiConfig.json`.
>
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
>
> For an intuitive overview, return to [Home](Home).
---
## Topic contract
The registry lives in `src/commands/index.js`. dashboardAPI has **one** canonical input topic.
<!-- BEGIN AUTOGEN: topic-contract -->
| Canonical topic | Aliases | Payload | Unit | Effect |
|---|---|---|---|---|
| `child.register` | `registerChild` | any | — | — |
<!-- END AUTOGEN: topic-contract -->
The `registerChild` alias logs a one-time deprecation warning on first use. There is **no HTTP endpoint contract** for dashboardAPI as a Node-RED node &mdash; it is an input-on-wire only. The outbound HTTP call shape is documented in [Port-0 envelope](#port-0-envelope-data-model) below.
### Payload resolution rules
| Payload shape | Resolved as | Source code |
|:---|:---|:---|
| `{source: {config: {...}}, ...}` | `payload.source` &mdash; use directly | `handlers.js` `resolveChildSource` line 6 |
| `{config: {...}}` | `{config: payload.config}` &mdash; wrap minimally | `handlers.js` `resolveChildSource` line 7 |
| `"<node-id>"` (bare string) | `RED.nodes.getNode(id).source` &rarr; fallback `node._flow.getNode(id).source` | `handlers.js` `resolveChildNode` |
| anything else | `null` &rarr; throws `'Missing or invalid child node'` | `handlers.js` `registerChild` line 30 |
`msg.includeChildren` (default `true`) controls graph-walk depth: `true` walks `extractChildren(rootSource)` and emits one dashboard per discovered child plus the root; `false` emits just the root dashboard.
---
## Data model &mdash; Port-0 envelope
<!-- BEGIN AUTOGEN: data-model — populate via wiki-gen tool (TODO) -->
dashboardAPI **has no domain output** &mdash; it does not extend `BaseDomain` and does not implement `getOutput()`. Port 0 carries one **HTTP request envelope** per generated dashboard, shaped for a downstream `http request` core node:
```js
{
topic: 'create',
url: 'http://<grafana-host>:<grafana-port>/api/dashboards/db',
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Bearer <token>' // only when grafanaConnector.bearerToken is set
},
payload: {
dashboard: { uid: '<12-char-sha1>', title: '<node-name>', templating: {...}, ... },
folderId: 0,
overwrite: true
},
meta: {
nodeId: '<from config.general.id or .name>',
softwareType: '<from config.functionality.softwareType>',
uid: '<same 12-char-sha1>',
title: '<same node name>'
}
}
```
Port 1 (InfluxDB telemetry) and Port 2 (registration / control plumbing) are **unused** &mdash; dashboardAPI has no measurements and does not register with a parent.
<!-- END AUTOGEN: data-model -->
### Envelope fields
| Key | Type | Source | Notes |
|:---|:---|:---|:---|
| `topic` | string | constant `'create'` | Signals "Grafana dashboard upsert". |
| `url` | string | `grafanaUpsertUrl()` | `${protocol}://${host}:${port}/api/dashboards/db`. |
| `method` | string | constant `'POST'` | &mdash; |
| `headers.Accept` | string | constant | `application/json` |
| `headers.Content-Type` | string | constant | `application/json` |
| `headers.Authorization` | string &#124; absent | `Bearer ${bearerToken}` | **Omitted entirely** when `bearerToken` is empty. |
| `payload.dashboard` | object | `buildUpsertRequest({dashboard, folderId, overwrite}).dashboard` | The composed Grafana dashboard JSON. |
| `payload.folderId` | integer | constant `0` | Root folder. Not configurable. |
| `payload.overwrite` | boolean | constant `true` | Required for idempotent re-deploys. |
| `meta.nodeId` | string | `config.general.id` or `config.general.name` or `softwareType` | Correlation id. |
| `meta.softwareType` | string | `config.functionality.softwareType` (case-insensitive lookup) | Used for template selection. |
| `meta.uid` | string | `sha1(softwareType:nodeId).slice(0, 12)` | Stable across re-deploys &mdash; same `(softwareType, nodeId)` &rarr; same UID. |
| `meta.title` | string | `config.general.name` or `nodeId` | Human-readable dashboard title. |
**`msg` propagation:** inbound `msg.*` fields are merged via `{...msg, topic:'create', ...}` spread &mdash; caller-supplied correlation / trace fields (e.g. `msg._msgid`, `msg.requestId`) survive the hop.
### Dashboard composition
For each generated dashboard, `buildDashboard({nodeConfig, positionVsParent})` performs:
1. **Template load** &mdash; `loadTemplate(softwareType)` from `config/<softwareType>.json` (case-insensitive fallback, `machineGroupControl &rarr; machineGroup.json` alias). Missing template &rarr; logs `warn` and returns `null` (the dashboard is skipped from the output).
2. **UID stamp** &mdash; `dashboard.uid = stableUid(softwareType:nodeId)`.
3. **Title stamp** &mdash; `dashboard.title = config.general.name || nodeId`.
4. **Tags merge** &mdash; existing `template.tags` + `['EVOLV', softwareType, positionVsParent]` (deduplicated, empty values filtered).
5. **Templating var fill** &mdash; `dashboard.templating.list[]` entries named `measurement` and `bucket` are mutated in place:
- `measurement` &larr; `${softwareType}_${nodeId}` (used as InfluxDB measurement name in panel queries).
- `bucket` &larr; resolved bucket (see [Bucket resolution](#bucket-resolution) below).
6. **Links append** (root dashboard only, when `includeChildren=true` and `children.length > 0`) &mdash; one `{type:'link', title, url:'/d/<uid>/<slug>', keepTime, keepVariables}` entry per direct child.
If `dashboard.templating.list` is not an array or the named variable doesn't exist, the templating step is a no-op (no error).
### Bucket resolution
`bucket` (the InfluxDB bucket templating var) is resolved in priority order:
| Priority | Source | When applied |
|:---:|:---|:---|
| 1 | `config.defaultBucket` (editor field or `INFLUXDB_BUCKET` env) | When set to a non-empty string |
| 2 | `config.bucketMap[positionVsParent]` | When the position has an entry |
| 3 | `defaultBucketForPosition(positionVsParent)` | Falls through &mdash; `upstream &rarr; lvl1`, `downstream &rarr; lvl3`, else `lvl2` |
> [!NOTE]
> Priorities 1 and 2 read order from `specificClass.js` `buildDashboard`. Verify against the editor's intended semantics during full review &mdash; "global override beats per-position map" is the current behaviour. Flagged.
---
## Configuration schema &mdash; editor form to config keys
Source of truth: `dependencies/dashboardapi/dashboardapiConfig.json` + `src/nodeClass.js` `_buildConfig`. The runtime config slice is built by `configManager.buildConfig(name, uiConfig, nodeId, overrides)`.
### General (`config.general`)
| Form field | Config key | Default | Notes |
|:---|:---|:---|:---|
| Name | `general.name` | `'dashboardapi'` | Display label; falls through to nodeId in `meta.title`. |
| (auto-assigned) | `general.id` | `null` | Node-RED node id. |
| Enable logging | `general.logging.enabled` | `false` (per `_buildConfig`) / `true` (per `dashboardapiConfig.json`) | **Mismatch** &mdash; see [Limitations](Reference-Limitations#config-default-mismatch). |
| Log level | `general.logging.logLevel` | `'info'` | `debug` / `info` / `warn` / `error`. |
### Functionality (`config.functionality`)
| Form field | Config key | Default | Notes |
|:---|:---|:---|:---|
| (hidden) | `functionality.softwareType` | `'dashboardapi'` | Constant. Set in `_buildConfig` from `this.name.toLowerCase()`. |
| (hidden) | `functionality.role` | `'auto ui generator'` | Constant. |
### Grafana connector (`config.grafanaConnector`)
| Form field | Config key | Default | Range / values | Where used |
|:---|:---|:---|:---|:---|
| Protocol | `grafanaConnector.protocol` | `'http'` | `http` / `https` | `grafanaUpsertUrl()` |
| Grafana Host | `grafanaConnector.host` | `'localhost'` | hostname / IP | `grafanaUpsertUrl()` |
| Grafana Port | `grafanaConnector.port` | `3000` | 1&ndash;65535 (`Number(uiConfig.port \|\| 3000)`) | `grafanaUpsertUrl()` |
| Bearer Token | `grafanaConnector.bearerToken` | `''` | string (Grafana service-account token) | `Authorization: Bearer ...` header; omitted when empty |
### Bucket configuration
| Form field | Config key | Default | Notes |
|:---|:---|:---|:---|
| InfluxDB Bucket | `defaultBucket` | `''` &rarr; falls back to `process.env.INFLUXDB_BUCKET` &rarr; position default | Set in `_buildConfig`; consumed by `buildDashboard` templating fill. |
| (no editor field) | `bucketMap` | `{}` | Programmatic only &mdash; pass via `uiConfig.bucketMap` or future editor field. |
### Editor menu / logger fields
The `dashboardapi.html` template invokes `window.EVOLV.nodes.dashboardapi.loggerMenu.initEditor / saveEditor` via the shared `MenuManager`-served `/dashboardapi/menu.js` endpoint. The logger fields (`enableLog`, `logLevel`) are persisted on the node via the standard EVOLV editor menu pattern.
> [!WARNING]
> **Editor `defaults` use legacy field names.** `dashboardapi.html` declares `{enableLog, logLevel}` as Node-RED defaults but the runtime config reads `general.logging.{enabled, logLevel}`. The bridge is the shared logger menu (`MenuManager`) &mdash; confirm during full review that the editor menu correctly maps `enableLog` &rarr; `general.logging.enabled`.
---
## Template alias map
`_templateFileForSoftwareType(softwareType)` lookup order:
| Order | Candidate filename | Notes |
|:---:|:---|:---|
| 1 | `<softwareType>.json` | Exact case. |
| 2 | `<softwareType.toLowerCase()>.json` | Case-insensitive fallback. |
| 3 | `machineGroup.json` | **Only** when `softwareType === 'machineGroupControl'` (one-off alias). |
If none of the candidates exist in `config/`, the logger emits `No dashboard template found for softwareType=<st>` at `warn` level and `loadTemplate` returns `null`. `buildDashboard` then logs `Skipping dashboard generation: no template for softwareType=<st>` and returns `null`; `generateDashboardsForGraph` skips that node and continues with the rest of the graph walk.
Currently shipped templates:
| softwareType (canonical) | Template file | Notes |
|:---|:---|:---|
| `aeration` | `aeration.json` | &mdash; |
| `dashboardapi` | `dashboardapi.json` | Self-template (when a dashboardAPI registers as a child of another dashboardAPI &mdash; unusual). |
| `machine` (or `rotatingmachine`) | `machine.json` | softwareType to verify in full review &mdash; flagged. |
| `machineGroupControl` | `machineGroup.json` | Via one-off alias. |
| `measurement` | `measurement.json` | &mdash; |
| `monster` | `monster.json` | &mdash; |
| `pumpingStation` | `pumpingStation.json` | &mdash; |
| `reactor` | `reactor.json` | &mdash; |
| `settler` | `settler.json` | &mdash; |
| `valve` | `valve.json` | &mdash; |
| `valveGroupControl` | `valveGroupControl.json` | &mdash; |
Adding support for a new EVOLV node type = drop a `config/<newType>.json` file matching the `softwareType` lowercase name (or add an alias arm to `_templateFileForSoftwareType`).
---
## Child resolution (NOT a registry)
dashboardAPI does **not** maintain a child registry of its own. There is no `_registeredChildren` map, no `child.register` &rarr; `child.unregister` lifecycle, no parent &rarr; child emitter wiring. Every inbound `child.register` is a **one-shot** dashboard generation:
```mermaid
flowchart LR
src["any EVOLV node<br/>(has functionality.softwareType)"]:::other -->|child.register| dash[dashboardAPI<br/>Utility]:::neutral
dash --> resolve["resolveChildSource(payload, ctx)<br/>RED.nodes.getNode → _flow.getNode → inline"]
resolve --> walk["generateDashboardsForGraph(childSource, {includeChildren})"]
walk --> emit["emit one msg per dashboard<br/>topic='create'"]
emit --> http[(downstream<br/>http request node)]
classDef neutral fill:#dddddd,color:#000
classDef other fill:#ffffff,stroke:#666
```
### What graph walk reads from the child source
`extractChildren(rootSource)` reads `rootSource.childRegistrationUtils.registeredChildren` (a Map). For each `entry`:
- `entry.child` &mdash; the child source object (must have `.config`).
- `entry.position` (or `child.positionVsParent`) &mdash; used for the bucket fallback and tag composition.
Children without a `.config` are silently skipped. If `rootSource.childRegistrationUtils` is absent or `registeredChildren.values` is not a function, the result is an empty array &mdash; just the root dashboard is emitted.
| Inbound softwareType | Filter | Side effect |
|:---|:---|:---|
| any | child has `functionality.softwareType` AND the matching `config/*.json` exists | Loads template; emits one upsert msg per dashboard in the walk. |
| any | child has `functionality.softwareType` but the template is missing | Warns and skips that node's dashboard. No error thrown. Graph walk continues. |
| absent / malformed | `resolveChildSource` returns null | Throws `Missing or invalid child node` &rarr; nodeClass sets red status, calls `node.error`. |
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Intuitive overview |
| [Reference &mdash; Architecture](Reference-Architecture) | Code map, lifecycle, graph walk |
| [Reference &mdash; Examples](Reference-Examples) | Shipped flows + debug recipes |
| [Reference &mdash; Limitations](Reference-Limitations) | Filename drift, stub flows, open questions |
| [EVOLV &mdash; Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) | Platform-wide topic rules |
| [EVOLV &mdash; Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 layout (dashboardAPI is an exception &mdash; Port 0 carries HTTP envelopes) |

171
wiki/Reference-Examples.md Normal file
View File

@@ -0,0 +1,171 @@
# Reference &mdash; Examples
![code-ref](https://img.shields.io/badge/code--ref-a6f09d8-blue)
> [!NOTE]
> Every example flow shipped under `nodes/dashboardAPI/examples/`, plus how to load them, what they show, and the debug recipes that go with them. Live source: `nodes/dashboardAPI/examples/`.
>
> Pending full node review (2026-05). The shipped example flows are **stubs** &mdash; they wire up the node but the inject payloads do not yet match the `child.register` resolver's expected shape. Working wiring patterns are documented inline below.
---
## Shipped examples
| File | Tier | Dependencies | What it shows | Status |
|:---|:---:|:---|:---|:---|
| `basic.flow.json` | 1 | EVOLV only | Inject a `ping` topic into a stand-alone dashboardAPI node + debug tap on Port 0. | &#9203; **Stub** &mdash; inject topic is `ping`, not `child.register`; the registry will silently drop the msg. |
| `integration.flow.json` | 2 | EVOLV only | Inject a `registerChild` alias topic with a bare-string node id (`'example-child-id'`) + debug tap. | &#9203; **Stub** &mdash; the bare-string id resolves to `null` via `RED.nodes.getNode`; throws `'Missing or invalid child node'`. |
| `edge.flow.json` | 3 | EVOLV only | Inject an unknown topic to confirm the dispatcher silently drops it. | &#10003; Works as a registry-coverage probe. |
All three are tracked for replacement in the next wiki-cleanup pass &mdash; see [Limitations &mdash; Example flow stubs](Reference-Limitations#example-flow-stubs).
---
## Loading a flow
### Via the editor
1. Open the Node-RED editor at `http://localhost:1880`.
2. Menu &rarr; Import &rarr; drag the JSON file.
3. Click Deploy.
### Via the Admin API
```bash
curl -X POST -H 'Content-Type: application/json' \
--data @nodes/dashboardAPI/examples/basic.flow.json \
http://localhost:1880/flows
```
---
## Working wiring patterns
These are the shapes that actually exercise the resolver. Use them as the basis for any new example flow until the stubs above are replaced.
### Wiring pattern A &mdash; inline `source` payload (no real EVOLV node needed)
```json
[
{
"type": "inject",
"topic": "child.register",
"props": [
{"p": "topic", "vt": "str"},
{"p": "payload", "v": "{\"source\":{\"config\":{\"functionality\":{\"softwareType\":\"measurement\"},\"general\":{\"id\":\"pump-a-flow\",\"name\":\"Pump A flow\"}}}}", "vt": "json"}
]
},
{ "type": "dashboardapi" },
{ "type": "http request", "method": "POST" },
{ "type": "debug", "complete": "true" }
]
```
What happens:
1. The inject fires a msg with `topic: 'child.register'` and `payload.source.config.functionality.softwareType = 'measurement'`.
2. `resolveChildSource` matches the `payload.source.config` branch and returns `payload.source` directly.
3. `loadTemplate('measurement')` reads `config/measurement.json`.
4. `stableUid('measurement:pump-a-flow')` &rarr; deterministic 12-char hex.
5. The Port-0 envelope flows to the debug node AND to the `http request` node which POSTs to Grafana.
### Wiring pattern B &mdash; bare `config` payload
Same as pattern A but with the outer `source` wrapper dropped:
```json
"payload": "{\"config\":{\"functionality\":{\"softwareType\":\"pumpingStation\"},\"general\":{\"id\":\"ps_demo\",\"name\":\"Pumping Station Demo\"}}}"
```
`resolveChildSource` falls through to the `payload.config` branch and wraps as `{config: payload.config}`. No `childRegistrationUtils` is present, so the graph walk emits only the root dashboard (no children even if `includeChildren=true`).
### Wiring pattern C &mdash; real EVOLV node via Port 2
The canonical production wiring: any EVOLV node's Port 2 (`registerChild` emission) wired into dashboardAPI's input.
```text
[rotatingMachine] Port 2 ──► [dashboardAPI] Port 0 ──► [http request] ──► Grafana
└─► [debug]
```
The emitting node's `child.register` payload is the bare node id (a string). `resolveChildNode` then runs `RED.nodes.getNode(id)` to fetch the live runtime node and reads `node.source.config`. Walks `node.source.childRegistrationUtils.registeredChildren` so direct children also get dashboards.
> [!IMPORTANT]
> **Example needed.** A Tier-2 example that wires a real `rotatingMachine` or `pumpingStation` Port 2 to dashboardAPI input is the missing canonical demo. Save as `nodes/dashboardAPI/examples/02-Integration-with-EVOLV-node.json`. Track in `IMPROVEMENTS_BACKLOG.md`.
---
## Docker compose snippet
To bring up Node-RED + Grafana (+ optional InfluxDB) for end-to-end testing:
```yaml
services:
nodered:
build: ./docker/nodered
ports: ['1880:1880']
volumes:
- ./docker/nodered/data:/data/evolv
environment:
INFLUXDB_BUCKET: lvl2
grafana:
image: grafana/grafana:11.0.0
ports: ['3000:3000']
environment:
GF_SECURITY_ADMIN_PASSWORD: admin
influxdb:
image: influxdb:2.7
ports: ['8086:8086']
```
A Grafana service account token (created via Grafana UI &rarr; Administration &rarr; Service accounts) goes into the dashboardAPI's Bearer Token editor field.
Full file: [EVOLV/docker-compose.yml](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/docker-compose.yml).
---
## Debug recipes
| Symptom | First thing to check | Where to look |
|:---|:---|:---|
| No HTTP message emitted on Port 0; node shows red `dashboardapi error` status | `resolveChildSource` returned `null`. Check payload shape against [Payload resolution rules](Reference-Contracts#payload-resolution-rules). The most common cause: bare-string id that doesn't match a live Node-RED node. | `src/commands/handlers.js` `resolveChildSource` + `resolveChildNode`. |
| Dispatch silently drops msg (no error, no output) | Topic is not `child.register` and not the `registerChild` alias. The registry's catch-all is "no match &rarr; ignore". | `src/commands/index.js` + `createRegistry` source in `generalFunctions/`. |
| `Skipping dashboard generation: no template for softwareType=<st>` warn | `config/<softwareType>.json` (or its lowercase variant or alias) doesn't exist. | `config/` directory &mdash; add a template JSON, or fix the emitting node's `functionality.softwareType`. |
| `machineGroupControl` produces no dashboard | The alias maps to `machineGroup.json` &mdash; verify that file exists in `config/`. | `_templateFileForSoftwareType` in `src/specificClass.js`. |
| Empty `Authorization` header | `bearerToken` not set in editor form &mdash; the header is omitted entirely when the token is empty, not set to `'Bearer '`. | Editor &rarr; Bearer Token field. |
| Wrong InfluxDB bucket in Grafana template variables | `defaultBucket` config (or `INFLUXDB_BUCKET` env) overrides the position-based default. Priority order: `defaultBucket` &rarr; `bucketMap[position]` &rarr; `defaultBucketForPosition`. | `_buildConfig` in `nodeClass.js` + `defaultBucketForPosition` in `specificClass.js`. |
| Dashboard UID changes between deploys | Node id or `softwareType` changed &mdash; UID is `sha1(softwareType:nodeId).slice(0, 12)`. Stable only if both are stable. | `stableUid` in `specificClass.js`. |
| `registerChild` alias warns once | Expected &mdash; deprecation warning on first use only. Migrate caller to `child.register`. | Caller `msg.topic`. |
| Grafana 404 on `POST /api/dashboards/db` | Wrong path = check Grafana version. The `/api/dashboards/db` endpoint exists in Grafana 7&ndash;11. For newer Grafana with org-scoped endpoints, the upsert URL may differ. | `grafanaUpsertUrl` in `specificClass.js`. |
| Grafana 401 / 403 | Bearer token missing, expired, or insufficient permissions. The service account needs at least `Editor` role on the target folder. | Grafana UI &rarr; Administration &rarr; Service accounts. |
| Root dashboard has no `links[]` to children | `includeChildren=false` was passed, OR the root source's `childRegistrationUtils.registeredChildren` is empty / absent. | `generateDashboardsForGraph` + `extractChildren`. |
| Editor form shows blank fields after re-open | `oneditprepare` waits for `window.EVOLV.nodes.dashboardapi.loggerMenu` which is loaded by `/dashboardapi/menu.js`. If the menu endpoint 500s, the editor stays blank. | Browser devtools &rarr; Network &rarr; `menu.js`; check the entry file's logger menu endpoint. |
> Never ship `enableLog: 'debug'` in a demo &mdash; fills the container log within seconds and obscures real errors. Use only for live debugging sessions.
---
## Quick smoke test (no Grafana required)
To verify the node loads and the registry dispatches correctly without standing up Grafana:
1. Import `examples/basic.flow.json` (or any of the stubs).
2. Edit the inject node: set topic to `child.register` and payload to a JSON object matching wiring pattern A above.
3. Deploy.
4. Fire the inject. The debug pane should show a `topic: 'create'` envelope with a populated `payload.dashboard`.
5. If `headers.Authorization` is absent, the editor's Bearer Token field is empty &mdash; that's correct behaviour.
The downstream `http request` node is **optional** for the smoke test &mdash; the dashboardAPI emits regardless of whether anything POSTs the envelope to Grafana.
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Intuitive overview |
| [Reference &mdash; Contracts](Reference-Contracts) | Topic + payload resolution + envelope shape |
| [Reference &mdash; Architecture](Reference-Architecture) | Code map, lifecycle, graph walk |
| [Reference &mdash; Limitations](Reference-Limitations) | Stub flows, filename drift, open questions |
| [EVOLV &mdash; Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) | Where dashboardAPI fits in a larger plant |

View File

@@ -0,0 +1,156 @@
# Reference &mdash; Limitations
![code-ref](https://img.shields.io/badge/code--ref-a6f09d8-blue)
> [!NOTE]
> What `dashboardAPI` does not do, current rough edges, and open questions. Open items live in `.agents/improvements/IMPROVEMENTS_BACKLOG.md` and `.claude/refactor/OPEN_QUESTIONS.md` in the superproject.
>
> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
---
## When you would not use this node
| Scenario | Use instead |
|:---|:---|
| You maintain Grafana dashboards by hand | Skip dashboardAPI &mdash; it will overwrite your customisations on every `child.register` (upsert is `overwrite: true`). |
| You need arbitrary Grafana API calls (annotations, alerts, data sources, folders) | A plain `http request` node. dashboardAPI only emits `POST /api/dashboards/db` envelopes. |
| You want to forward tick / measurement data to Grafana | This is not what dashboardAPI does. Wire telemetry through Port 1 of an EVOLV process node directly into InfluxDB; Grafana queries InfluxDB. |
| You want to use dashboardAPI as a BaseDomain-capable child of something else | Not supported &mdash; dashboardAPI does not extend `BaseDomain` and cannot register as a child of `machineGroupControl` / `pumpingStation` / similar. See [No BaseNodeAdapter / BaseDomain](#no-basenodeadapter--basedomain) below. |
| You expect EVOLV nodes to auto-discover dashboardAPI | They don't. Port 2 of the emitter must be wired into dashboardAPI's input explicitly. |
---
## Known limitations
### Legacy filename drift
The entry file and editor HTML are currently lowercase &mdash; `dashboardapi.js` and `dashboardapi.html` &mdash; rather than `dashboardAPI.js` / `dashboardAPI.html` per the canonical folder-name convention in `.claude/rules/node-architecture.md`.
The convention rule explicitly calls this out as legacy drift to fix when the file is next touched. A rename is a four-touch change:
1. `dashboardapi.js` &rarr; `dashboardAPI.js`
2. `dashboardapi.html` &rarr; `dashboardAPI.html`
3. `package.json#node-red.nodes` &mdash; key remains `dashboardapi` (the Node-RED type id is independent of the filename) but the value becomes `dashboardAPI.js`.
4. Superproject submodule references and any `require()` paths.
The Node-RED **type id** (`dashboardapi`, lowercase, registered via `RED.nodes.registerType('dashboardapi', …)`) must stay `dashboardapi` to avoid breaking existing flows in the wild. The rename is purely the source-file path. Tracked.
### Example flow stubs
The three shipped flows (`basic.flow.json`, `integration.flow.json`, `edge.flow.json`) are placeholders. Their inject nodes don't fire a payload that matches the `child.register` resolver:
| File | Current behaviour | What's wrong |
|:---|:---|:---|
| `basic.flow.json` | Inject `topic: 'ping'` | Not `child.register`; registry silently drops. |
| `integration.flow.json` | Inject `topic: 'registerChild'` with `payload: 'example-child-id'` (string) | The string id has no live Node-RED node behind it; `RED.nodes.getNode('example-child-id')` returns null; throws `'Missing or invalid child node'`. |
| `edge.flow.json` | Inject `topic: 'doesNotExist'` | Works as a registry-coverage probe (silent drop is correct) but exercises nothing. |
Working wiring patterns are documented inline in [Reference &mdash; Examples](Reference-Examples#working-wiring-patterns). Replacement of the stubs is tracked in `IMPROVEMENTS_BACKLOG.md` (P9 wiki cleanup follow-up).
### No BaseNodeAdapter / BaseDomain
Most EVOLV nodes inherit a common adapter / domain base class. dashboardAPI does not. The decision is recorded in `OPEN_QUESTIONS.md` (2026-05-10) &mdash; four blockers (no platform config JSON, no periodic output, no parent registration, no status badge / tick / measurements). Until `BaseNodeAdapter` grows passive-mode flags (skip-registration + skip-output-stream), the bespoke adapter shape is the correct compromise.
Consequence: dashboardAPI cannot be introspected via the standard `getOutput()` channel. Debugging relies on watching Port 0 in a debug node.
### No domain output / no manifest
Per `.claude/rules/output-coverage.md`, every node should ship a `test/_output-manifest.md` enumerating every Port-0/1/2 key in populated and degraded states. dashboardAPI's output surface is **one envelope shape**, emitted only when a dashboard is successfully generated &mdash; there is no degraded "partial envelope" state to test. The manifest collapses to:
| Port | Output | Populated state | Degraded state |
|:---|:---|:---|:---|
| 0 | `{topic, url, method, headers, payload, meta}` envelope | Emitted once per generated dashboard | **Not emitted** &mdash; on resolution failure the handler throws and nodeClass sets a red status badge instead |
| 1 | (unused) | &mdash; | &mdash; |
| 2 | (unused) | &mdash; | &mdash; |
The full output-coverage rule applies prospectively; no backfill manifest exists yet. Tracked.
### Template discovery is filename-based
The template lookup is `softwareType` &harr; filename. Renaming a node's `softwareType` (e.g. `rotatingmachine` &rarr; `rotatingMachine`) requires either renaming the template file or adding an alias arm in `_templateFileForSoftwareType`. The `machineGroupControl &rarr; machineGroup.json` mapping is a one-off alias because the historical filename was abbreviated.
> [!NOTE]
> Verify in full review: which softwareType does the current `rotatingMachine` emit? The shipped template is `config/machine.json` &mdash; if `rotatingMachine`'s `functionality.softwareType` is `'rotatingmachine'` (lowercase), the case-insensitive fallback won't find it and dashboard generation will warn-and-skip. Flagged.
### No retry / circuit-breaker on downstream HTTP
dashboardAPI emits the upsert envelope and is done. If the downstream `http request` node fails (Grafana down, 5xx, network timeout), the dashboard upsert is silently dropped &mdash; no retry, no DLQ, no status badge propagation back to dashboardAPI. The caller is responsible for wiring retry logic into the http-request path.
### `oneditsave` doesn't read all editor fields uniformly
`dashboardapi.html` `oneditsave` reads `['name', 'protocol', 'host', 'port', 'bearerToken', 'defaultBucket']` via direct DOM lookups, separately from the logger menu's `saveEditor`. Adding a new editor field requires touching both the form HTML and the `oneditsave` whitelist. Mild; not load-bearing.
### Config default mismatch
The runtime `_buildConfig` defaults `general.logging.enabled` to `Boolean(config?.general?.logging?.enabled)` &mdash; effectively `false` when the editor doesn't set it. But `dependencies/dashboardapi/dashboardapiConfig.json` declares the default as `true`. The editor menu (`loggerMenu`) bridges these via the standard EVOLV logger pattern, but the divergence is worth confirming &mdash; logger enabled vs disabled changes whether `Skipping dashboard generation: no template …` warns appear at all.
> [!NOTE]
> Confirm in full review which side wins by default for a freshly-dropped node. Flagged.
### `bucket` resolution priority is global-then-per-position
`buildDashboard` reads `this.config.defaultBucket || this.config.bucketMap[position] || defaultBucketForPosition(position)`. The global override fires **before** the per-position map. If you want per-position buckets to win over a global default, the current code doesn't do that &mdash; you'd need to leave `defaultBucket` empty and rely solely on `bucketMap` + the position fallback.
Open question whether the "global beats per-position" priority is the intended semantics. Flagged.
### No InfluxDB bucket validation
The bucket name is templated into the Grafana dashboard JSON without any check that the bucket exists in InfluxDB. A typo produces a dashboard that renders panels saying "no data" with no upstream warning. Tracked.
---
## Open questions (tracked)
| Question | Where it lives |
|:---|:---|
| Should `BaseNodeAdapter` grow a passive / HTTP-only mode (skip-registration + skip-output-stream) so dashboardAPI can extend it? | `.claude/refactor/OPEN_QUESTIONS.md` (2026-05-10) &mdash; "dashboardAPI skipped BaseNodeAdapter + BaseDomain" |
| Confirm `rotatingMachine` softwareType &harr; `config/machine.json` mapping | Internal &mdash; flag during full review |
| Bucket priority: should per-position `bucketMap` beat global `defaultBucket`? | Internal |
| Should dashboardAPI emit a Port-2 status / health pulse so other EVOLV nodes can detect it? | Internal |
| Should `child.register` aliases include older topic names (e.g. `RegisterChild`, `register-child`) for legacy compat? | Internal |
| Add an explicit `child.unregister` / `dashboard.delete` topic to remove orphaned Grafana dashboards | Internal |
| Provide a programmatic way to bulk-regenerate all dashboards for an existing deployment (e.g. `cmd.regenerate-all`) | Internal |
| Retry / DLQ for failed Grafana upserts | TBD |
---
## Migration notes
### From the `registerChild` alias
The canonical topic since 2026-Q1 is `child.register`. The `registerChild` alias still works but logs a one-time deprecation warning on first use. Migrate callers when convenient:
```diff
- msg.topic = 'registerChild';
+ msg.topic = 'child.register';
```
Both topics accept identical payloads.
### From bare-string node-id payloads
The handler resolves bare-string payloads via `RED.nodes.getNode(id) &rarr; node._flow.getNode(id) &rarr; null`. This works at runtime but is brittle for tests and for flows where the emitter and dashboardAPI live on different `_flow` instances. Prefer the inline `{source: {config: {...}}}` or `{config: {...}}` shapes for tests and for any flow that imports both sides as JSON (no `RED.nodes` registry at compile time).
### From hand-curated Grafana dashboards
If you're moving from hand-curated dashboards to dashboardAPI-generated ones:
1. Export your existing dashboard JSON from Grafana.
2. Replace the templating-var values for `measurement` and `bucket` with placeholders.
3. Save as `nodes/dashboardAPI/config/<softwareType>.json`.
4. The next `child.register` for that softwareType will upsert (overwrite) the existing dashboard, preserving the UID if you set it to match `stableUid(softwareType:nodeId)`.
If you want to **preserve the UID** of an existing hand-curated dashboard, compute `sha1(softwareType:nodeId).slice(0, 12)` and check it matches your existing UID. If not, either rename the node id, or accept that the first upsert will create a new dashboard alongside the old one.
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Intuitive overview |
| [Reference &mdash; Contracts](Reference-Contracts) | Topic + payload resolution + envelope shape |
| [Reference &mdash; Architecture](Reference-Architecture) | Code map, lifecycle, "no BaseNodeAdapter" rationale |
| [Reference &mdash; Examples](Reference-Examples) | Shipped flows + debug recipes + working wiring patterns |
| [EVOLV &mdash; Open Questions](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/OPEN_QUESTIONS.md) | Cross-node open questions and decisions log |

20
wiki/_Sidebar.md Normal file
View File

@@ -0,0 +1,20 @@
### dashboardAPI
- [Home](Home)
**Reference**
- [Contracts](Reference-Contracts)
- [Architecture](Reference-Architecture)
- [Examples](Reference-Examples)
- [Limitations](Reference-Limitations)
**Related**
- [EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home)
- [rotatingMachine wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home)
- [pumpingStation wiki](https://gitea.wbd-rd.nl/RnD/pumpingStation/wiki/Home)
- [machineGroupControl wiki](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Home)
- [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns)
- [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)
- [Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry)