david 1ac9ef948b nodered: bake Dashboard 2.0 into Dockerfile for idempotent install
NR's automatic package.json install at container start was slow and
required internet. Move npm install to image build time, preserve the
installed node_modules via an anonymous volume so the bind mount of
./nodered:/data doesn't clobber them.
2026-07-01 01:50:39 -07:00

nr-flow-validator

Declarative Node-RED flows with conftest/Rego validation. Generates flows.json from a YAML spec, validates against a Rego policy pack, deploys via the Node-RED HTTP API.

Layout

infra/                           # host stack config (compose, z2m, mosquitto, nodered settings)
flows/<name>.yaml                # declarative spec
scripts/flow2json.py             # YAML → JSON generator
policy/flow.rego                 # Rego policy pack (conftest)
output/<name>.json               # generated flows.json

Quick start

just new my-flow           # copy template
$EDITOR flows/my-flow.yaml
just validate              # generate + validate
just deploy                # push to Node-RED at 192.168.81.147:1880

See infra/README.md for the full Docker stack and rebuild procedure.

Diagnostics

All diagnostic commands live in the justfile. Run just --list to see them.

  • just nr-show — live flow summary (node count, functions, button wiring, mqtt-out topics)
  • just nr-diff — diff deployed /flows against output/<name>.json (ignores random uuid ids)
  • just mqtt-button [action] — E2E test: publish a button action and watch which /set topics fire
  • just z2m-devices — paired zigbee devices + group membership
  • just mqtt-pub TOPIC PAYLOAD / just mqtt-sub TOPIC / just mqtt-watch — raw MQTT
  • just nr-logs / just z2m-logs / just mqtt-logs / just doorbell-logs — container log tails
  • just restart SERVICE / just restart-all — docker restart
  • just doorbell-publish "Title" "Body" — publish a test alert to ntfy
  • just rpi CMD — run any command on the RPi over SSH

scripts/mqtt.py and scripts/diag.py are the underlying helpers. Override the target host with just RPI_HOST=user@host mqtt-button single.

Generation invariants

These are enforced by policy/flow.rego and burned in from real bugs found in deployment. The generator follows them by construction; the policy is the second line of defense.

  1. Flat structure only. Node-RED 5.0 silently breaks MQTT broker connection when a tab has an internal nodes: [...] array. Always emit all nodes at top level with a z field referencing the tab id. Use policy/flow.rego::deny_wrapped_tab to reject wrapped structure.
  2. Every node needs wires: []. Missing or non-array wires throws TypeError in Flow.js::rewireNode. deny_missing_wires enforces this.
  3. MQTT brokers must have autoConnect: true. Without it the broker never reconnects after the first user disconnects. deny_no_autoconnect.
  4. protocolVersion must be "3", "4", or "5". deny_invalid_protocol.
  5. ui-button must have icon: "". Dashboard 2.0 throws TypeError on prependIcon otherwise. Generator sets this explicitly.
  6. Dashboard widgets need a z field referencing their tab id, otherwise the SPA renders the group but the widgets never appear. Generator sets this.
  7. Wire references must resolve to known node ids. deny_wire_ref.

Validation rules summary

Rule Catches
deny_missing_type node without type
deny_missing_field required field missing for type
deny_broker_ref mqtt in/out references unknown broker
deny_group_ref ui-* references unknown group
deny_page_ref ui-group references unknown page
deny_wire_ref wire targets unknown node
deny_missing_flow wire references a node not in z of a tab
deny_isolated_node node with no wires in/out (config nodes exempted)
deny_broker_port mqtt-broker port outside 1-65535
deny_wrapped_tab tab with internal nodes array (nodered 5.0 bug)
deny_missing_wires node without wires array
deny_no_autoconnect mqtt-broker without autoConnect: true
deny_invalid_protocol mqtt-broker protocolVersion not in 3/4/5
deny_unused_broker broker defined but never referenced

Deployment target

Node-RED on RPi 3B+ DietPi at 192.168.81.147:1880, persisted via Docker bind mount to /root/nodered. The deploy target's flows.json is the source of truth in production; this generator is for declarative editing.

Dashboard 2.0 SPA quirk

Dashboard 2.0's dist/index.html ships with relative asset paths that break at /dashboard/home. /data/start.sh on the host patches in <base href="/dashboard/"> on container start. Without that patch the SPA loads but assets 404 and nothing renders.

S
Description
Declarative Node-RED flow generator with conftest/Rego validation
Readme 188 KiB
Languages
JavaScript 29.7%
Python 26.6%
C++ 20.1%
Just 7.9%
Open Policy Agent 6.3%
Other 9.4%