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:
@@ -3,33 +3,37 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- ── Status bar ─────────────────────────────────────────────────────── -->
|
||||
<!-- ── Status bar ──────────────────────────────────────────────────── -->
|
||||
<div class="status-bar">
|
||||
<div class="status-chips">
|
||||
{% if summary.critical %}
|
||||
<span class="chip chip-critical">⬤ {{ summary.critical }} Critical</span>
|
||||
<span class="chip chip-critical">● {{ summary.critical }} CRITICAL</span>
|
||||
{% endif %}
|
||||
{% if summary.warning %}
|
||||
<span class="chip chip-warning">⬤ {{ summary.warning }} Warning</span>
|
||||
<span class="chip chip-warning">● {{ summary.warning }} WARNING</span>
|
||||
{% endif %}
|
||||
{% if not summary.critical and not summary.warning %}
|
||||
<span class="chip chip-ok">✔ All systems nominal</span>
|
||||
<span class="chip chip-ok">✔ ALL SYSTEMS NOMINAL</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="status-meta">
|
||||
<span class="last-check" id="last-check">Last check: {{ last_check }}</span>
|
||||
<button class="btn-refresh" onclick="refreshAll()">↻ Refresh</button>
|
||||
<span class="last-check" id="last-check">{{ last_check }}</span>
|
||||
<button class="btn-refresh" onclick="refreshAll()">↻ REFRESH</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Network topology + host grid ──────────────────────────────────── -->
|
||||
<!-- ── Network topology + host grid ───────────────────────────────── -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">Network Hosts</h2>
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Network Hosts</h2>
|
||||
</div>
|
||||
|
||||
<!-- Simple topology diagram -->
|
||||
<div class="topology" id="topology-diagram">
|
||||
<div class="topo-row topo-row-internet">
|
||||
<div class="topo-node topo-internet">🌐 Internet</div>
|
||||
<div class="topo-node topo-internet">
|
||||
<span class="topo-icon">◈</span>
|
||||
<span class="topo-label">Internet</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topo-connectors single">
|
||||
<div class="topo-line"></div>
|
||||
@@ -87,7 +91,7 @@
|
||||
<span class="host-status-dot dot-{{ host.status }}"></span>
|
||||
<span class="host-name">{{ name }}</span>
|
||||
{% if suppressed %}
|
||||
<span class="badge badge-suppressed" title="Suppressed">🔕</span>
|
||||
<span class="badge-suppressed" title="Suppressed">🔕</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="host-meta">
|
||||
@@ -107,15 +111,19 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="host-ping-note">Monitored via ping only</div>
|
||||
<div class="host-ping-note">ping-only / no node_exporter</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="host-actions">
|
||||
<button class="btn-sm btn-suppress"
|
||||
onclick="openSuppressModal('host', '{{ name }}', '')"
|
||||
title="Suppress alerts for this host">
|
||||
🔕 Suppress Host
|
||||
🔕 Suppress
|
||||
</button>
|
||||
<a href="{{ url_for('links_page') }}#{{ name }}"
|
||||
class="btn-sm btn-secondary" style="text-decoration:none">
|
||||
↗ Links
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
@@ -124,10 +132,12 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── UniFi devices ──────────────────────────────────────────────────── -->
|
||||
<!-- ── UniFi devices ────────────────────────────────────────────────── -->
|
||||
{% if snapshot.unifi %}
|
||||
<section class="section">
|
||||
<h2 class="section-title">UniFi Devices</h2>
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">UniFi Devices</h2>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table" id="unifi-table">
|
||||
<thead>
|
||||
@@ -145,7 +155,7 @@
|
||||
<tr class="{% if not d.connected %}row-critical{% endif %}">
|
||||
<td>
|
||||
<span class="dot-{{ 'up' if d.connected else 'down' }}"></span>
|
||||
{{ 'Online' if d.connected else 'Offline' }}
|
||||
{{ 'ONLINE' if d.connected else 'OFFLINE' }}
|
||||
</td>
|
||||
<td><strong>{{ d.name }}</strong></td>
|
||||
<td>{{ d.type }}</td>
|
||||
@@ -167,70 +177,68 @@
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- ── Active alerts ─────────────────────────────────────────────────── -->
|
||||
<!-- ── Active alerts ───────────────────────────────────────────────── -->
|
||||
<section class="section">
|
||||
<h2 class="section-title">
|
||||
Active Alerts
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Active Alerts</h2>
|
||||
{% if summary.critical or summary.warning %}
|
||||
<span class="section-badge badge-critical">{{ (summary.critical or 0) + (summary.warning or 0) }} open</span>
|
||||
<span class="section-badge">{{ (summary.critical or 0) + (summary.warning or 0) }}</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<div class="table-wrap" id="events-table-wrap">
|
||||
</div>
|
||||
<div id="events-table-wrap">
|
||||
{% if events %}
|
||||
<table class="data-table" id="events-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Severity</th>
|
||||
<th>Type</th>
|
||||
<th>Target</th>
|
||||
<th>Detail</th>
|
||||
<th>Description</th>
|
||||
<th>First Seen</th>
|
||||
<th>Failures</th>
|
||||
<th>Ticket</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in events %}
|
||||
{% if e.severity != 'info' %}
|
||||
<tr class="row-{{ e.severity }}">
|
||||
<td><span class="badge badge-{{ e.severity }}">{{ e.severity }}</span></td>
|
||||
<td>{{ e.event_type | replace('_', ' ') }}</td>
|
||||
<td><strong>{{ e.target_name }}</strong></td>
|
||||
<td>{{ e.target_detail or '–' }}</td>
|
||||
<td class="desc-cell" title="{{ e.description }}">{{ e.description | truncate(60) }}</td>
|
||||
<td class="ts-cell">{{ e.first_seen }}</td>
|
||||
<td>{{ e.consecutive_failures }}</td>
|
||||
<td>
|
||||
{% if e.ticket_id %}
|
||||
<a href="http://t.lotusguild.org/ticket/{{ e.ticket_id }}" target="_blank"
|
||||
class="ticket-link">#{{ e.ticket_id }}</a>
|
||||
{% else %}–{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn-sm btn-suppress"
|
||||
onclick="openSuppressModal('{{ 'unifi_device' if e.event_type == 'unifi_device_offline' else 'interface' if e.event_type == 'interface_down' else 'host' }}', '{{ e.target_name }}', '{{ e.target_detail or '' }}')"
|
||||
title="Suppress this alert">
|
||||
🔕
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr class="empty-row">
|
||||
<td colspan="9" class="empty-state">No active alerts ✔</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table" id="events-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sev</th>
|
||||
<th>Type</th>
|
||||
<th>Target</th>
|
||||
<th>Detail</th>
|
||||
<th>Description</th>
|
||||
<th>First Seen</th>
|
||||
<th>Failures</th>
|
||||
<th>Ticket</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for e in events %}
|
||||
{% if e.severity != 'info' %}
|
||||
<tr class="row-{{ e.severity }}">
|
||||
<td><span class="badge badge-{{ e.severity }}">{{ e.severity }}</span></td>
|
||||
<td>{{ e.event_type | replace('_', ' ') }}</td>
|
||||
<td><strong>{{ e.target_name }}</strong></td>
|
||||
<td>{{ e.target_detail or '–' }}</td>
|
||||
<td class="desc-cell" title="{{ e.description }}">{{ e.description | truncate(60) }}</td>
|
||||
<td class="ts-cell">{{ e.first_seen }}</td>
|
||||
<td>{{ e.consecutive_failures }}</td>
|
||||
<td>
|
||||
{% if e.ticket_id %}
|
||||
<a href="http://t.lotusguild.org/ticket/{{ e.ticket_id }}" target="_blank"
|
||||
class="ticket-link">#{{ e.ticket_id }}</a>
|
||||
{% else %}–{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn-sm btn-suppress"
|
||||
onclick="openSuppressModal('{{ 'unifi_device' if e.event_type == 'unifi_device_offline' else 'interface' if e.event_type == 'interface_down' else 'host' }}', '{{ e.target_name }}', '{{ e.target_detail or '' }}')"
|
||||
title="Suppress">🔕</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr><td colspan="9" class="empty-state">No active alerts ✔</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="empty-state">No active alerts ✔</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Quick-suppress modal ───────────────────────────────────────────── -->
|
||||
<!-- ── Quick-suppress modal ─────────────────────────────────────────── -->
|
||||
<div id="suppress-modal" class="modal-overlay" style="display:none">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
@@ -238,42 +246,43 @@
|
||||
<button class="modal-close" onclick="closeSuppressModal()">✕</button>
|
||||
</div>
|
||||
<form id="suppress-form" onsubmit="submitSuppress(event)">
|
||||
<div class="form-group">
|
||||
<div class="form-group" style="margin-bottom:10px">
|
||||
<label>Target Type</label>
|
||||
<select id="sup-type" name="target_type" onchange="updateSuppressForm()">
|
||||
<option value="host">Host (all interfaces)</option>
|
||||
<option value="interface">Specific Interface</option>
|
||||
<option value="unifi_device">UniFi Device</option>
|
||||
<option value="all">Everything (global maintenance)</option>
|
||||
<option value="all">Global Maintenance</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="sup-name-group">
|
||||
<div class="form-group" id="sup-name-group" style="margin-bottom:10px">
|
||||
<label>Target Name</label>
|
||||
<input type="text" id="sup-name" name="target_name" placeholder="e.g. large1">
|
||||
</div>
|
||||
<div class="form-group" id="sup-detail-group">
|
||||
<label>Interface Name <span class="form-hint">(for interface type)</span></label>
|
||||
<div class="form-group" id="sup-detail-group" style="margin-bottom:10px;display:none">
|
||||
<label>Interface <span class="form-hint">(interface type only)</span></label>
|
||||
<input type="text" id="sup-detail" name="target_detail" placeholder="e.g. enp35s0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group" style="margin-bottom:10px">
|
||||
<label>Reason <span class="required">*</span></label>
|
||||
<input type="text" id="sup-reason" name="reason" placeholder="e.g. Planned switch reboot" required>
|
||||
<input type="text" id="sup-reason" name="reason"
|
||||
placeholder="e.g. Planned switch reboot" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group" style="margin-bottom:0">
|
||||
<label>Duration</label>
|
||||
<div class="duration-pills">
|
||||
<button type="button" class="pill" onclick="setDuration(30)">30 min</button>
|
||||
<button type="button" class="pill" onclick="setDuration(60)">1 hr</button>
|
||||
<button type="button" class="pill" onclick="setDuration(240)">4 hr</button>
|
||||
<button type="button" class="pill" onclick="setDuration(480)">8 hr</button>
|
||||
<button type="button" class="pill pill-manual active" onclick="setDuration(null)">Manual</button>
|
||||
<button type="button" class="pill pill-manual active" onclick="setDuration(null)">Manual ∞</button>
|
||||
</div>
|
||||
<input type="hidden" id="sup-expires" name="expires_minutes" value="">
|
||||
<div class="form-hint" id="duration-hint">Suppression will persist until manually removed.</div>
|
||||
<div class="form-hint" id="duration-hint">Persists until manually removed.</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeSuppressModal()">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Apply Suppression</button>
|
||||
<button type="submit" class="btn btn-primary">Apply</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -283,7 +292,6 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(refreshAll, 30000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user