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>
14 KiB
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, anddependencies/dashboardapi/dashboardapiConfig.json.Pending full node review (2026-05). Content reflects
CONTRACT.mdand current source only.For an intuitive overview, return to 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 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
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:
{
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.
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:
- Template load —
loadTemplate(softwareType)fromconfig/<softwareType>.json(case-insensitive fallback,machineGroupControl → machineGroup.jsonalias). Missing template → logswarnand returnsnull(the dashboard is skipped from the output). - UID stamp —
dashboard.uid = stableUid(softwareType:nodeId). - Title stamp —
dashboard.title = config.general.name || nodeId. - Tags merge — existing
template.tags+['EVOLV', softwareType, positionVsParent](deduplicated, empty values filtered). - Templating var fill —
dashboard.templating.list[]entries namedmeasurementandbucketare mutated in place:measurement←${softwareType}_${nodeId}(used as InfluxDB measurement name in panel queries).bucket← resolved bucket (see Bucket resolution below).
- Links append (root dashboard only, when
includeChildren=trueandchildren.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.jsbuildDashboard. 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. |
| 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
defaultsuse legacy field names.dashboardapi.htmldeclares{enableLog, logLevel}as Node-RED defaults but the runtime config readsgeneral.logging.{enabled, logLevel}. The bridge is the shared logger menu (MenuManager) — confirm during full review that the editor menu correctly mapsenableLog→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:
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(orchild.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 | Intuitive overview |
| Reference — Architecture | Code map, lifecycle, graph walk |
| Reference — Examples | Shipped flows + debug recipes |
| Reference — Limitations | Filename drift, stub flows, open questions |
| EVOLV — Topic Conventions | Platform-wide topic rules |
| EVOLV — Telemetry | Port 0 / 1 / 2 layout (dashboardAPI is an exception — Port 0 carries HTTP envelopes) |