244 lines
14 KiB
Markdown
244 lines
14 KiB
Markdown
|
|
# Reference — Contracts
|
||
|
|
|
||
|
|

|
||
|
|
|
||
|
|
> [!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 — populate via wiki-gen tool (TODO) -->
|
||
|
|
|
||
|
|
| Canonical topic | Aliases | Payload | Unit | Effect |
|
||
|
|
|:---|:---|:---|:---|:---|
|
||
|
|
| `child.register` | `registerChild` | `string` (child node id) **or** `{source: {...}}` **or** `{config: {...}}`; optional `msg.includeChildren: boolean` (default `true`) | — | Resolves the child source (`RED.nodes.getNode` → `node._flow.getNode` → inline payload), calls `source.generateDashboardsForGraph(child, {includeChildren})`, then emits one `topic: 'create'` HTTP-upsert message on Port 0 per generated dashboard. |
|
||
|
|
|
||
|
|
<!-- 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 — 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` — use directly | `handlers.js` `resolveChildSource` line 6 |
|
||
|
|
| `{config: {...}}` | `{config: payload.config}` — wrap minimally | `handlers.js` `resolveChildSource` line 7 |
|
||
|
|
| `"<node-id>"` (bare string) | `RED.nodes.getNode(id).source` → fallback `node._flow.getNode(id).source` | `handlers.js` `resolveChildNode` |
|
||
|
|
| anything else | `null` → 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 — Port-0 envelope
|
||
|
|
|
||
|
|
<!-- BEGIN AUTOGEN: data-model — populate via wiki-gen tool (TODO) -->
|
||
|
|
|
||
|
|
dashboardAPI **has no domain output** — 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** — 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'` | — |
|
||
|
|
| `headers.Accept` | string | constant | `application/json` |
|
||
|
|
| `headers.Content-Type` | string | constant | `application/json` |
|
||
|
|
| `headers.Authorization` | string | 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 — same `(softwareType, nodeId)` → 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 — 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** — `loadTemplate(softwareType)` from `config/<softwareType>.json` (case-insensitive fallback, `machineGroupControl → machineGroup.json` alias). Missing template → logs `warn` and returns `null` (the dashboard is skipped from the output).
|
||
|
|
2. **UID stamp** — `dashboard.uid = stableUid(softwareType:nodeId)`.
|
||
|
|
3. **Title stamp** — `dashboard.title = config.general.name || nodeId`.
|
||
|
|
4. **Tags merge** — existing `template.tags` + `['EVOLV', softwareType, positionVsParent]` (deduplicated, empty values filtered).
|
||
|
|
5. **Templating var fill** — `dashboard.templating.list[]` entries named `measurement` and `bucket` are mutated in place:
|
||
|
|
- `measurement` ← `${softwareType}_${nodeId}` (used as InfluxDB measurement name in panel queries).
|
||
|
|
- `bucket` ← resolved bucket (see [Bucket resolution](#bucket-resolution) below).
|
||
|
|
6. **Links append** (root dashboard only, when `includeChildren=true` and `children.length > 0`) — 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 — `upstream → lvl1`, `downstream → lvl3`, else `lvl2` |
|
||
|
|
|
||
|
|
> [!NOTE]
|
||
|
|
> Priorities 1 and 2 read order from `specificClass.js` `buildDashboard`. Verify against the editor's intended semantics during full review — "global override beats per-position map" is the current behaviour. Flagged.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Configuration schema — 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** — 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–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` | `''` → falls back to `process.env.INFLUXDB_BUCKET` → position default | Set in `_buildConfig`; consumed by `buildDashboard` templating fill. |
|
||
|
|
| (no editor field) | `bucketMap` | `{}` | Programmatic only — 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`) — confirm during full review that the editor menu correctly maps `enableLog` → `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` | — |
|
||
|
|
| `dashboardapi` | `dashboardapi.json` | Self-template (when a dashboardAPI registers as a child of another dashboardAPI — unusual). |
|
||
|
|
| `machine` (or `rotatingmachine`) | `machine.json` | softwareType to verify in full review — flagged. |
|
||
|
|
| `machineGroupControl` | `machineGroup.json` | Via one-off alias. |
|
||
|
|
| `measurement` | `measurement.json` | — |
|
||
|
|
| `monster` | `monster.json` | — |
|
||
|
|
| `pumpingStation` | `pumpingStation.json` | — |
|
||
|
|
| `reactor` | `reactor.json` | — |
|
||
|
|
| `settler` | `settler.json` | — |
|
||
|
|
| `valve` | `valve.json` | — |
|
||
|
|
| `valveGroupControl` | `valveGroupControl.json` | — |
|
||
|
|
|
||
|
|
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` → `child.unregister` lifecycle, no parent → 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` — the child source object (must have `.config`).
|
||
|
|
- `entry.position` (or `child.positionVsParent`) — 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 — 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` → nodeClass sets red status, calls `node.error`. |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Related pages
|
||
|
|
|
||
|
|
| Page | Why |
|
||
|
|
|:---|:---|
|
||
|
|
| [Home](Home) | Intuitive overview |
|
||
|
|
| [Reference — Architecture](Reference-Architecture) | Code map, lifecycle, graph walk |
|
||
|
|
| [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes |
|
||
|
|
| [Reference — Limitations](Reference-Limitations) | Filename drift, stub flows, open questions |
|
||
|
|
| [EVOLV — Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) | Platform-wide topic rules |
|
||
|
|
| [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 layout (dashboardAPI is an exception — Port 0 carries HTTP envelopes) |
|