Add control logic engine, panel logic dialogs, logic-edit restriction

Introduce server-side control-logic flow graphs (cron/alarm triggers,
Lua blocks) with CRUD endpoints, panel-logic lifecycle triggers and
user-interaction dialog nodes, and a synthetic node-graph editor.

Add an optional logic-editor allowlist (server.logic_editors) gating who
may add/edit panel logic and control logic, surfaced via /api/v1/me and
enforced in the API; hide logic affordances in the UI accordingly.

Update README, example config, and functional/technical specs to cover
all current features (plot panels, panel/control logic, local variables,
access control) and refresh the in-app manual and contextual help.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-06-19 07:27:35 +02:00
parent aba394b84d
commit afefba3184
35 changed files with 4633 additions and 467 deletions
+48 -9
View File
@@ -56,19 +56,29 @@ func ParseLevel(s string) Level {
// Policy holds the resolved global access configuration. It is immutable after
// construction and safe for concurrent use.
type Policy struct {
defaultUser string
blacklist map[string]Level // user → downgraded level
userGroups map[string][]string // user → groups they belong to
groupNames []string // all configured group names (sorted)
defaultUser string
blacklist map[string]Level // user → downgraded level
userGroups map[string][]string // user → groups they belong to
groupNames []string // all configured group names (sorted)
logicEditors map[string]bool // users + group names allowed to edit logic
}
// New builds a Policy. blacklist maps a username to a config level string;
// groups maps a group name to its member usernames.
func New(defaultUser string, blacklist map[string]string, groups map[string][]string) *Policy {
// groups maps a group name to its member usernames. logicEditors optionally
// restricts who may edit panel/control logic (usernames or group names); empty
// means no restriction.
func New(defaultUser string, blacklist map[string]string, groups map[string][]string, logicEditors []string) *Policy {
p := &Policy{
defaultUser: strings.TrimSpace(defaultUser),
blacklist: make(map[string]Level),
userGroups: make(map[string][]string),
defaultUser: strings.TrimSpace(defaultUser),
blacklist: make(map[string]Level),
userGroups: make(map[string][]string),
logicEditors: make(map[string]bool),
}
for _, e := range logicEditors {
e = strings.TrimSpace(e)
if e != "" {
p.logicEditors[e] = true
}
}
for user, lvl := range blacklist {
u := strings.TrimSpace(user)
@@ -126,6 +136,35 @@ func (p *Policy) Level(user string) Level {
return LevelWrite
}
// LogicRestricted reports whether a logic-editor allowlist is configured. When
// false, any write-capable user may edit panel/control logic.
func (p *Policy) LogicRestricted() bool {
return len(p.logicEditors) > 0
}
// CanEditLogic reports whether a user may add or edit panel logic and
// server-side control logic. When no allowlist is configured everyone with
// write access qualifies; otherwise the user (or one of their groups) must be
// listed. Anonymous/trusted-LAN callers (user=="") are always permitted.
func (p *Policy) CanEditLogic(user string) bool {
user = strings.TrimSpace(user)
if user == "" {
return true
}
if !p.LogicRestricted() {
return true
}
if p.logicEditors[user] {
return true
}
for _, g := range p.userGroups[user] {
if p.logicEditors[g] {
return true
}
}
return false
}
// GroupsOf returns a copy of the groups a user belongs to.
func (p *Policy) GroupsOf(user string) []string {
src := p.userGroups[strings.TrimSpace(user)]