Files
gandalf/templates/index.html
Jared Vititoe 14eaa6a8c9 De-hardcode ticket URL and cluster name; improve diagnostic polling UX
app.py:
- Context processor injects config.ticket_api.web_url into all templates
  (falls back to 'http://t.lotusguild.org/ticket/' if not set in config)

templates/base.html:
- Inject GANDALF_CONFIG JS global with ticket_web_url before app.js loads

static/app.js:
- Use GANDALF_CONFIG.ticket_web_url instead of hardcoded domain

templates/index.html:
- Use {{ config.ticket_api.web_url }} Jinja var instead of hardcoded domain

monitor.py:
- CLUSTER_NAME constant kept as default; NetworkMonitor now reads cluster_name
  from config monitor.cluster_name, falling back to the constant
- All CLUSTER_NAME references inside class methods replaced with self.cluster_name

templates/inspector.html:
- pollDiagnostic() .catch() now clears interval and shows error message instead
  of silently ignoring network failures during active polling

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-14 14:31:57 -04:00

304 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Dashboard GANDALF{% endblock %}
{% block content %}
<!-- ── Status bar ──────────────────────────────────────────────────── -->
<div class="status-bar">
<div class="status-chips">
{% if summary.critical %}
<span class="chip chip-critical">● {{ summary.critical }} CRITICAL</span>
{% endif %}
{% if summary.warning %}
<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>
{% endif %}
</div>
<div class="status-meta">
<span class="last-check" id="last-check">{{ last_check }}</span>
<button class="btn-refresh" onclick="refreshAll()">↻ REFRESH</button>
</div>
</div>
<!-- ── Network topology + host grid ───────────────────────────────── -->
<section class="section">
<div class="section-header">
<h2 class="section-title">Network Hosts</h2>
</div>
<div class="topology" id="topology-diagram">
<div class="topo-row topo-row-internet">
<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>
</div>
<div class="topo-row">
<div class="topo-node topo-unifi" id="topo-gateway">
<span class="topo-icon"></span>
<span class="topo-label">UDM-Pro</span>
<span class="topo-status-dot" data-topo-target="gateway"></span>
</div>
</div>
<div class="topo-connectors single">
<div class="topo-line topo-line-labeled" data-link-label="10G DAC"></div>
</div>
<div class="topo-row">
<div class="topo-node topo-switch" id="topo-switch-agg">
<span class="topo-icon"></span>
<span class="topo-label">Agg Switch</span>
<span class="topo-status-dot" data-topo-target="switch-agg"></span>
</div>
</div>
<div class="topo-connectors single">
<div class="topo-line topo-line-labeled" data-link-label="10G DAC"></div>
</div>
<div class="topo-row">
<div class="topo-node topo-switch" id="topo-switch-poe">
<span class="topo-icon"></span>
<span class="topo-label">PoE Switch</span>
<span class="topo-status-dot" data-topo-target="switch-poe"></span>
</div>
</div>
<div class="topo-connectors wide">
{% for name in snapshot.hosts %}
<div class="topo-line"></div>
{% endfor %}
</div>
<div class="topo-row topo-hosts-row">
{% for name, host in snapshot.hosts.items() %}
<div class="topo-node topo-host topo-status-{{ host.status }}" data-host="{{ name }}">
<span class="topo-icon"></span>
<span class="topo-label">{{ name }}</span>
<span class="topo-badge topo-badge-{{ host.status }}">{{ host.status }}</span>
</div>
{% endfor %}
</div>
</div>
<!-- Host cards -->
<div class="host-grid" id="host-grid">
{% for name, host in snapshot.hosts.items() %}
{% set suppressed = suppressions | selectattr('target_name', 'equalto', name) | list %}
<div class="host-card host-card-{{ host.status }}" data-host="{{ name }}">
<div class="host-card-header">
<div class="host-name-row">
<span class="host-status-dot dot-{{ host.status }}"></span>
<span class="host-name">{{ name }}</span>
{% if suppressed %}
<span class="badge-suppressed" title="Suppressed">🔕</span>
{% endif %}
</div>
<div class="host-meta">
<span class="host-ip">{{ host.ip }}</span>
<span class="host-source source-{{ host.source }}">{{ host.source }}</span>
</div>
</div>
{% if host.interfaces %}
<div class="iface-list">
{% for iface, state in host.interfaces.items() | sort %}
<div class="iface-row">
<span class="iface-dot dot-{{ state }}"></span>
<span class="iface-name">{{ iface }}</span>
<span class="iface-state state-{{ state }}">{{ state }}</span>
</div>
{% endfor %}
</div>
{% else %}
<div class="host-ping-note">ping-only / no node_exporter</div>
{% endif %}
<div class="host-actions">
<button class="btn-sm btn-suppress"
data-sup-type="host"
data-sup-name="{{ name }}"
data-sup-detail=""
title="Suppress alerts for this host">
🔕 Suppress
</button>
<a href="{{ url_for('links_page') }}#{{ name }}"
class="btn-sm btn-secondary" style="text-decoration:none">
↗ Links
</a>
</div>
</div>
{% else %}
<p class="empty-state">No host data yet monitor is initializing.</p>
{% endfor %}
</div>
</section>
<!-- ── UniFi devices ────────────────────────────────────────────────── -->
{% if snapshot.unifi %}
<section class="section">
<div class="section-header">
<h2 class="section-title">UniFi Devices</h2>
</div>
<div class="table-wrap">
<table class="data-table" id="unifi-table">
<thead>
<tr>
<th>Status</th>
<th>Name</th>
<th>Type</th>
<th>Model</th>
<th>IP</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for d in snapshot.unifi %}
<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' }}
</td>
<td><strong>{{ d.name }}</strong></td>
<td>{{ d.type }}</td>
<td>{{ d.model }}</td>
<td>{{ d.ip }}</td>
<td>
{% if not d.connected %}
<button class="btn-sm btn-suppress"
data-sup-type="unifi_device"
data-sup-name="{{ d.name }}"
data-sup-detail="">
🔕 Suppress
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
{% endif %}
<!-- ── Active alerts ───────────────────────────────────────────────── -->
<section class="section">
<div class="section-header">
<h2 class="section-title">Active Alerts</h2>
{% if summary.critical or summary.warning %}
<span class="section-badge">{{ (summary.critical or 0) + (summary.warning or 0) }}</span>
{% endif %}
</div>
<div id="events-table-wrap">
{% if events %}
<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 }}">{{ 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="{{ config.ticket_api.web_url }}{{ e.ticket_id }}" target="_blank"
class="ticket-link">#{{ e.ticket_id }}</a>
{% else %}{% endif %}
</td>
<td>
<button class="btn-sm btn-suppress"
data-sup-type="{{ 'unifi_device' if e.event_type == 'unifi_device_offline' else 'interface' if e.event_type == 'interface_down' else 'host' }}"
data-sup-name="{{ e.target_name }}"
data-sup-detail="{{ 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 ─────────────────────────────────────────── -->
<div id="suppress-modal" class="modal-overlay" style="display:none">
<div class="modal">
<div class="modal-header">
<h3>Suppress Alert</h3>
<button class="modal-close" onclick="closeSuppressModal()"></button>
</div>
<form id="suppress-form" onsubmit="submitSuppress(event)">
<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">Global Maintenance</option>
</select>
</div>
<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" 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" 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>
</div>
<div class="form-group" style="margin-bottom:0">
<label>Duration</label>
<div class="duration-pills">
<button type="button" class="pill" onclick="setDuration(30, this)">30 min</button>
<button type="button" class="pill" onclick="setDuration(60, this)">1 hr</button>
<button type="button" class="pill" onclick="setDuration(240, this)">4 hr</button>
<button type="button" class="pill" onclick="setDuration(480, this)">8 hr</button>
<button type="button" class="pill pill-manual active" onclick="setDuration(null, this)">Manual ∞</button>
</div>
<input type="hidden" id="sup-expires" name="expires_minutes" value="">
<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</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
setInterval(refreshAll, 30000);
</script>
{% endblock %}