Files
dashboardAPI/wiki/Reference-Architecture.md

295 lines
15 KiB
Markdown
Raw Permalink Normal View History

# 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/<n>.json`. There is no `dashboardapi.json` in `generalFunctions` &mdash; 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 &mdash; 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 &mdash; 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 &rarr; use `payload.source` directly (inline shape A).
- Else if `payload.config` exists &rarr; wrap as `{ config: payload.config }` (inline shape B).
- Else if `typeof payload === 'string'` &rarr; treat as a node id and resolve via `RED.nodes.getNode(id)` &rarr; fall back to `ctx.node._flow.getNode(id)`.
2. **Throw** `Missing or invalid child node` if neither path yields a `.config` &mdash; 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` &mdash; 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` &mdash; 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 &harr; template mapping (esp. `machine.json` vs the lowercase `rotatingmachine` softwareType emitted by `rotatingMachine`'s `functionality.softwareType`) needs verification during the full review &mdash; flagged.
### UID stability
`stableUid(input) = sha1(input).slice(0, 12)` &mdash; 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` &rarr; `config.bucketMap[position]` &rarr; the table above. `INFLUXDB_BUCKET` env is read in `_buildConfig` and lands in `config.defaultBucket`.
### Root &rarr; child links
When `includeChildren=true` and the root has &ge; 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 &mdash; clicking a link keeps the time range and templating selections.
---
## Lifecycle &mdash; 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 &mdash; 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. | &mdash; |
| 2 (registration / control) | **Unused.** dashboardAPI is a sink for `child.register`, not a source. | &mdash; |
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 &mdash; 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` &rarr; `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 &rarr; lvl1/lvl2/lvl3) | `src/specificClass.js` `defaultBucketForPosition` |
| Editor form &harr; 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` &mdash; copy the closest existing one and adjust |
---
## Related pages
| Page | Why |
|:---|:---|
| [Home](Home) | Intuitive overview |
| [Reference &mdash; Contracts](Reference-Contracts) | Topic + config + template alias map |
| [Reference &mdash; Examples](Reference-Examples) | Shipped flows + debug recipes |
| [Reference &mdash; Limitations](Reference-Limitations) | Filename drift, stub flows, open questions |
| [EVOLV &mdash; Architecture](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Architecture) | Platform-wide three-tier pattern |
| [EVOLV &mdash; Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 InfluxDB layout (dashboardAPI is an exception &mdash; Port 0 carries HTTP envelopes) |