Flat-structure generator + Rego guards, rename Laser & Fog to DJ Booth

Closes #1.

Generator (scripts/flow2json.py):
- Emit all nodes at top level with z field (no wrapped tabs)
- Set icon: "" on ui-button to avoid Dashboard 2.0 prependIcon TypeError
- Set z field on ui-* widgets so they render in Dashboard 2.0 SPA
- Use ui-text label from spec, not name

Policy (policy/flow.rego) — 9 new deny rules:
- deny_wrapped_tab: tab with internal nodes[] (nodered 5.0 bug)
- deny_missing_wires: missing or non-array wires (Flow.js rewireNode crash)
- deny_no_autoconnect: mqtt-broker must reconnect
- deny_invalid_protocol: protocolVersion must be 3/4/5
- deny_unused_broker: broker declared but never referenced
- Plus deny_broker_port / deny_group_ref / deny_page_ref / deny_wire_ref refinements

Flow (zigbee-monitor.*):
- 22-node flat flow: button-in → btn-filter → 4 parallel plug-outs
- 4 ui-text widgets + ui-button for permit-join (60s)
- Renamed plug: "Laser & Fog" → "DJ Booth" (matches z2m friendly_name)

Repo hygiene:
- justfile: fix host IP 192.168.9.147 → 192.168.81.147, /ui → /dashboard
- README.md: document 8 generation invariants and rule table

Verified end-to-end: publishing {"action":"single"} on
zigbee2mqtt/Door Button triggers {"state":"TOGGLE"} on all four
.../set topics including the renamed DJ Booth.

462/462 conftest tests pass.
This commit is contained in:
2026-06-30 23:16:17 -07:00
parent 517f3dbdae
commit f95e8b8ecb
6 changed files with 274 additions and 67 deletions
+14 -1
View File
@@ -40,7 +40,7 @@ def make_broker_node(name, cfg):
def make_node(nd, col, row, broker_map, group_map):
ntype = nd["type"]
nid = nd.get("id", new_id())
node = {"id": nid, "z": FLOW_ID, "type": ntype, "wires": [],
node = {"id": nid, "type": ntype, "wires": [],
"name": nd.get("label", nd.get("name", ""))}
node["x"] = COLUMNS[col] if col < len(COLUMNS) else col * 300 + 170
node["y"] = row * ROW_HEIGHT + 80
@@ -145,9 +145,22 @@ def build_flow(doc, flow_def, node_positions):
})
group_map[grp["name"]] = gid
nodes.append({
"id": FLOW_ID,
"type": "tab",
"label": flow_def.get("tab", "Flow"),
"disabled": False,
"info": flow_def.get("description", ""),
"env": [],
})
for i, nd in enumerate(flow_def["nodes"]):
col, row = node_positions.get(nd["id"], (0, i))
node = make_node(nd, col, row, broker_map, group_map)
node["z"] = FLOW_ID
if node.get("type") == "ui-button":
node["icon"] = ""
node["label"] = nd.get("label", nd.get("name", ""))
nodes.append(node)
wires = flow_def.get("wires", {})