Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7b4c263a40 | |||
| 40a0c2af78 |
@@ -222,10 +222,17 @@ def get_status_summary() -> dict:
|
|||||||
WHERE resolved_at IS NULL GROUP BY severity"""
|
WHERE resolved_at IS NULL GROUP BY severity"""
|
||||||
)
|
)
|
||||||
counts = {r['severity']: r['cnt'] for r in cur.fetchall()}
|
counts = {r['severity']: r['cnt'] for r in cur.fetchall()}
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT COUNT(*) as cnt FROM network_events
|
||||||
|
WHERE resolved_at IS NOT NULL
|
||||||
|
AND resolved_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)"""
|
||||||
|
)
|
||||||
|
resolved_24h = cur.fetchone()['cnt']
|
||||||
return {
|
return {
|
||||||
'critical': counts.get('critical', 0),
|
'critical': counts.get('critical', 0),
|
||||||
'warning': counts.get('warning', 0),
|
'warning': counts.get('warning', 0),
|
||||||
'info': counts.get('info', 0),
|
'info': counts.get('info', 0),
|
||||||
|
'resolved_24h': resolved_24h,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -92,8 +92,10 @@ function updateStatusBar(summary, lastCheck, daemonOk) {
|
|||||||
// Update stat cards
|
// Update stat cards
|
||||||
const scCrit = document.getElementById('stat-critical-val');
|
const scCrit = document.getElementById('stat-critical-val');
|
||||||
const scWarn = document.getElementById('stat-warning-val');
|
const scWarn = document.getElementById('stat-warning-val');
|
||||||
|
const scRes = document.getElementById('stat-resolved-val');
|
||||||
if (scCrit) scCrit.textContent = critCount;
|
if (scCrit) scCrit.textContent = critCount;
|
||||||
if (scWarn) scWarn.textContent = warnCount;
|
if (scWarn) scWarn.textContent = warnCount;
|
||||||
|
if (scRes && summary.resolved_24h !== null && summary.resolved_24h !== undefined) scRes.textContent = summary.resolved_24h;
|
||||||
const statCritCard = document.getElementById('stat-critical');
|
const statCritCard = document.getElementById('stat-critical');
|
||||||
if (statCritCard) statCritCard.classList.toggle('lt-stat-card--alert', critCount > 0);
|
if (statCritCard) statCritCard.classList.toggle('lt-stat-card--alert', critCount > 0);
|
||||||
|
|
||||||
@@ -205,7 +207,7 @@ function updateEventsTable(events, totalActive) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const truncated = totalActive != null && totalActive > active.length;
|
const truncated = totalActive !== null && totalActive !== undefined && totalActive > active.length;
|
||||||
const countNotice = truncated
|
const countNotice = truncated
|
||||||
? `<div class="pagination-notice">Showing ${active.length} of ${totalActive} active alerts — <a href="/api/events?limit=1000">view all via API</a></div>`
|
? `<div class="pagination-notice">Showing ${active.length} of ${totalActive} active alerts — <a href="/api/events?limit=1000">view all via API</a></div>`
|
||||||
: '';
|
: '';
|
||||||
|
|||||||
+6
-21
@@ -83,13 +83,16 @@
|
|||||||
.lt-main.lt-container { padding-top: calc(46px + var(--space-sm)); }
|
.lt-main.lt-container { padding-top: calc(46px + var(--space-sm)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Refresh button loading state ────────────────────────────────── */
|
/* ── Button loading state ─────────────────────────────────────────── */
|
||||||
[data-action="refresh"].is-loading {
|
[data-action="refresh"].is-loading,
|
||||||
|
.lt-btn.is-loading {
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
[data-action="refresh"].is-loading::after {
|
[data-action="refresh"].is-loading::after,
|
||||||
|
.lt-btn.is-loading::after {
|
||||||
content: '…';
|
content: '…';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -587,7 +590,6 @@
|
|||||||
.panel-toggle { font-size: .65em; color: var(--text-muted); flex-shrink: 0; margin-left: 6px; padding: 0 4px; border: 1px solid var(--border-color); }
|
.panel-toggle { font-size: .65em; color: var(--text-muted); flex-shrink: 0; margin-left: 6px; padding: 0 4px; border: 1px solid var(--border-color); }
|
||||||
.link-host-panel.collapsed > .link-ifaces-grid { display: none; }
|
.link-host-panel.collapsed > .link-ifaces-grid { display: none; }
|
||||||
|
|
||||||
.link-collapse-bar { display: flex; gap: 8px; margin-bottom: 10px; }
|
|
||||||
|
|
||||||
.link-ifaces-grid {
|
.link-ifaces-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -706,23 +708,6 @@
|
|||||||
.poe-bar-warn { background: var(--amber); }
|
.poe-bar-warn { background: var(--amber); }
|
||||||
.poe-bar-crit { background: var(--red); }
|
.poe-bar-crit { background: var(--red); }
|
||||||
|
|
||||||
/* UniFi section divider */
|
|
||||||
.unifi-section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin: 24px 0 12px;
|
|
||||||
color: var(--cyan);
|
|
||||||
font-size: .75em;
|
|
||||||
letter-spacing: .1em;
|
|
||||||
}
|
|
||||||
.unifi-section-header::before,
|
|
||||||
.unifi-section-header::after {
|
|
||||||
content: '';
|
|
||||||
flex: 1;
|
|
||||||
height: 1px;
|
|
||||||
background: linear-gradient(90deg, transparent, var(--cyan), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.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; }
|
||||||
|
|||||||
+2
-2
@@ -210,7 +210,7 @@
|
|||||||
<span class="lt-footer-sep">|</span>
|
<span class="lt-footer-sep">|</span>
|
||||||
<span class="lt-footer-hint"><span class="lt-footer-key">[ S ]</span> SUPPRESS</span>
|
<span class="lt-footer-hint"><span class="lt-footer-key">[ S ]</span> SUPPRESS</span>
|
||||||
<span class="lt-footer-sep">|</span>
|
<span class="lt-footer-sep">|</span>
|
||||||
{% elif request.endpoint == 'links_page' %}
|
{% elif request.endpoint in ('links_page', 'inspector') %}
|
||||||
<span class="lt-footer-hint"><span class="lt-footer-key">[ R ]</span> REFRESH</span>
|
<span class="lt-footer-hint"><span class="lt-footer-key">[ R ]</span> REFRESH</span>
|
||||||
<span class="lt-footer-sep">|</span>
|
<span class="lt-footer-sep">|</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -233,7 +233,7 @@
|
|||||||
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
|
<thead><tr><th>Shortcut</th><th>Action</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><td>Ctrl / ⌘ + K</td><td>Command palette</td></tr>
|
<tr><td>Ctrl / ⌘ + K</td><td>Command palette</td></tr>
|
||||||
<tr><td>R</td><td>Refresh data (Dashboard / Link Debug)</td></tr>
|
<tr><td>R</td><td>Refresh data (Dashboard / Link Debug / Inspector)</td></tr>
|
||||||
<tr><td>S</td><td>Quick-suppress alert (Dashboard)</td></tr>
|
<tr><td>S</td><td>Quick-suppress alert (Dashboard)</td></tr>
|
||||||
<tr><td>*</td><td>Open settings</td></tr>
|
<tr><td>*</td><td>Open settings</td></tr>
|
||||||
<tr><td>?</td><td>Show this help</td></tr>
|
<tr><td>?</td><td>Show this help</td></tr>
|
||||||
|
|||||||
@@ -208,6 +208,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Host cards -->
|
<!-- Host cards -->
|
||||||
|
<div class="lt-toolbar" style="margin-bottom:10px" id="host-toolbar">
|
||||||
|
<div class="lt-toolbar-left">
|
||||||
|
<div class="lt-search">
|
||||||
|
<input type="search" class="lt-input lt-search-input" id="host-search"
|
||||||
|
placeholder="Filter hosts…" autocomplete="off" style="width:180px">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="host-grid" id="host-grid">
|
<div class="host-grid" id="host-grid">
|
||||||
{% for name, host in snapshot.hosts.items() %}
|
{% for name, host in snapshot.hosts.items() %}
|
||||||
{% set suppressed = suppressions | selectattr('target_name', 'equalto', name) | list %}
|
{% set suppressed = suppressions | selectattr('target_name', 'equalto', name) | list %}
|
||||||
@@ -568,6 +576,15 @@
|
|||||||
new MutationObserver(applyEventsFilter)
|
new MutationObserver(applyEventsFilter)
|
||||||
.observe(document.getElementById('events-table-wrap'), { childList: true, subtree: true });
|
.observe(document.getElementById('events-table-wrap'), { childList: true, subtree: true });
|
||||||
|
|
||||||
|
// Host grid search filter
|
||||||
|
document.getElementById('host-search')?.addEventListener('input', function() {
|
||||||
|
const q = this.value.trim().toLowerCase();
|
||||||
|
document.querySelectorAll('#host-grid .host-card').forEach(card => {
|
||||||
|
const name = (card.dataset.host || '').toLowerCase();
|
||||||
|
card.style.display = (!q || name.includes(q)) ? '' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Stat card clicks — filter events table by severity
|
// Stat card clicks — filter events table by severity
|
||||||
document.querySelectorAll('.lt-stat-card[data-stat-filter]').forEach(card => {
|
document.querySelectorAll('.lt-stat-card[data-stat-filter]').forEach(card => {
|
||||||
card.addEventListener('click', () => {
|
card.addEventListener('click', () => {
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ function renderUnifiSwitches(unifiSwitches, dataUpdated) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
return `<div class="unifi-section-header">UNIFI SWITCH PORTS</div>${html}`;
|
return `<div class="lt-divider" style="margin:20px 0 12px"><span class="lt-divider-label" style="color:var(--cyan);letter-spacing:.1em">UNIFI SWITCH PORTS</span></div>${html}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Panel collapse / expand ───────────────────────────────────────
|
// ── Panel collapse / expand ───────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user