Improving all side of app
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user