Implemented admin pane + user permission
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/uopi/uopi/internal/controllogic"
|
||||
)
|
||||
|
||||
// DebugHub fans control-logic node-execution events out to the editors watching
|
||||
// them, and owns the per-client debug subscriptions. It implements
|
||||
// controllogic.DebugObserver; the engine (and simulate sandboxes) call Observe
|
||||
// on every node execution.
|
||||
//
|
||||
// Two subscription modes share one event shape:
|
||||
// - "live" — observe the running, enabled graph by its id.
|
||||
// - "simulate" — dry-run an unsaved graph in a throwaway sandbox. Each
|
||||
// simulate sub gets a unique route id so its events never mix with the live
|
||||
// graph's (which may share the same persisted id).
|
||||
//
|
||||
// Routing is by event GraphID: live events carry the real graph id (only
|
||||
// emitted while that id is in the engine's debugWatch set, which the hub keeps
|
||||
// in sync with its live subs), simulate events carry the sandbox's unique id.
|
||||
type DebugHub struct {
|
||||
engine *controllogic.Engine
|
||||
log *slog.Logger
|
||||
|
||||
seq uint64 // unique simulate route ids
|
||||
|
||||
mu sync.Mutex
|
||||
subs map[*wsClient]*debugSub // one debug sub per client
|
||||
routes map[string]map[*wsClient]struct{} // route id → watching clients
|
||||
liveCount map[string]int // live-subscribed graph id → count
|
||||
}
|
||||
|
||||
type debugSub struct {
|
||||
route string // graph id (live) or unique sandbox id (simulate)
|
||||
live bool // true for "live", false for "simulate"
|
||||
stop func() // simulate sandbox teardown (nil for live)
|
||||
}
|
||||
|
||||
// NewDebugHub builds an empty hub bound to the engine it observes.
|
||||
func NewDebugHub(engine *controllogic.Engine, log *slog.Logger) *DebugHub {
|
||||
return &DebugHub{
|
||||
engine: engine,
|
||||
log: log,
|
||||
subs: map[*wsClient]*debugSub{},
|
||||
routes: map[string]map[*wsClient]struct{}{},
|
||||
liveCount: map[string]int{},
|
||||
}
|
||||
}
|
||||
|
||||
// debugOut is the client-bound JSON form of a node-execution event.
|
||||
type debugOut struct {
|
||||
Type string `json:"type"` // always "debugNode"
|
||||
GraphID string `json:"graphId"`
|
||||
NodeID string `json:"nodeId"`
|
||||
Value float64 `json:"value"`
|
||||
HasValue bool `json:"hasValue"`
|
||||
TS int64 `json:"ts"`
|
||||
}
|
||||
|
||||
// Observe implements controllogic.DebugObserver: push the event to every client
|
||||
// watching its route (drop-on-full so a slow editor never stalls the engine).
|
||||
func (h *DebugHub) Observe(ev controllogic.DebugEvent) {
|
||||
h.mu.Lock()
|
||||
watchers := h.routes[ev.GraphID]
|
||||
if len(watchers) == 0 {
|
||||
h.mu.Unlock()
|
||||
return
|
||||
}
|
||||
targets := make([]*wsClient, 0, len(watchers))
|
||||
for c := range watchers {
|
||||
targets = append(targets, c)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
b, err := json.Marshal(debugOut{
|
||||
Type: "debugNode",
|
||||
GraphID: ev.GraphID,
|
||||
NodeID: ev.NodeID,
|
||||
Value: ev.Value,
|
||||
HasValue: ev.HasValue,
|
||||
TS: ev.TS,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, c := range targets {
|
||||
select {
|
||||
case c.outCh <- b:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// subscribeLive starts (or replaces) a client's live observation of graphID.
|
||||
func (h *DebugHub) subscribeLive(c *wsClient, graphID string) {
|
||||
h.unsubscribe(c)
|
||||
if graphID == "" {
|
||||
return
|
||||
}
|
||||
sub := &debugSub{route: graphID, live: true}
|
||||
h.mu.Lock()
|
||||
h.addRoute(c, sub)
|
||||
h.liveCount[graphID]++
|
||||
watch := h.snapshotWatch()
|
||||
h.mu.Unlock()
|
||||
h.engine.SetDebugWatch(watch)
|
||||
}
|
||||
|
||||
// subscribeSimulate starts (or replaces) a client's dry-run of an unsaved graph.
|
||||
func (h *DebugHub) subscribeSimulate(c *wsClient, g controllogic.Graph) {
|
||||
h.unsubscribe(c)
|
||||
id := "sim-" + strconv.FormatUint(atomic.AddUint64(&h.seq, 1), 10)
|
||||
g.ID = id // route sandbox events under a unique id (never collides with live)
|
||||
stop := h.engine.StartSimulate(g)
|
||||
sub := &debugSub{route: id, live: false, stop: stop}
|
||||
h.mu.Lock()
|
||||
h.addRoute(c, sub)
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
// fire forces nodeID's trigger to run in the graph the client is currently
|
||||
// debugging (live or simulate), routed by that session's id. No-op if the
|
||||
// client has no active debug session.
|
||||
func (h *DebugHub) fire(c *wsClient, nodeID string) {
|
||||
h.mu.Lock()
|
||||
sub := h.subs[c]
|
||||
h.mu.Unlock()
|
||||
if sub == nil {
|
||||
return
|
||||
}
|
||||
h.engine.FireTrigger(sub.route, nodeID)
|
||||
}
|
||||
|
||||
// addRoute records sub for c; caller holds h.mu.
|
||||
func (h *DebugHub) addRoute(c *wsClient, sub *debugSub) {
|
||||
h.subs[c] = sub
|
||||
if h.routes[sub.route] == nil {
|
||||
h.routes[sub.route] = map[*wsClient]struct{}{}
|
||||
}
|
||||
h.routes[sub.route][c] = struct{}{}
|
||||
}
|
||||
|
||||
// unsubscribe tears down a client's debug sub (stopping its sandbox if any) and
|
||||
// refreshes the engine's watch set. Safe when the client has no sub.
|
||||
func (h *DebugHub) unsubscribe(c *wsClient) {
|
||||
h.mu.Lock()
|
||||
sub := h.subs[c]
|
||||
if sub == nil {
|
||||
h.mu.Unlock()
|
||||
return
|
||||
}
|
||||
delete(h.subs, c)
|
||||
if m := h.routes[sub.route]; m != nil {
|
||||
delete(m, c)
|
||||
if len(m) == 0 {
|
||||
delete(h.routes, sub.route)
|
||||
}
|
||||
}
|
||||
if sub.live {
|
||||
if h.liveCount[sub.route]--; h.liveCount[sub.route] <= 0 {
|
||||
delete(h.liveCount, sub.route)
|
||||
}
|
||||
}
|
||||
watch := h.snapshotWatch()
|
||||
h.mu.Unlock()
|
||||
|
||||
if sub.stop != nil {
|
||||
sub.stop()
|
||||
}
|
||||
h.engine.SetDebugWatch(watch)
|
||||
}
|
||||
|
||||
// snapshotWatch builds a fresh immutable set of live-watched graph ids. Caller
|
||||
// holds h.mu; the result is published to the engine which reads it lock-free.
|
||||
func (h *DebugHub) snapshotWatch() map[string]bool {
|
||||
w := make(map[string]bool, len(h.liveCount))
|
||||
for k := range h.liveCount {
|
||||
w[k] = true
|
||||
}
|
||||
return w
|
||||
}
|
||||
Reference in New Issue
Block a user