# Reference — Architecture ![code-ref](https://img.shields.io/badge/code--ref-a6f09d8-blue) > [!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](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/.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: ```js 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 ```mermaid flowchart TB in[child.register payload]:::input --> res[resolveChildSource
RED.nodes.getNode → _flow.getNode → inline] res --> walk[generateDashboardsForGraph
root + direct children if includeChildren] walk --> bld[buildDashboard per node] bld --> tpl[loadTemplate softwareType
config/-st-.json with case-insensitive fallback
+ machineGroupControl → machineGroup.json alias] tpl --> uid[stableUid
sha1 softwareType:nodeId .slice 0,12] bld --> vars[updateTemplatingVar
measurement = softwareType_nodeId
bucket = position-based default or override] walk --> links[Add root.links of child uid + slugify title] links --> shape[buildUpsertRequest
dashboard + folderId 0 + overwrite true] shape --> emit[ctx.send one msg per dashboard
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/.json` (exact case) 2. `config/.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=`) 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.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: ```js { 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 ```mermaid 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/.json` — copy the closest existing one and adjust | --- ## Related pages | Page | Why | |:---|:---| | [Home](Home) | Intuitive overview | | [Reference — Contracts](Reference-Contracts) | Topic + config + template alias map | | [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes | | [Reference — Limitations](Reference-Limitations) | Filename drift, stub flows, open questions | | [EVOLV — Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Architecture) | Platform-wide three-tier pattern | | [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 InfluxDB layout (dashboardAPI is an exception — Port 0 carries HTTP envelopes) |