feat: terminal aesthetic rewrite + link debug page
- Full dark terminal aesthetic (Pulse/TinkerTickets style): - #0a0a0a background, #00ff41 green, #ffb000 amber, #00ffff cyan - CRT scanline overlay, phosphor glow, ASCII corner pseudoelements - Bracket-notation badges [CRITICAL], monospace font throughout - style.css, base.html, index.html, suppressions.html all rewritten - New Link Debug page (/links, /api/links): - Per-host, per-interface cards with speed/duplex/port type/auto-neg - Traffic bars (TX cyan, RX green) with rate labels - Error/drop counters, carrier change history - SFP/DOM optical panel: vendor, temp, voltage, bias, TX/RX power dBm bars - RX-TX delta shown; color-coded warn/crit thresholds - Auto-refresh every 60s, anchor-jump to #hostname - LinkStatsCollector in monitor.py: - SSHes to each host (one connection, all ifaces batched) - Parses ethtool + ethtool -m (SFP DOM) output - Merges with Prometheus traffic/error/carrier metrics - Stores as link_stats in monitor_state table - config.json: added ssh section for ethtool collection - app.js: terminal chip style consistency (uppercase, ● bullet) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,13 +5,15 @@
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Alert Suppressions</h1>
|
||||
<p class="page-sub">Manage maintenance windows and alert suppression rules.</p>
|
||||
<p class="page-sub">Manage maintenance windows and per-target alert suppression rules.</p>
|
||||
</div>
|
||||
|
||||
<!-- ── Create suppression ─────────────────────────────────────────────── -->
|
||||
<!-- ── Create suppression ─────────────────────────────────────────── -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">Create Suppression</h2>
|
||||
<div class="card form-card">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Create Suppression</h2>
|
||||
</div>
|
||||
<div class="form-card">
|
||||
<form id="create-suppression-form" onsubmit="createSuppression(event)">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
@@ -23,13 +25,11 @@
|
||||
<option value="all">Global (suppress everything)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="name-group">
|
||||
<label for="s-name">Target Name <span class="required">*</span></label>
|
||||
<input type="text" id="s-name" name="target_name"
|
||||
placeholder="hostname or device name" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="detail-group" style="display:none">
|
||||
<label for="s-detail">Interface Name</label>
|
||||
<input type="text" id="s-detail" name="target_detail"
|
||||
@@ -57,39 +57,29 @@
|
||||
<button type="button" class="pill pill-manual active" onclick="setDur(null)">Manual ∞</button>
|
||||
</div>
|
||||
<input type="hidden" id="s-expires" name="expires_minutes" value="">
|
||||
<div class="form-hint" id="s-dur-hint">
|
||||
This suppression will persist until manually removed.
|
||||
</div>
|
||||
<div class="form-hint" id="s-dur-hint">Persists until manually removed.</div>
|
||||
</div>
|
||||
<div class="form-group form-group-submit">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
🔕 Apply Suppression
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary btn-lg">🔕 Apply Suppression</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Active suppressions ────────────────────────────────────────────── -->
|
||||
<!-- ── Active suppressions ────────────────────────────────────────── -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">
|
||||
Active Suppressions
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Active Suppressions</h2>
|
||||
<span class="section-badge">{{ active | length }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
{% if active %}
|
||||
<div class="table-wrap">
|
||||
<table class="data-table" id="active-sup-table">
|
||||
<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>
|
||||
<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>
|
||||
@@ -101,14 +91,9 @@
|
||||
<td>{{ s.reason }}</td>
|
||||
<td>{{ s.suppressed_by }}</td>
|
||||
<td class="ts-cell">{{ s.created_at }}</td>
|
||||
<td class="ts-cell">
|
||||
{% if s.expires_at %}{{ s.expires_at }}{% else %}<em>manual</em>{% endif %}
|
||||
</td>
|
||||
<td class="ts-cell">{% if s.expires_at %}{{ s.expires_at }}{% else %}<em>manual</em>{% endif %}</td>
|
||||
<td>
|
||||
<button class="btn-sm btn-danger"
|
||||
onclick="removeSuppression({{ s.id }})">
|
||||
Remove
|
||||
</button>
|
||||
<button class="btn-sm btn-danger" onclick="removeSuppression({{ s.id }})">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -120,22 +105,19 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- ── Suppression history ────────────────────────────────────────────── -->
|
||||
<!-- ── Suppression history ────────────────────────────────────────── -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">History <span class="section-badge">{{ history | length }}</span></h2>
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">History</h2>
|
||||
<span class="section-badge">{{ history | length }}</span>
|
||||
</div>
|
||||
{% if history %}
|
||||
<div class="table-wrap">
|
||||
<table class="data-table data-table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Target</th>
|
||||
<th>Detail</th>
|
||||
<th>Reason</th>
|
||||
<th>By</th>
|
||||
<th>Created</th>
|
||||
<th>Expires</th>
|
||||
<th>Active</th>
|
||||
<th>Type</th><th>Target</th><th>Detail</th><th>Reason</th>
|
||||
<th>By</th><th>Created</th><th>Expires</th><th>Active</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -147,9 +129,7 @@
|
||||
<td>{{ s.reason }}</td>
|
||||
<td>{{ s.suppressed_by }}</td>
|
||||
<td class="ts-cell">{{ s.created_at }}</td>
|
||||
<td class="ts-cell">
|
||||
{% if s.expires_at %}{{ s.expires_at }}{% else %}<em>manual</em>{% endif %}
|
||||
</td>
|
||||
<td class="ts-cell">{% if s.expires_at %}{{ s.expires_at }}{% else %}<em>manual</em>{% endif %}</td>
|
||||
<td>
|
||||
{% if s.active %}
|
||||
<span class="badge badge-ok">Yes</span>
|
||||
@@ -167,22 +147,22 @@
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- ── Available targets reference ───────────────────────────────────── -->
|
||||
<!-- ── Available targets reference ───────────────────────────────── -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">Available Targets</h2>
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Available Targets</h2>
|
||||
</div>
|
||||
<div class="targets-grid">
|
||||
{% for name, host in snapshot.hosts.items() %}
|
||||
<div class="target-card">
|
||||
<div class="target-name">{{ name }}</div>
|
||||
<div class="target-type">Proxmox Host</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>
|
||||
{% else %}
|
||||
<div class="target-type">ping-only</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -195,11 +175,9 @@
|
||||
<script>
|
||||
function onTypeChange() {
|
||||
const t = document.getElementById('s-type').value;
|
||||
const nameGrp = document.getElementById('name-group');
|
||||
const detailGrp = document.getElementById('detail-group');
|
||||
nameGrp.style.display = (t === 'all') ? 'none' : '';
|
||||
detailGrp.style.display = (t === 'interface') ? '' : 'none';
|
||||
document.getElementById('s-name').required = (t !== 'all');
|
||||
document.getElementById('name-group').style.display = (t==='all') ? 'none' : '';
|
||||
document.getElementById('detail-group').style.display = (t==='interface') ? '' : 'none';
|
||||
document.getElementById('s-name').required = (t!=='all');
|
||||
}
|
||||
|
||||
function setDur(mins) {
|
||||
@@ -208,10 +186,10 @@
|
||||
event.target.classList.add('active');
|
||||
const hint = document.getElementById('s-dur-hint');
|
||||
if (mins) {
|
||||
const h = Math.floor(mins / 60), m = mins % 60;
|
||||
hint.textContent = `Suppression expires in ${h ? h+'h ' : ''}${m ? m+'m' : ''}.`;
|
||||
const h = Math.floor(mins/60), m = mins%60;
|
||||
hint.textContent = `Expires in ${h?h+'h ':''} ${m?m+'m':''}`.trim()+'.';
|
||||
} else {
|
||||
hint.textContent = 'This suppression will persist until manually removed.';
|
||||
hint.textContent = 'Persists until manually removed.';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,10 +197,10 @@
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const payload = {
|
||||
target_type: form.target_type.value,
|
||||
target_name: form.target_name ? form.target_name.value : '',
|
||||
target_detail: document.getElementById('s-detail').value,
|
||||
reason: form.reason.value,
|
||||
target_type: form.target_type.value,
|
||||
target_name: form.target_name ? form.target_name.value : '',
|
||||
target_detail: document.getElementById('s-detail').value,
|
||||
reason: form.reason.value,
|
||||
expires_minutes: form.expires_minutes.value ? parseInt(form.expires_minutes.value) : null,
|
||||
};
|
||||
const resp = await fetch('/api/suppressions', {
|
||||
@@ -235,13 +213,13 @@
|
||||
showToast('Suppression applied', 'success');
|
||||
setTimeout(() => location.reload(), 800);
|
||||
} else {
|
||||
showToast(data.error || 'Error applying suppression', 'error');
|
||||
showToast(data.error || 'Error', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeSuppression(id) {
|
||||
if (!confirm('Remove this suppression?')) return;
|
||||
const resp = await fetch(`/api/suppressions/${id}`, { method: 'DELETE' });
|
||||
const resp = await fetch(`/api/suppressions/${id}`, {method:'DELETE'});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
document.getElementById(`sup-row-${id}`)?.remove();
|
||||
|
||||
Reference in New Issue
Block a user