docs(wiki): full 5-page wiki matching the rotatingMachine reference format
Replaces the prior stub/partial wiki with a Home + Reference-{Architecture,
Contracts,Examples,Limitations} + _Sidebar structure. Topic-contract and
data-model sections wrapped in AUTOGEN markers for the future wiki-gen tool.
Source-vs-spec contradictions surfaced and flagged inline (not silently
fixed). Pending-review notes mark sections that need a full node review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
243
wiki/Reference-Contracts.md
Normal file
243
wiki/Reference-Contracts.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# 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) |
|
||||
Reference in New Issue
Block a user