Files
dashboardAPI/wiki/Home.md
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

288 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# dashboardAPI
> **Reflects code as of `7b3da23` · 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 a utility node that converts EVOLV node topology into Grafana dashboards. On each `child.register` event it resolves the child's 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. It has no measurements, no tick loop, no parent registration, and no BaseDomain/BaseNodeAdapter.
## 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)]
grafana -.renders dashboards for.-> ff[FlowFuse / Browser]
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 is a utility node (`#dddddd` neutral). Dashed arrows = inbound `child.register` events (fired at deploy time). The solid arrow is the outbound HTTP upsert on Port 0. The Grafana dashboards that result are what FlowFuse / browser clients view.
## 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` node. |
| Walk child graph + emit per-child dashboards | ✅ | `msg.includeChildren: true` by default; opt-out per call. |
| Add root → child dashboard `links[]` | ✅ | Each direct child appears as a navigation link on the root dashboard. |
| Template selection by `softwareType` | ✅ | Reads from `config/<softwareType>.json`; case-insensitive fallback. `machineGroupControl``machineGroup.json` alias. |
| Stable dashboard UID across re-deploys | ✅ | SHA-1(`softwareType:nodeId`) first 12 chars — deterministic, idempotent upsert. |
| Bearer-token auth header | ✅ | Set via editor `bearerToken` field; omitted if empty. |
| InfluxDB bucket injection per position | ✅ | `upstream → lvl1`, `downstream → lvl3`, else `lvl2`; overridden by `defaultBucket` or `INFLUXDB_BUCKET` env. |
| Domain output on Port 0 | ❌ | Port 0 carries HTTP request envelopes only, not measurement data. |
| 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 entry["dashboardapi.js — entry (Node-RED registration)"]
e["RED.nodes.registerType('dashboardapi')<br/>admin: GET /dashboardapi/menu.js<br/>admin: GET /dashboardapi/configData.js"]
end
subgraph nodeRED["src/nodeClass.js — passive adapter"]
nc["_buildConfig(uiConfig)<br/>createRegistry(commands)<br/>_attachInputHandler() → dispatch<br/>NO BaseNodeAdapter"]
end
subgraph domain["src/specificClass.js — DashboardApi service"]
sc["buildDashboard({ nodeConfig, positionVsParent })<br/>generateDashboardsForGraph(rootSource)<br/>extractChildren(nodeSource)<br/>grafanaUpsertUrl()<br/>loadTemplate(softwareType)"]
end
subgraph cmd["src/commands/"]
h["index.js — child.register + registerChild alias<br/>handlers.js — resolveChildSource + registerChild"]
end
subgraph tpl["config/ — Grafana JSON templates"]
t["aeration | dashboardapi | machine<br/>machineGroup | measurement | monster<br/>pumpingStation | reactor | settler<br/>valve | valveGroupControl"]
end
e --> nodeRED
nodeRED --> domain
nodeRED --> cmd
domain --> tpl
```
| Module | Owns | Read first if you're changing… |
|---|---|---|
| `dashboardapi.js` | Node-RED registration, admin HTTP endpoints | Adding editor endpoints, node category. |
| `src/nodeClass.js` | Input wiring, command dispatch, config build | Topic dispatching, config key mapping. |
| `src/specificClass.js` | Template loading, dashboard composition, graph walk | UID stability, links, bucket injection. |
| `src/commands/index.js` | Command registry definition | Adding or renaming inbound topics. |
| `src/commands/handlers.js` | `child.register` handler logic | Payload resolution, emit loop. |
| `config/` | Per-softwareType Grafana JSON templates | Adding support for new EVOLV node types. |
dashboardAPI deliberately does NOT use the `concerns/` module pattern — its logic surface is too narrow. See `CONTRACT.md → "Why no BaseNodeAdapter / BaseDomain"` for the full 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 node id), `{ source: {...} }`, or `{ config: {...} }`; `msg.includeChildren` (default `true`) controls graph-walk depth.
There is **no HTTP endpoint contract** for dashboardAPI — it is a Node-RED input node only. The outbound HTTP call shape is documented in Section 8.
## 6. Child registration
dashboardAPI does **not** maintain a child registry of its own. 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["resolveChildSource(payload, ctx)<br/>RED.nodes.getNode → _flow → inline"]
resolve --> walk["generateDashboardsForGraph(childSource)<br/>(walks direct children if includeChildren=true)"]
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 at `warn` level and skips that dashboard. No error thrown. |
## 7. Lifecycle — what one event does
```mermaid
sequenceDiagram
participant emitter as any EVOLV node
participant dash as dashboardAPI (nodeClass)
participant api as DashboardApi (specificClass)
participant out as Port-0 output
participant grafana as Grafana HTTP API
emitter->>dash: child.register {source / config / id}
dash->>dash: commandRegistry.dispatch → handlers.registerChild
dash->>dash: resolveChildSource(payload, ctx)
dash->>api: generateDashboardsForGraph(childSource, {includeChildren})
api->>api: buildDashboard({ nodeConfig, positionVsParent })
api->>api: loadTemplate(softwareType) from config/
api->>api: stableUid = sha1(softwareType:nodeId).slice(0,12)
api->>api: updateTemplatingVar(measurement, bucket)
api->>api: extractChildren → build child dashboards
api->>api: add links[] on root dashboard
api-->>dash: [{dashboard, uid, title, softwareType, nodeId}, ...]
loop per dashboard in results
dash->>out: msg{topic:'create', url, method, headers, payload, meta}
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 — output shape
> **dashboardAPI has no domain output.** It does not extend `BaseDomain` and does not implement `getOutput()`. The `wiki:datamodel` script falls back to the hand-curated template 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 -->
**`meta` fields:**
| Field | Type | Value |
|---|---|---|
| `nodeId` | string | `config.general.id` or `config.general.name` |
| `softwareType` | string | `config.functionality.softwareType` |
| `uid` | string | SHA-1(`softwareType:nodeId`) first 12 chars — stable across re-deploys |
| `title` | string | `config.general.name` |
Inbound `msg` fields propagate via spread (`{...msg, ...envelope}`) — caller-supplied correlation/trace fields survive.
See `CONTRACT.md` for the full envelope spec and port definitions.
## 9. Configuration — editor form ↔ config keys
```mermaid
flowchart TB
subgraph editor["Node-RED editor form (dashboardapi.html)"]
f1[Protocol — select http/https]
f2[Grafana Host — text]
f3[Grafana Port — number]
f4[Bearer Token — password]
f5[InfluxDB Bucket — text]
f6[Enable Log — checkbox]
f7[Log Level — select]
end
subgraph config["Runtime config slice (_buildConfig)"]
c1[grafanaConnector.protocol]
c2[grafanaConnector.host]
c3[grafanaConnector.port]
c4[grafanaConnector.bearerToken]
c5[defaultBucket]
c6[general.logging.enabled]
c7[general.logging.logLevel]
end
f1 --> c1
f2 --> c2
f3 --> c3
f4 --> c4
f5 --> c5
f6 --> c6
f7 --> c7
```
| 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` | 165535 | `grafanaUpsertUrl()` |
| Bearer Token | `grafanaConnector.bearerToken` | `''` | string (Grafana service account token) | `Authorization: Bearer …` header |
| InfluxDB Bucket | `defaultBucket` | `''` → falls back to `INFLUXDB_BUCKET` env → position default | string | `updateTemplatingVar('bucket', …)` |
| Enable Log | `general.logging.enabled` | `false` | boolean | Logger constructor |
| Log Level | `general.logging.logLevel` | `'info'` | `info` \| `debug` \| `warn` \| `error` | Logger constructor |
**Position-based bucket fallback** (when `defaultBucket` is empty):
| `positionVsParent` | Bucket used |
|---|---|
| `upstream` | `lvl1` |
| `downstream` | `lvl3` |
| any other / absent | `lvl2` |
## 10. State chart
> **Skipped.** dashboardAPI is stateless — no FSM, no tick loop, no operating states. See template rule: "Skip this section for stateless nodes (`measurement`, `dashboardAPI`)."
## 11. Examples
| Tier | File | What it shows | Status |
|---|---|---|---|
| Basic | `examples/basic.flow.json` | Inject `child.register` payload (inline config) + downstream `http request` → Grafana | ⏳ TBD — stub exists |
| Integration | `examples/integration.flow.json` | Real EVOLV node (e.g. pumpingStation) → child.register → dashboardAPI → Grafana | ⏳ TBD — stub exists |
| Dashboard | _n/a_ | dashboardAPI **generates** Grafana dashboards — no FlowFuse chart tier for this node | — |
**Wiring pattern** (inline-payload basic test):
```json
[
{ "type": "inject", "payload": { "source": { "config": { "functionality": { "softwareType": "measurement" }, "general": { "id": "pump-a-flow", "name": "Pump A flow" } } } }, "topic": "child.register" },
{ "type": "dashboardapi" },
{ "type": "http request", "method": "POST" }
]
```
## 12. Debug recipes
| Symptom | First thing to check | Where to look |
|---|---|---|
| No HTTP message emitted on Port 0 | Did `resolveChildSource` return a non-null source? Check that payload has `.source.config` or `.config` or a valid node id. | Container log for "generateDashboardsForGraph skipped" warning. |
| `Skipping dashboard generation: no template` | `config/<softwareType>.json` missing. | `config/` directory — add a template JSON file for the new node type. |
| `machineGroupControl` produces no dashboard | The alias maps to `machineGroup.json` — verify that file exists in `config/`. | `_templateFileForSoftwareType` in `specificClass.js`. |
| Empty `Authorization` header | `bearerToken` not set in editor form. | Editor → Bearer Token field. |
| Wrong InfluxDB bucket in Grafana template variables | `defaultBucket` config or `INFLUXDB_BUCKET` env overrides the position-based default. | `_buildConfig` in `nodeClass.js` + `defaultBucketForPosition` in `specificClass.js`. |
| Dashboard UID changes between deploys | Node id or `softwareType` changed — UID is `sha1(softwareType:nodeId)`. | `stableUid` in `specificClass.js`. |
| `registerChild` alias warns once | Expected — deprecation warning on first use. Migrate caller to topic `child.register`. | Caller `msg.topic`. |
> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors. Use only for live debugging sessions.
## 13. When you would NOT use this node
- **Use dashboardAPI only for auto-generating Grafana dashboards from EVOLV topology.** If you maintain dashboards manually in Grafana, skip it — it will overwrite your customisations on every registration event.
- **Don't use dashboardAPI as a generic Grafana HTTP client.** It only emits dashboard upserts (`POST /api/dashboards/db`). For arbitrary Grafana API calls (annotations, alerts, data sources) use a plain `http request` node.
- **Don't wire tick/measurement data into dashboardAPI.** It fires on `child.register` events (deploy time), not on the measurement tick. Wiring Port-0 data from a rotatingMachine or pumpingStation here is a misuse.
- **Don't expect EVOLV child registration to happen automatically.** dashboardAPI passively receives `child.register`; the emitting node (e.g. pumpingStation) must have its Port 2 wired to dashboardAPI's input. See Section 7.
- **Not a BaseDomain node.** dashboardAPI cannot be used wherever a BaseDomain-capable node is required (e.g. as a registered child of machineGroupControl). See OPEN_QUESTIONS.md (2026-05-10) and Section 14.
## 14. Known limitations / current issues
| # | Issue | Tracked in |
|---|---|---|
| 1 | No domain output — cannot be introspected via the standard `getOutput()` channel. Debugging relies on watching Port 0 HTTP envelopes in a debug node. | `CONTRACT.md → "Why no BaseNodeAdapter / BaseDomain"` |
| 2 | Does not extend `BaseNodeAdapter` / `BaseDomain` — decision deferred pending a passive/HTTP-only mode on `BaseNodeAdapter` (skip-registration + skip-output-stream flags). Until that ships the bespoke adapter shape is correct. | `OPEN_QUESTIONS.md` (2026-05-10) — "dashboardAPI skipped BaseNodeAdapter + BaseDomain". Confirm with team whether to revisit when `BaseNodeAdapter` grows a passive mode. |
| 3 | Template discovery is filename-based. Renaming a node's `softwareType` requires renaming (or aliasing) the template file. The `machineGroupControl → machineGroup.json` mapping is a one-off alias in `_templateFileForSoftwareType`. | `src/specificClass.js → _templateFileForSoftwareType` |
| 4 | No retry / circuit-breaker on the downstream `http request` node — Grafana outages silently drop dashboard upserts. | TBD — no issue filed yet |
| 5 | Tier 1/2 example flows exist as stubs only (`basic.flow.json`, `integration.flow.json`) — not yet validated on a live Node-RED instance. | P9 wiki cleanup follow-up |