diff --git a/wiki/Home.md b/wiki/Home.md
index c79dace..542a7ab 100644
--- a/wiki/Home.md
+++ b/wiki/Home.md
@@ -1,13 +1,28 @@
# dashboardAPI
-> **Reflects code as of `7b3da23` · regenerated `2026-05-11` via `npm run wiki:all`**
-> If this banner is stale, the page may be out of date. Treat as informative, not authoritative.
+  
-## 1. What this node is
+A `dashboardAPI` node converts EVOLV node topology into Grafana dashboards. On each inbound `child.register` event it resolves the child source, walks its direct children, loads per-`softwareType` Grafana JSON templates from `config/`, and emits one HTTP upsert request per dashboard on Port 0 to a downstream `http request` node. Sits adjacent to the S88 hierarchy as a passive HTTP emitter — **no measurements, no tick loop, no parent registration**.
-**dashboardAPI** is a utility node that converts EVOLV node topology into Grafana dashboards. On each `child.register` event it resolves the child's source, walks its direct children, loads per-`softwareType` Grafana JSON templates from `config/`, and emits one HTTP upsert request per dashboard on Port 0 to a downstream `http request` node. It has no measurements, no tick loop, no parent registration, and no BaseDomain/BaseNodeAdapter.
+> [!NOTE]
+> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
-## 2. Position in the platform
+---
+
+## At a glance
+
+| Thing | Value |
+|:---|:---|
+| What it represents | Utility bridge between EVOLV topology and Grafana — auto-generates dashboards from `child.register` events |
+| S88 level | **Utility** — not in the S88 hierarchy; sits adjacent to it |
+| Use it when | You want Grafana dashboards to materialise automatically when an EVOLV node graph is deployed |
+| Don't use it for | Maintaining hand-curated Grafana dashboards (will overwrite); arbitrary Grafana API calls; tick / measurement data plumbing |
+| Children it accepts | Any EVOLV node whose `nodeSource.config` carries `functionality.softwareType` |
+| Parents it talks to | None — dashboardAPI is a passive sink; it does not register with a parent |
+
+---
+
+## How it fits
```mermaid
flowchart LR
@@ -15,7 +30,8 @@ flowchart LR
mgc[machineGroupControl
Unit]:::unit -.child.register.-> dash
rm[rotatingMachine
Equipment]:::equip -.child.register.-> dash
meas[measurement
Control Module]:::ctrl -.child.register.-> dash
- dash[dashboardAPI
Utility]:::neutral -->|"POST /api/dashboards/db"| grafana[(Grafana
HTTP API)]
+ dash[dashboardAPI
Utility]:::neutral -->|"POST /api/dashboards/db"| http[http request
node-red core]:::neutral
+ http --> grafana[(Grafana
HTTP API)]
grafana -.renders dashboards for.-> ff[FlowFuse / Browser]
classDef pc fill:#0c99d9,color:#fff
classDef unit fill:#50a8d9,color:#000
@@ -24,264 +40,110 @@ flowchart LR
classDef neutral fill:#dddddd,color:#000
```
-dashboardAPI has **no S88 level** — it is a utility node (`#dddddd` neutral). Dashed arrows = inbound `child.register` events (fired at deploy time). The solid arrow is the outbound HTTP upsert on Port 0. The Grafana dashboards that result are what FlowFuse / browser clients view.
+Dashed arrows = inbound `child.register` events from any EVOLV process node. The solid arrow is the outbound HTTP upsert envelope on Port 0 — emitted **once per generated dashboard** in the walked graph. S88 colours and the utility-neutral `#dddddd` are anchored in `.claude/rules/node-red-flow-layout.md`.
-## 3. Capability matrix
+---
-| Capability | Status | Notes |
-|---|---|---|
-| Accept `child.register` from any EVOLV node | ✅ | Resolves via `RED.nodes.getNode` → `node._flow.getNode` → inline payload. |
-| Emit Grafana dashboard upsert (Port 0) | ✅ | One msg per generated dashboard, shaped for `http request` node. |
-| Walk child graph + emit per-child dashboards | ✅ | `msg.includeChildren: true` by default; opt-out per call. |
-| Add root → child dashboard `links[]` | ✅ | Each direct child appears as a navigation link on the root dashboard. |
-| Template selection by `softwareType` | ✅ | Reads from `config/.json`; case-insensitive fallback. `machineGroupControl` → `machineGroup.json` alias. |
-| Stable dashboard UID across re-deploys | ✅ | SHA-1(`softwareType:nodeId`) first 12 chars — deterministic, idempotent upsert. |
-| Bearer-token auth header | ✅ | Set via editor `bearerToken` field; omitted if empty. |
-| InfluxDB bucket injection per position | ✅ | `upstream → lvl1`, `downstream → lvl3`, else `lvl2`; overridden by `defaultBucket` or `INFLUXDB_BUCKET` env. |
-| Domain output on Port 0 | ❌ | Port 0 carries HTTP request envelopes only, not measurement data. |
-| Port 1 telemetry / Port 2 registration | ❌ | Both unused — see Section 8. |
-| Status badge / tick loop / FSM | ❌ | Stateless; no periodic emission. |
+## Try it — 3-minute demo
-## 4. Code map
+Import the basic example flow, deploy, and watch a `child.register` payload turn into a Grafana dashboard upsert request.
-```mermaid
-flowchart TB
- subgraph entry["dashboardapi.js — entry (Node-RED registration)"]
- e["RED.nodes.registerType('dashboardapi')
admin: GET /dashboardapi/menu.js
admin: GET /dashboardapi/configData.js"]
- end
- subgraph nodeRED["src/nodeClass.js — passive adapter"]
- nc["_buildConfig(uiConfig)
createRegistry(commands)
_attachInputHandler() → dispatch
NO BaseNodeAdapter"]
- end
- subgraph domain["src/specificClass.js — DashboardApi service"]
- sc["buildDashboard({ nodeConfig, positionVsParent })
generateDashboardsForGraph(rootSource)
extractChildren(nodeSource)
grafanaUpsertUrl()
loadTemplate(softwareType)"]
- end
- subgraph cmd["src/commands/"]
- h["index.js — child.register + registerChild alias
handlers.js — resolveChildSource + registerChild"]
- end
- subgraph tpl["config/ — Grafana JSON templates"]
- t["aeration | dashboardapi | machine
machineGroup | measurement | monster
pumpingStation | reactor | settler
valve | valveGroupControl"]
- end
- e --> nodeRED
- nodeRED --> domain
- nodeRED --> cmd
- domain --> tpl
+```bash
+curl -X POST -H 'Content-Type: application/json' \
+ --data @nodes/dashboardAPI/examples/basic.flow.json \
+ http://localhost:1880/flow
```
-| Module | Owns | Read first if you're changing… |
-|---|---|---|
-| `dashboardapi.js` | Node-RED registration, admin HTTP endpoints | Adding editor endpoints, node category. |
-| `src/nodeClass.js` | Input wiring, command dispatch, config build | Topic dispatching, config key mapping. |
-| `src/specificClass.js` | Template loading, dashboard composition, graph walk | UID stability, links, bucket injection. |
-| `src/commands/index.js` | Command registry definition | Adding or renaming inbound topics. |
-| `src/commands/handlers.js` | `child.register` handler logic | Payload resolution, emit loop. |
-| `config/` | Per-softwareType Grafana JSON templates | Adding support for new EVOLV node types. |
+What to click after deploy:
-dashboardAPI deliberately does NOT use the `concerns/` module pattern — its logic surface is too narrow. See `CONTRACT.md → "Why no BaseNodeAdapter / BaseDomain"` for the full rationale.
+1. Open the inject node (`basic trigger`) and edit the payload to a `{source: {config: {...}}}` shape — see [Reference — Examples](Reference-Examples#wiring-pattern) for the minimal inline-payload shape.
+2. Fire the inject. Watch the debug pane: one `topic: 'create'` HTTP envelope appears per dashboard in the walked graph (root + direct children).
+3. Wire a downstream `http request` node (method `POST`) to the dashboardAPI output to actually POST the envelope to Grafana.
-## 5. Topic contract
+> [!IMPORTANT]
+> **GIF needed.** Demo recording of the inject → Port-0 envelope → Grafana dashboard upsert path. Save as `wiki/_partial-gifs/dashboardAPI/01-basic-demo.gif`, target ≤ 1 MB after `gifsicle -O3 --lossy=80`.
-> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`.
+> [!WARNING]
+> The shipped `basic.flow.json` / `integration.flow.json` / `edge.flow.json` are stubs — the inject payloads do not yet conform to the `child.register` resolver's expected shape. They will trigger `Missing or invalid child node` errors until updated. Tracked in [Limitations — Example flow stubs](Reference-Limitations#example-flow-stubs).
-
+---
-| Canonical topic | Aliases | Payload | Unit | Effect |
-|---|---|---|---|---|
-| `child.register` | `registerChild` | `any` | — | Parent/child plumbing — registers or unregisters a child node. |
+## The one thing you'll send
-
+| Topic | Aliases | Payload | What it does |
+|:---|:---|:---|:---|
+| `child.register` | `registerChild` | `string` (child node id) **or** `{source: {...}}` **or** `{config: {...}}` (optionally `msg.includeChildren: boolean`, default `true`) | Resolves the child source (`RED.nodes.getNode` → `node._flow.getNode` → inline payload), calls `source.generateDashboardsForGraph(child, {includeChildren})`, then emits one `topic: 'create'` HTTP-upsert message on Port 0 per generated dashboard. |
-The legacy `registerChild` alias logs a one-time deprecation warning on first use. The payload can be a string (child node id), `{ source: {...} }`, or `{ config: {...} }`; `msg.includeChildren` (default `true`) controls graph-walk depth.
+That's it. There is no `set.*`, no `cmd.*`, no `query.*` — the registry has a single canonical topic (alias-with-deprecation). The legacy `registerChild` alias logs a one-time deprecation warning on first use.
-There is **no HTTP endpoint contract** for dashboardAPI — it is a Node-RED input node only. The outbound HTTP call shape is documented in Section 8.
+---
-## 6. Child registration
+## What you'll see come out
-dashboardAPI does **not** maintain a child registry of its own. Every inbound `child.register` triggers a one-shot resolution + dashboard emission. No state is held between calls.
+Sample Port 0 message after a `child.register` for a `pumpingStation` node with two direct children:
-```mermaid
-flowchart LR
- src["any EVOLV node
(has functionality.softwareType)"]:::other -->|child.register| dash[dashboardAPI
Utility]:::neutral
- dash --> resolve["resolveChildSource(payload, ctx)
RED.nodes.getNode → _flow → inline"]
- resolve --> walk["generateDashboardsForGraph(childSource)
(walks direct children if includeChildren=true)"]
- walk --> emit["emit one msg per dashboard
topic='create'"]
- emit --> http[(downstream
http request node)]
- classDef neutral fill:#dddddd,color:#000
- classDef other fill:#ffffff,stroke:#666
-```
-
-| Inbound softwareType | Filter | Side effect |
-|---|---|---|
-| any | child has `functionality.softwareType` | Loads `config/.json`; emits one upsert msg per dashboard in the graph walk. |
-| (template missing) | no matching `config/*.json` | Warns at `warn` level and skips that dashboard. No error thrown. |
-
-## 7. Lifecycle — what one event does
-
-```mermaid
-sequenceDiagram
- participant emitter as any EVOLV node
- participant dash as dashboardAPI (nodeClass)
- participant api as DashboardApi (specificClass)
- participant out as Port-0 output
- participant grafana as Grafana HTTP API
-
- emitter->>dash: child.register {source / config / id}
- dash->>dash: commandRegistry.dispatch → handlers.registerChild
- dash->>dash: resolveChildSource(payload, ctx)
- dash->>api: generateDashboardsForGraph(childSource, {includeChildren})
- api->>api: buildDashboard({ nodeConfig, positionVsParent })
- api->>api: loadTemplate(softwareType) from config/
- api->>api: stableUid = sha1(softwareType:nodeId).slice(0,12)
- api->>api: updateTemplatingVar(measurement, bucket)
- api->>api: extractChildren → build child dashboards
- api->>api: add links[] on root dashboard
- api-->>dash: [{dashboard, uid, title, softwareType, nodeId}, ...]
- loop per dashboard in results
- dash->>out: msg{topic:'create', url, method, headers, payload, meta}
- out->>grafana: POST /api/dashboards/db
- end
-```
-
-One inbound event yields N outbound HTTP messages (N = 1 + direct child count when `includeChildren=true`).
-
-## 8. Data model — output shape
-
-> **dashboardAPI has no domain output.** It does not extend `BaseDomain` and does not implement `getOutput()`. The `wiki:datamodel` script falls back to the hand-curated template below.
-
-
-
-No domain output. dashboardAPI emits **HTTP request envelopes on Port 0**, shaped for a downstream `http request` node:
-
-```js
+```json
{
- topic: 'create',
- url: 'http://:/api/dashboards/db',
- method: 'POST',
- headers: {
- Accept: 'application/json',
- 'Content-Type': 'application/json',
- Authorization: 'Bearer …' // only when bearerToken is set
+ "topic": "create",
+ "url": "http://grafana:3000/api/dashboards/db",
+ "method": "POST",
+ "headers": {
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ "Authorization": "Bearer eyJ..."
},
- payload: { dashboard: {…}, folderId: 0, overwrite: true },
- meta: { nodeId, softwareType, uid, title }
+ "payload": {
+ "dashboard": { "uid": "a1b2c3d4e5f6", "title": "Pumping Station Demo", "templating": {...} },
+ "folderId": 0,
+ "overwrite": true
+ },
+ "meta": {
+ "nodeId": "ps_demo",
+ "softwareType": "pumpingStation",
+ "uid": "a1b2c3d4e5f6",
+ "title": "Pumping Station Demo"
+ }
}
```
-Port 1 (InfluxDB telemetry) and Port 2 (registration / control plumbing) are unused — dashboardAPI has no measurements and does not register with a parent.
+| Field | Meaning |
+|:---|:---|
+| `topic` | Always `'create'` — signals a dashboard-upsert HTTP envelope. |
+| `url` | `grafanaUpsertUrl()` = `://:/api/dashboards/db`. |
+| `method` | Always `POST`. |
+| `headers.Authorization` | Present only when `bearerToken` is configured; omitted otherwise. |
+| `payload.dashboard` | The composed Grafana dashboard JSON (template + templating vars filled in). |
+| `payload.dashboard.uid` | `sha1(softwareType:nodeId).slice(0, 12)` — stable across re-deploys. |
+| `meta.*` | Correlation fields for the downstream consumer (nodeId, softwareType, uid, title). |
-
+Inbound `msg` fields propagate via spread (`{...msg, ...envelope}`) so any caller-supplied correlation / trace fields survive.
-**`meta` fields:**
+> Port 1 (InfluxDB telemetry) and Port 2 (registration / control plumbing) are **unused** — dashboardAPI has no measurements and does not register with a parent. See [Reference — Architecture](Reference-Architecture#output-ports).
-| Field | Type | Value |
-|---|---|---|
-| `nodeId` | string | `config.general.id` or `config.general.name` |
-| `softwareType` | string | `config.functionality.softwareType` |
-| `uid` | string | SHA-1(`softwareType:nodeId`) first 12 chars — stable across re-deploys |
-| `title` | string | `config.general.name` |
+---
-Inbound `msg` fields propagate via spread (`{...msg, ...envelope}`) — caller-supplied correlation/trace fields survive.
+## The new bit — no BaseNodeAdapter / BaseDomain
-See `CONTRACT.md` for the full envelope spec and port definitions.
+Most EVOLV nodes extend `BaseNodeAdapter` + `BaseDomain` from `generalFunctions/`. `dashboardAPI` does **not** — per `OPEN_QUESTIONS.md` (2026-05-10) the decision is to keep a bespoke adapter until `BaseNodeAdapter` grows passive / HTTP-only flags.
-## 9. Configuration — editor form ↔ config keys
+Reasons:
-```mermaid
-flowchart TB
- subgraph editor["Node-RED editor form (dashboardapi.html)"]
- f1[Protocol — select http/https]
- f2[Grafana Host — text]
- f3[Grafana Port — number]
- f4[Bearer Token — password]
- f5[InfluxDB Bucket — text]
- f6[Enable Log — checkbox]
- f7[Log Level — select]
- end
- subgraph config["Runtime config slice (_buildConfig)"]
- c1[grafanaConnector.protocol]
- c2[grafanaConnector.host]
- c3[grafanaConnector.port]
- c4[grafanaConnector.bearerToken]
- c5[defaultBucket]
- c6[general.logging.enabled]
- c7[general.logging.logLevel]
- end
- f1 --> c1
- f2 --> c2
- f3 --> c3
- f4 --> c4
- f5 --> c5
- f6 --> c6
- f7 --> c7
-```
+- No `generalFunctions/src/configs/dashboardapi.json` — `BaseDomain`'s constructor unconditionally calls `configManager.getConfig(ctor.name)` and would throw. The local `dependencies/dashboardapi/dashboardapiConfig.json` is for the editor menu endpoint, not the runtime config pipeline.
+- No periodic output — `BaseNodeAdapter._emitOutputs()` / `outputUtils.formatMsg` assumes a delta-compressed Port 0 / 1 stream; dashboardAPI emits HTTP-shaped messages instead.
+- No registration to a parent — `BaseNodeAdapter._scheduleRegistration` would emit a spurious `child.register` of its own.
+- No status badge / tick / measurements / children of its own.
-| Form field | Config key | Default | Range / values | Where used |
-|---|---|---|---|---|
-| Protocol | `grafanaConnector.protocol` | `http` | `http` \| `https` | `grafanaUpsertUrl()` |
-| Grafana Host | `grafanaConnector.host` | `localhost` | hostname / IP | `grafanaUpsertUrl()` |
-| Grafana Port | `grafanaConnector.port` | `3000` | 1–65535 | `grafanaUpsertUrl()` |
-| Bearer Token | `grafanaConnector.bearerToken` | `''` | string (Grafana service account token) | `Authorization: Bearer …` header |
-| InfluxDB Bucket | `defaultBucket` | `''` → falls back to `INFLUXDB_BUCKET` env → position default | string | `updateTemplatingVar('bucket', …)` |
-| Enable Log | `general.logging.enabled` | `false` | boolean | Logger constructor |
-| Log Level | `general.logging.logLevel` | `'info'` | `info` \| `debug` \| `warn` \| `error` | Logger constructor |
+dashboardAPI uses the shared `commandRegistry` (canonical-topic naming + alias-with-deprecation) and stops there. See [Reference — Architecture](Reference-Architecture#why-no-basenodeadapter--basedomain) for the full rationale.
-**Position-based bucket fallback** (when `defaultBucket` is empty):
+---
-| `positionVsParent` | Bucket used |
-|---|---|
-| `upstream` | `lvl1` |
-| `downstream` | `lvl3` |
-| any other / absent | `lvl2` |
+## Need more?
-## 10. State chart
+| Page | What you'll find |
+|:---|:---|
+| [Reference — Contracts](Reference-Contracts) | Full topic contract, config schema, child resolution rules, template alias table |
+| [Reference — Architecture](Reference-Architecture) | Code map, HTTP-endpoint lifecycle, template loader, UID stability, graph walk |
+| [Reference — Examples](Reference-Examples) | Shipped example flows + debug recipes |
+| [Reference — Limitations](Reference-Limitations) | Legacy filename drift, stub flows, missing template handling, open questions |
-> **Skipped.** dashboardAPI is stateless — no FSM, no tick loop, no operating states. See template rule: "Skip this section for stateless nodes (`measurement`, `dashboardAPI`)."
-
-## 11. Examples
-
-| Tier | File | What it shows | Status |
-|---|---|---|---|
-| Basic | `examples/basic.flow.json` | Inject `child.register` payload (inline config) + downstream `http request` → Grafana | ⏳ TBD — stub exists |
-| Integration | `examples/integration.flow.json` | Real EVOLV node (e.g. pumpingStation) → child.register → dashboardAPI → Grafana | ⏳ TBD — stub exists |
-| Dashboard | _n/a_ | dashboardAPI **generates** Grafana dashboards — no FlowFuse chart tier for this node | — |
-
-**Wiring pattern** (inline-payload basic test):
-
-```json
-[
- { "type": "inject", "payload": { "source": { "config": { "functionality": { "softwareType": "measurement" }, "general": { "id": "pump-a-flow", "name": "Pump A flow" } } } }, "topic": "child.register" },
- { "type": "dashboardapi" },
- { "type": "http request", "method": "POST" }
-]
-```
-
-## 12. Debug recipes
-
-| Symptom | First thing to check | Where to look |
-|---|---|---|
-| No HTTP message emitted on Port 0 | Did `resolveChildSource` return a non-null source? Check that payload has `.source.config` or `.config` or a valid node id. | Container log for "generateDashboardsForGraph skipped" warning. |
-| `Skipping dashboard generation: no template` | `config/.json` missing. | `config/` directory — add a template JSON file for the new node type. |
-| `machineGroupControl` produces no dashboard | The alias maps to `machineGroup.json` — verify that file exists in `config/`. | `_templateFileForSoftwareType` in `specificClass.js`. |
-| Empty `Authorization` header | `bearerToken` not set in editor form. | Editor → Bearer Token field. |
-| Wrong InfluxDB bucket in Grafana template variables | `defaultBucket` config or `INFLUXDB_BUCKET` env overrides the position-based default. | `_buildConfig` in `nodeClass.js` + `defaultBucketForPosition` in `specificClass.js`. |
-| Dashboard UID changes between deploys | Node id or `softwareType` changed — UID is `sha1(softwareType:nodeId)`. | `stableUid` in `specificClass.js`. |
-| `registerChild` alias warns once | Expected — deprecation warning on first use. Migrate caller to topic `child.register`. | Caller `msg.topic`. |
-
-> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors. Use only for live debugging sessions.
-
-## 13. When you would NOT use this node
-
-- **Use dashboardAPI only for auto-generating Grafana dashboards from EVOLV topology.** If you maintain dashboards manually in Grafana, skip it — it will overwrite your customisations on every registration event.
-- **Don't use dashboardAPI as a generic Grafana HTTP client.** It only emits dashboard upserts (`POST /api/dashboards/db`). For arbitrary Grafana API calls (annotations, alerts, data sources) use a plain `http request` node.
-- **Don't wire tick/measurement data into dashboardAPI.** It fires on `child.register` events (deploy time), not on the measurement tick. Wiring Port-0 data from a rotatingMachine or pumpingStation here is a misuse.
-- **Don't expect EVOLV child registration to happen automatically.** dashboardAPI passively receives `child.register`; the emitting node (e.g. pumpingStation) must have its Port 2 wired to dashboardAPI's input. See Section 7.
-- **Not a BaseDomain node.** dashboardAPI cannot be used wherever a BaseDomain-capable node is required (e.g. as a registered child of machineGroupControl). See OPEN_QUESTIONS.md (2026-05-10) and Section 14.
-
-## 14. Known limitations / current issues
-
-| # | Issue | Tracked in |
-|---|---|---|
-| 1 | No domain output — cannot be introspected via the standard `getOutput()` channel. Debugging relies on watching Port 0 HTTP envelopes in a debug node. | `CONTRACT.md → "Why no BaseNodeAdapter / BaseDomain"` |
-| 2 | Does not extend `BaseNodeAdapter` / `BaseDomain` — decision deferred pending a passive/HTTP-only mode on `BaseNodeAdapter` (skip-registration + skip-output-stream flags). Until that ships the bespoke adapter shape is correct. | `OPEN_QUESTIONS.md` (2026-05-10) — "dashboardAPI skipped BaseNodeAdapter + BaseDomain". Confirm with team whether to revisit when `BaseNodeAdapter` grows a passive mode. |
-| 3 | Template discovery is filename-based. Renaming a node's `softwareType` requires renaming (or aliasing) the template file. The `machineGroupControl → machineGroup.json` mapping is a one-off alias in `_templateFileForSoftwareType`. | `src/specificClass.js → _templateFileForSoftwareType` |
-| 4 | No retry / circuit-breaker on the downstream `http request` node — Grafana outages silently drop dashboard upserts. | TBD — no issue filed yet |
-| 5 | Tier 1/2 example flows exist as stubs only (`basic.flow.json`, `integration.flow.json`) — not yet validated on a live Node-RED instance. | P9 wiki cleanup follow-up |
+[EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home) · [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) · [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)
diff --git a/wiki/Reference-Architecture.md b/wiki/Reference-Architecture.md
new file mode 100644
index 0000000..312d1c7
--- /dev/null
+++ b/wiki/Reference-Architecture.md
@@ -0,0 +1,294 @@
+# 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/.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) |
diff --git a/wiki/Reference-Contracts.md b/wiki/Reference-Contracts.md
new file mode 100644
index 0000000..e90e651
--- /dev/null
+++ b/wiki/Reference-Contracts.md
@@ -0,0 +1,243 @@
+# Reference — Contracts
+
+
+
+> [!NOTE]
+> Full topic contract, configuration schema, child-resolution rules, and Port-0 envelope spec for `dashboardAPI`. Source of truth: `src/commands/index.js`, `src/commands/handlers.js`, `src/specificClass.js`, `src/nodeClass.js`, and `dependencies/dashboardapi/dashboardapiConfig.json`.
+>
+> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
+>
+> For an intuitive overview, return to [Home](Home).
+
+---
+
+## Topic contract
+
+The registry lives in `src/commands/index.js`. dashboardAPI has **one** canonical input topic.
+
+
+
+| Canonical topic | Aliases | Payload | Unit | Effect |
+|:---|:---|:---|:---|:---|
+| `child.register` | `registerChild` | `string` (child node id) **or** `{source: {...}}` **or** `{config: {...}}`; optional `msg.includeChildren: boolean` (default `true`) | — | Resolves the child source (`RED.nodes.getNode` → `node._flow.getNode` → inline payload), calls `source.generateDashboardsForGraph(child, {includeChildren})`, then emits one `topic: 'create'` HTTP-upsert message on Port 0 per generated dashboard. |
+
+
+
+The `registerChild` alias logs a one-time deprecation warning on first use. There is **no HTTP endpoint contract** for dashboardAPI as a Node-RED node — it is an input-on-wire only. The outbound HTTP call shape is documented in [Port-0 envelope](#port-0-envelope-data-model) below.
+
+### Payload resolution rules
+
+| Payload shape | Resolved as | Source code |
+|:---|:---|:---|
+| `{source: {config: {...}}, ...}` | `payload.source` — use directly | `handlers.js` `resolveChildSource` line 6 |
+| `{config: {...}}` | `{config: payload.config}` — wrap minimally | `handlers.js` `resolveChildSource` line 7 |
+| `""` (bare string) | `RED.nodes.getNode(id).source` → fallback `node._flow.getNode(id).source` | `handlers.js` `resolveChildNode` |
+| anything else | `null` → throws `'Missing or invalid child node'` | `handlers.js` `registerChild` line 30 |
+
+`msg.includeChildren` (default `true`) controls graph-walk depth: `true` walks `extractChildren(rootSource)` and emits one dashboard per discovered child plus the root; `false` emits just the root dashboard.
+
+---
+
+## Data model — Port-0 envelope
+
+
+
+dashboardAPI **has no domain output** — it does not extend `BaseDomain` and does not implement `getOutput()`. Port 0 carries one **HTTP request envelope** per generated dashboard, shaped for a downstream `http request` core node:
+
+```js
+{
+ topic: 'create',
+ url: 'http://:/api/dashboards/db',
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ Authorization: 'Bearer ' // only when grafanaConnector.bearerToken is set
+ },
+ payload: {
+ dashboard: { uid: '<12-char-sha1>', title: '', templating: {...}, ... },
+ folderId: 0,
+ overwrite: true
+ },
+ meta: {
+ nodeId: '',
+ softwareType: '',
+ uid: '',
+ title: ''
+ }
+}
+```
+
+Port 1 (InfluxDB telemetry) and Port 2 (registration / control plumbing) are **unused** — dashboardAPI has no measurements and does not register with a parent.
+
+
+
+### Envelope fields
+
+| Key | Type | Source | Notes |
+|:---|:---|:---|:---|
+| `topic` | string | constant `'create'` | Signals "Grafana dashboard upsert". |
+| `url` | string | `grafanaUpsertUrl()` | `${protocol}://${host}:${port}/api/dashboards/db`. |
+| `method` | string | constant `'POST'` | — |
+| `headers.Accept` | string | constant | `application/json` |
+| `headers.Content-Type` | string | constant | `application/json` |
+| `headers.Authorization` | string | absent | `Bearer ${bearerToken}` | **Omitted entirely** when `bearerToken` is empty. |
+| `payload.dashboard` | object | `buildUpsertRequest({dashboard, folderId, overwrite}).dashboard` | The composed Grafana dashboard JSON. |
+| `payload.folderId` | integer | constant `0` | Root folder. Not configurable. |
+| `payload.overwrite` | boolean | constant `true` | Required for idempotent re-deploys. |
+| `meta.nodeId` | string | `config.general.id` or `config.general.name` or `softwareType` | Correlation id. |
+| `meta.softwareType` | string | `config.functionality.softwareType` (case-insensitive lookup) | Used for template selection. |
+| `meta.uid` | string | `sha1(softwareType:nodeId).slice(0, 12)` | Stable across re-deploys — same `(softwareType, nodeId)` → same UID. |
+| `meta.title` | string | `config.general.name` or `nodeId` | Human-readable dashboard title. |
+
+**`msg` propagation:** inbound `msg.*` fields are merged via `{...msg, topic:'create', ...}` spread — caller-supplied correlation / trace fields (e.g. `msg._msgid`, `msg.requestId`) survive the hop.
+
+### Dashboard composition
+
+For each generated dashboard, `buildDashboard({nodeConfig, positionVsParent})` performs:
+
+1. **Template load** — `loadTemplate(softwareType)` from `config/.json` (case-insensitive fallback, `machineGroupControl → machineGroup.json` alias). Missing template → logs `warn` and returns `null` (the dashboard is skipped from the output).
+2. **UID stamp** — `dashboard.uid = stableUid(softwareType:nodeId)`.
+3. **Title stamp** — `dashboard.title = config.general.name || nodeId`.
+4. **Tags merge** — existing `template.tags` + `['EVOLV', softwareType, positionVsParent]` (deduplicated, empty values filtered).
+5. **Templating var fill** — `dashboard.templating.list[]` entries named `measurement` and `bucket` are mutated in place:
+ - `measurement` ← `${softwareType}_${nodeId}` (used as InfluxDB measurement name in panel queries).
+ - `bucket` ← resolved bucket (see [Bucket resolution](#bucket-resolution) below).
+6. **Links append** (root dashboard only, when `includeChildren=true` and `children.length > 0`) — one `{type:'link', title, url:'/d//', keepTime, keepVariables}` entry per direct child.
+
+If `dashboard.templating.list` is not an array or the named variable doesn't exist, the templating step is a no-op (no error).
+
+### Bucket resolution
+
+`bucket` (the InfluxDB bucket templating var) is resolved in priority order:
+
+| Priority | Source | When applied |
+|:---:|:---|:---|
+| 1 | `config.defaultBucket` (editor field or `INFLUXDB_BUCKET` env) | When set to a non-empty string |
+| 2 | `config.bucketMap[positionVsParent]` | When the position has an entry |
+| 3 | `defaultBucketForPosition(positionVsParent)` | Falls through — `upstream → lvl1`, `downstream → lvl3`, else `lvl2` |
+
+> [!NOTE]
+> Priorities 1 and 2 read order from `specificClass.js` `buildDashboard`. Verify against the editor's intended semantics during full review — "global override beats per-position map" is the current behaviour. Flagged.
+
+---
+
+## Configuration schema — editor form to config keys
+
+Source of truth: `dependencies/dashboardapi/dashboardapiConfig.json` + `src/nodeClass.js` `_buildConfig`. The runtime config slice is built by `configManager.buildConfig(name, uiConfig, nodeId, overrides)`.
+
+### General (`config.general`)
+
+| Form field | Config key | Default | Notes |
+|:---|:---|:---|:---|
+| Name | `general.name` | `'dashboardapi'` | Display label; falls through to nodeId in `meta.title`. |
+| (auto-assigned) | `general.id` | `null` | Node-RED node id. |
+| Enable logging | `general.logging.enabled` | `false` (per `_buildConfig`) / `true` (per `dashboardapiConfig.json`) | **Mismatch** — see [Limitations](Reference-Limitations#config-default-mismatch). |
+| Log level | `general.logging.logLevel` | `'info'` | `debug` / `info` / `warn` / `error`. |
+
+### Functionality (`config.functionality`)
+
+| Form field | Config key | Default | Notes |
+|:---|:---|:---|:---|
+| (hidden) | `functionality.softwareType` | `'dashboardapi'` | Constant. Set in `_buildConfig` from `this.name.toLowerCase()`. |
+| (hidden) | `functionality.role` | `'auto ui generator'` | Constant. |
+
+### Grafana connector (`config.grafanaConnector`)
+
+| Form field | Config key | Default | Range / values | Where used |
+|:---|:---|:---|:---|:---|
+| Protocol | `grafanaConnector.protocol` | `'http'` | `http` / `https` | `grafanaUpsertUrl()` |
+| Grafana Host | `grafanaConnector.host` | `'localhost'` | hostname / IP | `grafanaUpsertUrl()` |
+| Grafana Port | `grafanaConnector.port` | `3000` | 1–65535 (`Number(uiConfig.port \|\| 3000)`) | `grafanaUpsertUrl()` |
+| Bearer Token | `grafanaConnector.bearerToken` | `''` | string (Grafana service-account token) | `Authorization: Bearer ...` header; omitted when empty |
+
+### Bucket configuration
+
+| Form field | Config key | Default | Notes |
+|:---|:---|:---|:---|
+| InfluxDB Bucket | `defaultBucket` | `''` → falls back to `process.env.INFLUXDB_BUCKET` → position default | Set in `_buildConfig`; consumed by `buildDashboard` templating fill. |
+| (no editor field) | `bucketMap` | `{}` | Programmatic only — pass via `uiConfig.bucketMap` or future editor field. |
+
+### Editor menu / logger fields
+
+The `dashboardapi.html` template invokes `window.EVOLV.nodes.dashboardapi.loggerMenu.initEditor / saveEditor` via the shared `MenuManager`-served `/dashboardapi/menu.js` endpoint. The logger fields (`enableLog`, `logLevel`) are persisted on the node via the standard EVOLV editor menu pattern.
+
+> [!WARNING]
+> **Editor `defaults` use legacy field names.** `dashboardapi.html` declares `{enableLog, logLevel}` as Node-RED defaults but the runtime config reads `general.logging.{enabled, logLevel}`. The bridge is the shared logger menu (`MenuManager`) — confirm during full review that the editor menu correctly maps `enableLog` → `general.logging.enabled`.
+
+---
+
+## Template alias map
+
+`_templateFileForSoftwareType(softwareType)` lookup order:
+
+| Order | Candidate filename | Notes |
+|:---:|:---|:---|
+| 1 | `.json` | Exact case. |
+| 2 | `.json` | Case-insensitive fallback. |
+| 3 | `machineGroup.json` | **Only** when `softwareType === 'machineGroupControl'` (one-off alias). |
+
+If none of the candidates exist in `config/`, the logger emits `No dashboard template found for softwareType=` at `warn` level and `loadTemplate` returns `null`. `buildDashboard` then logs `Skipping dashboard generation: no template for softwareType=` and returns `null`; `generateDashboardsForGraph` skips that node and continues with the rest of the graph walk.
+
+Currently shipped templates:
+
+| softwareType (canonical) | Template file | Notes |
+|:---|:---|:---|
+| `aeration` | `aeration.json` | — |
+| `dashboardapi` | `dashboardapi.json` | Self-template (when a dashboardAPI registers as a child of another dashboardAPI — unusual). |
+| `machine` (or `rotatingmachine`) | `machine.json` | softwareType to verify in full review — flagged. |
+| `machineGroupControl` | `machineGroup.json` | Via one-off alias. |
+| `measurement` | `measurement.json` | — |
+| `monster` | `monster.json` | — |
+| `pumpingStation` | `pumpingStation.json` | — |
+| `reactor` | `reactor.json` | — |
+| `settler` | `settler.json` | — |
+| `valve` | `valve.json` | — |
+| `valveGroupControl` | `valveGroupControl.json` | — |
+
+Adding support for a new EVOLV node type = drop a `config/.json` file matching the `softwareType` lowercase name (or add an alias arm to `_templateFileForSoftwareType`).
+
+---
+
+## Child resolution (NOT a registry)
+
+dashboardAPI does **not** maintain a child registry of its own. There is no `_registeredChildren` map, no `child.register` → `child.unregister` lifecycle, no parent → child emitter wiring. Every inbound `child.register` is a **one-shot** dashboard generation:
+
+```mermaid
+flowchart LR
+ src["any EVOLV node
(has functionality.softwareType)"]:::other -->|child.register| dash[dashboardAPI
Utility]:::neutral
+ dash --> resolve["resolveChildSource(payload, ctx)
RED.nodes.getNode → _flow.getNode → inline"]
+ resolve --> walk["generateDashboardsForGraph(childSource, {includeChildren})"]
+ walk --> emit["emit one msg per dashboard
topic='create'"]
+ emit --> http[(downstream
http request node)]
+ classDef neutral fill:#dddddd,color:#000
+ classDef other fill:#ffffff,stroke:#666
+```
+
+### What graph walk reads from the child source
+
+`extractChildren(rootSource)` reads `rootSource.childRegistrationUtils.registeredChildren` (a Map). For each `entry`:
+
+- `entry.child` — the child source object (must have `.config`).
+- `entry.position` (or `child.positionVsParent`) — used for the bucket fallback and tag composition.
+
+Children without a `.config` are silently skipped. If `rootSource.childRegistrationUtils` is absent or `registeredChildren.values` is not a function, the result is an empty array — just the root dashboard is emitted.
+
+| Inbound softwareType | Filter | Side effect |
+|:---|:---|:---|
+| any | child has `functionality.softwareType` AND the matching `config/*.json` exists | Loads template; emits one upsert msg per dashboard in the walk. |
+| any | child has `functionality.softwareType` but the template is missing | Warns and skips that node's dashboard. No error thrown. Graph walk continues. |
+| absent / malformed | `resolveChildSource` returns null | Throws `Missing or invalid child node` → nodeClass sets red status, calls `node.error`. |
+
+---
+
+## Related pages
+
+| Page | Why |
+|:---|:---|
+| [Home](Home) | Intuitive overview |
+| [Reference — Architecture](Reference-Architecture) | Code map, lifecycle, graph walk |
+| [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes |
+| [Reference — Limitations](Reference-Limitations) | Filename drift, stub flows, open questions |
+| [EVOLV — Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions) | Platform-wide topic rules |
+| [EVOLV — Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry) | Port 0 / 1 / 2 layout (dashboardAPI is an exception — Port 0 carries HTTP envelopes) |
diff --git a/wiki/Reference-Examples.md b/wiki/Reference-Examples.md
new file mode 100644
index 0000000..e80cb58
--- /dev/null
+++ b/wiki/Reference-Examples.md
@@ -0,0 +1,171 @@
+# Reference — Examples
+
+
+
+> [!NOTE]
+> Every example flow shipped under `nodes/dashboardAPI/examples/`, plus how to load them, what they show, and the debug recipes that go with them. Live source: `nodes/dashboardAPI/examples/`.
+>
+> Pending full node review (2026-05). The shipped example flows are **stubs** — they wire up the node but the inject payloads do not yet match the `child.register` resolver's expected shape. Working wiring patterns are documented inline below.
+
+---
+
+## Shipped examples
+
+| File | Tier | Dependencies | What it shows | Status |
+|:---|:---:|:---|:---|:---|
+| `basic.flow.json` | 1 | EVOLV only | Inject a `ping` topic into a stand-alone dashboardAPI node + debug tap on Port 0. | ⏳ **Stub** — inject topic is `ping`, not `child.register`; the registry will silently drop the msg. |
+| `integration.flow.json` | 2 | EVOLV only | Inject a `registerChild` alias topic with a bare-string node id (`'example-child-id'`) + debug tap. | ⏳ **Stub** — the bare-string id resolves to `null` via `RED.nodes.getNode`; throws `'Missing or invalid child node'`. |
+| `edge.flow.json` | 3 | EVOLV only | Inject an unknown topic to confirm the dispatcher silently drops it. | ✓ Works as a registry-coverage probe. |
+
+All three are tracked for replacement in the next wiki-cleanup pass — see [Limitations — Example flow stubs](Reference-Limitations#example-flow-stubs).
+
+---
+
+## Loading a flow
+
+### Via the editor
+
+1. Open the Node-RED editor at `http://localhost:1880`.
+2. Menu → Import → drag the JSON file.
+3. Click Deploy.
+
+### Via the Admin API
+
+```bash
+curl -X POST -H 'Content-Type: application/json' \
+ --data @nodes/dashboardAPI/examples/basic.flow.json \
+ http://localhost:1880/flows
+```
+
+---
+
+## Working wiring patterns
+
+These are the shapes that actually exercise the resolver. Use them as the basis for any new example flow until the stubs above are replaced.
+
+### Wiring pattern A — inline `source` payload (no real EVOLV node needed)
+
+```json
+[
+ {
+ "type": "inject",
+ "topic": "child.register",
+ "props": [
+ {"p": "topic", "vt": "str"},
+ {"p": "payload", "v": "{\"source\":{\"config\":{\"functionality\":{\"softwareType\":\"measurement\"},\"general\":{\"id\":\"pump-a-flow\",\"name\":\"Pump A flow\"}}}}", "vt": "json"}
+ ]
+ },
+ { "type": "dashboardapi" },
+ { "type": "http request", "method": "POST" },
+ { "type": "debug", "complete": "true" }
+]
+```
+
+What happens:
+
+1. The inject fires a msg with `topic: 'child.register'` and `payload.source.config.functionality.softwareType = 'measurement'`.
+2. `resolveChildSource` matches the `payload.source.config` branch and returns `payload.source` directly.
+3. `loadTemplate('measurement')` reads `config/measurement.json`.
+4. `stableUid('measurement:pump-a-flow')` → deterministic 12-char hex.
+5. The Port-0 envelope flows to the debug node AND to the `http request` node which POSTs to Grafana.
+
+### Wiring pattern B — bare `config` payload
+
+Same as pattern A but with the outer `source` wrapper dropped:
+
+```json
+"payload": "{\"config\":{\"functionality\":{\"softwareType\":\"pumpingStation\"},\"general\":{\"id\":\"ps_demo\",\"name\":\"Pumping Station Demo\"}}}"
+```
+
+`resolveChildSource` falls through to the `payload.config` branch and wraps as `{config: payload.config}`. No `childRegistrationUtils` is present, so the graph walk emits only the root dashboard (no children even if `includeChildren=true`).
+
+### Wiring pattern C — real EVOLV node via Port 2
+
+The canonical production wiring: any EVOLV node's Port 2 (`registerChild` emission) wired into dashboardAPI's input.
+
+```text
+[rotatingMachine] Port 2 ──► [dashboardAPI] Port 0 ──► [http request] ──► Grafana
+ │
+ └─► [debug]
+```
+
+The emitting node's `child.register` payload is the bare node id (a string). `resolveChildNode` then runs `RED.nodes.getNode(id)` to fetch the live runtime node and reads `node.source.config`. Walks `node.source.childRegistrationUtils.registeredChildren` so direct children also get dashboards.
+
+> [!IMPORTANT]
+> **Example needed.** A Tier-2 example that wires a real `rotatingMachine` or `pumpingStation` Port 2 to dashboardAPI input is the missing canonical demo. Save as `nodes/dashboardAPI/examples/02-Integration-with-EVOLV-node.json`. Track in `IMPROVEMENTS_BACKLOG.md`.
+
+---
+
+## Docker compose snippet
+
+To bring up Node-RED + Grafana (+ optional InfluxDB) for end-to-end testing:
+
+```yaml
+services:
+ nodered:
+ build: ./docker/nodered
+ ports: ['1880:1880']
+ volumes:
+ - ./docker/nodered/data:/data/evolv
+ environment:
+ INFLUXDB_BUCKET: lvl2
+ grafana:
+ image: grafana/grafana:11.0.0
+ ports: ['3000:3000']
+ environment:
+ GF_SECURITY_ADMIN_PASSWORD: admin
+ influxdb:
+ image: influxdb:2.7
+ ports: ['8086:8086']
+```
+
+A Grafana service account token (created via Grafana UI → Administration → Service accounts) goes into the dashboardAPI's Bearer Token editor field.
+
+Full file: [EVOLV/docker-compose.yml](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/docker-compose.yml).
+
+---
+
+## Debug recipes
+
+| Symptom | First thing to check | Where to look |
+|:---|:---|:---|
+| No HTTP message emitted on Port 0; node shows red `dashboardapi error` status | `resolveChildSource` returned `null`. Check payload shape against [Payload resolution rules](Reference-Contracts#payload-resolution-rules). The most common cause: bare-string id that doesn't match a live Node-RED node. | `src/commands/handlers.js` `resolveChildSource` + `resolveChildNode`. |
+| Dispatch silently drops msg (no error, no output) | Topic is not `child.register` and not the `registerChild` alias. The registry's catch-all is "no match → ignore". | `src/commands/index.js` + `createRegistry` source in `generalFunctions/`. |
+| `Skipping dashboard generation: no template for softwareType=` warn | `config/.json` (or its lowercase variant or alias) doesn't exist. | `config/` directory — add a template JSON, or fix the emitting node's `functionality.softwareType`. |
+| `machineGroupControl` produces no dashboard | The alias maps to `machineGroup.json` — verify that file exists in `config/`. | `_templateFileForSoftwareType` in `src/specificClass.js`. |
+| Empty `Authorization` header | `bearerToken` not set in editor form — the header is omitted entirely when the token is empty, not set to `'Bearer '`. | Editor → Bearer Token field. |
+| Wrong InfluxDB bucket in Grafana template variables | `defaultBucket` config (or `INFLUXDB_BUCKET` env) overrides the position-based default. Priority order: `defaultBucket` → `bucketMap[position]` → `defaultBucketForPosition`. | `_buildConfig` in `nodeClass.js` + `defaultBucketForPosition` in `specificClass.js`. |
+| Dashboard UID changes between deploys | Node id or `softwareType` changed — UID is `sha1(softwareType:nodeId).slice(0, 12)`. Stable only if both are stable. | `stableUid` in `specificClass.js`. |
+| `registerChild` alias warns once | Expected — deprecation warning on first use only. Migrate caller to `child.register`. | Caller `msg.topic`. |
+| Grafana 404 on `POST /api/dashboards/db` | Wrong path = check Grafana version. The `/api/dashboards/db` endpoint exists in Grafana 7–11. For newer Grafana with org-scoped endpoints, the upsert URL may differ. | `grafanaUpsertUrl` in `specificClass.js`. |
+| Grafana 401 / 403 | Bearer token missing, expired, or insufficient permissions. The service account needs at least `Editor` role on the target folder. | Grafana UI → Administration → Service accounts. |
+| Root dashboard has no `links[]` to children | `includeChildren=false` was passed, OR the root source's `childRegistrationUtils.registeredChildren` is empty / absent. | `generateDashboardsForGraph` + `extractChildren`. |
+| Editor form shows blank fields after re-open | `oneditprepare` waits for `window.EVOLV.nodes.dashboardapi.loggerMenu` which is loaded by `/dashboardapi/menu.js`. If the menu endpoint 500s, the editor stays blank. | Browser devtools → Network → `menu.js`; check the entry file's logger menu endpoint. |
+
+> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors. Use only for live debugging sessions.
+
+---
+
+## Quick smoke test (no Grafana required)
+
+To verify the node loads and the registry dispatches correctly without standing up Grafana:
+
+1. Import `examples/basic.flow.json` (or any of the stubs).
+2. Edit the inject node: set topic to `child.register` and payload to a JSON object matching wiring pattern A above.
+3. Deploy.
+4. Fire the inject. The debug pane should show a `topic: 'create'` envelope with a populated `payload.dashboard`.
+5. If `headers.Authorization` is absent, the editor's Bearer Token field is empty — that's correct behaviour.
+
+The downstream `http request` node is **optional** for the smoke test — the dashboardAPI emits regardless of whether anything POSTs the envelope to Grafana.
+
+---
+
+## Related pages
+
+| Page | Why |
+|:---|:---|
+| [Home](Home) | Intuitive overview |
+| [Reference — Contracts](Reference-Contracts) | Topic + payload resolution + envelope shape |
+| [Reference — Architecture](Reference-Architecture) | Code map, lifecycle, graph walk |
+| [Reference — Limitations](Reference-Limitations) | Stub flows, filename drift, open questions |
+| [EVOLV — Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns) | Where dashboardAPI fits in a larger plant |
diff --git a/wiki/Reference-Limitations.md b/wiki/Reference-Limitations.md
new file mode 100644
index 0000000..450aede
--- /dev/null
+++ b/wiki/Reference-Limitations.md
@@ -0,0 +1,156 @@
+# Reference — Limitations
+
+
+
+> [!NOTE]
+> What `dashboardAPI` does not do, current rough edges, and open questions. Open items live in `.agents/improvements/IMPROVEMENTS_BACKLOG.md` and `.claude/refactor/OPEN_QUESTIONS.md` in the superproject.
+>
+> Pending full node review (2026-05). Content reflects `CONTRACT.md` and current source only.
+
+---
+
+## When you would not use this node
+
+| Scenario | Use instead |
+|:---|:---|
+| You maintain Grafana dashboards by hand | Skip dashboardAPI — it will overwrite your customisations on every `child.register` (upsert is `overwrite: true`). |
+| You need arbitrary Grafana API calls (annotations, alerts, data sources, folders) | A plain `http request` node. dashboardAPI only emits `POST /api/dashboards/db` envelopes. |
+| You want to forward tick / measurement data to Grafana | This is not what dashboardAPI does. Wire telemetry through Port 1 of an EVOLV process node directly into InfluxDB; Grafana queries InfluxDB. |
+| You want to use dashboardAPI as a BaseDomain-capable child of something else | Not supported — dashboardAPI does not extend `BaseDomain` and cannot register as a child of `machineGroupControl` / `pumpingStation` / similar. See [No BaseNodeAdapter / BaseDomain](#no-basenodeadapter--basedomain) below. |
+| You expect EVOLV nodes to auto-discover dashboardAPI | They don't. Port 2 of the emitter must be wired into dashboardAPI's input explicitly. |
+
+---
+
+## Known limitations
+
+### Legacy filename drift
+
+The entry file and editor HTML are currently lowercase — `dashboardapi.js` and `dashboardapi.html` — rather than `dashboardAPI.js` / `dashboardAPI.html` per the canonical folder-name convention in `.claude/rules/node-architecture.md`.
+
+The convention rule explicitly calls this out as legacy drift to fix when the file is next touched. A rename is a four-touch change:
+
+1. `dashboardapi.js` → `dashboardAPI.js`
+2. `dashboardapi.html` → `dashboardAPI.html`
+3. `package.json#node-red.nodes` — key remains `dashboardapi` (the Node-RED type id is independent of the filename) but the value becomes `dashboardAPI.js`.
+4. Superproject submodule references and any `require()` paths.
+
+The Node-RED **type id** (`dashboardapi`, lowercase, registered via `RED.nodes.registerType('dashboardapi', …)`) must stay `dashboardapi` to avoid breaking existing flows in the wild. The rename is purely the source-file path. Tracked.
+
+### Example flow stubs
+
+The three shipped flows (`basic.flow.json`, `integration.flow.json`, `edge.flow.json`) are placeholders. Their inject nodes don't fire a payload that matches the `child.register` resolver:
+
+| File | Current behaviour | What's wrong |
+|:---|:---|:---|
+| `basic.flow.json` | Inject `topic: 'ping'` | Not `child.register`; registry silently drops. |
+| `integration.flow.json` | Inject `topic: 'registerChild'` with `payload: 'example-child-id'` (string) | The string id has no live Node-RED node behind it; `RED.nodes.getNode('example-child-id')` returns null; throws `'Missing or invalid child node'`. |
+| `edge.flow.json` | Inject `topic: 'doesNotExist'` | Works as a registry-coverage probe (silent drop is correct) but exercises nothing. |
+
+Working wiring patterns are documented inline in [Reference — Examples](Reference-Examples#working-wiring-patterns). Replacement of the stubs is tracked in `IMPROVEMENTS_BACKLOG.md` (P9 wiki cleanup follow-up).
+
+### No BaseNodeAdapter / BaseDomain
+
+Most EVOLV nodes inherit a common adapter / domain base class. dashboardAPI does not. The decision is recorded in `OPEN_QUESTIONS.md` (2026-05-10) — four blockers (no platform config JSON, no periodic output, no parent registration, no status badge / tick / measurements). Until `BaseNodeAdapter` grows passive-mode flags (skip-registration + skip-output-stream), the bespoke adapter shape is the correct compromise.
+
+Consequence: dashboardAPI cannot be introspected via the standard `getOutput()` channel. Debugging relies on watching Port 0 in a debug node.
+
+### No domain output / no manifest
+
+Per `.claude/rules/output-coverage.md`, every node should ship a `test/_output-manifest.md` enumerating every Port-0/1/2 key in populated and degraded states. dashboardAPI's output surface is **one envelope shape**, emitted only when a dashboard is successfully generated — there is no degraded "partial envelope" state to test. The manifest collapses to:
+
+| Port | Output | Populated state | Degraded state |
+|:---|:---|:---|:---|
+| 0 | `{topic, url, method, headers, payload, meta}` envelope | Emitted once per generated dashboard | **Not emitted** — on resolution failure the handler throws and nodeClass sets a red status badge instead |
+| 1 | (unused) | — | — |
+| 2 | (unused) | — | — |
+
+The full output-coverage rule applies prospectively; no backfill manifest exists yet. Tracked.
+
+### Template discovery is filename-based
+
+The template lookup is `softwareType` ↔ filename. Renaming a node's `softwareType` (e.g. `rotatingmachine` → `rotatingMachine`) requires either renaming the template file or adding an alias arm in `_templateFileForSoftwareType`. The `machineGroupControl → machineGroup.json` mapping is a one-off alias because the historical filename was abbreviated.
+
+> [!NOTE]
+> Verify in full review: which softwareType does the current `rotatingMachine` emit? The shipped template is `config/machine.json` — if `rotatingMachine`'s `functionality.softwareType` is `'rotatingmachine'` (lowercase), the case-insensitive fallback won't find it and dashboard generation will warn-and-skip. Flagged.
+
+### No retry / circuit-breaker on downstream HTTP
+
+dashboardAPI emits the upsert envelope and is done. If the downstream `http request` node fails (Grafana down, 5xx, network timeout), the dashboard upsert is silently dropped — no retry, no DLQ, no status badge propagation back to dashboardAPI. The caller is responsible for wiring retry logic into the http-request path.
+
+### `oneditsave` doesn't read all editor fields uniformly
+
+`dashboardapi.html` `oneditsave` reads `['name', 'protocol', 'host', 'port', 'bearerToken', 'defaultBucket']` via direct DOM lookups, separately from the logger menu's `saveEditor`. Adding a new editor field requires touching both the form HTML and the `oneditsave` whitelist. Mild; not load-bearing.
+
+### Config default mismatch
+
+The runtime `_buildConfig` defaults `general.logging.enabled` to `Boolean(config?.general?.logging?.enabled)` — effectively `false` when the editor doesn't set it. But `dependencies/dashboardapi/dashboardapiConfig.json` declares the default as `true`. The editor menu (`loggerMenu`) bridges these via the standard EVOLV logger pattern, but the divergence is worth confirming — logger enabled vs disabled changes whether `Skipping dashboard generation: no template …` warns appear at all.
+
+> [!NOTE]
+> Confirm in full review which side wins by default for a freshly-dropped node. Flagged.
+
+### `bucket` resolution priority is global-then-per-position
+
+`buildDashboard` reads `this.config.defaultBucket || this.config.bucketMap[position] || defaultBucketForPosition(position)`. The global override fires **before** the per-position map. If you want per-position buckets to win over a global default, the current code doesn't do that — you'd need to leave `defaultBucket` empty and rely solely on `bucketMap` + the position fallback.
+
+Open question whether the "global beats per-position" priority is the intended semantics. Flagged.
+
+### No InfluxDB bucket validation
+
+The bucket name is templated into the Grafana dashboard JSON without any check that the bucket exists in InfluxDB. A typo produces a dashboard that renders panels saying "no data" with no upstream warning. Tracked.
+
+---
+
+## Open questions (tracked)
+
+| Question | Where it lives |
+|:---|:---|
+| Should `BaseNodeAdapter` grow a passive / HTTP-only mode (skip-registration + skip-output-stream) so dashboardAPI can extend it? | `.claude/refactor/OPEN_QUESTIONS.md` (2026-05-10) — "dashboardAPI skipped BaseNodeAdapter + BaseDomain" |
+| Confirm `rotatingMachine` softwareType ↔ `config/machine.json` mapping | Internal — flag during full review |
+| Bucket priority: should per-position `bucketMap` beat global `defaultBucket`? | Internal |
+| Should dashboardAPI emit a Port-2 status / health pulse so other EVOLV nodes can detect it? | Internal |
+| Should `child.register` aliases include older topic names (e.g. `RegisterChild`, `register-child`) for legacy compat? | Internal |
+| Add an explicit `child.unregister` / `dashboard.delete` topic to remove orphaned Grafana dashboards | Internal |
+| Provide a programmatic way to bulk-regenerate all dashboards for an existing deployment (e.g. `cmd.regenerate-all`) | Internal |
+| Retry / DLQ for failed Grafana upserts | TBD |
+
+---
+
+## Migration notes
+
+### From the `registerChild` alias
+
+The canonical topic since 2026-Q1 is `child.register`. The `registerChild` alias still works but logs a one-time deprecation warning on first use. Migrate callers when convenient:
+
+```diff
+- msg.topic = 'registerChild';
++ msg.topic = 'child.register';
+```
+
+Both topics accept identical payloads.
+
+### From bare-string node-id payloads
+
+The handler resolves bare-string payloads via `RED.nodes.getNode(id) → node._flow.getNode(id) → null`. This works at runtime but is brittle for tests and for flows where the emitter and dashboardAPI live on different `_flow` instances. Prefer the inline `{source: {config: {...}}}` or `{config: {...}}` shapes for tests and for any flow that imports both sides as JSON (no `RED.nodes` registry at compile time).
+
+### From hand-curated Grafana dashboards
+
+If you're moving from hand-curated dashboards to dashboardAPI-generated ones:
+
+1. Export your existing dashboard JSON from Grafana.
+2. Replace the templating-var values for `measurement` and `bucket` with placeholders.
+3. Save as `nodes/dashboardAPI/config/.json`.
+4. The next `child.register` for that softwareType will upsert (overwrite) the existing dashboard, preserving the UID if you set it to match `stableUid(softwareType:nodeId)`.
+
+If you want to **preserve the UID** of an existing hand-curated dashboard, compute `sha1(softwareType:nodeId).slice(0, 12)` and check it matches your existing UID. If not, either rename the node id, or accept that the first upsert will create a new dashboard alongside the old one.
+
+---
+
+## Related pages
+
+| Page | Why |
+|:---|:---|
+| [Home](Home) | Intuitive overview |
+| [Reference — Contracts](Reference-Contracts) | Topic + payload resolution + envelope shape |
+| [Reference — Architecture](Reference-Architecture) | Code map, lifecycle, "no BaseNodeAdapter" rationale |
+| [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes + working wiring patterns |
+| [EVOLV — Open Questions](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/development/.claude/refactor/OPEN_QUESTIONS.md) | Cross-node open questions and decisions log |
diff --git a/wiki/_Sidebar.md b/wiki/_Sidebar.md
new file mode 100644
index 0000000..6ceb699
--- /dev/null
+++ b/wiki/_Sidebar.md
@@ -0,0 +1,20 @@
+### dashboardAPI
+
+- [Home](Home)
+
+**Reference**
+
+- [Contracts](Reference-Contracts)
+- [Architecture](Reference-Architecture)
+- [Examples](Reference-Examples)
+- [Limitations](Reference-Limitations)
+
+**Related**
+
+- [EVOLV master wiki](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Home)
+- [rotatingMachine wiki](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Home)
+- [pumpingStation wiki](https://gitea.wbd-rd.nl/RnD/pumpingStation/wiki/Home)
+- [machineGroupControl wiki](https://gitea.wbd-rd.nl/RnD/machineGroupControl/wiki/Home)
+- [Topology Patterns](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topology-Patterns)
+- [Topic Conventions](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Topic-Conventions)
+- [Telemetry](https://gitea.wbd-rd.nl/RnD/EVOLV/wiki/Telemetry)