Add error LastSeen tracking, port uptime and last error to table/hovercards
This commit is contained in:
@@ -54,7 +54,8 @@ export function createNodeElement(node, switchConnection, nodeLocation, uplinkIn
|
||||
const errOut = switchConnection.errors?.out || 0;
|
||||
const r = switchConnection.rates;
|
||||
buildLinkStats(statsEl, portLabel, switchConnection.speed, errIn, errOut,
|
||||
r ? {rxBytes: r.outBytes, rxPkts: r.outPkts, txBytes: r.inBytes, txPkts: r.inPkts} : null);
|
||||
r ? {rxBytes: r.outBytes, rxPkts: r.outPkts, txBytes: r.inBytes, txPkts: r.inPkts} : null,
|
||||
switchConnection.uptime, switchConnection.lastError);
|
||||
} else {
|
||||
const container = div.querySelector(':scope > .port-hover');
|
||||
if (container) container.remove();
|
||||
@@ -163,7 +164,8 @@ export function createNodeElement(node, switchConnection, nodeLocation, uplinkIn
|
||||
const errOut = uplinkInfo.errors?.out || 0;
|
||||
const r = uplinkInfo.rates;
|
||||
buildLinkStats(statsEl, uplinkLabel, uplinkInfo.speed, errIn, errOut,
|
||||
r ? {rxBytes: r.inBytes, rxPkts: r.inPkts, txBytes: r.outBytes, txPkts: r.outPkts} : null);
|
||||
r ? {rxBytes: r.inBytes, rxPkts: r.inPkts, txBytes: r.outBytes, txPkts: r.outPkts} : null,
|
||||
uplinkInfo.uptime, uplinkInfo.lastError);
|
||||
} else {
|
||||
const rootEl = div.querySelector(':scope > .root-label');
|
||||
if (rootEl) rootEl.remove();
|
||||
|
||||
@@ -99,3 +99,13 @@ export function getInterfaceRates(node, ifaceName) {
|
||||
outBytes: iface.stats.out_bytes_rate || 0
|
||||
};
|
||||
}
|
||||
|
||||
export function getInterfaceUptime(node, ifaceName) {
|
||||
const iface = findInterface(node, ifaceName);
|
||||
return iface?.stats?.uptime || 0;
|
||||
}
|
||||
|
||||
export function getInterfaceLastError(node, ifaceName) {
|
||||
const iface = findInterface(node, ifaceName);
|
||||
return iface?.stats?.last_error || null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getLabel, getShortLabel, isSwitch, getInterfaceSpeed, getInterfaceErrors, getInterfaceRates, getFirstName } from './nodes.js';
|
||||
import { getLabel, getShortLabel, isSwitch, getInterfaceSpeed, getInterfaceErrors, getInterfaceRates, getInterfaceUptime, getInterfaceLastError, getFirstName } from './nodes.js';
|
||||
import { buildSwitchUplinks, buildLocationTree, buildNodeIndex, findLocationForNode, findEffectiveSwitch } from './topology.js';
|
||||
import { formatUniverse } from './format.js';
|
||||
import { createNodeElement, renderLocation } from './components.js';
|
||||
@@ -71,7 +71,11 @@ export function render(data, config) {
|
||||
errorsA: getInterfaceErrors(nodeA, link.interface_a),
|
||||
errorsB: getInterfaceErrors(nodeB, link.interface_b),
|
||||
ratesA: getInterfaceRates(nodeA, link.interface_a),
|
||||
ratesB: getInterfaceRates(nodeB, link.interface_b)
|
||||
ratesB: getInterfaceRates(nodeB, link.interface_b),
|
||||
uptimeA: getInterfaceUptime(nodeA, link.interface_a),
|
||||
uptimeB: getInterfaceUptime(nodeB, link.interface_b),
|
||||
lastErrorA: getInterfaceLastError(nodeA, link.interface_a),
|
||||
lastErrorB: getInterfaceLastError(nodeB, link.interface_b)
|
||||
});
|
||||
} else if (aIsSwitch && !bIsSwitch) {
|
||||
const nodeLoc = nodeLocations.get(nodeB.id);
|
||||
@@ -84,7 +88,9 @@ export function render(data, config) {
|
||||
external: effectiveSwitch && !isLocalSwitch,
|
||||
speed: getInterfaceSpeed(nodeA, link.interface_a),
|
||||
errors: getInterfaceErrors(nodeA, link.interface_a),
|
||||
rates: getInterfaceRates(nodeA, link.interface_a)
|
||||
rates: getInterfaceRates(nodeA, link.interface_a),
|
||||
uptime: getInterfaceUptime(nodeA, link.interface_a),
|
||||
lastError: getInterfaceLastError(nodeA, link.interface_a)
|
||||
});
|
||||
} else if (bIsSwitch && !aIsSwitch) {
|
||||
const nodeLoc = nodeLocations.get(nodeA.id);
|
||||
@@ -97,7 +103,9 @@ export function render(data, config) {
|
||||
external: effectiveSwitch && !isLocalSwitch,
|
||||
speed: getInterfaceSpeed(nodeB, link.interface_b),
|
||||
errors: getInterfaceErrors(nodeB, link.interface_b),
|
||||
rates: getInterfaceRates(nodeB, link.interface_b)
|
||||
rates: getInterfaceRates(nodeB, link.interface_b),
|
||||
uptime: getInterfaceUptime(nodeB, link.interface_b),
|
||||
lastError: getInterfaceLastError(nodeB, link.interface_b)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getLabel, getFirstName, isSwitch, getInterfaceSpeed, getInterfaceErrors, getInterfaceRates } from './nodes.js';
|
||||
import { getLabel, getFirstName, isSwitch, getInterfaceSpeed, getInterfaceErrors, getInterfaceRates, getInterfaceUptime, getInterfaceLastError } from './nodes.js';
|
||||
import { buildSwitchUplinks } from './topology.js';
|
||||
import { escapeHtml, formatUniverse } from './format.js';
|
||||
import { tableData, tableSortKeys, setTableSortKeys } from './state.js';
|
||||
@@ -107,6 +107,8 @@ export function renderNetworkTable() {
|
||||
speed: getInterfaceSpeed(nodeA, link.interface_a),
|
||||
errors: getInterfaceErrors(nodeA, link.interface_a),
|
||||
rates: getInterfaceRates(nodeA, link.interface_a),
|
||||
uptime: getInterfaceUptime(nodeA, link.interface_a),
|
||||
lastError: getInterfaceLastError(nodeA, link.interface_a),
|
||||
isLocalPort: false
|
||||
});
|
||||
} else if (bIsSwitch && !aIsSwitch) {
|
||||
@@ -116,6 +118,8 @@ export function renderNetworkTable() {
|
||||
speed: getInterfaceSpeed(nodeB, link.interface_b),
|
||||
errors: getInterfaceErrors(nodeB, link.interface_b),
|
||||
rates: getInterfaceRates(nodeB, link.interface_b),
|
||||
uptime: getInterfaceUptime(nodeB, link.interface_b),
|
||||
lastError: getInterfaceLastError(nodeB, link.interface_b),
|
||||
isLocalPort: false
|
||||
});
|
||||
} else if (aIsSwitch && bIsSwitch) {
|
||||
@@ -129,7 +133,11 @@ export function renderNetworkTable() {
|
||||
errorsA: getInterfaceErrors(nodeA, link.interface_a),
|
||||
errorsB: getInterfaceErrors(nodeB, link.interface_b),
|
||||
ratesA: getInterfaceRates(nodeA, link.interface_a),
|
||||
ratesB: getInterfaceRates(nodeB, link.interface_b)
|
||||
ratesB: getInterfaceRates(nodeB, link.interface_b),
|
||||
uptimeA: getInterfaceUptime(nodeA, link.interface_a),
|
||||
uptimeB: getInterfaceUptime(nodeB, link.interface_b),
|
||||
lastErrorA: getInterfaceLastError(nodeA, link.interface_a),
|
||||
lastErrorB: getInterfaceLastError(nodeB, link.interface_b)
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -145,6 +153,8 @@ export function renderNetworkTable() {
|
||||
speed: uplink.speed,
|
||||
errors: uplink.errors,
|
||||
rates: uplink.rates,
|
||||
uptime: uplink.uptime,
|
||||
lastError: uplink.lastError,
|
||||
isLocalPort: true
|
||||
});
|
||||
}
|
||||
@@ -165,6 +175,30 @@ export function renderNetworkTable() {
|
||||
return util.toFixed(0);
|
||||
};
|
||||
|
||||
const formatUptime = (seconds) => {
|
||||
if (!seconds || seconds <= 0) return '';
|
||||
const d = Math.floor(seconds / 86400);
|
||||
const h = Math.floor((seconds % 86400) / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
if (d > 0) return d + 'd' + (h > 0 ? ' ' + h + 'h' : '');
|
||||
if (h > 0) return h + 'h' + (m > 0 ? ' ' + m + 'm' : '');
|
||||
return m + 'm';
|
||||
};
|
||||
|
||||
const formatTimeSince = (utcString) => {
|
||||
if (!utcString) return '';
|
||||
const date = new Date(utcString);
|
||||
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
||||
if (seconds < 0) return '';
|
||||
const d = Math.floor(seconds / 86400);
|
||||
const h = Math.floor((seconds % 86400) / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
if (d > 0) return d + 'd' + (h > 0 ? ' ' + h + 'h' : '');
|
||||
if (h > 0) return h + 'h' + (m > 0 ? ' ' + m + 'm' : '');
|
||||
if (m > 0) return m + 'm';
|
||||
return '<1m';
|
||||
};
|
||||
|
||||
let rows = nodes.map(node => {
|
||||
const name = getLabel(node);
|
||||
const ips = [];
|
||||
@@ -178,6 +212,8 @@ export function renderNetworkTable() {
|
||||
const speed = isRoot ? null : (conn?.speed || 0);
|
||||
const errors = isRoot ? null : (conn?.errors || { in: 0, out: 0 });
|
||||
const rates = isRoot ? null : (conn?.rates || { inBytes: 0, outBytes: 0 });
|
||||
const uptime = isRoot ? null : (conn?.uptime || 0);
|
||||
const lastErrorTime = isRoot ? null : (conn?.lastError || null);
|
||||
const useLocalPerspective = isRoot || conn?.isLocalPort;
|
||||
|
||||
const isUnreachable = node.unreachable;
|
||||
@@ -200,7 +236,11 @@ export function renderNetworkTable() {
|
||||
outUtil: outRateVal == null || !speed ? null : (outRateVal * 8 / speed) * 100,
|
||||
inPkts: rates == null ? null : (useLocalPerspective ? rates.inPkts : rates.outPkts),
|
||||
outPkts: rates == null ? null : (useLocalPerspective ? rates.outPkts : rates.inPkts),
|
||||
status: isUnreachable ? 'unreachable' : (errors && (errors.in + errors.out) > 0 ? 'errors' : 'ok')
|
||||
status: isUnreachable ? 'unreachable' : (errors && (errors.in + errors.out) > 0 ? 'errors' : 'ok'),
|
||||
uptime,
|
||||
uptimeStr: formatUptime(uptime),
|
||||
lastErrorTime,
|
||||
lastErrorStr: formatTimeSince(lastErrorTime)
|
||||
};
|
||||
});
|
||||
|
||||
@@ -211,7 +251,7 @@ export function renderNetworkTable() {
|
||||
html += '<th colspan="4"></th>';
|
||||
html += '<th colspan="4" class="group-in">In</th>';
|
||||
html += '<th colspan="4" class="group-out">Out</th>';
|
||||
html += '<th></th>';
|
||||
html += '<th colspan="3"></th>';
|
||||
html += '</tr><tr>';
|
||||
html += '<th data-sort="name">Name</th>';
|
||||
html += '<th data-sort="ip">IP</th>';
|
||||
@@ -225,6 +265,8 @@ export function renderNetworkTable() {
|
||||
html += '<th data-sort="outUtil" class="group-out">%</th>';
|
||||
html += '<th data-sort="outRate" class="group-out">Mb</th>';
|
||||
html += '<th data-sort="outPkts" class="group-out">Kp</th>';
|
||||
html += '<th data-sort="uptime">Uptime</th>';
|
||||
html += '<th data-sort="lastErrorTime">Last Err</th>';
|
||||
html += '<th data-sort="status">Status</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
@@ -243,6 +285,8 @@ export function renderNetworkTable() {
|
||||
html += '<td class="numeric group-out">' + (r.outRate == null ? '' : formatUtilLocal(r.outRate, r.speed)) + '</td>';
|
||||
html += '<td class="numeric group-out">' + (r.outRate == null ? '' : formatMbpsLocal(r.outRate)) + '</td>';
|
||||
html += '<td class="numeric group-out">' + (r.outPkts == null ? '' : formatKppsLocal(r.outPkts)) + '</td>';
|
||||
html += '<td class="numeric">' + r.uptimeStr + '</td>';
|
||||
html += '<td class="numeric">' + r.lastErrorStr + '</td>';
|
||||
html += '<td class="' + statusClass + '">' + r.status + '</td>';
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
@@ -15,7 +15,9 @@ export function buildSwitchUplinks(allSwitches, switchLinks) {
|
||||
remotePort: link.portB,
|
||||
localSpeed: link.speedA,
|
||||
localErrors: link.errorsA,
|
||||
localRates: link.ratesA
|
||||
localRates: link.ratesA,
|
||||
localUptime: link.uptimeA,
|
||||
localLastError: link.lastErrorA
|
||||
});
|
||||
adjacency.get(link.switchB.id).push({
|
||||
neighbor: link.switchA,
|
||||
@@ -23,7 +25,9 @@ export function buildSwitchUplinks(allSwitches, switchLinks) {
|
||||
remotePort: link.portA,
|
||||
localSpeed: link.speedB,
|
||||
localErrors: link.errorsB,
|
||||
localRates: link.ratesB
|
||||
localRates: link.ratesB,
|
||||
localUptime: link.uptimeB,
|
||||
localLastError: link.lastErrorB
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,7 +85,9 @@ export function buildSwitchUplinks(allSwitches, switchLinks) {
|
||||
parentName: getLabel(current),
|
||||
speed: reverseEdge?.localSpeed || 0,
|
||||
errors: reverseEdge?.localErrors || null,
|
||||
rates: reverseEdge?.localRates || null
|
||||
rates: reverseEdge?.localRates || null,
|
||||
uptime: reverseEdge?.localUptime || 0,
|
||||
lastError: reverseEdge?.localLastError || null
|
||||
});
|
||||
queue.push(edge.neighbor);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,31 @@ function formatUtilization(bytesPerSec, speed) {
|
||||
return util.toFixed(0) + '%';
|
||||
}
|
||||
|
||||
export function buildLinkStats(container, portLabel, speed, errIn, errOut, rates) {
|
||||
function formatUptime(seconds) {
|
||||
if (!seconds || seconds <= 0) return '?';
|
||||
const d = Math.floor(seconds / 86400);
|
||||
const h = Math.floor((seconds % 86400) / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
if (d > 0) return d + 'd' + (h > 0 ? ' ' + h + 'h' : '');
|
||||
if (h > 0) return h + 'h' + (m > 0 ? ' ' + m + 'm' : '');
|
||||
return m + 'm';
|
||||
}
|
||||
|
||||
function formatTimeSince(utcString) {
|
||||
if (!utcString) return '';
|
||||
const date = new Date(utcString);
|
||||
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
||||
if (seconds < 0) return '';
|
||||
const d = Math.floor(seconds / 86400);
|
||||
const h = Math.floor((seconds % 86400) / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
if (d > 0) return d + 'd' + (h > 0 ? ' ' + h + 'h' : '');
|
||||
if (h > 0) return h + 'h' + (m > 0 ? ' ' + m + 'm' : '');
|
||||
if (m > 0) return m + 'm';
|
||||
return '<1m';
|
||||
}
|
||||
|
||||
export function buildLinkStats(container, portLabel, speed, errIn, errOut, rates, uptime, lastError) {
|
||||
const plainLines = [];
|
||||
if (portLabel) {
|
||||
addClickableValue(container, 'PORT', portLabel, plainLines);
|
||||
@@ -81,6 +105,15 @@ export function buildLinkStats(container, portLabel, speed, errIn, errOut, rates
|
||||
container.appendChild(document.createTextNode('\n'));
|
||||
addClickableValue(container, 'TX', txUtil + ' ' + formatShortMbps(rates.txBytes) + ' ' + formatShortKpps(rates.txPkts), plainLines);
|
||||
}
|
||||
if (uptime) {
|
||||
container.appendChild(document.createTextNode('\n'));
|
||||
addClickableValue(container, 'UP', formatUptime(uptime), plainLines);
|
||||
}
|
||||
if (lastError) {
|
||||
const errorAge = formatTimeSince(lastError);
|
||||
container.appendChild(document.createTextNode('\n'));
|
||||
addClickableValue(container, 'LERR', errorAge + ' ago', plainLines);
|
||||
}
|
||||
container.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(plainLines.join('\n'));
|
||||
@@ -160,6 +193,12 @@ export async function clearAllErrors() {
|
||||
await fetch('/tendrils/api/errors/clear?all=true', { method: 'POST' });
|
||||
}
|
||||
|
||||
function formatLocalTime(utcString) {
|
||||
if (!utcString) return '';
|
||||
const date = new Date(utcString);
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
}
|
||||
|
||||
export function updateErrorPanel() {
|
||||
const panel = document.getElementById('error-panel');
|
||||
const countEl = document.getElementById('error-count');
|
||||
@@ -241,6 +280,11 @@ export function updateErrorPanel() {
|
||||
item.appendChild(typeEl);
|
||||
}
|
||||
|
||||
const timestampEl = document.createElement('div');
|
||||
timestampEl.className = 'error-timestamp';
|
||||
timestampEl.textContent = 'First: ' + formatLocalTime(err.first_seen) + ' / Last: ' + formatLocalTime(err.last_seen);
|
||||
item.appendChild(timestampEl);
|
||||
|
||||
const dismissBtn = document.createElement('button');
|
||||
dismissBtn.textContent = 'Dismiss';
|
||||
dismissBtn.addEventListener('click', () => clearError(err.id));
|
||||
|
||||
@@ -1151,6 +1151,12 @@ body.sacn-mode .node:not(.sacn-out):not(.sacn-in):hover .node-info-wrapper {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.error-item .error-timestamp {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.error-item button {
|
||||
align-self: flex-end;
|
||||
padding: 2px 6px;
|
||||
|
||||
Reference in New Issue
Block a user