Implemented admin pane + user permission

This commit is contained in:
Martino Ferrari
2026-06-22 17:49:14 +02:00
parent 73fcbe7b28
commit ac24011487
67 changed files with 5925 additions and 411 deletions
@@ -49,6 +49,17 @@ type NodeDef struct {
type Graph struct {
Nodes []GraphNode `json:"nodes"`
Output string `json:"output"` // id of the output node
// Groups is cosmetic editor metadata (node grouping/collapsing). It has no
// effect on evaluation; stored and round-tripped so the editor keeps shape.
Groups []NodeGroup `json:"groups,omitempty"`
}
// NodeGroup is a cosmetic, editor-side grouping of graph nodes (no eval effect).
type NodeGroup struct {
ID string `json:"id"`
Label string `json:"label,omitempty"`
Members []string `json:"members"`
Collapsed bool `json:"collapsed,omitempty"`
}
// GraphNode is one node in a Graph.
+16 -3
View File
@@ -52,7 +52,20 @@ func (rg *runtimeGraph) sourceRefs() []broker.SignalRef {
// - stateful ops (filters) and lua are scalar-only; an array input errors,
// since their per-evaluation state cannot be split across array lanes.
func (rg *runtimeGraph) evalSample(sourceVals map[string]dsp.Sample) (dsp.Sample, error) {
vals := make(map[string]dsp.Sample, len(rg.order))
vals, err := rg.evalSampleTrace(sourceVals)
if err != nil {
return dsp.Sample{}, err
}
return vals[rg.outputID], nil
}
// evalSampleTrace runs a full forward pass like evalSample but returns the value
// computed for *every* node (sources, ops and the output), keyed by node id. It
// is used by the editor's live/debug trace to show each node's current value.
// On an op error it returns the values computed so far together with the error,
// so partial results can still be displayed.
func (rg *runtimeGraph) evalSampleTrace(sourceVals map[string]dsp.Sample) (map[string]dsp.Sample, error) {
vals := make(map[string]dsp.Sample, len(rg.order)+len(sourceVals))
for id, v := range sourceVals {
vals[id] = v
}
@@ -65,7 +78,7 @@ func (rg *runtimeGraph) evalSample(sourceVals map[string]dsp.Sample) (dsp.Sample
}
r, err := evalOp(n, in)
if err != nil {
return dsp.Sample{}, fmt.Errorf("node %s (%s): %w", n.id, n.op.Type(), err)
return vals, fmt.Errorf("node %s (%s): %w", n.id, n.op.Type(), err)
}
vals[n.id] = r
case "output":
@@ -74,7 +87,7 @@ func (rg *runtimeGraph) evalSample(sourceVals map[string]dsp.Sample) (dsp.Sample
}
}
}
return vals[rg.outputID], nil
return vals, nil
}
// evalOp runs a single op node over its Sample inputs, choosing the right
@@ -462,6 +462,65 @@ func outTypeOf(st *signalState) dsp.ValType {
return st.rg.outType
}
// TraceNode is a single node's computed value in a debug trace.
type TraceNode struct {
Value any `json:"value"`
Type string `json:"type"` // "scalar" | "array"
Approx bool `json:"approx,omitempty"`
}
// TraceResult is the per-node outcome of a single-shot evaluation of an
// (unsaved) synthetic graph, used by the editor's live/debug overlay.
type TraceResult struct {
Nodes map[string]TraceNode `json:"nodes"`
Error string `json:"error,omitempty"`
}
// Trace compiles def and evaluates it once with fresh state, returning the value
// computed for every node. Source node values are obtained via read(ds, name)
// (typically a broker ReadNow snapshot); a source that cannot be read defaults to
// scalar 0. Stateful ops are flagged Approx since their true running value
// depends on accumulated state this single-shot pass does not have. A per-node
// evaluation error is returned in Error with the partial values still populated.
func (s *Synthetic) Trace(def SignalDef, read func(ds, name string) (any, error)) (TraceResult, error) {
rg, err := compileGraph(def)
if err != nil {
return TraceResult{}, err
}
sourceVals := make(map[string]dsp.Sample, len(rg.sources))
for _, src := range rg.sources {
raw, rerr := read(src.ref.DS, src.ref.Name)
if rerr != nil || raw == nil {
sourceVals[src.id] = dsp.Scalar(0)
continue
}
sourceVals[src.id] = toSample(raw)
}
vals, evalErr := rg.evalSampleTrace(sourceVals)
opType := make(map[string]string, len(rg.order))
for _, n := range rg.order {
if n.kind == "op" && n.op != nil {
opType[n.id] = n.op.Type()
}
}
res := TraceResult{Nodes: make(map[string]TraceNode, len(vals))}
for id, sample := range vals {
tn := TraceNode{Value: sample.AsAny(), Type: "scalar"}
if sample.IsArray {
tn.Type = "array"
}
if ot, ok := opType[id]; ok && dsp.IsStateful(ot) {
tn.Approx = true
}
res.Nodes[id] = tn
}
if evalErr != nil {
res.Error = evalErr.Error()
}
return res, nil
}
// toSample coerces a datasource.Value.Data into a dsp.Sample: arrays become
// array Samples (waveforms), everything else a scalar Sample.
func toSample(v any) dsp.Sample {