Implemented admin pane + user permission
This commit is contained in:
@@ -60,6 +60,20 @@ type Engine struct {
|
||||
// holds while it waits for those same goroutines to drain).
|
||||
notifier atomic.Value // notifierBox
|
||||
|
||||
// debugObs receives per-node execution events for the live debug view, and
|
||||
// debugWatch is the set of graph ids with at least one live subscriber. Both
|
||||
// are read lock-free by flow goroutines (same trick as notifier); emitDebug
|
||||
// short-circuits cheaply when the firing graph has no watcher.
|
||||
debugObs atomic.Value // debugObsBox
|
||||
debugWatch atomic.Value // map[string]bool (immutable snapshot)
|
||||
|
||||
// fireChs maps a debug route id (live graph id or simulate sandbox id) to its
|
||||
// compiled graph's manual-fire channel, so the debug UI can force a trigger to
|
||||
// run. Entries are added/removed as generations (and simulate sandboxes) come
|
||||
// and go; FireTrigger does a non-blocking send so a torn-down graph drops.
|
||||
fireMu sync.Mutex
|
||||
fireChs map[string]chan string
|
||||
|
||||
// Shared live signal cache for the current generation (key "ds\0name").
|
||||
liveMu sync.RWMutex
|
||||
live map[string]float64
|
||||
@@ -88,9 +102,10 @@ func NewEngine(root context.Context, brk *broker.Broker, store *Store, cfg *conf
|
||||
store: store,
|
||||
cfg: cfg,
|
||||
audit: rec,
|
||||
log: log,
|
||||
root: root,
|
||||
live: map[string]float64{},
|
||||
log: log,
|
||||
root: root,
|
||||
live: map[string]float64{},
|
||||
fireChs: map[string]chan string{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +202,7 @@ func (e *Engine) Reload() {
|
||||
cg.engine = e
|
||||
cg.genCtx = genCtx
|
||||
cg.wg = wg
|
||||
e.registerFire(cg.id, cg.fireCh)
|
||||
}
|
||||
|
||||
// One shared updates channel feeds a single dispatch goroutine; every
|
||||
@@ -209,6 +225,9 @@ func (e *Engine) Reload() {
|
||||
for _, u := range unsubs {
|
||||
u()
|
||||
}
|
||||
for _, cg := range compiled {
|
||||
e.unregisterFire(cg.id, cg.fireCh)
|
||||
}
|
||||
}()
|
||||
|
||||
// Dispatch goroutine: keep the live cache fresh and drive level/edge triggers.
|
||||
@@ -268,6 +287,9 @@ func (e *Engine) write(cg *compiledGraph, target string, val float64) {
|
||||
cg.setLocal(name, val)
|
||||
return
|
||||
}
|
||||
if cg.dryRun {
|
||||
return // simulate: no real data-source write
|
||||
}
|
||||
src, ok := e.broker.Source(ds)
|
||||
if !ok {
|
||||
e.log.Warn("control logic: write to unknown data source", "ds", ds, "signal", name)
|
||||
@@ -558,6 +580,14 @@ type compiledGraph struct {
|
||||
genCtx context.Context
|
||||
wg *sync.WaitGroup
|
||||
|
||||
// dryRun suppresses real side effects (data-source writes, config mutations,
|
||||
// dialogs) — used by the debug "simulate" sandbox. Local-variable writes stay
|
||||
// live so the flow still computes correctly. alwaysDebug forces emitDebug to
|
||||
// fire regardless of debugWatch (the sandbox is its own subscriber).
|
||||
dryRun bool
|
||||
alwaysDebug bool
|
||||
|
||||
id string
|
||||
name string
|
||||
byId map[string]Node
|
||||
out map[string][]wireOut
|
||||
@@ -567,6 +597,11 @@ type compiledGraph struct {
|
||||
watchers map[string][]string // signal key → trigger node ids
|
||||
luaNodes map[string]*luaRuntime
|
||||
|
||||
// fireCh receives node ids of triggers the debug UI wants to fire manually.
|
||||
// Drained by a generation goroutine (started in startTriggers) so the wg.Add
|
||||
// in activate always happens on a tracked goroutine.
|
||||
fireCh chan string
|
||||
|
||||
stateMu sync.Mutex
|
||||
levelState map[string]bool // current truth of level triggers (threshold/alarm)
|
||||
prevBool map[string]bool // edge detection for threshold/alarm
|
||||
@@ -578,6 +613,7 @@ type compiledGraph struct {
|
||||
|
||||
func compile(g Graph) *compiledGraph {
|
||||
cg := &compiledGraph{
|
||||
id: g.ID,
|
||||
name: g.Name,
|
||||
byId: map[string]Node{},
|
||||
out: map[string][]wireOut{},
|
||||
@@ -591,6 +627,7 @@ func compile(g Graph) *compiledGraph {
|
||||
hasVal: map[string]bool{},
|
||||
lastFire: map[string]int64{},
|
||||
locals: map[string]float64{},
|
||||
fireCh: make(chan string, 16),
|
||||
}
|
||||
for _, n := range g.Nodes {
|
||||
cg.byId[n.ID] = n
|
||||
@@ -663,6 +700,24 @@ func (cg *compiledGraph) getLocal(name string) float64 {
|
||||
|
||||
// startTriggers launches timer and cron trigger goroutines for the generation.
|
||||
func (cg *compiledGraph) startTriggers() {
|
||||
// Manual-fire listener: drain fireCh on a tracked goroutine so activate's
|
||||
// wg.Add never races the generation teardown's wg.Wait. Only fires nodes that
|
||||
// are actual triggers in this graph.
|
||||
cg.wg.Add(1)
|
||||
go func() {
|
||||
defer cg.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case id := <-cg.fireCh:
|
||||
if n, ok := cg.byId[id]; ok && strings.HasPrefix(n.Kind, "trigger.") {
|
||||
cg.activate(id)
|
||||
}
|
||||
case <-cg.genCtx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
hasCron := false
|
||||
for _, n := range cg.byId {
|
||||
switch n.Kind {
|
||||
@@ -856,6 +911,8 @@ func (cg *compiledGraph) activate(triggerID string) {
|
||||
}
|
||||
}
|
||||
|
||||
cg.emitDebug(triggerID, 0, false)
|
||||
|
||||
cg.wg.Add(1)
|
||||
go func() {
|
||||
defer cg.wg.Done()
|
||||
@@ -888,6 +945,8 @@ func (cg *compiledGraph) run(nodeID string, ctx *runCtx) {
|
||||
default:
|
||||
}
|
||||
|
||||
cg.emitDebug(node.ID, 0, false)
|
||||
|
||||
switch node.Kind {
|
||||
case "gate.and":
|
||||
if cg.gateSatisfied(node.ID, ctx.fired) {
|
||||
@@ -896,9 +955,15 @@ func (cg *compiledGraph) run(nodeID string, ctx *runCtx) {
|
||||
|
||||
case "flow.if":
|
||||
branch := "else"
|
||||
if EvalBool(node.param("cond"), ctx.resolve) {
|
||||
pass := EvalBool(node.param("cond"), ctx.resolve)
|
||||
if pass {
|
||||
branch = "then"
|
||||
}
|
||||
v := 0.0
|
||||
if pass {
|
||||
v = 1
|
||||
}
|
||||
cg.emitDebug(node.ID, v, true)
|
||||
cg.follow(node.ID, branch, ctx)
|
||||
|
||||
case "flow.loop":
|
||||
@@ -922,11 +987,14 @@ func (cg *compiledGraph) run(nodeID string, ctx *runCtx) {
|
||||
|
||||
case "action.write":
|
||||
val := EvalExpr(node.param("expr"), ctx.resolve)
|
||||
cg.emitDebug(node.ID, val, true)
|
||||
cg.engine.write(cg, node.param("target"), val)
|
||||
cg.follow(node.ID, "out", ctx)
|
||||
|
||||
case "action.config.apply":
|
||||
cg.engine.applyConfig(cg, strings.TrimSpace(node.param("instance")))
|
||||
if !cg.dryRun {
|
||||
cg.engine.applyConfig(cg, strings.TrimSpace(node.param("instance")))
|
||||
}
|
||||
cg.follow(node.ID, "out", ctx)
|
||||
|
||||
case "action.config.read":
|
||||
@@ -938,15 +1006,22 @@ func (cg *compiledGraph) run(nodeID string, ctx *runCtx) {
|
||||
|
||||
case "action.config.write":
|
||||
val := EvalExpr(node.param("expr"), ctx.resolve)
|
||||
cg.engine.writeConfigParam(cg, strings.TrimSpace(node.param("instance")), strings.TrimSpace(node.param("key")), val)
|
||||
cg.emitDebug(node.ID, val, true)
|
||||
if !cg.dryRun {
|
||||
cg.engine.writeConfigParam(cg, strings.TrimSpace(node.param("instance")), strings.TrimSpace(node.param("key")), val)
|
||||
}
|
||||
cg.follow(node.ID, "out", ctx)
|
||||
|
||||
case "action.config.create":
|
||||
cg.engine.createConfigInstance(cg, strings.TrimSpace(node.param("set")), strings.TrimSpace(node.param("name")), strings.TrimSpace(node.param("from")))
|
||||
if !cg.dryRun {
|
||||
cg.engine.createConfigInstance(cg, strings.TrimSpace(node.param("set")), strings.TrimSpace(node.param("name")), strings.TrimSpace(node.param("from")))
|
||||
}
|
||||
cg.follow(node.ID, "out", ctx)
|
||||
|
||||
case "action.config.snapshot":
|
||||
cg.engine.snapshotConfig(cg, strings.TrimSpace(node.param("set")), strings.TrimSpace(node.param("name")))
|
||||
if !cg.dryRun {
|
||||
cg.engine.snapshotConfig(cg, strings.TrimSpace(node.param("set")), strings.TrimSpace(node.param("name")))
|
||||
}
|
||||
cg.follow(node.ID, "out", ctx)
|
||||
|
||||
case "action.delay":
|
||||
@@ -967,6 +1042,7 @@ func (cg *compiledGraph) run(nodeID string, ctx *runCtx) {
|
||||
|
||||
case "action.log":
|
||||
val := EvalExpr(node.param("expr"), ctx.resolve)
|
||||
cg.emitDebug(node.ID, val, true)
|
||||
label := strings.TrimSpace(node.param("label"))
|
||||
cg.engine.log.Info("control logic log", "graph", cg.name, "label", label, "value", val)
|
||||
cg.follow(node.ID, "out", ctx)
|
||||
@@ -976,7 +1052,9 @@ func (cg *compiledGraph) run(nodeID string, ctx *runCtx) {
|
||||
cg.follow(node.ID, "out", ctx)
|
||||
|
||||
case "action.dialog":
|
||||
cg.engine.emitDialog(node)
|
||||
if !cg.dryRun {
|
||||
cg.engine.emitDialog(node)
|
||||
}
|
||||
cg.follow(node.ID, "out", ctx)
|
||||
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user