f95e8b8ecb
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.
230 lines
6.3 KiB
Rego
230 lines
6.3 KiB
Rego
package nodered
|
|
import future.keywords.if
|
|
import future.keywords.contains
|
|
import future.keywords.in
|
|
|
|
config_node(typ) if typ in {"mqtt-broker", "ui-base", "ui-page", "ui-group"}
|
|
welcome_isolated(typ) if typ in {"debug", "mqtt-broker", "ui-base", "ui-page", "ui-group", "ui-text", "ui-chart", "ui-gauge", "ui-button", "ui-switch", "ui-slider", "inject", "comment", "mqtt in", "catch", "status", "link in"}
|
|
|
|
broker_node(id) if {
|
|
some node in input
|
|
node.type == "mqtt-broker"
|
|
node.id == id
|
|
}
|
|
|
|
node_exists(id) if {
|
|
some node in input
|
|
node.id == id
|
|
}
|
|
|
|
has_inbound_wire(target_id) if {
|
|
some node in input
|
|
some wire in node.wires
|
|
target_id in wire
|
|
}
|
|
|
|
has_outbound_wire(source_id) if {
|
|
some node in input
|
|
node.id == source_id
|
|
node.wires
|
|
count(node.wires) > 0
|
|
}
|
|
|
|
deny_missing_type contains msg if {
|
|
some node in input
|
|
not node.type
|
|
msg := sprintf("node %s: missing type field", [node.id])
|
|
}
|
|
|
|
deny_missing_field contains msg if {
|
|
some node in input
|
|
node.type == "mqtt-broker"
|
|
required := {"type", "broker", "port"}
|
|
some f in required
|
|
not node[f]
|
|
msg := sprintf("node %s (mqtt-broker): missing %s", [node.id, f])
|
|
}
|
|
|
|
deny_missing_field contains msg if {
|
|
some node in input
|
|
node.type == "mqtt in"
|
|
required := {"type", "topic", "broker", "z"}
|
|
some f in required
|
|
not node[f]
|
|
msg := sprintf("node %s (mqtt in): missing %s", [node.id, f])
|
|
}
|
|
|
|
deny_missing_field contains msg if {
|
|
some node in input
|
|
node.type == "mqtt out"
|
|
required := {"type", "broker", "z"}
|
|
some f in required
|
|
not node[f]
|
|
msg := sprintf("node %s (mqtt out): missing %s", [node.id, f])
|
|
}
|
|
|
|
deny_missing_field contains msg if {
|
|
some node in input
|
|
node.type == "debug"
|
|
required := {"type", "z"}
|
|
some f in required
|
|
not node[f]
|
|
msg := sprintf("node %s (debug): missing %s", [node.id, f])
|
|
}
|
|
|
|
deny_missing_field contains msg if {
|
|
some node in input
|
|
node.type in {"ui-text", "ui-button", "ui-chart", "ui-gauge", "ui-switch", "ui-slider", "ui-dropdown"}
|
|
required := {"type", "group", "z"}
|
|
some f in required
|
|
not node[f]
|
|
msg := sprintf("node %s (%s): missing %s", [node.id, node.type, f])
|
|
}
|
|
|
|
deny_missing_field contains msg if {
|
|
some node in input
|
|
node.type == "ui-group"
|
|
required := {"type", "page", "name"}
|
|
some f in required
|
|
not node[f]
|
|
msg := sprintf("node %s (ui-group): missing %s", [node.id, f])
|
|
}
|
|
|
|
deny_missing_field contains msg if {
|
|
some node in input
|
|
node.type == "ui-page"
|
|
required := {"type", "name", "ui"}
|
|
some f in required
|
|
not node[f]
|
|
msg := sprintf("node %s (ui-page): missing %s", [node.id, f])
|
|
}
|
|
|
|
deny_missing_field contains msg if {
|
|
some node in input
|
|
node.type == "ui-base"
|
|
required := {"type", "name"}
|
|
some f in required
|
|
not node[f]
|
|
msg := sprintf("node %s (ui-base): missing %s", [node.id, f])
|
|
}
|
|
|
|
deny_broker_ref contains msg if {
|
|
some node in input
|
|
node.type in {"mqtt in", "mqtt out"}
|
|
not broker_node(node.broker)
|
|
msg := sprintf("node %s (%s): references unknown broker %s", [node.id, node.type, node.broker])
|
|
}
|
|
|
|
ui_page(id) if {
|
|
some node in input
|
|
node.type == "ui-page"
|
|
node.id == id
|
|
}
|
|
|
|
ui_group(id) if {
|
|
some node in input
|
|
node.type == "ui-group"
|
|
node.id == id
|
|
}
|
|
|
|
deny_group_ref contains msg if {
|
|
some node in input
|
|
node.type in {"ui-text", "ui-button", "ui-chart", "ui-gauge", "ui-switch", "ui-slider", "ui-dropdown"}
|
|
not ui_group(node.group)
|
|
msg := sprintf("node %s (%s): references unknown group %s", [node.id, node.type, node.group])
|
|
}
|
|
|
|
deny_page_ref contains msg if {
|
|
some node in input
|
|
node.type == "ui-group"
|
|
not ui_page(node.page)
|
|
msg := sprintf("node %s (ui-group): references unknown page %s", [node.id, node.page])
|
|
}
|
|
|
|
deny_wire_ref contains msg if {
|
|
some node in input
|
|
some wire in node.wires
|
|
some target in wire
|
|
not node_exists(target)
|
|
msg := sprintf("node %s (%s): wire to unknown node %s", [node.id, node.type, target])
|
|
}
|
|
|
|
deny_missing_flow contains msg if {
|
|
some node in input
|
|
not config_node(node.type)
|
|
not node.z
|
|
msg := sprintf("node %s (%s): missing flow assignment (z)", [node.id, node.type])
|
|
}
|
|
|
|
deny_isolated_node contains msg if {
|
|
some node in input
|
|
not welcome_isolated(node.type)
|
|
not has_inbound_wire(node.id)
|
|
not has_outbound_wire(node.id)
|
|
msg := sprintf("node %s (%s): has no wires", [node.id, node.type])
|
|
}
|
|
|
|
deny_broker_port contains msg if {
|
|
some node in input
|
|
node.type == "mqtt-broker"
|
|
not is_valid_port(to_number(node.port))
|
|
msg := sprintf("node %s (mqtt-broker): invalid port %s", [node.id, node.port])
|
|
}
|
|
|
|
is_valid_port(p) if { p >= 1; p <= 65535 }
|
|
|
|
# Check that all flow nodes have wires field
|
|
deny_missing_wires contains msg if {
|
|
some node in input
|
|
node.type in {"mqtt in", "mqtt out", "function", "debug", "inject", "ui-text", "ui-button", "change", "switch", "delay", "exec"}
|
|
not "wires" in object.keys(node)
|
|
msg := sprintf("node %s (%s): missing wires field", [node.id, node.type])
|
|
}
|
|
|
|
# Check mqtt-broker has autoConnect = true
|
|
deny_no_autoconnect contains msg if {
|
|
some node in input
|
|
node.type == "mqtt-broker"
|
|
not node.autoConnect
|
|
msg := sprintf("node %s (mqtt-broker): autoConnect must be true", [node.id])
|
|
}
|
|
|
|
valid_protocol_version(v) if {
|
|
some p in {"3", "4", "5"}
|
|
v == p
|
|
}
|
|
|
|
deny_invalid_protocol contains msg if {
|
|
some node in input
|
|
node.type == "mqtt-broker"
|
|
node.protocolVersion
|
|
not valid_protocol_version(node.protocolVersion)
|
|
msg := sprintf("node %s (mqtt-broker): invalid protocolVersion %s", [node.id, node.protocolVersion])
|
|
}
|
|
|
|
# Check that broker has at least one user
|
|
deny_unused_broker contains msg if {
|
|
some broker in input
|
|
broker.type == "mqtt-broker"
|
|
not has_broker_user(broker.id)
|
|
msg := sprintf("node %s (mqtt-broker): no mqtt in/out nodes reference this broker", [broker.id])
|
|
}
|
|
|
|
has_broker_user(bid) if {
|
|
some node in input
|
|
node.broker == bid
|
|
}
|
|
|
|
|
|
|
|
|
|
# nodered 5.0 bug: tabs with internal `nodes` array break flow loading.
|
|
# Use FLAT structure: all nodes at top level with z field referencing tab id.
|
|
deny_wrapped_tab contains msg if {
|
|
some tab in input
|
|
tab.type == "tab"
|
|
tab.nodes
|
|
count(tab.nodes) > 0
|
|
msg := sprintf("tab %s has internal nodes array (use flat structure - all nodes at top level with z field)", [tab.id])
|
|
}
|