1 Commits

Author SHA1 Message Date
jared 29267c9933 Integrate test code improvements using web_template components
Lint / Python (flake8) (push) Successful in 45s
Lint / JS (eslint) (push) Successful in 8s
Security / Python Security (bandit) (push) Successful in 41s
Test / Python Tests (pytest) (push) Successful in 52s
Lint / Notify on failure (push) Has been skipped
Lint / Deploy (push) Successful in 3s
lt-alert:
- Replace custom .stale-banner with lt-alert lt-alert--warning in app.js
  and links.html; remove stale-banner CSS, reuse lt-alert margin rule

lt-progress:
- Replace custom .traffic-bar-track/.traffic-bar-fill in links.html with
  lt-progress from base.css; TX uses default (orange), RX uses --cyan,
  both flip to --red when utilisation >85% (trafficBarClass helper)
- Keep traffic layout classes (.traffic-section/.traffic-row etc.) for structure

Suppression type badges:
- Map target_type to distinct badge colors: host→badge-warning (orange),
  interface→badge-info (cyan), unifi_device→badge-purple (new alias using
  --accent-purple from base.css), all→badge-critical (red)
- Applied in both server-rendered table (Jinja2 dict lookup) and
  renderActiveRows() JS

Topology animated down-wire:
- Add data-host attribute to .topo-v2-wire-10g/.topo-v2-wire-1g elements
- updateTopology() toggles .wire-down class on the 10G drop-wire when
  host.status === 'down'
- .wire-down CSS: animated repeating-linear-gradient dashed red line
  via wire-dash-anim @keyframes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 23:37:47 -04:00
5 changed files with 42 additions and 28 deletions
+9 -2
View File
@@ -86,11 +86,13 @@ function updateStatusBar(summary, lastCheck, daemonOk) {
if (!staleBanner) { if (!staleBanner) {
staleBanner = document.createElement('div'); staleBanner = document.createElement('div');
staleBanner.id = 'stale-banner'; staleBanner.id = 'stale-banner';
staleBanner.className = 'stale-banner'; staleBanner.className = 'lt-alert lt-alert--warning';
staleBanner.innerHTML = '<span class="lt-alert-icon">⚠</span><div class="lt-alert-body"><div class="lt-alert-msg"></div></div>';
document.querySelector('.lt-main').prepend(staleBanner); document.querySelector('.lt-main').prepend(staleBanner);
} }
const mins = Math.floor(checkAge / 60); const mins = Math.floor(checkAge / 60);
staleBanner.textContent = `⚠ Monitoring data is stale — last check was ${mins} minute${mins !== 1 ? 's' : ''} ago. The monitor daemon may be down.`; staleBanner.querySelector('.lt-alert-msg').textContent =
`Monitoring data is stale — last check was ${mins} minute${mins !== 1 ? 's' : ''} ago. The monitor daemon may be down.`;
staleBanner.style.display = ''; staleBanner.style.display = '';
} else if (staleBanner) { } else if (staleBanner) {
staleBanner.style.display = 'none'; staleBanner.style.display = 'none';
@@ -138,6 +140,11 @@ function updateTopology(hosts) {
badge.className = `topo-badge topo-badge-${host.status}`; badge.className = `topo-badge topo-badge-${host.status}`;
badge.textContent = host.status; badge.textContent = host.status;
} }
// Animate the 10G drop-wire red+dashed when host is down
document.querySelectorAll(`.topo-v2-wire-10g[data-host="${CSS.escape(name)}"]`).forEach(wire => {
wire.classList.toggle('wire-down', host.status === 'down');
});
}); });
} }
+14 -16
View File
@@ -132,6 +132,7 @@
.badge-neutral { color: var(--text-muted); border-color: var(--text-muted); } .badge-neutral { color: var(--text-muted); border-color: var(--text-muted); }
.badge-resolved { color: var(--text-muted); border-color: var(--border-color); text-decoration: line-through; } .badge-resolved { color: var(--text-muted); border-color: var(--border-color); text-decoration: line-through; }
.badge-suppressed { font-size: .9em; padding: 0; border: none; color: var(--text-muted); } .badge-suppressed { font-size: .9em; padding: 0; border: none; color: var(--text-muted); }
.badge-purple { color: var(--accent-purple); border-color: var(--accent-purple); }
/* ── Table row state colors ───────────────────────────────────────── */ /* ── Table row state colors ───────────────────────────────────────── */
.lt-table tr.row-critical td { background: rgba(255,45,85,.04); } .lt-table tr.row-critical td { background: rgba(255,45,85,.04); }
@@ -179,15 +180,7 @@
.last-check { font-size: .72em; color: var(--text-muted); } .last-check { font-size: .72em; color: var(--text-muted); }
/* ── Stale monitoring banner ──────────────────────────────────────── */ /* ── Stale monitoring banner ──────────────────────────────────────── */
.stale-banner { /* .stale-banner replaced by lt-alert--warning */
background: var(--amber-dim);
border: 1px solid var(--amber);
border-left: 4px solid var(--amber);
color: var(--amber);
padding: 10px 16px;
margin: 0 0 14px;
font-size: .88em;
}
/* ── Error / empty state containers ───────────────────────────────── */ /* ── Error / empty state containers ───────────────────────────────── */
.error-state { .error-state {
@@ -397,9 +390,17 @@
height: 28px; height: 28px;
align-items: flex-start; align-items: flex-start;
} }
.topo-v2-wire-10g { width: 2px; height: 100%; background: var(--green); opacity: .55; } .topo-v2-wire-10g { width: 2px; height: 100%; background: var(--green); opacity: .55; transition: background .3s, opacity .3s; }
.topo-v2-wire-1g { width: 0; height: 100%; border-left: 2px dashed var(--amber); opacity: .45; } .topo-v2-wire-1g { width: 0; height: 100%; border-left: 2px dashed var(--amber); opacity: .45; }
@keyframes wire-dash-anim { to { background-position: 0 -20px; } }
.topo-v2-wire-10g.wire-down {
background: repeating-linear-gradient(to bottom, var(--red) 0 6px, transparent 6px 10px) !important;
background-size: 2px 10px !important;
opacity: .9 !important;
animation: wire-dash-anim .7s linear infinite;
}
/* Bus rails */ /* Bus rails */
.topo-bus-section { .topo-bus-section {
width: 100%; width: 100%;
@@ -557,14 +558,11 @@
.counter-zero { color: var(--green); } .counter-zero { color: var(--green); }
.counter-nonzero { color: var(--red); text-shadow: var(--glow-red); } .counter-nonzero { color: var(--red); text-shadow: var(--glow-red); }
/* Traffic bars */ /* Traffic bars — use lt-progress from base.css */
.traffic-section { margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255,107,0,.08); } .traffic-section { margin-top: 8px; padding-top: 8px; border-top: 1px solid rgba(255,107,0,.08); }
.traffic-row { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; } .traffic-row { display: flex; align-items: center; gap: 8px; margin-bottom: 5px; }
.traffic-label { font-size: .62em; color: var(--text-muted); width: 20px; text-transform: uppercase; letter-spacing: .04em; flex-shrink: 0; } .traffic-label { font-size: .62em; color: var(--text-muted); width: 20px; text-transform: uppercase; letter-spacing: .04em; flex-shrink: 0; }
.traffic-bar-track{ flex: 1; height: 5px; background: var(--bg-primary); border: 1px solid rgba(255,107,0,.15); position: relative; overflow: hidden; } .traffic-row .lt-progress { flex: 1; height: 5px; }
.traffic-bar-fill { height: 100%; position: absolute; left: 0; top: 0; transition: width .4s; }
.traffic-tx { background: var(--cyan); box-shadow: 0 0 3px rgba(0,212,255,.4); }
.traffic-rx { background: var(--green); box-shadow: 0 0 3px rgba(0,255,136,.4); }
.traffic-value { font-size: .7em; color: var(--text-dim); width: 68px; text-align: right; flex-shrink: 0; } .traffic-value { font-size: .7em; color: var(--text-dim); width: 68px; text-align: right; flex-shrink: 0; }
/* SFP / optical panel */ /* SFP / optical panel */
@@ -665,7 +663,7 @@
.link-loading { padding: 20px; text-align: center; color: var(--text-muted); font-size: .8em; } .link-loading { padding: 20px; text-align: center; color: var(--text-muted); font-size: .8em; }
.link-loading::after { content: ' ...'; animation: blink 1s step-end infinite; } .link-loading::after { content: ' ...'; animation: blink 1s step-end infinite; }
.link-no-data { padding: 14px; color: var(--text-muted); font-size: .78em; text-align: center; } .link-no-data { padding: 14px; color: var(--text-muted); font-size: .78em; text-align: center; }
.stale-banner { margin-bottom: 12px; } .lt-alert { margin-bottom: 12px; }
/* ── Inspector page ───────────────────────────────────────────────── */ /* ── Inspector page ───────────────────────────────────────────────── */
.inspector-layout { .inspector-layout {
+2 -2
View File
@@ -138,8 +138,8 @@
<div class="topo-v2-host-wrap"> <div class="topo-v2-host-wrap">
<!-- dual-homing wires: 10G solid green + 1G dashed amber --> <!-- dual-homing wires: 10G solid green + 1G dashed amber -->
<div class="topo-v2-host-wires"> <div class="topo-v2-host-wires">
<div class="topo-v2-wire-10g" title="10G SFP+ → USW-Agg"></div> <div class="topo-v2-wire-10g" data-host="{{ hname }}" title="10G SFP+ → USW-Agg"></div>
<div class="topo-v2-wire-1g" title="1G → Pro 24 PoE"></div> <div class="topo-v2-wire-1g" data-host="{{ hname }}" title="1G → Pro 24 PoE"></div>
</div> </div>
<!-- host box --> <!-- host box -->
<div class="topo-v2-node topo-v2-host topo-host topo-v2-status-{{ st }}{{ ' topo-v2-offrack' if off_rack else '' }}" <div class="topo-v2-node topo-v2-host topo-host topo-v2-status-{{ st }}{{ ' topo-v2-offrack' if off_rack else '' }}"
+13 -6
View File
@@ -38,6 +38,11 @@ function fmtRateBar(bytesPerSec, linkSpeedMbps) {
return Math.min(100, (mbps / linkSpeedMbps) * 100); return Math.min(100, (mbps / linkSpeedMbps) * 100);
} }
function trafficBarClass(pct, isTx) {
if (pct > 85) return 'lt-progress--red';
return isTx ? '' : 'lt-progress--cyan';
}
function fmtSpeed(mbps) { function fmtSpeed(mbps) {
if (mbps === null || mbps === undefined) return ''; if (mbps === null || mbps === undefined) return '';
if (mbps >= 1000) return (mbps/1000).toFixed(0) + ' Gbps'; if (mbps >= 1000) return (mbps/1000).toFixed(0) + ' Gbps';
@@ -226,12 +231,12 @@ function renderIfaceCard(ifaceName, d) {
<div class="traffic-section"> <div class="traffic-section">
<div class="traffic-row"> <div class="traffic-row">
<span class="traffic-label">TX</span> <span class="traffic-label">TX</span>
<div class="traffic-bar-track"><div class="traffic-bar-fill traffic-tx" style="width:${txPct}%"></div></div> <div class="lt-progress ${trafficBarClass(txPct, true)}"><div class="lt-progress-bar" style="width:${txPct}%"></div></div>
<span class="traffic-value">${fmtRate(d.tx_bytes_rate)}</span> <span class="traffic-value">${fmtRate(d.tx_bytes_rate)}</span>
</div> </div>
<div class="traffic-row"> <div class="traffic-row">
<span class="traffic-label">RX</span> <span class="traffic-label">RX</span>
<div class="traffic-bar-track"><div class="traffic-bar-fill traffic-rx" style="width:${rxPct}%"></div></div> <div class="lt-progress ${trafficBarClass(rxPct, false)}"><div class="lt-progress-bar" style="width:${rxPct}%"></div></div>
<span class="traffic-value">${fmtRate(d.rx_bytes_rate)}</span> <span class="traffic-value">${fmtRate(d.rx_bytes_rate)}</span>
</div> </div>
</div> </div>
@@ -289,12 +294,12 @@ function renderPortCard(portName, d) {
<div class="traffic-section"> <div class="traffic-section">
<div class="traffic-row"> <div class="traffic-row">
<span class="traffic-label">TX</span> <span class="traffic-label">TX</span>
<div class="traffic-bar-track"><div class="traffic-bar-fill traffic-tx" style="width:${txPct}%"></div></div> <div class="lt-progress ${trafficBarClass(txPct, true)}"><div class="lt-progress-bar" style="width:${txPct}%"></div></div>
<span class="traffic-value">${fmtRate(d.tx_bytes_rate)}</span> <span class="traffic-value">${fmtRate(d.tx_bytes_rate)}</span>
</div> </div>
<div class="traffic-row"> <div class="traffic-row">
<span class="traffic-label">RX</span> <span class="traffic-label">RX</span>
<div class="traffic-bar-track"><div class="traffic-bar-fill traffic-rx" style="width:${rxPct}%"></div></div> <div class="lt-progress ${trafficBarClass(rxPct, false)}"><div class="lt-progress-bar" style="width:${rxPct}%"></div></div>
<span class="traffic-value">${fmtRate(d.rx_bytes_rate)}</span> <span class="traffic-value">${fmtRate(d.rx_bytes_rate)}</span>
</div> </div>
</div> </div>
@@ -477,10 +482,12 @@ function checkLinksStale(updatedStr) {
if (!banner) { if (!banner) {
banner = document.createElement('div'); banner = document.createElement('div');
banner.id = 'links-stale-banner'; banner.id = 'links-stale-banner';
banner.className = 'stale-banner'; banner.className = 'lt-alert lt-alert--warning';
banner.innerHTML = '<span class="lt-alert-icon">⚠</span><div class="lt-alert-body"><div class="lt-alert-msg"></div></div>';
document.getElementById('links-container').prepend(banner); document.getElementById('links-container').prepend(banner);
} }
banner.textContent = `⚠ Link data may be stale — last updated ${Math.floor(age/60)}m ago.`; banner.querySelector('.lt-alert-msg').textContent =
`Link data may be stale — last updated ${Math.floor(age/60)}m ago.`;
banner.style.display = ''; banner.style.display = '';
} else if (banner) { } else if (banner) {
banner.style.display = 'none'; banner.style.display = 'none';
+4 -2
View File
@@ -95,7 +95,8 @@
<tbody> <tbody>
{% for s in active %} {% for s in active %}
<tr id="sup-row-{{ s.id }}"> <tr id="sup-row-{{ s.id }}">
<td><span class="lt-badge badge-info">{{ s.target_type }}</span></td> {%- set _sup_badge = {'host':'badge-warning','interface':'badge-info','unifi_device':'badge-purple','all':'badge-critical'} -%}
<td><span class="lt-badge {{ _sup_badge.get(s.target_type, 'badge-neutral') }}">{{ s.target_type }}</span></td>
<td>{{ s.target_name or 'all' }}</td> <td>{{ s.target_name or 'all' }}</td>
<td>{{ s.target_detail or '' }}</td> <td>{{ s.target_detail or '' }}</td>
<td>{{ s.reason }}</td> <td>{{ s.reason }}</td>
@@ -214,9 +215,10 @@
return; return;
} }
if (badge) badge.textContent = rows.length; if (badge) badge.textContent = rows.length;
const SUP_BADGE = {host:'badge-warning', interface:'badge-info', unifi_device:'badge-purple', all:'badge-critical'};
const tbody = rows.map(s => ` const tbody = rows.map(s => `
<tr id="sup-row-${s.id}"> <tr id="sup-row-${s.id}">
<td><span class="lt-badge badge-info">${lt.escHtml(s.target_type)}</span></td> <td><span class="lt-badge ${SUP_BADGE[s.target_type] || 'badge-neutral'}">${lt.escHtml(s.target_type)}</span></td>
<td>${lt.escHtml(s.target_name || 'all')}</td> <td>${lt.escHtml(s.target_name || 'all')}</td>
<td>${lt.escHtml(s.target_detail || '')}</td> <td>${lt.escHtml(s.target_detail || '')}</td>
<td>${lt.escHtml(s.reason)}</td> <td>${lt.escHtml(s.reason)}</td>