Add port error tracking with UI display
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
152
errors.go
Normal file
152
errors.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package tendrils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PortErrorType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrorTypeStartup PortErrorType = "startup"
|
||||||
|
ErrorTypeNew PortErrorType = "new"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PortError struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
NodeTypeID string `json:"node_typeid"`
|
||||||
|
NodeName string `json:"node_name"`
|
||||||
|
PortName string `json:"port_name"`
|
||||||
|
ErrorType PortErrorType `json:"error_type"`
|
||||||
|
InErrors uint64 `json:"in_errors"`
|
||||||
|
OutErrors uint64 `json:"out_errors"`
|
||||||
|
InDelta uint64 `json:"in_delta,omitempty"`
|
||||||
|
OutDelta uint64 `json:"out_delta,omitempty"`
|
||||||
|
FirstSeen time.Time `json:"first_seen"`
|
||||||
|
LastUpdated time.Time `json:"last_updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type portErrorBaseline struct {
|
||||||
|
InErrors uint64
|
||||||
|
OutErrors uint64
|
||||||
|
HasData bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorTracker struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
errors map[string]*PortError
|
||||||
|
baselines map[string]*portErrorBaseline
|
||||||
|
nextID int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewErrorTracker() *ErrorTracker {
|
||||||
|
return &ErrorTracker{
|
||||||
|
errors: map[string]*PortError{},
|
||||||
|
baselines: map[string]*portErrorBaseline{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorTracker) CheckPort(node *Node, portName string, stats *InterfaceStats) {
|
||||||
|
if stats == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
key := node.TypeID + ":" + portName
|
||||||
|
baseline := e.baselines[key]
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
if baseline == nil || !baseline.HasData {
|
||||||
|
e.baselines[key] = &portErrorBaseline{
|
||||||
|
InErrors: stats.InErrors,
|
||||||
|
OutErrors: stats.OutErrors,
|
||||||
|
HasData: true,
|
||||||
|
}
|
||||||
|
if stats.InErrors > 0 || stats.OutErrors > 0 {
|
||||||
|
e.nextID++
|
||||||
|
e.errors[key] = &PortError{
|
||||||
|
ID: fmt.Sprintf("err-%d", e.nextID),
|
||||||
|
NodeTypeID: node.TypeID,
|
||||||
|
NodeName: node.DisplayName(),
|
||||||
|
PortName: portName,
|
||||||
|
ErrorType: ErrorTypeStartup,
|
||||||
|
InErrors: stats.InErrors,
|
||||||
|
OutErrors: stats.OutErrors,
|
||||||
|
FirstSeen: now,
|
||||||
|
LastUpdated: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inDelta := uint64(0)
|
||||||
|
outDelta := uint64(0)
|
||||||
|
if stats.InErrors > baseline.InErrors {
|
||||||
|
inDelta = stats.InErrors - baseline.InErrors
|
||||||
|
}
|
||||||
|
if stats.OutErrors > baseline.OutErrors {
|
||||||
|
outDelta = stats.OutErrors - baseline.OutErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
if inDelta > 0 || outDelta > 0 {
|
||||||
|
if existing, ok := e.errors[key]; ok {
|
||||||
|
existing.InErrors = stats.InErrors
|
||||||
|
existing.OutErrors = stats.OutErrors
|
||||||
|
existing.InDelta += inDelta
|
||||||
|
existing.OutDelta += outDelta
|
||||||
|
existing.LastUpdated = now
|
||||||
|
} else {
|
||||||
|
e.nextID++
|
||||||
|
e.errors[key] = &PortError{
|
||||||
|
ID: fmt.Sprintf("err-%d", e.nextID),
|
||||||
|
NodeTypeID: node.TypeID,
|
||||||
|
NodeName: node.DisplayName(),
|
||||||
|
PortName: portName,
|
||||||
|
ErrorType: ErrorTypeNew,
|
||||||
|
InErrors: stats.InErrors,
|
||||||
|
OutErrors: stats.OutErrors,
|
||||||
|
InDelta: inDelta,
|
||||||
|
OutDelta: outDelta,
|
||||||
|
FirstSeen: now,
|
||||||
|
LastUpdated: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e.baselines[key].InErrors = stats.InErrors
|
||||||
|
e.baselines[key].OutErrors = stats.OutErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorTracker) ClearError(errorID string) {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
for key, err := range e.errors {
|
||||||
|
if err.ID == errorID {
|
||||||
|
delete(e.errors, key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorTracker) ClearAllErrors() {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
|
|
||||||
|
e.errors = map[string]*PortError{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorTracker) GetErrors() []*PortError {
|
||||||
|
e.mu.RLock()
|
||||||
|
defer e.mu.RUnlock()
|
||||||
|
|
||||||
|
errors := make([]*PortError, 0, len(e.errors))
|
||||||
|
for _, err := range e.errors {
|
||||||
|
errors = append(errors, err)
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
21
http.go
21
http.go
@@ -29,6 +29,7 @@ type StatusResponse struct {
|
|||||||
MulticastGroups []*MulticastGroupMembers `json:"multicast_groups"`
|
MulticastGroups []*MulticastGroupMembers `json:"multicast_groups"`
|
||||||
ArtNetNodes []*ArtNetNode `json:"artnet_nodes"`
|
ArtNetNodes []*ArtNetNode `json:"artnet_nodes"`
|
||||||
DanteFlows []*DanteFlow `json:"dante_flows"`
|
DanteFlows []*DanteFlow `json:"dante_flows"`
|
||||||
|
PortErrors []*PortError `json:"port_errors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tendrils) startHTTPServer() {
|
func (t *Tendrils) startHTTPServer() {
|
||||||
@@ -40,6 +41,7 @@ func (t *Tendrils) startHTTPServer() {
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/api/status", t.handleAPIStatus)
|
mux.HandleFunc("/api/status", t.handleAPIStatus)
|
||||||
mux.HandleFunc("/api/config", t.handleAPIConfig)
|
mux.HandleFunc("/api/config", t.handleAPIConfig)
|
||||||
|
mux.HandleFunc("/api/errors/clear", t.handleClearError)
|
||||||
mux.Handle("/", http.FileServer(http.Dir("static")))
|
mux.Handle("/", http.FileServer(http.Dir("static")))
|
||||||
|
|
||||||
log.Printf("[https] listening on :443")
|
log.Printf("[https] listening on :443")
|
||||||
@@ -133,9 +135,28 @@ func (t *Tendrils) GetStatus() *StatusResponse {
|
|||||||
MulticastGroups: t.getMulticastGroups(),
|
MulticastGroups: t.getMulticastGroups(),
|
||||||
ArtNetNodes: t.getArtNetNodes(),
|
ArtNetNodes: t.getArtNetNodes(),
|
||||||
DanteFlows: t.getDanteFlows(),
|
DanteFlows: t.getDanteFlows(),
|
||||||
|
PortErrors: t.errors.GetErrors(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) handleClearError(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.URL.Query().Get("all") == "true" {
|
||||||
|
t.errors.ClearAllErrors()
|
||||||
|
} else if id := r.URL.Query().Get("id"); id != "" {
|
||||||
|
t.errors.ClearError(id)
|
||||||
|
} else {
|
||||||
|
http.Error(w, "missing id or all parameter", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Tendrils) getNodes() []*Node {
|
func (t *Tendrils) getNodes() []*Node {
|
||||||
t.nodes.mu.RLock()
|
t.nodes.mu.RLock()
|
||||||
defer t.nodes.mu.RUnlock()
|
defer t.nodes.mu.RUnlock()
|
||||||
|
|||||||
1
snmp.go
1
snmp.go
@@ -221,6 +221,7 @@ func (t *Tendrils) queryInterfaceStats(snmp *gosnmp.GoSNMP, node *Node, ifNames
|
|||||||
}
|
}
|
||||||
|
|
||||||
iface.Stats = stats
|
iface.Stats = stats
|
||||||
|
t.errors.CheckPort(node, name, stats)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -271,6 +271,126 @@
|
|||||||
top: auto;
|
top: auto;
|
||||||
bottom: -8px;
|
bottom: -8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node.has-error {
|
||||||
|
box-shadow: 0 0 0 3px #f66;
|
||||||
|
animation: error-pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes error-pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 3px #f66; }
|
||||||
|
50% { box-shadow: 0 0 0 3px #f00; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#error-panel {
|
||||||
|
position: fixed;
|
||||||
|
top: 50px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 1000;
|
||||||
|
background: #2a1a1a;
|
||||||
|
border: 1px solid #f66;
|
||||||
|
border-radius: 6px;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#error-panel.has-errors {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
#error-panel.collapsed #error-list {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#error-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #3a2a2a;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#error-count {
|
||||||
|
flex: 1;
|
||||||
|
color: #f99;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#error-header button {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: none;
|
||||||
|
background: #444;
|
||||||
|
color: #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#error-header button:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
#error-list {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item {
|
||||||
|
background: #3a2a2a;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item .error-node {
|
||||||
|
color: #faa;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item .error-node:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item .error-port {
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item .error-counts {
|
||||||
|
color: #f66;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item .error-type {
|
||||||
|
font-size: 9px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item button {
|
||||||
|
align-self: flex-end;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border: none;
|
||||||
|
background: #555;
|
||||||
|
color: #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-item button:hover {
|
||||||
|
background: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node.scroll-highlight {
|
||||||
|
outline: 3px solid white;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -278,6 +398,14 @@
|
|||||||
<button id="mode-network" class="active">Network</button>
|
<button id="mode-network" class="active">Network</button>
|
||||||
<button id="mode-dante">Dante</button>
|
<button id="mode-dante">Dante</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="error-panel">
|
||||||
|
<div id="error-header">
|
||||||
|
<span id="error-count">0 Errors</span>
|
||||||
|
<button id="clear-all-errors">Clear All</button>
|
||||||
|
<button id="toggle-errors">Hide</button>
|
||||||
|
</div>
|
||||||
|
<div id="error-list"></div>
|
||||||
|
</div>
|
||||||
<div id="error"></div>
|
<div id="error"></div>
|
||||||
<div id="container"></div>
|
<div id="container"></div>
|
||||||
|
|
||||||
@@ -405,9 +533,11 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createNodeElement(node, switchConnection, nodeLocation, uplinkInfo, danteInfo) {
|
function createNodeElement(node, switchConnection, nodeLocation, uplinkInfo, danteInfo, hasError) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'node' + (isSwitch(node) ? ' switch' : '');
|
div.className = 'node' + (isSwitch(node) ? ' switch' : '');
|
||||||
|
div.dataset.typeid = node.typeid;
|
||||||
|
if (hasError) div.classList.add('has-error');
|
||||||
|
|
||||||
if (danteInfo) {
|
if (danteInfo) {
|
||||||
if (danteInfo.isTx) div.classList.add('dante-tx');
|
if (danteInfo.isTx) div.classList.add('dante-tx');
|
||||||
@@ -466,12 +596,12 @@
|
|||||||
return div;
|
return div;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLocation(loc, assignedNodes, isTopLevel, switchConnections, switchUplinks, danteNodes) {
|
function renderLocation(loc, assignedNodes, isTopLevel, switchConnections, switchUplinks, danteNodes, errorNodeIds) {
|
||||||
const nodes = assignedNodes.get(loc) || [];
|
const nodes = assignedNodes.get(loc) || [];
|
||||||
const hasNodes = nodes.length > 0;
|
const hasNodes = nodes.length > 0;
|
||||||
|
|
||||||
const childElements = loc.children
|
const childElements = loc.children
|
||||||
.map(child => renderLocation(child, assignedNodes, false, switchConnections, switchUplinks, danteNodes))
|
.map(child => renderLocation(child, assignedNodes, false, switchConnections, switchUplinks, danteNodes, errorNodeIds))
|
||||||
.filter(el => el !== null);
|
.filter(el => el !== null);
|
||||||
|
|
||||||
if (!hasNodes && childElements.length === 0) {
|
if (!hasNodes && childElements.length === 0) {
|
||||||
@@ -499,7 +629,8 @@
|
|||||||
switches.forEach(node => {
|
switches.forEach(node => {
|
||||||
const uplink = switchUplinks.get(node.typeid);
|
const uplink = switchUplinks.get(node.typeid);
|
||||||
const danteInfo = danteNodes.get(node.typeid);
|
const danteInfo = danteNodes.get(node.typeid);
|
||||||
switchRow.appendChild(createNodeElement(node, null, loc, uplink, danteInfo));
|
const hasError = errorNodeIds.has(node.typeid);
|
||||||
|
switchRow.appendChild(createNodeElement(node, null, loc, uplink, danteInfo, hasError));
|
||||||
});
|
});
|
||||||
container.appendChild(switchRow);
|
container.appendChild(switchRow);
|
||||||
}
|
}
|
||||||
@@ -510,7 +641,8 @@
|
|||||||
nonSwitches.forEach(node => {
|
nonSwitches.forEach(node => {
|
||||||
const conn = switchConnections.get(node.typeid);
|
const conn = switchConnections.get(node.typeid);
|
||||||
const danteInfo = danteNodes.get(node.typeid);
|
const danteInfo = danteNodes.get(node.typeid);
|
||||||
nodeRow.appendChild(createNodeElement(node, conn, loc, null, danteInfo));
|
const hasError = errorNodeIds.has(node.typeid);
|
||||||
|
nodeRow.appendChild(createNodeElement(node, conn, loc, null, danteInfo, hasError));
|
||||||
});
|
});
|
||||||
container.appendChild(nodeRow);
|
container.appendChild(nodeRow);
|
||||||
}
|
}
|
||||||
@@ -526,6 +658,76 @@
|
|||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let portErrors = [];
|
||||||
|
let errorPanelCollapsed = false;
|
||||||
|
|
||||||
|
function updateErrorPanel() {
|
||||||
|
const panel = document.getElementById('error-panel');
|
||||||
|
const countEl = document.getElementById('error-count');
|
||||||
|
const listEl = document.getElementById('error-list');
|
||||||
|
|
||||||
|
if (portErrors.length === 0) {
|
||||||
|
panel.classList.remove('has-errors');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panel.classList.add('has-errors');
|
||||||
|
countEl.textContent = portErrors.length + ' Error' + (portErrors.length !== 1 ? 's' : '');
|
||||||
|
|
||||||
|
listEl.innerHTML = '';
|
||||||
|
portErrors.forEach(err => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'error-item';
|
||||||
|
|
||||||
|
const nodeEl = document.createElement('div');
|
||||||
|
nodeEl.className = 'error-node';
|
||||||
|
nodeEl.textContent = err.node_name || err.node_typeid;
|
||||||
|
nodeEl.addEventListener('click', () => scrollToNode(err.node_typeid));
|
||||||
|
item.appendChild(nodeEl);
|
||||||
|
|
||||||
|
const portEl = document.createElement('div');
|
||||||
|
portEl.className = 'error-port';
|
||||||
|
portEl.textContent = 'Port: ' + err.port_name;
|
||||||
|
item.appendChild(portEl);
|
||||||
|
|
||||||
|
const countsEl = document.createElement('div');
|
||||||
|
countsEl.className = 'error-counts';
|
||||||
|
countsEl.textContent = 'In: ' + err.in_errors + ' (+' + (err.in_delta || 0) + ') / Out: ' + err.out_errors + ' (+' + (err.out_delta || 0) + ')';
|
||||||
|
item.appendChild(countsEl);
|
||||||
|
|
||||||
|
const typeEl = document.createElement('div');
|
||||||
|
typeEl.className = 'error-type';
|
||||||
|
typeEl.textContent = err.error_type === 'startup' ? 'Present at startup' : 'New errors detected';
|
||||||
|
item.appendChild(typeEl);
|
||||||
|
|
||||||
|
const dismissBtn = document.createElement('button');
|
||||||
|
dismissBtn.textContent = 'Dismiss';
|
||||||
|
dismissBtn.addEventListener('click', () => clearError(err.id));
|
||||||
|
item.appendChild(dismissBtn);
|
||||||
|
|
||||||
|
listEl.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToNode(typeid) {
|
||||||
|
const nodeEl = document.querySelector('.node[data-typeid="' + typeid + '"]');
|
||||||
|
if (nodeEl) {
|
||||||
|
nodeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
nodeEl.classList.add('scroll-highlight');
|
||||||
|
setTimeout(() => nodeEl.classList.remove('scroll-highlight'), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearError(id) {
|
||||||
|
await fetch('/api/errors/clear?id=' + encodeURIComponent(id), { method: 'POST' });
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearAllErrors() {
|
||||||
|
await fetch('/api/errors/clear?all=true', { method: 'POST' });
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
anonCounter = 0;
|
anonCounter = 0;
|
||||||
const [statusResp, configResp] = await Promise.all([
|
const [statusResp, configResp] = await Promise.all([
|
||||||
@@ -538,6 +740,9 @@
|
|||||||
const nodes = data.nodes || [];
|
const nodes = data.nodes || [];
|
||||||
const links = data.links || [];
|
const links = data.links || [];
|
||||||
|
|
||||||
|
portErrors = data.port_errors || [];
|
||||||
|
const errorNodeIds = new Set(portErrors.map(e => e.node_typeid));
|
||||||
|
|
||||||
|
|
||||||
const locationTree = buildLocationTree(config.locations || [], null);
|
const locationTree = buildLocationTree(config.locations || [], null);
|
||||||
const nodeIndex = new Map();
|
const nodeIndex = new Map();
|
||||||
@@ -723,7 +928,7 @@
|
|||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
locationTree.forEach(loc => {
|
locationTree.forEach(loc => {
|
||||||
const el = renderLocation(loc, assignedNodes, true, switchConnections, switchUplinks, danteNodes);
|
const el = renderLocation(loc, assignedNodes, true, switchConnections, switchUplinks, danteNodes, errorNodeIds);
|
||||||
if (el) container.appendChild(el);
|
if (el) container.appendChild(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -745,7 +950,8 @@
|
|||||||
switches.forEach(node => {
|
switches.forEach(node => {
|
||||||
const uplink = switchUplinks.get(node.typeid);
|
const uplink = switchUplinks.get(node.typeid);
|
||||||
const danteInfo = danteNodes.get(node.typeid);
|
const danteInfo = danteNodes.get(node.typeid);
|
||||||
switchRow.appendChild(createNodeElement(node, null, null, uplink, danteInfo));
|
const hasError = errorNodeIds.has(node.typeid);
|
||||||
|
switchRow.appendChild(createNodeElement(node, null, null, uplink, danteInfo, hasError));
|
||||||
});
|
});
|
||||||
unassignedLoc.appendChild(switchRow);
|
unassignedLoc.appendChild(switchRow);
|
||||||
}
|
}
|
||||||
@@ -756,13 +962,16 @@
|
|||||||
nonSwitches.forEach(node => {
|
nonSwitches.forEach(node => {
|
||||||
const conn = switchConnections.get(node.typeid);
|
const conn = switchConnections.get(node.typeid);
|
||||||
const danteInfo = danteNodes.get(node.typeid);
|
const danteInfo = danteNodes.get(node.typeid);
|
||||||
nodeRow.appendChild(createNodeElement(node, conn, null, null, danteInfo));
|
const hasError = errorNodeIds.has(node.typeid);
|
||||||
|
nodeRow.appendChild(createNodeElement(node, conn, null, null, danteInfo, hasError));
|
||||||
});
|
});
|
||||||
unassignedLoc.appendChild(nodeRow);
|
unassignedLoc.appendChild(nodeRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
container.appendChild(unassignedLoc);
|
container.appendChild(unassignedLoc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateErrorPanel();
|
||||||
}
|
}
|
||||||
|
|
||||||
init().catch(e => {
|
init().catch(e => {
|
||||||
@@ -786,6 +995,20 @@
|
|||||||
document.getElementById('mode-network').addEventListener('click', () => setMode('network'));
|
document.getElementById('mode-network').addEventListener('click', () => setMode('network'));
|
||||||
document.getElementById('mode-dante').addEventListener('click', () => setMode('dante'));
|
document.getElementById('mode-dante').addEventListener('click', () => setMode('dante'));
|
||||||
|
|
||||||
|
document.getElementById('clear-all-errors').addEventListener('click', clearAllErrors);
|
||||||
|
document.getElementById('toggle-errors').addEventListener('click', () => {
|
||||||
|
const panel = document.getElementById('error-panel');
|
||||||
|
const btn = document.getElementById('toggle-errors');
|
||||||
|
errorPanelCollapsed = !errorPanelCollapsed;
|
||||||
|
if (errorPanelCollapsed) {
|
||||||
|
panel.classList.add('collapsed');
|
||||||
|
btn.textContent = 'Show';
|
||||||
|
} else {
|
||||||
|
panel.classList.remove('collapsed');
|
||||||
|
btn.textContent = 'Hide';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (window.location.hash === '#dante') {
|
if (window.location.hash === '#dante') {
|
||||||
setMode('dante');
|
setMode('dante');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ type Tendrils struct {
|
|||||||
nodes *Nodes
|
nodes *Nodes
|
||||||
artnet *ArtNetNodes
|
artnet *ArtNetNodes
|
||||||
danteFlows *DanteFlows
|
danteFlows *DanteFlows
|
||||||
|
errors *ErrorTracker
|
||||||
config *Config
|
config *Config
|
||||||
|
|
||||||
Interface string
|
Interface string
|
||||||
@@ -67,6 +68,7 @@ func New() *Tendrils {
|
|||||||
activeInterfaces: map[string]context.CancelFunc{},
|
activeInterfaces: map[string]context.CancelFunc{},
|
||||||
artnet: NewArtNetNodes(),
|
artnet: NewArtNetNodes(),
|
||||||
danteFlows: NewDanteFlows(),
|
danteFlows: NewDanteFlows(),
|
||||||
|
errors: NewErrorTracker(),
|
||||||
}
|
}
|
||||||
t.nodes = NewNodes(t)
|
t.nodes = NewNodes(t)
|
||||||
return t
|
return t
|
||||||
|
|||||||
Reference in New Issue
Block a user