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

244 lines
14 KiB
Markdown

# 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) |