From b2ec349c51503df16c7ff9b47204bec83ec9d337 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sun, 25 Jan 2026 19:05:13 -0800 Subject: [PATCH] Track unreachable nodes separately for diagram highlighting Co-Authored-By: Claude Opus 4.5 --- errors.go | 41 ++++++++++++++++++++++++++++++----------- http.go | 26 ++++++++++++++------------ static/index.html | 2 ++ 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/errors.go b/errors.go index d1ad421..93ad4a1 100644 --- a/errors.go +++ b/errors.go @@ -35,20 +35,22 @@ type portErrorBaseline struct { } type ErrorTracker struct { - mu sync.RWMutex - errors map[string]*PortError - baselines map[string]*portErrorBaseline + mu sync.RWMutex + errors map[string]*PortError + baselines map[string]*portErrorBaseline suppressedUnreachable map[string]bool - nextID int - t *Tendrils + unreachableNodes map[string]bool + nextID int + t *Tendrils } func NewErrorTracker(t *Tendrils) *ErrorTracker { return &ErrorTracker{ - errors: map[string]*PortError{}, - baselines: map[string]*portErrorBaseline{}, + errors: map[string]*PortError{}, + baselines: map[string]*portErrorBaseline{}, suppressedUnreachable: map[string]bool{}, - t: t, + unreachableNodes: map[string]bool{}, + t: t, } } @@ -193,6 +195,17 @@ func (e *ErrorTracker) GetErrors() []*PortError { return errors } +func (e *ErrorTracker) GetUnreachableNodes() []string { + e.mu.RLock() + defer e.mu.RUnlock() + + nodes := make([]string, 0, len(e.unreachableNodes)) + for nodeTypeID := range e.unreachableNodes { + nodes = append(nodes, nodeTypeID) + } + return nodes +} + func (e *ErrorTracker) SetUnreachable(node *Node, ip string) { changed := e.setUnreachableLocked(node, ip) if changed { @@ -206,12 +219,15 @@ func (e *ErrorTracker) setUnreachableLocked(node *Node, ip string) bool { key := "unreachable:" + node.TypeID + ":" + ip + wasUnreachable := e.unreachableNodes[node.TypeID] + e.unreachableNodes[node.TypeID] = true + if e.suppressedUnreachable[key] { - return false + return !wasUnreachable } if _, exists := e.errors[key]; exists { - return false + return !wasUnreachable } now := time.Now() @@ -243,9 +259,12 @@ func (e *ErrorTracker) clearUnreachableLocked(node *Node, ip string) bool { delete(e.suppressedUnreachable, key) + wasUnreachable := e.unreachableNodes[node.TypeID] + delete(e.unreachableNodes, node.TypeID) + if _, exists := e.errors[key]; exists { delete(e.errors, key) return true } - return false + return wasUnreachable } diff --git a/http.go b/http.go index 0046a3a..ca2944c 100644 --- a/http.go +++ b/http.go @@ -25,12 +25,13 @@ const ( ) type StatusResponse struct { - Nodes []*Node `json:"nodes"` - Links []*Link `json:"links"` - MulticastGroups []*MulticastGroupMembers `json:"multicast_groups"` - ArtNetNodes []*ArtNetNode `json:"artnet_nodes"` - DanteFlows []*DanteFlow `json:"dante_flows"` - PortErrors []*PortError `json:"port_errors"` + Nodes []*Node `json:"nodes"` + Links []*Link `json:"links"` + MulticastGroups []*MulticastGroupMembers `json:"multicast_groups"` + ArtNetNodes []*ArtNetNode `json:"artnet_nodes"` + DanteFlows []*DanteFlow `json:"dante_flows"` + PortErrors []*PortError `json:"port_errors"` + UnreachableNodes []string `json:"unreachable_nodes"` } func (t *Tendrils) startHTTPServer() { @@ -132,12 +133,13 @@ func (t *Tendrils) handleAPIConfig(w http.ResponseWriter, r *http.Request) { func (t *Tendrils) GetStatus() *StatusResponse { return &StatusResponse{ - Nodes: t.getNodes(), - Links: t.getLinks(), - MulticastGroups: t.getMulticastGroups(), - ArtNetNodes: t.getArtNetNodes(), - DanteFlows: t.getDanteFlows(), - PortErrors: t.errors.GetErrors(), + Nodes: t.getNodes(), + Links: t.getLinks(), + MulticastGroups: t.getMulticastGroups(), + ArtNetNodes: t.getArtNetNodes(), + DanteFlows: t.getDanteFlows(), + PortErrors: t.errors.GetErrors(), + UnreachableNodes: t.errors.GetUnreachableNodes(), } } diff --git a/static/index.html b/static/index.html index 9190431..959548b 100644 --- a/static/index.html +++ b/static/index.html @@ -822,7 +822,9 @@ const links = data.links || []; portErrors = data.port_errors || []; + const unreachableNodes = new Set(data.unreachable_nodes || []); const errorNodeIds = new Set(portErrors.map(e => e.node_typeid)); + unreachableNodes.forEach(id => errorNodeIds.add(id)); const locationTree = buildLocationTree(config.locations || [], null);