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)
}