From 6b6eaa6227d9f9adb75a306881123a0781896cde Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sat, 14 Mar 2026 21:46:11 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20UI=20improvements=20=E2=80=94=20event?= =?UTF-8?q?=20ages,=20error=20badges,=20PoE=20bars,=20mismatch=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - events table: add Last Seen column; show relative times ("3h ago") with absolute timestamp on hover; update updateEventsTable() in app.js to match - links.html: add error/drop/flap alert badges to interface and port card headers - links.html: PoE power bar (draw/max ratio with colour-coded fill) and poe_mode - links.html: stale data warning banner when link_stats are >2 minutes old - links.html: improved error handler shows HTTP status instead of generic message - links.html: fix collapse state persisted to localStorage (was sessionStorage, lost on browser restart); fix collapseAll/expandAll to also persist state - inspector.html: duplex mismatch and speed mismatch warnings in path debug panel - inspector.html: carrier changes added to server column of path debug - style.css: new classes — .link-alert-badge, .poe-bar-*, .path-mismatch-alert, .error-state; fix .stale-banner to use CSS variables Co-Authored-By: Claude Sonnet 4.6 --- static/app.js | 17 +++++++-- static/style.css | 73 +++++++++++++++++++++++++++++++++++---- templates/index.html | 31 +++++++++++++++-- templates/inspector.html | 18 ++++++++++ templates/links.html | 74 +++++++++++++++++++++++++++++++++++----- 5 files changed, 194 insertions(+), 19 deletions(-) diff --git a/static/app.js b/static/app.js index 42706e0..38d1280 100644 --- a/static/app.js +++ b/static/app.js @@ -172,7 +172,8 @@ function updateEventsTable(events) { ${escHtml(e.target_name)} ${escHtml(e.target_detail || '–')} ${escHtml((e.description||'').substring(0,60))}${(e.description||'').length>60?'…':''} - ${escHtml(e.first_seen||'')} + ${fmtRelTime(e.first_seen)} + ${fmtRelTime(e.last_seen)} ${e.consecutive_failures} ${ticket} @@ -190,7 +191,7 @@ function updateEventsTable(events) { SevTypeTargetDetail - DescriptionFirst SeenFailuresTicketActions + DescriptionFirst SeenLast SeenFailuresTicketActions ${rows} @@ -300,6 +301,18 @@ document.addEventListener('click', e => { } }); +// ── Relative time ───────────────────────────────────────────────────── +function fmtRelTime(tsStr) { + if (!tsStr) return '–'; + const d = new Date(tsStr.replace(' UTC', 'Z').replace(' ', 'T')); + if (isNaN(d)) return tsStr; + const secs = Math.floor((Date.now() - d) / 1000); + if (secs < 60) return `${secs}s ago`; + if (secs < 3600) return `${Math.floor(secs/60)}m ago`; + if (secs < 86400) return `${Math.floor(secs/3600)}h ago`; + return `${Math.floor(secs/86400)}d ago`; +} + // ── Utility ─────────────────────────────────────────────────────────── function escHtml(str) { if (str === null || str === undefined) return ''; diff --git a/static/style.css b/static/style.css index a45fae1..9735f8e 100644 --- a/static/style.css +++ b/static/style.css @@ -1453,13 +1453,74 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-amber); } /* ── Stale monitoring banner ──────────────────────────────────────── */ .stale-banner { - background: rgba(255, 160, 0, 0.12); - border: 1px solid var(--warning); - border-left: 4px solid var(--warning); - color: var(--warning); + background: var(--amber-dim); + border: 1px solid var(--amber); + border-left: 4px solid var(--amber); + color: var(--amber); padding: 10px 16px; - margin: 12px 16px 0; + margin: 12px 0 0; font-size: 0.88em; - font-family: var(--font-mono); + font-family: var(--font); border-radius: 2px; } + +/* ── Link alert badges (error/flap indicators) ────────────────────── */ +.link-alert-badge { + display: inline-block; + font-size: .6em; + font-weight: bold; + padding: 1px 5px; + border-radius: 2px; + background: var(--red-dim); + color: var(--red); + border: 1px solid var(--red); + margin-left: 4px; + vertical-align: middle; + letter-spacing: .05em; +} + +.link-alert-badge.link-alert-amber { + background: var(--amber-dim); + color: var(--amber); + border-color: var(--amber); +} + +/* ── PoE utilisation bar ──────────────────────────────────────────── */ +.poe-bar-track { + height: 3px; + background: var(--bg3); + border-radius: 2px; + margin-top: 3px; + overflow: hidden; +} + +.poe-bar-fill { + height: 100%; + border-radius: 2px; + transition: width 0.4s ease; +} + +.poe-bar-ok { background: var(--green); } +.poe-bar-warn { background: var(--amber); } +.poe-bar-crit { background: var(--red); } + +/* ── Path mismatch alert ──────────────────────────────────────────── */ +.path-mismatch-alert { + background: var(--amber-dim); + border-left: 3px solid var(--amber); + color: var(--amber); + padding: 4px 8px; + margin-bottom: 6px; + font-size: .72em; + border-radius: 2px; +} + +/* ── Error state for data containers ─────────────────────────────── */ +.error-state { + padding: 16px 20px; + border-left: 3px solid var(--red); + background: var(--red-dim); + color: var(--red); + border-radius: 2px; + font-size: .88em; +} diff --git a/templates/index.html b/templates/index.html index 498d3ba..3f8ce62 100644 --- a/templates/index.html +++ b/templates/index.html @@ -201,6 +201,7 @@ Detail Description First Seen + Last Seen Failures Ticket Actions @@ -215,7 +216,12 @@ {{ e.target_name }} {{ e.target_detail or '–' }} {{ e.description | truncate(60) }} - {{ e.first_seen }} + + {{ e.first_seen }} + + + {{ e.last_seen }} + {{ e.consecutive_failures }} {% if e.ticket_id %} @@ -233,7 +239,7 @@ {% endif %} {% else %} - No active alerts ✔ + No active alerts ✔ {% endfor %} @@ -299,5 +305,26 @@ {% block scripts %} {% endblock %} diff --git a/templates/inspector.html b/templates/inspector.html index f1aee6c..5caed04 100644 --- a/templates/inspector.html +++ b/templates/inspector.html @@ -326,8 +326,25 @@ function buildPathDebug(swName, swPort, serverName, ifaceName, svrData) { const swErrTx = (swPort.tx_errs_rate > 0.001) ? 'val-crit' : 'val-good'; const swErrRx = (swPort.rx_errs_rate > 0.001) ? 'val-crit' : 'val-good'; + // Detect duplex mismatch (switch full_duplex vs server duplex string) + const swFull = swPort.full_duplex; + const svrFull = (svrData.duplex || '').toLowerCase().includes('full'); + const duplexMismatch = swPort.up && svrData.duplex && + ((swFull && !svrFull) || (!swFull && svrFull)); + const duplexWarnHtml = duplexMismatch + ? `
⚠ DUPLEX MISMATCH — Switch: ${swFull ? 'Full' : 'Half'} · Server: ${escHtml(svrData.duplex)}
` + : ''; + + // Detect speed mismatch + const swSpd = swPort.speed_mbps, svrSpd = svrData.speed_mbps; + const speedMismatch = swSpd && svrSpd && swSpd > 0 && svrSpd > 0 && swSpd !== svrSpd; + const speedWarnHtml = speedMismatch + ? `
⚠ SPEED MISMATCH — Switch: ${fmtSpeed(swSpd)} · Server: ${fmtSpeed(svrSpd)}
` + : ''; + return `
Path Debug ${escHtml(connType)}
+ ${duplexWarnHtml}${speedWarnHtml}
Switch
@@ -347,6 +364,7 @@ function buildPathDebug(swName, swPort, serverName, ifaceName, svrData) {
RX${fmtRate(svrData.rx_bytes_rate)}
TX Err${fmtErrors(svrData.tx_errs_rate)}
RX Err${fmtErrors(svrData.rx_errs_rate)}
+ ${svrData.carrier_changes != null ? `
Carrier Chg${svrData.carrier_changes}
` : ''} ${sfpDomHtml}
`; diff --git a/templates/links.html b/templates/links.html index 853d3c7..5f7238b 100644 --- a/templates/links.html +++ b/templates/links.html @@ -124,6 +124,18 @@ function portTypeLabel(pt) { return {label: pt, cls: 'type-copper'}; } +// ── Error alert badge ───────────────────────────────────────────── +function errorBadges(d) { + const badges = []; + if ((d.tx_errs_rate || 0) > 0.01 || (d.rx_errs_rate || 0) > 0.01) + badges.push('ERR'); + if ((d.tx_drops_rate || 0) > 0.1 || (d.rx_drops_rate || 0) > 0.1) + badges.push('DROP'); + if ((d.carrier_changes || 0) > 10) + badges.push('FLAP'); + return badges.join(''); +} + // ── Render a single interface card ──────────────────────────────── function renderIfaceCard(ifaceName, d) { const speed = fmtSpeed(d.speed_mbps); @@ -204,6 +216,7 @@ function renderIfaceCard(ifaceName, d) { ${escHtml(ifaceName)} ${speed !== '–' ? `${speed}` : ''} ${ptype.label !== '–' ? `${escHtml(ptype.label)}` : ''} + ${errorBadges(d)}