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>
15 KiB
Reference — Architecture
Note
Code structure for
dashboardAPI: the (intentionally shallow) three-tier layout, the command registry, the dashboard composition pipeline, the HTTP-endpoint event lifecycle, and the output-port pipeline. For an intuitive overview, return to Home.Pending full node review (2026-05). Content reflects
CONTRACT.mdand current source only.
Three-tier code layout
nodes/dashboardAPI/
|
+-- dashboardapi.js entry: RED.nodes.registerType('dashboardapi', NodeClass)
| (legacy lowercase filename — see Limitations)
|
+-- dashboardapi.html editor: form + oneditprepare / oneditsave
| (legacy lowercase filename — see Limitations)
|
+-- src/
| nodeClass.js passive adapter — buildConfig + createRegistry + input dispatch
| DOES NOT extend BaseNodeAdapter
| specificClass.js DashboardApi service — loadTemplate / buildDashboard /
| generateDashboardsForGraph / extractChildren
| DOES NOT extend BaseDomain
| |
| +-- commands/
| index.js topic descriptors (child.register only)
| handlers.js resolveChildSource + registerChild handler
|
+-- config/ Grafana JSON templates, one per softwareType
| aeration.json machineGroup.json pumpingStation.json
| dashboardapi.json measurement.json reactor.json
| machine.json monster.json settler.json
| valve.json valveGroupControl.json
|
+-- dependencies/
| dashboardapi/
| dashboardapiConfig.json editor menu config (NOT runtime config)
|
+-- examples/
| basic.flow.json currently stubs — see Examples & Limitations
| integration.flow.json
| edge.flow.json
|
+-- test/
basic/ structure-module-load test
integration/ structure-examples test
edge/ structure-examples-node-type test
helpers/
Tier responsibilities
| Tier | File | What it owns | Touches RED.* |
|---|---|---|---|
| entry | dashboardapi.js |
RED.nodes.registerType('dashboardapi', ...). Admin endpoints: GET /dashboardapi/menu.js (logger menu) + GET /dashboardapi/configData.js (editor metadata). |
Yes |
| nodeClass | src/nodeClass.js |
Builds runtime config via configManager.buildConfig. Creates command registry via createRegistry(commands). Attaches input and close handlers. No tick loop, no status badge, no Port 1 / 2 emissions. Sets a one-shot red dashboardapi error status on dispatch failure. |
Yes |
| specificClass | src/specificClass.js |
Pure dashboard composition: template loading, UID derivation, templating-var fill, child graph walk, links generation, upsert request shaping. No RED.* calls. |
No |
specificClass is small (~210 lines) and self-contained — no concern modules. The complexity surface is too narrow to warrant a concerns/ split.
Why no BaseNodeAdapter / BaseDomain
The decision is documented in OPEN_QUESTIONS.md (2026-05-10) and surfaced in CONTRACT.md. Four concrete blockers:
- No platform config JSON.
BaseDomain's constructor unconditionally callsconfigManager.getConfig(ctor.name)againstgeneralFunctions/src/configs/<n>.json. There is nodashboardapi.jsoningeneralFunctions— the localdependencies/dashboardapi/dashboardapiConfig.jsonis for the editor menu endpoint only. Adding a platform config JUST to satisfy the base class would be a synthetic decision. - No periodic output.
BaseNodeAdapter._emitOutputs()andoutputUtils.formatMsgassume a delta-compressed Port 0 / 1 telemetry stream tied to a tick loop. dashboardAPI emits HTTP envelopes asynchronously on inbound events; the formatter pipeline would coerce these into the wrong shape. - No parent registration.
BaseNodeAdapter._scheduleRegistrationautomatically emits achild.registeron Port 2 at startup. dashboardAPI is a sink forchild.register, not a source — emitting one of its own would feed into other dashboardAPI instances and cause loops. - No status badge, no tick, no measurements, no children of its own. Most of the base-class machinery would be inert or actively harmful.
What dashboardAPI does reuse from generalFunctions/:
configManager(forbuildConfig)createRegistry+ the canonical-topic / alias-with-deprecation patternloggerMenuManager(for the editor menu endpoint)
That's enough common platform surface to keep the node aligned with EVOLV conventions without inheriting machinery it can't use.
Command registry
src/commands/index.js declares one descriptor:
module.exports = [
{
topic: 'child.register',
aliases: ['registerChild'],
payloadSchema: { type: 'any' },
handler: handlers.registerChild,
},
];
createRegistry(commands, { logger }) returns a dispatcher with built-in alias-with-deprecation: the first time msg.topic === 'registerChild' fires, the logger emits a one-time deprecation warning; thereafter the alias is silently mapped to the canonical handler.
child.register handler — resolution pipeline
src/commands/handlers.js registerChild(source, msg, ctx):
- Resolve the child source via
resolveChildSource(msg.payload, ctx):- If
payload.source.configexists → usepayload.sourcedirectly (inline shape A). - Else if
payload.configexists → wrap as{ config: payload.config }(inline shape B). - Else if
typeof payload === 'string'→ treat as a node id and resolve viaRED.nodes.getNode(id)→ fall back toctx.node._flow.getNode(id).
- If
- Throw
Missing or invalid child nodeif neither path yields a.config— the nodeClass's catch sets the reddashboardapi errorstatus badge and re-throws vianode.error. - Walk the graph via
source.generateDashboardsForGraph(childSource, {includeChildren: msg.includeChildren ?? true}). - Emit one Port-0 envelope per generated dashboard, with the
{...msg, topic: 'create', ...}spread so caller fields propagate.
Dashboard composition pipeline
flowchart TB
in[child.register payload]:::input --> res[resolveChildSource<br/>RED.nodes.getNode → _flow.getNode → inline]
res --> walk[generateDashboardsForGraph<br/>root + direct children if includeChildren]
walk --> bld[buildDashboard per node]
bld --> tpl[loadTemplate softwareType<br/>config/-st-.json with case-insensitive fallback<br/>+ machineGroupControl → machineGroup.json alias]
tpl --> uid[stableUid<br/>sha1 softwareType:nodeId .slice 0,12]
bld --> vars[updateTemplatingVar<br/>measurement = softwareType_nodeId<br/>bucket = position-based default or override]
walk --> links[Add root.links of child uid + slugify title]
links --> shape[buildUpsertRequest<br/>dashboard + folderId 0 + overwrite true]
shape --> emit[ctx.send one msg per dashboard<br/>topic 'create', url, method, headers, payload, meta]
emit --> out[Port 0]
classDef input fill:#dddddd,color:#000
Template selection
_templateFileForSoftwareType(softwareType) tries these candidates in order:
config/<softwareType>.json(exact case)config/<softwareType.toLowerCase()>.json(case-insensitive fallback)config/machineGroup.json— only whensoftwareType === 'machineGroupControl'(one-off alias)
A missing template logs at warn level (No dashboard template found for softwareType=<st>) and the matching dashboard is skipped (no error thrown, the rest of the graph walk continues).
Currently shipped templates in config/:
| Template | Maps to softwareType |
|---|---|
aeration.json |
aeration |
dashboardapi.json |
dashboardapi (this node) |
machine.json |
(likely rotatingmachine / machine — verify when reviewing) |
machineGroup.json |
machineGroupControl (via alias) |
measurement.json |
measurement |
monster.json |
monster |
pumpingStation.json |
pumpingStation |
reactor.json |
reactor |
settler.json |
settler |
valve.json |
valve |
valveGroupControl.json |
valveGroupControl |
Note
The exact softwareType ↔ template mapping (esp.
machine.jsonvs the lowercaserotatingmachinesoftwareType emitted byrotatingMachine'sfunctionality.softwareType) needs verification during the full review — flagged.
UID stability
stableUid(input) = sha1(input).slice(0, 12) — the same softwareType:nodeId always yields the same dashboard UID. Combined with overwrite: true in the upsert payload, this makes the operation idempotent: re-deploying the EVOLV flow re-runs the upsert with the same UID and Grafana replaces the existing dashboard rather than creating a duplicate.
Position-based bucket fallback
When defaultBucket is empty AND bucketMap[position] has no entry:
positionVsParent |
Bucket used |
|---|---|
upstream (case-insensitive) |
lvl1 |
downstream (case-insensitive) |
lvl3 |
| any other / absent | lvl2 |
Overridden by (in order): config.defaultBucket → config.bucketMap[position] → the table above. INFLUXDB_BUCKET env is read in _buildConfig and lands in config.defaultBucket.
Root → child links
When includeChildren=true and the root has ≥ 1 direct child, the root dashboard's links[] is augmented with one entry per child:
{
type: 'link',
title: childTitle,
url: `/d/${childUid}/${slugify(childTitle)}`,
tags: [],
targetBlank: false,
keepTime: true,
keepVariables: true,
}
slugify is lowercase-kebab-case, truncated to 60 chars. keepTime and keepVariables are Grafana's "preserve dashboard state across navigation" flags — clicking a link keeps the time range and templating selections.
Lifecycle — what one event does
sequenceDiagram
autonumber
participant emitter as any EVOLV node
participant dash as dashboardAPI (nodeClass)
participant cr as commandRegistry
participant api as DashboardApi (specificClass)
participant out as Port 0
participant http as http request (downstream)
participant grafana as Grafana HTTP API
emitter->>dash: msg{topic: 'child.register', payload}
dash->>cr: dispatch(msg, source, ctx)
cr->>cr: canonicalise topic (alias→canonical, log deprecation once)
cr->>api: handlers.registerChild(source, msg, ctx)
api->>api: resolveChildSource(payload, ctx)
alt source missing
api-->>dash: throw 'Missing or invalid child node'
dash->>dash: node.status({fill:'red','dashboardapi error'})
dash->>dash: node.error(err, msg)
else source resolved
api->>api: generateDashboardsForGraph(childSource, {includeChildren})
api->>api: buildDashboard(root) → loadTemplate + stableUid + templating
api->>api: extractChildren → buildDashboard per child
api->>api: rootDash.links += child links
loop per dashboard in results
api->>out: ctx.send({...msg, topic:'create', url, method, headers, payload, meta})
out->>http: msg flows to downstream http request node
http->>grafana: POST /api/dashboards/db
end
end
One inbound event yields N outbound HTTP envelopes, where N = 1 (root) + count(direct children) when includeChildren=true, or 1 when includeChildren=false.
There is no FSM. There is no tick loop. There is no state.emitter. The node is event-driven and stateless — every child.register is handled independently and discarded.
Output ports
| Port | Carries | Sample shape |
|---|---|---|
| 0 (process) | One topic: 'create' HTTP envelope per generated dashboard |
{topic:'create', url, method:'POST', headers, payload:{dashboard,folderId:0,overwrite:true}, meta} |
| 1 (telemetry) | Unused. No measurements; nothing emitted. | — |
| 2 (registration / control) | Unused. dashboardAPI is a sink for child.register, not a source. |
— |
Port 0 deliberately diverges from the standard "process data + delta-compressed" convention: the envelope is a fully-formed HTTP request, shaped for a downstream http request core node. Caller-supplied msg.* fields propagate via the {...msg, ...envelope} spread so correlation / trace fields survive the hop.
Per
.claude/rules/output-coverage.md: this node has a small output surface (one Port-0 msg shape), and no tick / FSM states — the manifest is correspondingly small. The standard "every output, every state" sweep collapses to "every key in the envelope is present whenever a dashboard is generated; nothing is emitted when resolution fails."
Event sources
| Source | Where it fires | What it triggers |
|---|---|---|
Inbound msg.topic |
Node-RED input wire on Port 0 input | commandRegistry.dispatch → handlers.registerChild |
Admin HTTP GET /dashboardapi/menu.js |
Editor first-load | MenuManager.createEndpoint('dashboardapi', ['logger']) returns JS bootstrap |
Admin HTTP GET /dashboardapi/configData.js |
Editor first-load | Reads dependencies/dashboardapi/dashboardapiConfig.json and returns it as a JS-attached global on window.EVOLV.nodes.dashboardapi.config |
node.on('close') |
Node-RED redeploy / shutdown | No-op (handler exists but only calls done()) |
There is no setInterval, no state.emitter, no child.measurements.emitter. The node sleeps until child.register arrives.
Where to start reading
| If you're changing... | Read first |
|---|---|
| Adding a new topic / changing the alias map | src/commands/index.js + src/commands/handlers.js |
| Payload resolution rules (string id / inline source / inline config) | src/commands/handlers.js resolveChildSource + resolveChildNode |
| Grafana URL composition / bearer token / headers | src/specificClass.js grafanaUpsertUrl + handlers.registerChild header logic |
| Template selection, alias rules, missing-template behaviour | src/specificClass.js _templateFileForSoftwareType + loadTemplate |
| UID derivation, dashboard composition, links | src/specificClass.js buildDashboard + generateDashboardsForGraph |
| Bucket fallback (position → lvl1/lvl2/lvl3) | src/specificClass.js defaultBucketForPosition |
| Editor form ↔ config keys | dashboardapi.html + src/nodeClass.js _buildConfig |
| Editor menu / config endpoints | dashboardapi.js (entry, admin endpoints) + dependencies/dashboardapi/dashboardapiConfig.json |
| Template content for a new EVOLV node type | config/<softwareType>.json — copy the closest existing one and adjust |
Related pages
| Page | Why |
|---|---|
| Home | Intuitive overview |
| Reference — Contracts | Topic + config + template alias map |
| Reference — Examples | Shipped flows + debug recipes |
| Reference — Limitations | Filename drift, stub flows, open questions |
| EVOLV — Architecture | Platform-wide three-tier pattern |
| EVOLV — Telemetry | Port 0 / 1 / 2 InfluxDB layout (dashboardAPI is an exception — Port 0 carries HTTP envelopes) |