Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 760e45bb68 | |||
| c3aa3bea6f | |||
| b393d94e81 |
@@ -724,20 +724,6 @@
|
|||||||
background: linear-gradient(90deg, transparent, var(--cyan), transparent);
|
background: linear-gradient(90deg, transparent, var(--cyan), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Link health summary */
|
|
||||||
.link-summary-panel {
|
|
||||||
background: var(--bg2);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
padding: 12px 16px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.link-summary-panel.link-summary-has-alerts { border-color: var(--amber); }
|
|
||||||
.link-summary-grid { display: flex; flex-wrap: wrap; gap: 20px; align-items: flex-end; }
|
|
||||||
.link-summary-stat { min-width: 80px; }
|
|
||||||
.link-summary-stat.lss-alert .lss-label { color: var(--amber); }
|
|
||||||
.lss-label { display: block; font-size: .62em; color: var(--text-muted); text-transform: uppercase; letter-spacing: .05em; margin-bottom: 2px; }
|
|
||||||
.lss-value { font-size: 1.2em; font-weight: bold; color: var(--text); }
|
|
||||||
.lss-sub { font-size: .7em; color: var(--text-muted); font-weight: normal; }
|
|
||||||
|
|
||||||
.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; }
|
||||||
.link-loading::after { content: ' ...'; animation: blink 1s step-end infinite; }
|
.link-loading::after { content: ' ...'; animation: blink 1s step-end infinite; }
|
||||||
|
|||||||
+43
-38
@@ -270,44 +270,49 @@
|
|||||||
<div class="g-section-header">
|
<div class="g-section-header">
|
||||||
<h2 class="g-section-title">UniFi Devices</h2>
|
<h2 class="g-section-title">UniFi Devices</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-table-wrap">
|
<div class="lt-frame">
|
||||||
<table class="lt-table" id="unifi-table">
|
<span class="lt-frame-bl">╚</span>
|
||||||
<caption class="lt-sr-only">UniFi network devices</caption>
|
<span class="lt-frame-br">╝</span>
|
||||||
<thead>
|
<div class="lt-section-header">Device Inventory</div>
|
||||||
<tr>
|
<div class="lt-table-wrap">
|
||||||
<th>Status</th>
|
<table class="lt-table" id="unifi-table">
|
||||||
<th>Name</th>
|
<caption class="lt-sr-only">UniFi network devices</caption>
|
||||||
<th>Type</th>
|
<thead>
|
||||||
<th>Model</th>
|
<tr>
|
||||||
<th>IP</th>
|
<th>Status</th>
|
||||||
<th>Actions</th>
|
<th>Name</th>
|
||||||
</tr>
|
<th>Type</th>
|
||||||
</thead>
|
<th>Model</th>
|
||||||
<tbody>
|
<th>IP</th>
|
||||||
{% for d in snapshot.unifi %}
|
<th>Actions</th>
|
||||||
<tr class="{% if not d.connected %}row-critical{% endif %}">
|
</tr>
|
||||||
<td>
|
</thead>
|
||||||
<span class="dot-{{ 'up' if d.connected else 'down' }}"></span>
|
<tbody>
|
||||||
{{ 'ONLINE' if d.connected else 'OFFLINE' }}
|
{% for d in snapshot.unifi %}
|
||||||
</td>
|
<tr class="{% if not d.connected %}row-critical{% endif %}">
|
||||||
<td><strong>{{ d.name }}</strong></td>
|
<td>
|
||||||
<td>{{ d.type }}</td>
|
<span class="dot-{{ 'up' if d.connected else 'down' }}"></span>
|
||||||
<td>{{ d.model }}</td>
|
{{ 'ONLINE' if d.connected else 'OFFLINE' }}
|
||||||
<td>{{ d.ip }}</td>
|
</td>
|
||||||
<td>
|
<td><strong>{{ d.name }}</strong></td>
|
||||||
{% if not d.connected %}
|
<td>{{ d.type }}</td>
|
||||||
<button class="lt-btn lt-btn-ghost lt-btn-sm btn-suppress"
|
<td>{{ d.model }}</td>
|
||||||
data-sup-type="unifi_device"
|
<td>{{ d.ip }}</td>
|
||||||
data-sup-name="{{ d.name }}"
|
<td>
|
||||||
data-sup-detail="">
|
{% if not d.connected %}
|
||||||
🔕 Suppress
|
<button class="lt-btn lt-btn-ghost lt-btn-sm btn-suppress"
|
||||||
</button>
|
data-sup-type="unifi_device"
|
||||||
{% endif %}
|
data-sup-name="{{ d.name }}"
|
||||||
</td>
|
data-sup-detail="">
|
||||||
</tr>
|
🔕 Suppress
|
||||||
{% endfor %}
|
</button>
|
||||||
</tbody>
|
{% endif %}
|
||||||
</table>
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="g-page-header">
|
<div class="lt-page-header">
|
||||||
<h1 class="g-page-title">Network Inspector</h1>
|
<div>
|
||||||
<p class="g-page-sub">
|
<h1 class="lt-page-title">Network Inspector</h1>
|
||||||
Visual switch chassis diagrams. Click a port to see detailed stats and LLDP path debug.
|
<p class="g-page-sub" style="margin-top:4px">
|
||||||
<span id="inspector-updated" style="margin-left:10px; color:var(--text-muted); font-size:.85em;"></span>
|
Visual switch chassis diagrams. Click a port to see detailed stats and LLDP path debug.
|
||||||
</p>
|
<span id="inspector-updated" style="margin-left:8px"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="inspector-layout">
|
<div class="inspector-layout">
|
||||||
@@ -425,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>';
|
||||||
@@ -463,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();
|
||||||
});
|
});
|
||||||
|
|||||||
+73
-32
@@ -3,13 +3,27 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="g-page-header">
|
<div class="lt-page-header">
|
||||||
<h1 class="g-page-title">Link Debug</h1>
|
<div>
|
||||||
<p class="g-page-sub">
|
<h1 class="lt-page-title">Link Debug</h1>
|
||||||
Per-interface stats: speed, duplex, SFP optical levels, TX/RX rates, errors, and carrier changes.
|
<p class="g-page-sub" style="margin-top:4px">
|
||||||
Data collected via Prometheus node_exporter + SSH ethtool (servers) and UniFi API (switches) every poll cycle.
|
Per-interface stats: speed, duplex, SFP optical levels, TX/RX rates, errors, and carrier changes.
|
||||||
<span id="links-updated" style="margin-left:10px; color:var(--text-muted); font-size:.85em;"></span>
|
<span id="links-updated" style="margin-left:8px"></span>
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lt-toolbar" id="links-toolbar" style="display:none">
|
||||||
|
<div class="lt-toolbar-left">
|
||||||
|
<div class="lt-search">
|
||||||
|
<input type="search" class="lt-input lt-search-input" id="links-search"
|
||||||
|
placeholder="Filter by host or switch name…" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="lt-toolbar-right">
|
||||||
|
<button class="lt-btn lt-btn-ghost lt-btn-sm" data-action="collapse-all">Collapse All</button>
|
||||||
|
<button class="lt-btn lt-btn-ghost lt-btn-sm" data-action="expand-all">Expand All</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="links-container">
|
<div id="links-container">
|
||||||
@@ -383,33 +397,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>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,10 +452,6 @@ function renderLinks(data) {
|
|||||||
const parts = [];
|
const parts = [];
|
||||||
|
|
||||||
parts.push(buildLinkSummary(hosts, unifiSwitches));
|
parts.push(buildLinkSummary(hosts, unifiSwitches));
|
||||||
parts.push(`<div class="link-collapse-bar">
|
|
||||||
<button class="lt-btn lt-btn-ghost lt-btn-sm" data-action="collapse-all">Collapse All</button>
|
|
||||||
<button class="lt-btn lt-btn-ghost lt-btn-sm" data-action="expand-all">Expand All</button>
|
|
||||||
</div>`);
|
|
||||||
parts.push('<div class="link-host-list">');
|
parts.push('<div class="link-host-list">');
|
||||||
|
|
||||||
for (const [hostname, ifaces] of Object.entries(hosts)) {
|
for (const [hostname, ifaces] of Object.entries(hosts)) {
|
||||||
@@ -452,6 +480,17 @@ function renderLinks(data) {
|
|||||||
parts.push('</div>');
|
parts.push('</div>');
|
||||||
document.getElementById('links-container').innerHTML = parts.join('');
|
document.getElementById('links-container').innerHTML = parts.join('');
|
||||||
restoreCollapseState();
|
restoreCollapseState();
|
||||||
|
document.getElementById('links-toolbar').style.display = '';
|
||||||
|
applyLinksSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Host/switch search filter ─────────────────────────────────────
|
||||||
|
function applyLinksSearch() {
|
||||||
|
const q = (document.getElementById('links-search')?.value || '').trim().toLowerCase();
|
||||||
|
document.querySelectorAll('.link-host-panel').forEach(panel => {
|
||||||
|
const text = (panel.querySelector('.link-host-name')?.textContent || '').toLowerCase();
|
||||||
|
panel.style.display = (!q || text.includes(q)) ? '' : 'none';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function collapseAll() {
|
function collapseAll() {
|
||||||
@@ -533,5 +572,7 @@ document.addEventListener('click', e => {
|
|||||||
if (e.target.closest('[data-action="collapse-all"]')) { collapseAll(); return; }
|
if (e.target.closest('[data-action="collapse-all"]')) { collapseAll(); return; }
|
||||||
if (e.target.closest('[data-action="expand-all"]')) { expandAll(); return; }
|
if (e.target.closest('[data-action="expand-all"]')) { expandAll(); return; }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('links-search')?.addEventListener('input', applyLinksSearch);
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+104
-80
@@ -3,9 +3,11 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="g-page-header">
|
<div class="lt-page-header">
|
||||||
<h1 class="g-page-title">Alert Suppressions</h1>
|
<div>
|
||||||
<p class="g-page-sub">Manage maintenance windows and per-target alert suppression rules.</p>
|
<h1 class="lt-page-title">Alert Suppressions</h1>
|
||||||
|
<p class="g-page-sub" style="margin-top:4px">Manage maintenance windows and per-target alert suppression rules.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── Create suppression ─────────────────────────────────────────── -->
|
<!-- ── Create suppression ─────────────────────────────────────────── -->
|
||||||
@@ -83,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">╚</span>
|
||||||
<caption class="lt-sr-only">Active suppression rules</caption>
|
<span class="lt-frame-br">╝</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">
|
||||||
@@ -128,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">╚</span>
|
||||||
<caption class="lt-sr-only">Suppression history</caption>
|
<span class="lt-frame-br">╝</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">
|
||||||
@@ -172,20 +184,27 @@
|
|||||||
<div class="g-section-header">
|
<div class="g-section-header">
|
||||||
<h2 class="g-section-title">Available Targets</h2>
|
<h2 class="g-section-title">Available Targets</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="targets-grid">
|
<div class="lt-frame">
|
||||||
{% for name, host in snapshot.hosts.items() %}
|
<span class="lt-frame-bl">╚</span>
|
||||||
<div class="target-card">
|
<span class="lt-frame-br">╝</span>
|
||||||
<div class="target-name">{{ name }}</div>
|
<div class="lt-section-header">Host & Interface Reference</div>
|
||||||
<div class="target-type">{{ 'Proxmox Host (prometheus)' if host.source == 'prometheus' else 'Ping-only host' }}</div>
|
<div style="padding:12px 14px">
|
||||||
{% if host.interfaces %}
|
<div class="targets-grid">
|
||||||
<div class="target-ifaces">
|
{% for name, host in snapshot.hosts.items() %}
|
||||||
{% for iface in host.interfaces.keys() | sort %}
|
<div class="target-card">
|
||||||
<code class="iface-chip">{{ iface }}</code>
|
<div class="target-name">{{ name }}</div>
|
||||||
|
<div class="target-type">{{ 'Proxmox Host (prometheus)' if host.source == 'prometheus' else 'Ping-only host' }}</div>
|
||||||
|
{% if host.interfaces %}
|
||||||
|
<div class="target-ifaces">
|
||||||
|
{% for iface in host.interfaces.keys() | sort %}
|
||||||
|
<code class="iface-chip">{{ iface }}</code>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -235,15 +254,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">╚</span>
|
||||||
<caption class="lt-sr-only">Active suppression rules</caption>
|
<span class="lt-frame-br">╝</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>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user