# Reference — 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. | Canonical topic | Aliases | Payload | Unit | Effect | |---|---|---|---|---| | `child.register` | `registerChild` | any | — | — | 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 | | `""` (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 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://:/api/dashboards/db', method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: 'Bearer ' // only when grafanaConnector.bearerToken is set }, payload: { dashboard: { uid: '<12-char-sha1>', title: '', templating: {...}, ... }, 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. ### 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/.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//', 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 | `.json` | Exact case. | | 2 | `.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=` at `warn` level and `loadTemplate` returns `null`. `buildDashboard` then logs `Skipping dashboard generation: no template for softwareType=` 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/.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
(has functionality.softwareType)"]:::other -->|child.register| dash[dashboardAPI
Utility]:::neutral dash --> resolve["resolveChildSource(payload, ctx)
RED.nodes.getNode → _flow.getNode → inline"] resolve --> walk["generateDashboardsForGraph(childSource, {includeChildren})"] 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 ``` ### 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) |