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>
227 lines
11 KiB
Markdown
227 lines
11 KiB
Markdown
# dashboardAPI
|
||
|
||
> **Reflects code as of `92d7eba` · regenerated `2026-05-11` via `npm run wiki:all`**
|
||
> If this banner is stale, the page may be out of date. Treat as informative, not authoritative.
|
||
|
||
## 1. What this node is
|
||
|
||
**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.
|
||
|
||
## 2. Position in the platform
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
ps[pumpingStation<br/>Process Cell]:::pc -.child.register.-> dash
|
||
mgc[machineGroupControl<br/>Unit]:::unit -.child.register.-> dash
|
||
rm[rotatingMachine<br/>Equipment]:::equip -.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)]
|
||
classDef pc fill:#0c99d9,color:#fff
|
||
classDef unit fill:#50a8d9,color:#000
|
||
classDef equip fill:#86bbdd,color:#000
|
||
classDef ctrl fill:#a9daee,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.
|
||
|
||
## 3. Capability matrix
|
||
|
||
| Capability | Status | Notes |
|
||
|---|---|---|
|
||
| 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
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
subgraph nodeRED["nodeClass.js — passive adapter"]
|
||
nc["createRegistry(commands)<br/>dispatch on msg input<br/>NO BaseNodeAdapter"]
|
||
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… |
|
||
|---|---|---|
|
||
| `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.
|
||
|
||
## 5. Topic contract
|
||
|
||
> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`.
|
||
|
||
<!-- BEGIN AUTOGEN: topic-contract -->
|
||
|
||
| Canonical topic | Aliases | Payload | Unit | Effect |
|
||
|---|---|---|---|---|
|
||
| `child.register` | `registerChild` | `any` | — | Parent/child plumbing — registers or unregisters a child node. |
|
||
|
||
<!-- END AUTOGEN: topic-contract -->
|
||
|
||
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.
|
||
|
||
## 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.
|
||
|
||
```mermaid
|
||
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 |
|
||
|---|---|---|
|
||
| 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',
|
||
url: 'http://<grafana>:<port>/api/dashboards/db',
|
||
method: 'POST',
|
||
headers: {
|
||
Accept: 'application/json',
|
||
'Content-Type': 'application/json',
|
||
Authorization: 'Bearer …' // only when bearerToken is set
|
||
},
|
||
payload: { dashboard: {…}, folderId: 0, overwrite: true },
|
||
meta: { nodeId, softwareType, uid, title }
|
||
}
|
||
```
|
||
|
||
Port 1 (InfluxDB telemetry) and Port 2 (registration / control plumbing) are unused — dashboardAPI has no measurements and does not register with a parent.
|
||
|
||
<!-- END AUTOGEN: data-model -->
|
||
|
||
See CONTRACT.md for the full envelope spec.
|
||
|
||
## 9. Configuration — editor form ↔ config keys
|
||
|
||
```mermaid
|
||
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 |
|
||
|---|---|---|---|---|
|
||
| Protocol | `grafanaConnector.protocol` | `http` | `http`\|`https` | `grafanaUpsertUrl()` |
|
||
| Host | `grafanaConnector.host` | `localhost` | hostname | `grafanaUpsertUrl()` |
|
||
| Port | `grafanaConnector.port` | `3000` | 1–65535 | `grafanaUpsertUrl()` |
|
||
| Bearer token | `grafanaConnector.bearerToken` | `''` | string | `Authorization` header |
|
||
| Default bucket | `defaultBucket` | `''` / `INFLUXDB_BUCKET` env | string | `updateTemplatingVar(bucket)` |
|
||
|
||
## 11. Examples
|
||
|
||
| Tier | File | What it shows | Status |
|
||
|---|---|---|---|
|
||
| Basic | `examples/01-Basic.flow.json` | Inject `child.register` payload + a downstream `http request` → mock Grafana | ⏳ TBD |
|
||
| Integration | `examples/02-Integration.flow.json` | Real EVOLV node (e.g. pumpingStation) wired into dashboardAPI | ⏳ TBD |
|
||
| Dashboard | _n/a_ | dashboardAPI **is** the dashboard plumbing — no FlowFuse tier | — |
|
||
|
||
## 12. Debug recipes
|
||
|
||
| 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.
|
||
|
||
## 13. When you would NOT use this node
|
||
|
||
- Use dashboardAPI when you want **auto-generated Grafana dashboards** from EVOLV node topology. If you maintain dashboards by hand in Grafana, skip it.
|
||
- 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 |
|