feat: inspector page, link debug enhancements, security hardening
- Add /inspector page: visual model-accurate switch chassis diagrams (USF5P, USL8A, US24PRO, USPPDUP, USMINI), clickable port blocks with color coding (green=up, amber=PoE, cyan=uplink, grey=down), detail panel with stats/PoE/LLDP, LLDP-based path debug side-by-side - Link Debug: port number badges (#N), LLDP neighbor line, PoE class/max, collapsible host/switch panels with sessionStorage persistence - monitor.py: collect LLDP neighbor map + PoE class/max/mode per switch port; PulseClient uses requests.Session() for HTTP keep-alive; add shlex.quote() around interface names (defense-in-depth) - Security: suppress buttons use data-* attrs + delegated click handler instead of inline onclick with Jinja2 variable interpolation; remove | safe filter from user-controlled fields in suppressions.html; setDuration() takes explicit el param instead of implicit event global - db.py: thread-local connection reuse with ping(reconnect=True) to avoid a new TCP handshake per query - .gitignore: add config.json (contains credentials), __pycache__ - README: full rewrite covering architecture, all 4 pages, alert logic, config reference, deployment, troubleshooting, security notes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -105,7 +105,9 @@ function updateUnifiTable(devices) {
|
||||
const statusText = d.connected ? 'Online' : 'Offline';
|
||||
const suppressBtn = !d.connected
|
||||
? `<button class="btn-sm btn-suppress"
|
||||
onclick="openSuppressModal('unifi_device','${escHtml(d.name)}','')">🔕 Suppress</button>`
|
||||
data-sup-type="unifi_device"
|
||||
data-sup-name="${escHtml(d.name)}"
|
||||
data-sup-detail="">🔕 Suppress</button>`
|
||||
: '';
|
||||
return `
|
||||
<tr class="${statusClass}">
|
||||
@@ -149,9 +151,9 @@ function updateEventsTable(events) {
|
||||
<td>${ticket}</td>
|
||||
<td>
|
||||
<button class="btn-sm btn-suppress"
|
||||
onclick="openSuppressModal('${supType}','${escHtml(e.target_name)}','${escHtml(e.target_detail||'')}')">
|
||||
🔕
|
||||
</button>
|
||||
data-sup-type="${escHtml(supType)}"
|
||||
data-sup-name="${escHtml(e.target_name)}"
|
||||
data-sup-detail="${escHtml(e.target_detail||'')}">🔕</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
@@ -204,12 +206,10 @@ function updateSuppressForm() {
|
||||
if (detailGrp) detailGrp.style.display = (type === 'interface') ? '' : 'none';
|
||||
}
|
||||
|
||||
function setDuration(mins) {
|
||||
function setDuration(mins, el) {
|
||||
document.getElementById('sup-expires').value = mins || '';
|
||||
|
||||
document.querySelectorAll('#suppress-modal .pill').forEach(p => p.classList.remove('active'));
|
||||
event.currentTarget.classList.add('active');
|
||||
|
||||
if (el) el.classList.add('active');
|
||||
const hint = document.getElementById('duration-hint');
|
||||
if (hint) {
|
||||
if (mins) {
|
||||
@@ -257,10 +257,21 @@ async function submitSuppress(e) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Close modal on backdrop click ─────────────────────────────────────
|
||||
// ── Global click handler: modal backdrop + suppress button delegation ─
|
||||
document.addEventListener('click', e => {
|
||||
// Close modal when clicking backdrop
|
||||
const modal = document.getElementById('suppress-modal');
|
||||
if (modal && e.target === modal) closeSuppressModal();
|
||||
if (modal && e.target === modal) { closeSuppressModal(); return; }
|
||||
|
||||
// Suppress button via data attributes (avoids inline onclick XSS)
|
||||
const btn = e.target.closest('.btn-suppress[data-sup-type]');
|
||||
if (btn) {
|
||||
openSuppressModal(
|
||||
btn.dataset.supType || '',
|
||||
btn.dataset.supName || '',
|
||||
btn.dataset.supDetail || '',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ── Utility ───────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user