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>
295 lines
15 KiB
Markdown
295 lines
15 KiB
Markdown
# 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](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:
|
|
|
|
```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<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.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/<softwareType>.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) |
|