Fix interface merging, error ordering, sACN client, and add charset headers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ian Gulliver
2026-01-28 22:57:13 -08:00
parent aebd6f5e2c
commit ed9a0cd60d
4 changed files with 51 additions and 12 deletions

View File

@@ -2,6 +2,7 @@ package tendrils
import ( import (
"fmt" "fmt"
"sort"
"sync" "sync"
"time" "time"
) )
@@ -245,6 +246,12 @@ func (e *ErrorTracker) GetErrors() []*Error {
for _, err := range e.errors { for _, err := range e.errors {
errors = append(errors, err) errors = append(errors, err)
} }
sort.Slice(errors, func(i, j int) bool {
if errors[i].NodeName != errors[j].NodeName {
return errors[i].NodeName < errors[j].NodeName
}
return errors[i].Port < errors[j].Port
})
return errors return errors
} }

View File

@@ -115,7 +115,7 @@ func ensureCert() error {
} }
func (t *Tendrils) handleAPIStatus(w http.ResponseWriter, r *http.Request) { func (t *Tendrils) handleAPIStatus(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json; charset=utf-8")
data, err := t.GetStatusJSON() data, err := t.GetStatusJSON()
if err != nil { if err != nil {
log.Printf("[ERROR] failed to encode status: %v", err) log.Printf("[ERROR] failed to encode status: %v", err)
@@ -178,7 +178,7 @@ func (t *Tendrils) handleAPIStatusStream(w http.ResponseWriter, r *http.Request)
return return
} }
w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive") w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Origin", "*")

View File

@@ -171,13 +171,28 @@ func (n *Nodes) updateNodeIPs(node *Node, ips []net.IP) []string {
} }
} }
n.ipIndex[ipKey] = node n.ipIndex[ipKey] = node
iface, exists := node.Interfaces[ipKey]
if !exists { var targetIface *Interface
iface = &Interface{IPs: IPSet{}} for _, iface := range node.Interfaces {
node.Interfaces[ipKey] = iface if iface.MAC != "" {
targetIface = iface
break
}
}
if targetIface != nil {
if !targetIface.IPs.Has(ipKey) {
targetIface.IPs.Add(ip)
added = append(added, "ip="+ipKey)
}
} else {
iface, exists := node.Interfaces[ipKey]
if !exists {
iface = &Interface{IPs: IPSet{}}
node.Interfaces[ipKey] = iface
}
iface.IPs.Add(ip)
added = append(added, "ip="+ipKey)
} }
iface.IPs.Add(ip)
added = append(added, "ip="+ipKey)
go n.t.requestARP(ip) go n.t.requestARP(ip)
} }
return added return added
@@ -281,8 +296,13 @@ func (n *Nodes) updateNodeInterface(node *Node, mac net.HardwareAddr, ips []net.
iface.IPs.Add(ip) iface.IPs.Add(ip)
n.ipIndex[ipKey] = node n.ipIndex[ipKey] = node
if ipOnlyIface, exists := node.Interfaces[ipKey]; exists && ipOnlyIface != iface { for key, other := range node.Interfaces {
delete(node.Interfaces, ipKey) if other != iface && other.IPs.Has(ipKey) {
delete(other.IPs, ipKey)
if len(other.IPs) == 0 && other.MAC == "" {
delete(node.Interfaces, key)
}
}
} }
} }

View File

@@ -1992,9 +1992,21 @@
const sacnUniverseInputs = new Map(); const sacnUniverseInputs = new Map();
const sacnUniverseOutputs = new Map(); const sacnUniverseOutputs = new Map();
function getSacnInputsFromMulticast(node) {
const groups = node.multicast_groups || [];
const inputs = [];
groups.forEach(g => {
if (typeof g === 'string' && g.startsWith('sacn:')) {
const u = parseInt(g.substring(5), 10);
if (!isNaN(u)) inputs.push(u);
}
});
return inputs;
}
nodes.forEach(node => { nodes.forEach(node => {
const name = getShortLabel(node); const name = getShortLabel(node);
(node.sacn_inputs || []).forEach(u => { getSacnInputsFromMulticast(node).forEach(u => {
if (!sacnUniverseInputs.has(u)) sacnUniverseInputs.set(u, []); if (!sacnUniverseInputs.has(u)) sacnUniverseInputs.set(u, []);
sacnUniverseInputs.get(u).push(name); sacnUniverseInputs.get(u).push(name);
}); });
@@ -2012,7 +2024,7 @@
nodes.forEach(node => { nodes.forEach(node => {
const nodeId = node.id; const nodeId = node.id;
const sacnInputs = node.sacn_inputs || []; const sacnInputs = getSacnInputsFromMulticast(node);
const sacnOutputs = node.sacn_outputs || []; const sacnOutputs = node.sacn_outputs || [];
if (sacnInputs.length === 0 && sacnOutputs.length === 0) return; if (sacnInputs.length === 0 && sacnOutputs.length === 0) return;