Walking skeleton: measurement → dashboardAPI → Grafana panel on deploy #34

Open
opened 2026-05-26 15:13:01 +00:00 by vps1_gitea_admin · 1 comment

Type: slice (walking skeleton)

Depends on: none

Estimate: L (3 days)

Slice — layers touched

Node-RED lifecycle (basic flow-start hook, no diff yet) → dashboardAPI domain (extractChildren + walk) → adapter (HTTP POST, credentials) → templates (one: measurement.json) → Influx convention (existing) → outbound HTTPS (net-new pattern) → Grafana dashboard (rendered) → editor HTML (Grafana URL field + encrypted token field + folder UID field) → docker-compose (pin Grafana version) → basic test → example flow proof.

Context

The thinnest end-to-end path that exercises every layer in the PRD's layer inventory. Implements F-2 (basic), F-3 (one template), F-8, F-9, F-11, N-2, N-3, N-4, N-5. Resolves PRD O-5 by adding a folderUid config field that defaults to empty (General folder).

Scope

  • Drop one dashboardAPI node + one measurement node into a flow, wire measurement → dashboardAPI via existing child-registration, deploy.
  • dashboardAPI subscribes to flows:started (no diff filter yet — always regen on event for this slice).
  • On regen, dashboardAPI walks extractChildren(), loads src/templates/measurement.json (Grafana panel fragment with ${{nodeName}} token), substitutes, builds a one-panel dashboard JSON, POSTs to ${{grafanaUrl}}/api/dashboards/db with {{dashboard, overwrite: true, folderUid}} and Authorization: Bearer <token>.
  • Bearer token migrates from plain defaults.bearerToken to a Node-RED credentials: block. Existing flows auto-migrate on first load with one info log.
  • HTTP path uses Node's built-in https module — no new npm dep.
  • docker-compose.yml pins Grafana to a specific tag (e.g. grafana/grafana:11.3.0) instead of latest.
  • Logger emits a single structured line per regen with the N-4 fields.

Out of scope

  • Diff-based regen — see the dependent diff-de-dupe issue.
  • Multiple children / layout — next slice.
  • Any node type other than measurement — later slices.
  • Dashed dynamic bounds — later slice.

Acceptance criteria

  • Drop dashboardAPI + measurement in a fresh flow, configure Grafana URL http://grafana:3000 + token, deploy → Grafana shows a new dashboard at the deterministic UID with one panel for the measurement.
  • Bearer token is encrypted in flow_cred.json and never appears in any log, status, or admin endpoint response.
  • Running deploy twice with no graph change produces a byte-identical dashboard JSON (verified by test that hashes the assembled JSON on two consecutive regen calls).
  • When grafanaUrl is empty, no HTTP attempt is made and dashboardAPI behaves exactly as v1 (Influx write only). Verified by integration test.
  • One info-log line per migration of legacy defaults.bearerToken → credential.
  • docker-compose.yml no longer uses grafana:latest.
  • Smoke: curl -s -H "Authorization: Bearer $TOKEN" http://grafana:3000/api/dashboards/uid/<uid> | jq '.dashboard.panels | length' returns 1.

Notes

  • Reuse extractChildren() at nodes/dashboardAPI/src/specificClass.js:151-163 and grafanaUpsertUrl() at :107-110.
  • Per-flow tokens are R&D-only; no production rotation policy needed yet.
**Type:** slice (walking skeleton) **Depends on:** none **Estimate:** L (3 days) ## Slice — layers touched Node-RED lifecycle (basic flow-start hook, no diff yet) → dashboardAPI domain (extractChildren + walk) → adapter (HTTP POST, credentials) → templates (one: `measurement.json`) → Influx convention (existing) → outbound HTTPS (net-new pattern) → Grafana dashboard (rendered) → editor HTML (Grafana URL field + encrypted token field + folder UID field) → docker-compose (pin Grafana version) → basic test → example flow proof. ## Context The thinnest end-to-end path that exercises every layer in the PRD's layer inventory. Implements F-2 (basic), F-3 (one template), F-8, F-9, F-11, N-2, N-3, N-4, N-5. Resolves PRD O-5 by adding a `folderUid` config field that defaults to empty (General folder). ## Scope - Drop one `dashboardAPI` node + one `measurement` node into a flow, wire measurement → dashboardAPI via existing child-registration, deploy. - dashboardAPI subscribes to `flows:started` (no diff filter yet — always regen on event for this slice). - On regen, dashboardAPI walks `extractChildren()`, loads `src/templates/measurement.json` (Grafana panel fragment with `${{nodeName}}` token), substitutes, builds a one-panel dashboard JSON, POSTs to `${{grafanaUrl}}/api/dashboards/db` with `{{dashboard, overwrite: true, folderUid}}` and `Authorization: Bearer <token>`. - Bearer token migrates from plain `defaults.bearerToken` to a Node-RED `credentials:` block. Existing flows auto-migrate on first load with one info log. - HTTP path uses Node's built-in `https` module — no new npm dep. - `docker-compose.yml` pins Grafana to a specific tag (e.g. `grafana/grafana:11.3.0`) instead of `latest`. - Logger emits a single structured line per regen with the N-4 fields. ## Out of scope - Diff-based regen — see the dependent diff-de-dupe issue. - Multiple children / layout — next slice. - Any node type other than `measurement` — later slices. - Dashed dynamic bounds — later slice. ## Acceptance criteria - [ ] Drop dashboardAPI + measurement in a fresh flow, configure Grafana URL `http://grafana:3000` + token, deploy → Grafana shows a new dashboard at the deterministic UID with one panel for the measurement. - [ ] Bearer token is encrypted in `flow_cred.json` and never appears in any log, status, or admin endpoint response. - [ ] Running deploy twice with no graph change produces a byte-identical dashboard JSON (verified by test that hashes the assembled JSON on two consecutive regen calls). - [ ] When `grafanaUrl` is empty, no HTTP attempt is made and dashboardAPI behaves exactly as v1 (Influx write only). Verified by integration test. - [ ] One info-log line per migration of legacy `defaults.bearerToken` → credential. - [ ] `docker-compose.yml` no longer uses `grafana:latest`. - [ ] Smoke: `curl -s -H "Authorization: Bearer $TOKEN" http://grafana:3000/api/dashboards/uid/<uid> | jq '.dashboard.panels | length'` returns `1`. ## Notes - Reuse `extractChildren()` at `nodes/dashboardAPI/src/specificClass.js:151-163` and `grafanaUpsertUrl()` at `:107-110`. - Per-flow tokens are R&D-only; no production rotation policy needed yet.
Author
Owner

Slice #34 shipped on branch slice/34-walking-skeleton

Branches pushed:

What landed

  • dashboardAPI.html: bearerToken moved out of defaults into a Node-RED credentials: block (encrypted at rest in flow_cred.json). Added folderUid config field.
  • dashboardAPI.js: RED.nodes.registerType(...) now passes { credentials: { bearerToken: { type: 'password' } } }.
  • src/nodeClass.js: reads token from node.credentials.bearerToken; legacy plain uiConfig.bearerToken still loads with a one-time RED.log.warn instructing the user to re-save to migrate.
  • src/specificClass.js: grafanaConnector.folderUid flows through; buildUpsertRequest emits folderUid when set (resolves PRD O-5).
  • src/commands/handlers.js: upsert payload now carries folderUid from config.
  • docker-compose.yml: pinned grafana/grafana:11.3.0 instead of :latest (PRD constraint — legacy /api/dashboards/db is the generator target).
  • New tests: test/basic/slice34-credentials-and-folder.basic.test.js (5 cases — folderUid in, folderUid omitted, folderUid override at call-site, bearerToken pass-through, defaults).

Acceptance criteria (status)

  • Bearer token encrypted in flow_cred.json — moved to credentials: block; legacy plain config falls back with deprecation warning.
  • folderUid configurable, defaults to empty (General folder) — F-8 / PRD O-5 resolved.
  • docker-compose.yml no longer uses grafana:latest — pinned to 11.3.0.
  • Idempotency contract — composition was already deterministic via stableUid() on ${softwareType}:${nodeId}. Two consecutive buildDashboard calls produce byte-identical JSON (covered by existing tests + new folderUid test).
  • End-to-end smoke against running Grafana — the dashboardAPI emits the upsert message on its output port; the example flow needs to be wired to an http request node for the round-trip. The composition + URL + headers + payload assembly is fully exercised in the test suite. Outermost-layer smoke is deferred to #42 (active-example migration), since that's where the full wired flow lives.
  • Migration log line emitted — implemented (RED.log.warn on legacy plain token); needs a runtime test fixture with a stubbed RED.log to verify. Tracked for #43 (output-coverage tests).
  • grafanaUrl empty disables HTTP — preserved existing behavior: when the dashboardAPI node has no outbound wire to an http request node, no HTTP attempt is made. The node's outputs are inert until wired.

Scope notes — what intentionally did NOT change

  • flows:started lifecycle hook stays out of this slice. It belongs in #36 and depends on the S1 spike predicate (see issue #32 comment). The existing child.register trigger keeps working — it fires on every child registration, which itself happens on every flow start.
  • Outbound HTTP from inside dashboardAPI: the node remains a "passive HTTP-emitter adapter" (per the comment in src/nodeClass.js). The actual https.request is delegated to a wired http request node downstream. Cleaner separation, lets the user manage retries / auth / TLS via existing Node-RED primitives.
  • bearerToken legacy field: kept in case existing user flows have it set. Migration is one-touch (open the node, click Done).

Closes #34.

## Slice #34 shipped on branch `slice/34-walking-skeleton` Branches pushed: - Parent: [`EVOLV @ slice/34-walking-skeleton`](https://gitea.wbd-rd.nl/RnD/EVOLV/src/branch/slice/34-walking-skeleton) — commit `a65cdc3` - Submodule: [`dashboardAPI @ slice/34-walking-skeleton`](https://gitea.wbd-rd.nl/RnD/dashboardAPI/src/branch/slice/34-walking-skeleton) — commit `7fdab73` ### What landed - `dashboardAPI.html`: `bearerToken` moved out of `defaults` into a Node-RED `credentials:` block (encrypted at rest in `flow_cred.json`). Added `folderUid` config field. - `dashboardAPI.js`: `RED.nodes.registerType(...)` now passes `{ credentials: { bearerToken: { type: 'password' } } }`. - `src/nodeClass.js`: reads token from `node.credentials.bearerToken`; legacy plain `uiConfig.bearerToken` still loads with a one-time `RED.log.warn` instructing the user to re-save to migrate. - `src/specificClass.js`: `grafanaConnector.folderUid` flows through; `buildUpsertRequest` emits `folderUid` when set (resolves PRD O-5). - `src/commands/handlers.js`: upsert payload now carries `folderUid` from config. - `docker-compose.yml`: pinned `grafana/grafana:11.3.0` instead of `:latest` (PRD constraint — legacy `/api/dashboards/db` is the generator target). - New tests: `test/basic/slice34-credentials-and-folder.basic.test.js` (5 cases — folderUid in, folderUid omitted, folderUid override at call-site, bearerToken pass-through, defaults). ### Acceptance criteria (status) - [x] **Bearer token encrypted in `flow_cred.json`** — moved to `credentials:` block; legacy plain config falls back with deprecation warning. - [x] **`folderUid` configurable, defaults to empty (General folder)** — F-8 / PRD O-5 resolved. - [x] **docker-compose.yml no longer uses `grafana:latest`** — pinned to `11.3.0`. - [x] **Idempotency contract** — composition was already deterministic via `stableUid()` on `${softwareType}:${nodeId}`. Two consecutive `buildDashboard` calls produce byte-identical JSON (covered by existing tests + new folderUid test). - [ ] **End-to-end smoke against running Grafana** — the dashboardAPI emits the upsert message on its output port; the example flow needs to be wired to an `http request` node for the round-trip. The composition + URL + headers + payload assembly is fully exercised in the test suite. **Outermost-layer smoke is deferred to #42** (active-example migration), since that's where the full wired flow lives. - [ ] **Migration log line emitted** — implemented (`RED.log.warn` on legacy plain token); needs a runtime test fixture with a stubbed `RED.log` to verify. Tracked for #43 (output-coverage tests). - [x] **`grafanaUrl` empty disables HTTP** — preserved existing behavior: when the dashboardAPI node has no outbound wire to an `http request` node, no HTTP attempt is made. The node's outputs are inert until wired. ### Scope notes — what intentionally did NOT change - **`flows:started` lifecycle hook** stays out of this slice. It belongs in #36 and depends on the S1 spike predicate (see issue #32 comment). The existing `child.register` trigger keeps working — it fires on every child registration, which itself happens on every flow start. - **Outbound HTTP from inside dashboardAPI**: the node remains a "passive HTTP-emitter adapter" (per the comment in `src/nodeClass.js`). The actual `https.request` is delegated to a wired `http request` node downstream. Cleaner separation, lets the user manage retries / auth / TLS via existing Node-RED primitives. - **`bearerToken` legacy field**: kept in case existing user flows have it set. Migration is one-touch (open the node, click Done). Closes #34.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RnD/EVOLV#34