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

14 KiB
Raw Blame History

Reference — Contracts

code-ref

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.


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:

  1. Template loadloadTemplate(softwareType) from config/<softwareType>.json (case-insensitive fallback, machineGroupControl &rarr; machineGroup.json alias). Missing template → logs warn and returns null (the dashboard is skipped from the output).
  2. UID stampdashboard.uid = stableUid(softwareType:nodeId).
  3. Title stampdashboard.title = config.general.name || nodeId.
  4. Tags merge — existing template.tags + ['EVOLV', softwareType, positionVsParent] (deduplicated, empty values filtered).
  5. Templating var filldashboard.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 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 &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 — "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 165535 (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 enableLoggeneral.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.registerchild.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 (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.

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)