Improving all side of app

This commit is contained in:
Martino Ferrari
2026-06-20 14:28:28 +02:00
parent 901b87d407
commit 446de7f1ee
33 changed files with 1758 additions and 389 deletions
+45
View File
@@ -8,6 +8,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/uopi/uopi/internal/audit"
@@ -34,11 +35,28 @@ type Engine struct {
cancel context.CancelFunc // cancels the current generation
wg *sync.WaitGroup // tracks the current generation's goroutines
// notifier delivers action.dialog requests to connected clients. Stored in
// an atomic so flow goroutines can read it without taking e.mu (which Reload
// holds while it waits for those same goroutines to drain).
notifier atomic.Value // notifierBox
// Shared live signal cache for the current generation (key "ds\0name").
liveMu sync.RWMutex
live map[string]float64
}
// notifierBox wraps a Notifier so atomic.Value always sees one concrete type.
type notifierBox struct{ n Notifier }
// dialogSeq generates unique action.dialog ids across all graphs.
var dialogSeq uint64
// SetNotifier installs the sink for action.dialog requests. Safe to call once
// at startup before or after Reload; it is read lock-free by running flows.
func (e *Engine) SetNotifier(n Notifier) {
e.notifier.Store(notifierBox{n: n})
}
// NewEngine creates an engine bound to root. Call Reload to start it. rec records
// the writes performed by flows; pass audit.Nop() to disable auditing.
func NewEngine(root context.Context, brk *broker.Broker, store *Store, rec audit.Recorder, log *slog.Logger) *Engine {
@@ -252,6 +270,29 @@ func (e *Engine) write(cg *compiledGraph, target string, val float64) {
e.audit.Record(ev)
}
// emitDialog delivers an action.dialog node's request to the installed Notifier
// (the WebSocket dialog hub). It is lock-free so it never deadlocks against a
// concurrent Reload that is waiting for this flow goroutine to finish.
func (e *Engine) emitDialog(n Node) {
box, _ := e.notifier.Load().(notifierBox)
if box.n == nil {
return
}
kind := strings.TrimSpace(n.param("kind"))
if kind != "error" && kind != "input" {
kind = "info"
}
box.n.Notify(Dialog{
ID: strconv.FormatUint(atomic.AddUint64(&dialogSeq, 1), 10),
Kind: kind,
Title: n.param("title"),
Message: n.param("message"),
Target: strings.TrimSpace(n.param("target")),
Users: splitCSV(n.param("users")),
Groups: splitCSV(n.param("groups")),
})
}
// ── compiled graph ─────────────────────────────────────────────────────────────
type wireOut struct {
@@ -658,6 +699,10 @@ func (cg *compiledGraph) run(nodeID string, ctx *runCtx) {
cg.runLua(node.ID, ctx)
cg.follow(node.ID, "out", ctx)
case "action.dialog":
cg.engine.emitDialog(node)
cg.follow(node.ID, "out", ctx)
default:
cg.follow(node.ID, "out", ctx)
}
+2
View File
@@ -29,6 +29,8 @@ package controllogic
// action.delay — waits `ms` before continuing.
// action.log — logs an expression value to the server log.
// action.lua — runs a sandboxed Lua script with get/set/log host funcs.
// action.dialog — pushes an info/error/input dialog to connected clients
// filtered by user/group; input responses write `target`.
// Node is a single node in a control-logic graph. Params are stored as strings
// (matching the panel logic model) and parsed per-kind by the engine.
+37
View File
@@ -0,0 +1,37 @@
package controllogic
import "strings"
// Dialog is a user-facing notification or input request emitted by an
// action.dialog node. It is delivered to connected clients whose identity
// matches Users/Groups (both empty = everyone). For an "input" dialog the
// client's response is written back to Target (a "ds:name" reference, e.g.
// "srv:approved") so control logic can read it on a later activation.
type Dialog struct {
ID string `json:"id"`
Kind string `json:"kind"` // "info" | "error" | "input"
Title string `json:"title"`
Message string `json:"message"`
Target string `json:"target,omitempty"`
Users []string `json:"users,omitempty"`
Groups []string `json:"groups,omitempty"`
}
// Notifier delivers control-logic dialogs to connected clients. The server
// implements it; the engine calls Notify when an action.dialog node runs.
// Notify must not block (the hub fans out without waiting on slow clients).
type Notifier interface {
Notify(Dialog)
}
// splitCSV parses a comma-separated user/group filter into trimmed,
// non-empty tokens. An empty string yields a nil slice (no filter).
func splitCSV(s string) []string {
var out []string
for _, p := range strings.Split(s, ",") {
if t := strings.TrimSpace(p); t != "" {
out = append(out, t)
}
}
return out
}