1 Commits

Author SHA1 Message Date
jared c3aa3bea6f TDS polish: lt-frame tables, lt-stats-grid link summary, settings-aware refresh
Lint / Python (flake8) (push) Successful in 40s
Lint / JS (eslint) (push) Successful in 8s
Security / Python Security (bandit) (push) Failing after 42s
Test / Python Tests (pytest) (push) Successful in 50s
Lint / Notify on failure (push) Has been skipped
Lint / Deploy (push) Successful in 3s
- links.html: replace custom link-summary-panel with lt-stats-grid/lt-stat-card
  showing total interfaces, ports down, errors, and PoE load
- suppressions.html: wrap active suppressions and history tables in lt-frame
  with lt-section-header labels
- inspector.html: wire auto-refresh to gandalfSettings (respects interval pill),
  fix updated timestamp to use locale time

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 17:15:48 -04:00
3 changed files with 130 additions and 90 deletions
+10 -3
View File
@@ -427,9 +427,10 @@ function renderInspector(data) {
const main = document.getElementById('inspector-main'); const main = document.getElementById('inspector-main');
const switches = data.unifi_switches || {}; const switches = data.unifi_switches || {};
const upd = data.updated ? `Updated: ${data.updated}` : '';
const updEl = document.getElementById('inspector-updated'); const updEl = document.getElementById('inspector-updated');
if (updEl) updEl.textContent = upd; if (updEl && data.updated) {
updEl.textContent = 'Updated: ' + new Date(data.updated + (data.updated.includes('Z') ? '' : 'Z')).toLocaleTimeString();
}
if (!Object.keys(switches).length) { if (!Object.keys(switches).length) {
main.innerHTML = '<p class="empty-state">No switch data available. Monitor may still be initialising.</p>'; main.innerHTML = '<p class="empty-state">No switch data available. Monitor may still be initialising.</p>';
@@ -465,7 +466,13 @@ async function loadInspector() {
} }
loadInspector(); loadInspector();
lt.autoRefresh.start(loadInspector, 60000); var _inspInterval = (window.gandalfSettings && window.gandalfSettings.refreshInterval) || 60;
if (_inspInterval > 0) lt.autoRefresh.start(loadInspector, Math.max(_inspInterval, 15) * 1000);
window.onGandalfSettingsChanged = function(s) {
lt.autoRefresh.stop();
if (s.refreshInterval > 0) lt.autoRefresh.start(loadInspector, Math.max(s.refreshInterval, 15) * 1000);
};
lt.keys.on('Escape', () => { lt.keys.on('Escape', () => {
if (document.getElementById('inspector-panel').classList.contains('open')) closePanel(); if (document.getElementById('inspector-panel').classList.contains('open')) closePanel();
}); });
+39 -21
View File
@@ -384,33 +384,51 @@ function buildLinkSummary(hosts, unifiSwitches) {
if ((d.tx_errs_rate || 0) > 0 || (d.rx_errs_rate || 0) > 0) errIfaces++; if ((d.tx_errs_rate || 0) > 0 || (d.rx_errs_rate || 0) > 0) errIfaces++;
} }
} }
let swTotal = 0, swDown = 0;
for (const sw of Object.values(unifiSwitches || {})) { for (const sw of Object.values(unifiSwitches || {})) {
for (const p of Object.values(sw.ports || {})) { for (const p of Object.values(sw.ports || {})) {
totalPoe += p.poe_power || 0; totalPoe += p.poe_power || 0;
swTotal++;
if (!p.up) swDown++;
} }
} }
const hasAlerts = downIfaces > 0 || errIfaces > 0; const allTotal = totalIfaces + swTotal;
return ` const allDown = downIfaces + swDown;
<div class="link-summary-panel ${hasAlerts ? 'link-summary-has-alerts' : ''}"> const downColor = allDown > 0 ? 'var(--red)' : 'var(--green)';
<div class="link-summary-grid"> const errColor = errIfaces > 0 ? 'var(--amber)' : 'var(--green)';
<div class="link-summary-stat"> const downCardCls = allDown > 0 ? ' lt-stat-card--alert' : '';
<span class="lss-label">Total Interfaces</span> const poeCard = totalPoe > 0 ? `
<span class="lss-value">${totalIfaces}</span> <div class="lt-stat-card">
</div> <span class="lt-stat-icon" aria-hidden="true" style="color:var(--amber)">⚡</span>
<div class="link-summary-stat ${downIfaces ? 'lss-alert' : ''}"> <div class="lt-stat-info">
<span class="lss-label">Interfaces Down</span> <span class="lt-stat-value" style="color:var(--amber)">${totalPoe.toFixed(1)}</span>
<span class="lss-value ${downIfaces ? 'val-crit' : 'val-good'}">${downIfaces}</span> <span class="lt-stat-label">PoE Load (W)</span>
</div>
<div class="link-summary-stat ${errIfaces ? 'lss-alert' : ''}">
<span class="lss-label">With Errors</span>
<span class="lss-value ${errIfaces ? 'val-warn' : 'val-good'}">${errIfaces}</span>
</div>
${totalPoe > 0 ? `
<div class="link-summary-stat">
<span class="lss-label">PoE Load</span>
<span class="lss-value">${totalPoe.toFixed(1)} <span class="lss-sub">W</span></span>
</div>` : ''}
</div> </div>
</div>` : '';
return `
<div class="lt-stats-grid" style="margin-bottom:16px">
<div class="lt-stat-card">
<span class="lt-stat-icon" aria-hidden="true" style="color:var(--cyan)">⬡</span>
<div class="lt-stat-info">
<span class="lt-stat-value" style="color:var(--cyan)">${allTotal}</span>
<span class="lt-stat-label">Interfaces</span>
</div>
</div>
<div class="lt-stat-card${downCardCls}">
<span class="lt-stat-icon" aria-hidden="true" style="color:${downColor}">●</span>
<div class="lt-stat-info">
<span class="lt-stat-value" style="color:${downColor}">${allDown}</span>
<span class="lt-stat-label">Ports Down</span>
</div>
</div>
<div class="lt-stat-card">
<span class="lt-stat-icon" aria-hidden="true" style="color:${errColor}">▲</span>
<div class="lt-stat-info">
<span class="lt-stat-value" style="color:${errColor}">${errIfaces}</span>
<span class="lt-stat-label">With Errors</span>
</div>
</div>
${poeCard}
</div>`; </div>`;
} }
+81 -66
View File
@@ -85,33 +85,38 @@
</div> </div>
<div id="active-sup-wrap"> <div id="active-sup-wrap">
{% if active %} {% if active %}
<div class="lt-table-wrap"> <div class="lt-frame">
<table class="lt-table" id="active-sup-table"> <span class="lt-frame-bl">&#x255A;</span>
<caption class="lt-sr-only">Active suppression rules</caption> <span class="lt-frame-br">&#x255D;</span>
<thead> <div class="lt-section-header">Active Rules</div>
<tr> <div class="lt-table-wrap">
<th>Type</th><th>Target</th><th>Detail</th><th>Reason</th> <table class="lt-table" id="active-sup-table">
<th>By</th><th>Created</th><th>Expires</th><th>Actions</th> <caption class="lt-sr-only">Active suppression rules</caption>
</tr> <thead>
</thead> <tr>
<tbody> <th>Type</th><th>Target</th><th>Detail</th><th>Reason</th>
{% for s in active %} <th>By</th><th>Created</th><th>Expires</th><th>Actions</th>
<tr id="sup-row-{{ s.id }}"> </tr>
{%- set _sup_badge = {'host':'badge-warning','interface':'badge-info','unifi_device':'badge-purple','all':'badge-critical'} -%} </thead>
<td><span class="lt-badge {{ _sup_badge.get(s.target_type, 'badge-neutral') }}">{{ s.target_type }}</span></td> <tbody>
<td>{{ s.target_name or 'all' }}</td> {% for s in active %}
<td>{{ s.target_detail or '' }}</td> <tr id="sup-row-{{ s.id }}">
<td>{{ s.reason }}</td> {%- set _sup_badge = {'host':'badge-warning','interface':'badge-info','unifi_device':'badge-purple','all':'badge-critical'} -%}
<td>{{ s.suppressed_by }}</td> <td><span class="lt-badge {{ _sup_badge.get(s.target_type, 'badge-neutral') }}">{{ s.target_type }}</span></td>
<td class="ts-cell">{{ s.created_at }}</td> <td>{{ s.target_name or 'all' }}</td>
<td class="ts-cell">{% if s.expires_at %}{{ s.expires_at }}{% else %}<em>manual</em>{% endif %}</td> <td>{{ s.target_detail or '' }}</td>
<td> <td>{{ s.reason }}</td>
<button class="lt-btn lt-btn-danger lt-btn-sm" data-action="remove-sup" data-sup-id="{{ s.id }}">Remove</button> <td>{{ s.suppressed_by }}</td>
</td> <td class="ts-cell">{{ s.created_at }}</td>
</tr> <td class="ts-cell">{% if s.expires_at %}{{ s.expires_at }}{% else %}<em>manual</em>{% endif %}</td>
{% endfor %} <td>
</tbody> <button class="lt-btn lt-btn-danger lt-btn-sm" data-action="remove-sup" data-sup-id="{{ s.id }}">Remove</button>
</table> </td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
{% else %} {% else %}
<div class="lt-empty-state lt-empty-state--sm" id="no-active-msg"> <div class="lt-empty-state lt-empty-state--sm" id="no-active-msg">
@@ -130,36 +135,41 @@
<span class="g-section-badge">{{ history | length }}</span> <span class="g-section-badge">{{ history | length }}</span>
</div> </div>
{% if history %} {% if history %}
<div class="lt-table-wrap"> <div class="lt-frame">
<table class="lt-table lt-table-sm"> <span class="lt-frame-bl">&#x255A;</span>
<caption class="lt-sr-only">Suppression history</caption> <span class="lt-frame-br">&#x255D;</span>
<thead> <div class="lt-section-header">Suppression Log</div>
<tr> <div class="lt-table-wrap">
<th>Type</th><th>Target</th><th>Detail</th><th>Reason</th> <table class="lt-table lt-table-sm">
<th>By</th><th>Created</th><th>Expires</th><th>Active</th> <caption class="lt-sr-only">Suppression history</caption>
</tr> <thead>
</thead> <tr>
<tbody> <th>Type</th><th>Target</th><th>Detail</th><th>Reason</th>
{% for s in history %} <th>By</th><th>Created</th><th>Expires</th><th>Active</th>
<tr class="{% if not s.active %}row-resolved{% endif %}"> </tr>
<td>{{ s.target_type }}</td> </thead>
<td>{{ s.target_name or 'all' }}</td> <tbody>
<td>{{ s.target_detail or '' }}</td> {% for s in history %}
<td>{{ s.reason }}</td> <tr class="{% if not s.active %}row-resolved{% endif %}">
<td>{{ s.suppressed_by }}</td> <td>{{ s.target_type }}</td>
<td class="ts-cell">{{ s.created_at }}</td> <td>{{ s.target_name or 'all' }}</td>
<td class="ts-cell">{% if s.expires_at %}{{ s.expires_at }}{% else %}<em>manual</em>{% endif %}</td> <td>{{ s.target_detail or '' }}</td>
<td> <td>{{ s.reason }}</td>
{% if s.active %} <td>{{ s.suppressed_by }}</td>
<span class="lt-badge badge-ok">Yes</span> <td class="ts-cell">{{ s.created_at }}</td>
{% else %} <td class="ts-cell">{% if s.expires_at %}{{ s.expires_at }}{% else %}<em>manual</em>{% endif %}</td>
<span class="lt-badge badge-neutral">No</span> <td>
{% endif %} {% if s.active %}
</td> <span class="lt-badge badge-ok">Yes</span>
</tr> {% else %}
{% endfor %} <span class="lt-badge badge-neutral">No</span>
</tbody> {% endif %}
</table> </td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
{% else %} {% else %}
<div class="lt-empty-state lt-empty-state--sm"> <div class="lt-empty-state lt-empty-state--sm">
@@ -237,15 +247,20 @@
<td><button class="lt-btn lt-btn-danger lt-btn-sm" data-action="remove-sup" data-sup-id="${s.id}">Remove</button></td> <td><button class="lt-btn lt-btn-danger lt-btn-sm" data-action="remove-sup" data-sup-id="${s.id}">Remove</button></td>
</tr>`).join(''); </tr>`).join('');
wrap.innerHTML = ` wrap.innerHTML = `
<div class="lt-table-wrap"> <div class="lt-frame">
<table class="lt-table" id="active-sup-table"> <span class="lt-frame-bl">&#x255A;</span>
<caption class="lt-sr-only">Active suppression rules</caption> <span class="lt-frame-br">&#x255D;</span>
<thead><tr> <div class="lt-section-header">Active Rules</div>
<th>Type</th><th>Target</th><th>Detail</th><th>Reason</th> <div class="lt-table-wrap">
<th>By</th><th>Created</th><th>Expires</th><th>Actions</th> <table class="lt-table" id="active-sup-table">
</tr></thead> <caption class="lt-sr-only">Active suppression rules</caption>
<tbody>${tbody}</tbody> <thead><tr>
</table> <th>Type</th><th>Target</th><th>Detail</th><th>Reason</th>
<th>By</th><th>Created</th><th>Expires</th><th>Actions</th>
</tr></thead>
<tbody>${tbody}</tbody>
</table>
</div>
</div>`; </div>`;
} }