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
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:
@@ -275,6 +275,17 @@
|
||||
{% if summary.critical or summary.warning %}
|
||||
<span class="g-section-badge">{{ (summary.critical or 0) + (summary.warning or 0) }}</span>
|
||||
{% endif %}
|
||||
<div class="g-section-actions">
|
||||
<div class="events-filter-bar">
|
||||
<input type="search" class="lt-input lt-input-sm" id="events-search"
|
||||
placeholder="Filter by target, type, description…" autocomplete="off">
|
||||
<div class="sev-pills">
|
||||
<button type="button" class="pill active" data-sev="">All</button>
|
||||
<button type="button" class="pill" data-sev="critical">Critical</button>
|
||||
<button type="button" class="pill" data-sev="warning">Warning</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="events-table-wrap">
|
||||
{% if events %}
|
||||
@@ -462,5 +473,35 @@
|
||||
document.querySelectorAll('.event-duration[data-first][data-resolved]').forEach(el => {
|
||||
el.textContent = fmtDuration(el.dataset.first, el.dataset.resolved);
|
||||
});
|
||||
|
||||
// ── Events table filter ────────────────────────────────────────
|
||||
let _filterSev = '';
|
||||
|
||||
function applyEventsFilter() {
|
||||
const q = (document.getElementById('events-search')?.value || '').toLowerCase();
|
||||
const tbody = document.querySelector('#events-table tbody');
|
||||
if (!tbody) return;
|
||||
tbody.querySelectorAll('tr').forEach(row => {
|
||||
if (row.children.length < 3) { row.style.display = ''; return; }
|
||||
const sevMatch = !_filterSev || row.classList.contains(`row-${_filterSev}`);
|
||||
const textMatch = !q || row.textContent.toLowerCase().includes(q);
|
||||
row.style.display = (sevMatch && textMatch) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('events-search')?.addEventListener('input', applyEventsFilter);
|
||||
|
||||
document.querySelector('.sev-pills')?.addEventListener('click', e => {
|
||||
const pill = e.target.closest('.pill[data-sev]');
|
||||
if (!pill) return;
|
||||
document.querySelectorAll('.sev-pills .pill').forEach(p => p.classList.remove('active'));
|
||||
pill.classList.add('active');
|
||||
_filterSev = pill.dataset.sev;
|
||||
applyEventsFilter();
|
||||
});
|
||||
|
||||
// Re-apply filter after dynamic table updates
|
||||
new MutationObserver(applyEventsFilter)
|
||||
.observe(document.getElementById('events-table-wrap'), { childList: true, subtree: true });
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -464,6 +464,9 @@ async function loadInspector() {
|
||||
|
||||
loadInspector();
|
||||
lt.autoRefresh.start(loadInspector, 60000);
|
||||
lt.keys.on('Escape', () => {
|
||||
if (document.getElementById('inspector-panel').classList.contains('open')) closePanel();
|
||||
});
|
||||
|
||||
// ── Link Diagnostics ─────────────────────────────────────────────────
|
||||
let _diagPollTimer = null;
|
||||
|
||||
+44
-36
@@ -108,9 +108,9 @@ function voltageClass(v) {
|
||||
|
||||
function errorBadges(d) {
|
||||
const badges = [];
|
||||
if ((d.tx_errors_per_sec || 0) > 0 || (d.rx_errors_per_sec || 0) > 0)
|
||||
if ((d.tx_errs_rate || 0) > 0.001 || (d.rx_errs_rate || 0) > 0.001)
|
||||
badges.push('<span class="link-alert-badge">ERR</span>');
|
||||
if ((d.tx_drops_per_sec || 0) > 0 || (d.rx_drops_per_sec || 0) > 0)
|
||||
if ((d.tx_drops_rate || 0) > 0.001 || (d.rx_drops_rate || 0) > 0.001)
|
||||
badges.push('<span class="link-alert-badge link-alert-amber">DROP</span>');
|
||||
if ((d.carrier_changes || 0) > 3)
|
||||
badges.push('<span class="link-alert-badge link-alert-amber">FLAP</span>');
|
||||
@@ -119,14 +119,15 @@ function errorBadges(d) {
|
||||
|
||||
// ── Render a single server interface card ─────────────────────────
|
||||
function renderIfaceCard(ifaceName, d) {
|
||||
const isDown = d.link_detected === false || d.admin_status === 'down';
|
||||
const mediaTag = d.media_type === 'fibre' ? 'type-fibre'
|
||||
: d.media_type === 'da' ? 'type-da'
|
||||
const isDown = d.link_detected === false;
|
||||
const pt = (d.port_type || '').toUpperCase();
|
||||
const mediaTag = pt === 'FIBRE' || pt === 'SFP' || pt.includes('FIBRE') ? 'type-fibre'
|
||||
: pt === 'DA' ? 'type-da'
|
||||
: 'type-copper';
|
||||
const mediaLabel = d.media_type || '–';
|
||||
const mediaLabel = d.port_type || '–';
|
||||
const speedStr = d.speed_mbps ? fmtSpeed(d.speed_mbps) : '–';
|
||||
const txPct = fmtRateBar(d.tx_bytes_per_sec, d.speed_mbps);
|
||||
const rxPct = fmtRateBar(d.rx_bytes_per_sec, d.speed_mbps);
|
||||
const txPct = fmtRateBar(d.tx_bytes_rate, d.speed_mbps);
|
||||
const rxPct = fmtRateBar(d.rx_bytes_rate, d.speed_mbps);
|
||||
|
||||
let sfpHtml = '';
|
||||
if (d.sfp && Object.keys(d.sfp).length > 0) {
|
||||
@@ -142,7 +143,7 @@ function renderIfaceCard(ifaceName, d) {
|
||||
<div class="sfp-panel">
|
||||
<div class="sfp-vendor-row">
|
||||
${s.vendor ? `<span>${escHtml(s.vendor)}</span>` : ''}
|
||||
${s.part_number ? ` / <span>${escHtml(s.part_number)}</span>` : ''}
|
||||
${s.part_no ? ` / <span>${escHtml(s.part_no)}</span>` : ''}
|
||||
</div>
|
||||
<div class="sfp-grid">
|
||||
<div class="sfp-stat">
|
||||
@@ -199,7 +200,7 @@ function renderIfaceCard(ifaceName, d) {
|
||||
</div>
|
||||
<div class="link-stat">
|
||||
<span class="link-stat-label">Auto-neg</span>
|
||||
<span class="link-stat-value">${d.auto_negotiation == null ? '–' : d.auto_negotiation ? 'On' : 'Off'}</span>
|
||||
<span class="link-stat-value">${d.auto_neg == null ? '–' : d.auto_neg ? 'On' : 'Off'}</span>
|
||||
</div>
|
||||
<div class="link-stat">
|
||||
<span class="link-stat-label">Carrier Δ</span>
|
||||
@@ -207,31 +208,31 @@ function renderIfaceCard(ifaceName, d) {
|
||||
</div>
|
||||
<div class="link-stat">
|
||||
<span class="link-stat-label">TX Err/s</span>
|
||||
<span class="link-stat-value">${fmtErrors(d.tx_errors_per_sec)}</span>
|
||||
<span class="link-stat-value">${fmtErrors(d.tx_errs_rate)}</span>
|
||||
</div>
|
||||
<div class="link-stat">
|
||||
<span class="link-stat-label">RX Err/s</span>
|
||||
<span class="link-stat-value">${fmtErrors(d.rx_errors_per_sec)}</span>
|
||||
<span class="link-stat-value">${fmtErrors(d.rx_errs_rate)}</span>
|
||||
</div>
|
||||
<div class="link-stat">
|
||||
<span class="link-stat-label">TX Drop/s</span>
|
||||
<span class="link-stat-value">${fmtErrors(d.tx_drops_per_sec)}</span>
|
||||
<span class="link-stat-value">${fmtErrors(d.tx_drops_rate)}</span>
|
||||
</div>
|
||||
<div class="link-stat">
|
||||
<span class="link-stat-label">RX Drop/s</span>
|
||||
<span class="link-stat-value">${fmtErrors(d.rx_drops_per_sec)}</span>
|
||||
<span class="link-stat-value">${fmtErrors(d.rx_drops_rate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="traffic-section">
|
||||
<div class="traffic-row">
|
||||
<span class="traffic-label">TX</span>
|
||||
<div class="traffic-bar-track"><div class="traffic-bar-fill traffic-tx" style="width:${txPct}%"></div></div>
|
||||
<span class="traffic-value">${fmtRate(d.tx_bytes_per_sec)}</span>
|
||||
<span class="traffic-value">${fmtRate(d.tx_bytes_rate)}</span>
|
||||
</div>
|
||||
<div class="traffic-row">
|
||||
<span class="traffic-label">RX</span>
|
||||
<div class="traffic-bar-track"><div class="traffic-bar-fill traffic-rx" style="width:${rxPct}%"></div></div>
|
||||
<span class="traffic-value">${fmtRate(d.rx_bytes_per_sec)}</span>
|
||||
<span class="traffic-value">${fmtRate(d.rx_bytes_rate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
${sfpHtml}
|
||||
@@ -242,13 +243,13 @@ function renderIfaceCard(ifaceName, d) {
|
||||
function renderPortCard(portName, d) {
|
||||
const isDown = !d.up;
|
||||
const speedStr = d.speed_mbps ? fmtSpeed(d.speed_mbps) : '–';
|
||||
const txPct = fmtRateBar(d.tx_bytes_per_sec, d.speed_mbps);
|
||||
const rxPct = fmtRateBar(d.rx_bytes_per_sec, d.speed_mbps);
|
||||
const txPct = fmtRateBar(d.tx_bytes_rate, d.speed_mbps);
|
||||
const rxPct = fmtRateBar(d.rx_bytes_rate, d.speed_mbps);
|
||||
const numBadge = d.port_idx != null ? `<span class="port-badge port-badge-num">#${d.port_idx}</span>` : '';
|
||||
const uplinkBadge = d.is_uplink ? `<span class="port-badge port-badge-uplink">UPLINK</span>` : '';
|
||||
const poeBadge = d.poe_power_w ? `<span class="port-badge port-badge-poe">PoE ${d.poe_power_w.toFixed(1)}W</span>` : '';
|
||||
const poeBadge = d.poe_power ? `<span class="port-badge port-badge-poe">PoE ${d.poe_power.toFixed(1)}W</span>` : '';
|
||||
const lldpLine = d.lldp ? `<div class="port-lldp">→ ${escHtml(d.lldp.system_name || '')} (${escHtml(d.lldp.port_id || '')})</div>` : '';
|
||||
const poeLine = d.poe_class ? `<div class="port-poe-info">PoE ${escHtml(d.poe_class)} · max ${d.poe_power_w_max != null ? d.poe_power_w_max.toFixed(1)+'W' : '–'}</div>` : '';
|
||||
const poeLine = d.poe_class ? `<div class="port-poe-info">PoE ${escHtml(d.poe_class)} · max ${d.poe_max_power != null ? d.poe_max_power.toFixed(1)+'W' : '–'}</div>` : '';
|
||||
|
||||
return `
|
||||
<div class="link-iface-card ${isDown ? 'port-down' : ''}">
|
||||
@@ -266,55 +267,60 @@ function renderPortCard(portName, d) {
|
||||
</div>
|
||||
<div class="link-stat">
|
||||
<span class="link-stat-label">Duplex</span>
|
||||
<span class="link-stat-value">${fmtDuplex(d.duplex)}</span>
|
||||
<span class="link-stat-value">${d.full_duplex == null ? '–' : d.full_duplex ? 'Full' : 'Half'}</span>
|
||||
</div>
|
||||
<div class="link-stat">
|
||||
<span class="link-stat-label">Auto-neg</span>
|
||||
<span class="link-stat-value">${d.auto_negotiation == null ? '–' : d.auto_negotiation ? 'On' : 'Off'}</span>
|
||||
<span class="link-stat-value">${d.autoneg == null ? '–' : d.autoneg ? 'On' : 'Off'}</span>
|
||||
</div>
|
||||
<div class="link-stat">
|
||||
<span class="link-stat-label">TX Err/s</span>
|
||||
<span class="link-stat-value">${fmtErrors(d.tx_errors_per_sec)}</span>
|
||||
<span class="link-stat-value">${fmtErrors(d.tx_errs_rate)}</span>
|
||||
</div>
|
||||
<div class="link-stat">
|
||||
<span class="link-stat-label">RX Err/s</span>
|
||||
<span class="link-stat-value">${fmtErrors(d.rx_errors_per_sec)}</span>
|
||||
<span class="link-stat-value">${fmtErrors(d.rx_errs_rate)}</span>
|
||||
</div>
|
||||
<div class="link-stat">
|
||||
<span class="link-stat-label">TX Drop/s</span>
|
||||
<span class="link-stat-value">${fmtErrors(d.tx_drops_per_sec)}</span>
|
||||
<span class="link-stat-value">${fmtErrors(d.tx_drops_rate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="traffic-section">
|
||||
<div class="traffic-row">
|
||||
<span class="traffic-label">TX</span>
|
||||
<div class="traffic-bar-track"><div class="traffic-bar-fill traffic-tx" style="width:${txPct}%"></div></div>
|
||||
<span class="traffic-value">${fmtRate(d.tx_bytes_per_sec)}</span>
|
||||
<span class="traffic-value">${fmtRate(d.tx_bytes_rate)}</span>
|
||||
</div>
|
||||
<div class="traffic-row">
|
||||
<span class="traffic-label">RX</span>
|
||||
<div class="traffic-bar-track"><div class="traffic-bar-fill traffic-rx" style="width:${rxPct}%"></div></div>
|
||||
<span class="traffic-value">${fmtRate(d.rx_bytes_per_sec)}</span>
|
||||
<span class="traffic-value">${fmtRate(d.rx_bytes_rate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ── Render all UniFi switches ─────────────────────────────────────
|
||||
function renderUnifiSwitches(unifiSwitches) {
|
||||
function renderUnifiSwitches(unifiSwitches, dataUpdated) {
|
||||
if (!unifiSwitches || !Object.keys(unifiSwitches).length) return '';
|
||||
const updStr = dataUpdated
|
||||
? new Date(dataUpdated.replace(' UTC', 'Z').replace(' ', 'T')).toLocaleTimeString()
|
||||
: '';
|
||||
const html = Object.entries(unifiSwitches).map(([swName, sw]) => {
|
||||
const ports = sw.ports || {};
|
||||
const portValues = Object.values(ports);
|
||||
const portCards = Object.entries(ports)
|
||||
.sort(([,a],[,b]) => (a.port_idx||0) - (b.port_idx||0))
|
||||
.map(([pname, d]) => renderPortCard(pname, d)).join('');
|
||||
const updStr = sw.updated ? new Date(sw.updated + (sw.updated.includes('Z') ? '' : 'Z')).toLocaleTimeString() : '';
|
||||
const poeLoad = sw.poe_total_w != null ? ` · PoE ${sw.poe_total_w.toFixed(1)}W` : '';
|
||||
const poe_total_w = portValues.reduce((s, p) => s + (p.poe_power || 0), 0);
|
||||
const poe_max_w = portValues.reduce((s, p) => s + (p.poe_max_power || 0), 0);
|
||||
const poeLoad = poe_total_w > 0 ? ` · PoE ${poe_total_w.toFixed(1)}W` : '';
|
||||
|
||||
// PoE utilisation bar
|
||||
let poebar = '';
|
||||
if (sw.poe_total_w != null && sw.poe_max_w) {
|
||||
const pct = Math.min(100, (sw.poe_total_w / sw.poe_max_w) * 100);
|
||||
if (poe_total_w > 0 && poe_max_w > 0) {
|
||||
const pct = Math.min(100, (poe_total_w / poe_max_w) * 100);
|
||||
const cls = pct > 80 ? 'poe-bar-crit' : pct > 60 ? 'poe-bar-warn' : 'poe-bar-ok';
|
||||
poebar = `<div class="poe-bar-track"><div class="poe-bar-fill ${cls}" style="width:${pct}%"></div></div>`;
|
||||
}
|
||||
@@ -324,7 +330,7 @@ function renderUnifiSwitches(unifiSwitches) {
|
||||
<div class="link-host-title" onclick="togglePanel(this.closest('.link-host-panel'))">
|
||||
<span class="link-host-name">${escHtml(swName)}</span>
|
||||
<span class="link-host-ip">${escHtml(sw.ip || '')}</span>
|
||||
<span class="link-host-upd">${updStr}${poeLoad}</span>
|
||||
<span class="link-host-upd">${escHtml(sw.model || '')}${updStr ? ' · ' + updStr : ''}${poeLoad}</span>
|
||||
${poebar}
|
||||
<span class="panel-toggle">[–]</span>
|
||||
</div>
|
||||
@@ -368,11 +374,13 @@ function buildLinkSummary(hosts, unifiSwitches) {
|
||||
for (const d of Object.values(ifaces)) {
|
||||
totalIfaces++;
|
||||
if (d.link_detected === false) downIfaces++;
|
||||
if ((d.tx_errors_per_sec || 0) > 0 || (d.rx_errors_per_sec || 0) > 0) errIfaces++;
|
||||
if ((d.tx_errs_rate || 0) > 0 || (d.rx_errs_rate || 0) > 0) errIfaces++;
|
||||
}
|
||||
}
|
||||
for (const sw of Object.values(unifiSwitches || {})) {
|
||||
totalPoe += sw.poe_total_w || 0;
|
||||
for (const p of Object.values(sw.ports || {})) {
|
||||
totalPoe += p.poe_power || 0;
|
||||
}
|
||||
}
|
||||
const hasAlerts = downIfaces > 0 || errIfaces > 0;
|
||||
return `
|
||||
@@ -434,7 +442,7 @@ function renderLinks(data) {
|
||||
</div>`);
|
||||
}
|
||||
|
||||
parts.push(renderUnifiSwitches(unifiSwitches));
|
||||
parts.push(renderUnifiSwitches(unifiSwitches, data.updated));
|
||||
parts.push('</div>');
|
||||
document.getElementById('links-container').innerHTML = parts.join('');
|
||||
restoreCollapseState();
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user