feat: link health summary, recently resolved panel, event duration
- dashboard: pass recent_resolved (last 24h, limit 10) to index template; render "Recently Resolved" section showing type, target, resolved time, and calculated duration (first_seen → resolved_at) - dashboard: event-age spans now also update via setInterval; duration shown for resolved events (e.g. "2h 15m") - links page: link health summary panel shows server iface count, error/flap counts, switch port up/down, PoE total draw/capacity bar; only shows problematic stats if non-zero; shows "All OK ✔" when clean - style.css: new classes for summary panel, resolved row/badge Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -462,6 +462,74 @@ function expandAll() {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Link health summary ───────────────────────────────────────────
|
||||
function buildLinkSummary(hosts, unifiSwitches) {
|
||||
let svrTotal = 0, svrErrors = 0, svrFlap = 0;
|
||||
let swTotal = 0, swUp = 0, swDown = 0, swErrors = 0;
|
||||
let poeDrawW = 0, poeMaxW = 0;
|
||||
|
||||
for (const ifaces of Object.values(hosts)) {
|
||||
for (const d of Object.values(ifaces)) {
|
||||
svrTotal++;
|
||||
if ((d.tx_errs_rate || 0) > 0.01 || (d.rx_errs_rate || 0) > 0.01) svrErrors++;
|
||||
if ((d.carrier_changes || 0) > 10) svrFlap++;
|
||||
}
|
||||
}
|
||||
for (const sw of Object.values(unifiSwitches || {})) {
|
||||
for (const d of Object.values(sw.ports || {})) {
|
||||
swTotal++;
|
||||
if (d.up) swUp++; else swDown++;
|
||||
if ((d.tx_errs_rate || 0) > 0.01 || (d.rx_errs_rate || 0) > 0.01) swErrors++;
|
||||
if (d.poe_power != null) poeDrawW += d.poe_power;
|
||||
if (d.poe_max_power != null) poeMaxW += d.poe_max_power;
|
||||
}
|
||||
}
|
||||
|
||||
const poePct = poeMaxW > 0 ? (poeDrawW / poeMaxW * 100) : null;
|
||||
const poeBarCls = poePct >= 80 ? 'poe-bar-crit' : poePct >= 60 ? 'poe-bar-warn' : 'poe-bar-ok';
|
||||
const totalErrors = svrErrors + swErrors;
|
||||
const hasAlerts = totalErrors > 0 || svrFlap > 0 || swDown > 0;
|
||||
|
||||
return `
|
||||
<div class="link-summary-panel${hasAlerts ? ' link-summary-has-alerts' : ''}">
|
||||
<div class="link-summary-grid">
|
||||
<div class="link-summary-stat">
|
||||
<span class="lss-label">Server Ifaces</span>
|
||||
<span class="lss-value">${svrTotal}</span>
|
||||
</div>
|
||||
${svrErrors > 0 ? `<div class="link-summary-stat lss-alert">
|
||||
<span class="lss-label">Iface Errors</span>
|
||||
<span class="lss-value val-crit">${svrErrors}</span>
|
||||
</div>` : ''}
|
||||
${svrFlap > 0 ? `<div class="link-summary-stat lss-alert">
|
||||
<span class="lss-label">Flapping</span>
|
||||
<span class="lss-value val-warn">${svrFlap}</span>
|
||||
</div>` : ''}
|
||||
${swTotal > 0 ? `<div class="link-summary-stat">
|
||||
<span class="lss-label">Switch Ports</span>
|
||||
<span class="lss-value">${swUp}<span class="lss-sub">/${swTotal}</span></span>
|
||||
</div>` : ''}
|
||||
${swDown > 0 ? `<div class="link-summary-stat lss-alert">
|
||||
<span class="lss-label">Ports Down</span>
|
||||
<span class="lss-value val-crit">${swDown}</span>
|
||||
</div>` : ''}
|
||||
${swErrors > 0 ? `<div class="link-summary-stat lss-alert">
|
||||
<span class="lss-label">Port Errors</span>
|
||||
<span class="lss-value val-crit">${swErrors}</span>
|
||||
</div>` : ''}
|
||||
${poePct !== null ? `<div class="link-summary-stat">
|
||||
<span class="lss-label">PoE Load</span>
|
||||
<span class="lss-value ${poeBarCls === 'poe-bar-crit' ? 'val-crit' : poeBarCls === 'poe-bar-warn' ? 'val-warn' : 'val-good'}">${poeDrawW.toFixed(0)}W<span class="lss-sub">/${poeMaxW.toFixed(0)}W</span></span>
|
||||
<div class="poe-bar-track" style="margin-top:3px"><div class="poe-bar-fill ${poeBarCls}" style="width:${poePct.toFixed(1)}%"></div></div>
|
||||
</div>` : ''}
|
||||
${totalErrors === 0 && svrFlap === 0 && swDown === 0 ? `<div class="link-summary-stat">
|
||||
<span class="lss-label">Status</span>
|
||||
<span class="lss-value val-good">All OK ✔</span>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Render all hosts ──────────────────────────────────────────────
|
||||
function renderLinks(data) {
|
||||
const hosts = data.hosts || {};
|
||||
@@ -498,6 +566,7 @@ function renderLinks(data) {
|
||||
}).join('');
|
||||
|
||||
document.getElementById('links-container').innerHTML =
|
||||
buildLinkSummary(hosts, unifi) +
|
||||
`<div class="link-collapse-bar">
|
||||
<button class="btn btn-secondary btn-sm" onclick="collapseAll()">Collapse all</button>
|
||||
<button class="btn btn-secondary btn-sm" onclick="expandAll()">Expand all</button>
|
||||
|
||||
Reference in New Issue
Block a user