diff --git a/wiki/Home.md b/wiki/Home.md index 6014dad..c79dace 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -1,11 +1,11 @@ # dashboardAPI -> **Reflects code as of `92d7eba` · regenerated `2026-05-11` via `npm run wiki:all`** +> **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 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. +**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 @@ -15,7 +15,8 @@ flowchart LR mgc[machineGroupControl
Unit]:::unit -.child.register.-> dash rm[rotatingMachine
Equipment]:::equip -.child.register.-> dash meas[measurement
Control Module]:::ctrl -.child.register.-> dash - dash[dashboardAPI
Utility]:::neutral -->|POST /api/dashboards/db| grafana[(Grafana
HTTP API)] + dash[dashboardAPI
Utility]:::neutral -->|"POST /api/dashboards/db"| grafana[(Grafana
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 @@ -23,19 +24,21 @@ flowchart LR 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. +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`. | -| 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/.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. | +| 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/.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. | @@ -43,31 +46,37 @@ dashboardAPI has **no S88 level** — it's a utility node. Dashed arrows = inbou ```mermaid flowchart TB - subgraph nodeRED["nodeClass.js — passive adapter"] - nc["createRegistry(commands)
dispatch on msg input
NO BaseNodeAdapter"] + subgraph entry["dashboardapi.js — entry (Node-RED registration)"] + e["RED.nodes.registerType('dashboardapi')
admin: GET /dashboardapi/menu.js
admin: GET /dashboardapi/configData.js"] end - subgraph domain["specificClass.js — DashboardApi service"] - sc["buildDashboard()
generateDashboardsForGraph()
extractChildren()"] + subgraph nodeRED["src/nodeClass.js — passive adapter"] + nc["_buildConfig(uiConfig)
createRegistry(commands)
_attachInputHandler() → dispatch
NO BaseNodeAdapter"] + end + subgraph domain["src/specificClass.js — DashboardApi service"] + sc["buildDashboard({ nodeConfig, positionVsParent })
generateDashboardsForGraph(rootSource)
extractChildren(nodeSource)
grafanaUpsertUrl()
loadTemplate(softwareType)"] end subgraph cmd["src/commands/"] - h["child.register handler
+ registerChild alias"] + h["index.js — child.register + registerChild alias
handlers.js — resolveChildSource + registerChild"] end - subgraph tpl["config/ (templates)"] - t[".json
Grafana JSON skeletons"] + subgraph tpl["config/ — Grafana JSON templates"] + t["aeration | dashboardapi | machine
machineGroup | measurement | monster
pumpingStation | reactor | settler
valve | valveGroupControl"] end - nc --> sc - nc --> cmd - sc --> tpl + e --> nodeRED + nodeRED --> domain + nodeRED --> cmd + domain --> 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.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 split into `concerns/` — its surface is too narrow. See CONTRACT.md → "Why no BaseNodeAdapter / BaseDomain" for the rationale. +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 @@ -81,18 +90,20 @@ dashboardAPI deliberately does NOT split into `concerns/` — its surface is too -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. +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. Instead, every inbound `child.register` triggers a one-shot resolution + dashboard emission. No state is held between calls. +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
(has functionality.softwareType)"]:::other -->|child.register| dash[dashboardAPI
Utility]:::neutral - dash --> resolve[resolve child source
RED.nodes.getNode → _flow → inline] - resolve --> walk["source.generateDashboardsForGraph(child)
(includes children if flag set)"] - walk --> emit[emit one msg per dashboard
topic='create'] + dash --> resolve["resolveChildSource(payload, ctx)
RED.nodes.getNode → _flow → inline"] + resolve --> walk["generateDashboardsForGraph(childSource)
(walks direct children if includeChildren=true)"] + walk --> emit["emit one msg per dashboard
topic='create'"] emit --> http[(downstream
http request node)] classDef neutral fill:#dddddd,color:#000 classDef other fill:#ffffff,stroke:#666 @@ -101,36 +112,40 @@ flowchart LR | Inbound softwareType | Filter | Side effect | |---|---|---| | any | child has `functionality.softwareType` | Loads `config/.json`; emits one upsert msg per dashboard in the graph walk. | -| (template missing) | no matching `config/*.json` | Warns and skips that dashboard. No error. | +| (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 - participant api as DashboardApi + 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 - 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} + 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 — `getOutput()` +## 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 placeholder below. +> **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. @@ -155,72 +170,118 @@ Port 1 (InfluxDB telemetry) and Port 2 (registration / control plumbing) are unu -See CONTRACT.md for the full envelope spec. +**`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"] - f1[Protocol] - f2[Host] - f3[Port] - f4[Bearer token] - f5[Default bucket] + 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["Domain config slice"] + 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 | Where used | +| Form field | Config key | Default | Range / values | 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)` | +| Protocol | `grafanaConnector.protocol` | `http` | `http` \| `https` | `grafanaUpsertUrl()` | +| Grafana Host | `grafanaConnector.host` | `localhost` | hostname / IP | `grafanaUpsertUrl()` | +| Grafana Port | `grafanaConnector.port` | `3000` | 1–65535 | `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/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 | — | +| 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 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/.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`. | +| 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/.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. +> 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 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. +- **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 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 | +| 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 |