Files
dashboardAPI/wiki/Reference-Architecture.md
znetsixe a9fc51d6f0 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>
2026-05-19 09:42:14 +02:00

15 KiB

Reference — Architecture

code-ref

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.md and 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:

  1. No platform config JSON. BaseDomain's constructor unconditionally calls configManager.getConfig(ctor.name) against generalFunctions/src/configs/<n>.json. There is no dashboardapi.json in generalFunctions — the local dependencies/dashboardapi/dashboardapiConfig.json is for the editor menu endpoint only. Adding a platform config JUST to satisfy the base class would be a synthetic decision.
  2. No periodic output. BaseNodeAdapter._emitOutputs() and outputUtils.formatMsg assume 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.
  3. No parent registration. BaseNodeAdapter._scheduleRegistration automatically emits a child.register on Port 2 at startup. dashboardAPI is a sink for child.register, not a source — emitting one of its own would feed into other dashboardAPI instances and cause loops.
  4. 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 (for buildConfig)
  • createRegistry + the canonical-topic / alias-with-deprecation pattern
  • logger
  • MenuManager (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):

  1. Resolve the child source via resolveChildSource(msg.payload, ctx):
    • If payload.source.config exists → use payload.source directly (inline shape A).
    • Else if payload.config exists → wrap as { config: payload.config } (inline shape B).
    • Else if typeof payload === 'string' → treat as a node id and resolve via RED.nodes.getNode(id) → fall back to ctx.node._flow.getNode(id).
  2. Throw Missing or invalid child node if neither path yields a .config — the nodeClass's catch sets the red dashboardapi error status badge and re-throws via node.error.
  3. Walk the graph via source.generateDashboardsForGraph(childSource, {includeChildren: msg.includeChildren ?? true}).
  4. 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:

  1. config/<softwareType>.json (exact case)
  2. config/<softwareType.toLowerCase()>.json (case-insensitive fallback)
  3. config/machineGroup.json — only when softwareType === '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.json vs the lowercase rotatingmachine softwareType emitted by rotatingMachine's functionality.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.defaultBucketconfig.bucketMap[position] → the table above. INFLUXDB_BUCKET env is read in _buildConfig and lands in config.defaultBucket.

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.dispatchhandlers.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

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)