Fix field name mismatches, add events filter, in-place suppression refresh
Lint / Python (flake8) (push) Failing after 50s
Lint / JS (eslint) (push) Successful in 7s
Test / Python Tests (pytest) (push) Successful in 51s
Lint / Notify on failure (push) Successful in 2s
Lint / Deploy (push) Has been skipped
Security / Python Security (bandit) (push) Failing after 59s

- links.html: fix all field name bugs (auto_negotiation→autoneg, full_duplex,
  tx/rx_errors/drops_per_sec→_rate, tx/rx_bytes_per_sec→_rate, poe_total_w/poe_max_w
  computed from ports, renderUnifiSwitches uses top-level updated timestamp)
- suppressions.html: in-place DOM refresh after create/remove (no page reload),
  datalist autocomplete for target names, form reset after submit
- inspector.html: ESC key closes detail panel via lt.keys.on
- index.html: events filter bar with search input + severity pills (All/Critical/Warning),
  MutationObserver re-applies filter after dynamic updates
- style.css: g-section-actions, events-filter-bar, sev-pills layout
- app.js/db.py/monitor.py: carry forward prior session fixes (Promise.allSettled,
  daemon_ok, stale connection handling, double Prometheus call, self.cfg fix)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 23:35:02 -04:00
parent b6cd168542
commit c45dd007d1
9 changed files with 274 additions and 90 deletions
+73 -6
View File
@@ -29,7 +29,13 @@
<div class="lt-form-group" id="name-group">
<label class="lt-label" for="s-name">Target Name <span class="required">*</span></label>
<input type="text" class="lt-input" id="s-name" name="target_name"
placeholder="hostname or device name" autocomplete="off">
placeholder="hostname or device name" autocomplete="off"
list="target-name-list">
<datalist id="target-name-list">
{% for name in snapshot.hosts.keys() | sort %}
<option value="{{ name }}">
{% endfor %}
</datalist>
</div>
<div class="lt-form-group" id="detail-group" style="display:none">
<label class="lt-label" for="s-detail">Interface Name</label>
@@ -70,11 +76,12 @@
</section>
<!-- ── Active suppressions ────────────────────────────────────────── -->
<section class="g-section">
<section class="g-section" id="active-sup-section">
<div class="g-section-header">
<h2 class="g-section-title">Active Suppressions</h2>
<span class="g-section-badge">{{ active | length }}</span>
<span class="g-section-badge" id="active-sup-badge">{{ active | length }}</span>
</div>
<div id="active-sup-wrap">
{% if active %}
<div class="lt-table-wrap">
<table class="lt-table" id="active-sup-table">
@@ -104,8 +111,9 @@
</table>
</div>
{% else %}
<p class="empty-state">No active suppressions.</p>
<p class="empty-state" id="no-active-msg">No active suppressions.</p>
{% endif %}
</div>
</section>
<!-- ── Suppression history ────────────────────────────────────────── -->
@@ -191,15 +199,59 @@
const hint = document.getElementById('s-dur-hint');
if (mins) {
const h = Math.floor(mins/60), m = mins%60;
hint.textContent = `Expires in ${h?h+'h ':''} ${m?m+'m':''}`.trim()+'.';
hint.textContent = `Expires in ${h?h+'h ':''}${m?m+'m':''}`.trim()+'.';
} else {
hint.textContent = 'Persists until manually removed.';
}
}
function renderActiveRows(rows) {
const wrap = document.getElementById('active-sup-wrap');
const badge = document.getElementById('active-sup-badge');
if (!rows || !rows.length) {
wrap.innerHTML = '<p class="empty-state" id="no-active-msg">No active suppressions.</p>';
if (badge) badge.textContent = '0';
return;
}
if (badge) badge.textContent = rows.length;
const tbody = rows.map(s => `
<tr id="sup-row-${s.id}">
<td><span class="lt-badge badge-info">${lt.escHtml(s.target_type)}</span></td>
<td>${lt.escHtml(s.target_name || 'all')}</td>
<td>${lt.escHtml(s.target_detail || '')}</td>
<td>${lt.escHtml(s.reason)}</td>
<td>${lt.escHtml(s.suppressed_by)}</td>
<td class="ts-cell">${lt.escHtml(s.created_at || '')}</td>
<td class="ts-cell">${s.expires_at ? lt.escHtml(s.expires_at) : '<em>manual</em>'}</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('');
wrap.innerHTML = `
<div class="lt-table-wrap">
<table class="lt-table" id="active-sup-table">
<caption class="lt-sr-only">Active suppression rules</caption>
<thead><tr>
<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>`;
}
async function refreshActive() {
try {
const rows = await lt.api.get('/api/suppressions');
renderActiveRows(rows);
} catch (err) {
console.warn('Failed to refresh suppressions:', err);
}
}
async function createSuppression(e) {
e.preventDefault();
const form = e.target;
const btn = form.querySelector('[type="submit"]');
btn.classList.add('is-loading');
const payload = {
target_type: form.target_type.value,
target_name: form.target_name ? form.target_name.value : '',
@@ -210,9 +262,16 @@
try {
await lt.api.post('/api/suppressions', payload);
showToast('Suppression applied', 'success');
setTimeout(() => location.reload(), 800);
form.reset();
onTypeChange();
document.querySelectorAll('.duration-pills .pill').forEach(p => p.classList.remove('active'));
document.querySelector('.duration-pills .pill-manual')?.classList.add('active');
document.getElementById('s-dur-hint').textContent = 'Persists until manually removed.';
await refreshActive();
} catch (err) {
showToast(err.message || 'Error', 'error');
} finally {
btn.classList.remove('is-loading');
}
}
@@ -221,6 +280,14 @@
try {
await lt.api.delete(`/api/suppressions/${id}`);
document.getElementById(`sup-row-${id}`)?.remove();
const badge = document.getElementById('active-sup-badge');
if (badge) badge.textContent = Math.max(0, parseInt(badge.textContent || '0') - 1);
const tbody = document.querySelector('#active-sup-table tbody');
if (tbody && !tbody.children.length) {
document.getElementById('active-sup-wrap').innerHTML =
'<p class="empty-state" id="no-active-msg">No active suppressions.</p>';
if (badge) badge.textContent = '0';
}
showToast('Suppression removed', 'success');
} catch (err) {
showToast(err.message || 'Failed to remove suppression', 'error');