Files
gandalf/templates/suppressions.html
Jared Vititoe af26407363 Fix setDur implicit event, title XSS, hardcoded pulse URL, suppress error toast
- suppressions.html: setDur() now takes explicit element param instead of relying
  on implicit global event.target (which fails outside direct click handlers)
- suppressions.html: removeSuppression() now shows error toast on failed DELETE
- templates/index.html: escape description in title attribute with |e filter
  to prevent attribute breakout on quotes in description text
- diagnose.py: derive Pulse execution URL from pulse_client.url instead of
  hardcoding http://pulse.lotusguild.org

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

233 lines
8.9 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 %}Suppressions GANDALF{% endblock %}
{% block content %}
<div class="page-header">
<h1 class="page-title">Alert Suppressions</h1>
<p class="page-sub">Manage maintenance windows and per-target alert suppression rules.</p>
</div>
<!-- ── Create suppression ─────────────────────────────────────────── -->
<section class="section">
<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">
<label for="s-type">Target Type <span class="required">*</span></label>
<select id="s-type" name="target_type" onchange="onTypeChange()">
<option value="host">Host (all interfaces)</option>
<option value="interface">Specific Interface</option>
<option value="unifi_device">UniFi Device</option>
<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"
placeholder="e.g. enp35s0 or bond0" autocomplete="off">
</div>
</div>
<div class="form-row">
<div class="form-group form-group-wide">
<label for="s-reason">Reason <span class="required">*</span></label>
<input type="text" id="s-reason" name="reason"
placeholder="e.g. Planned switch maintenance, replacing SFP on large1/enp43s0"
required>
</div>
</div>
<div class="form-row form-row-align">
<div class="form-group">
<label>Duration</label>
<div class="duration-pills">
<button type="button" class="pill" onclick="setDur(30, this)">30 min</button>
<button type="button" class="pill" onclick="setDur(60, this)">1 hr</button>
<button type="button" class="pill" onclick="setDur(240, this)">4 hr</button>
<button type="button" class="pill" onclick="setDur(480, this)">8 hr</button>
<button type="button" class="pill pill-manual active" onclick="setDur(null, this)">Manual ∞</button>
</div>
<input type="hidden" id="s-expires" name="expires_minutes" value="">
<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>
</div>
</div>
</form>
</div>
</section>
<!-- ── Active suppressions ────────────────────────────────────────── -->
<section class="section">
<div class="section-header">
<h2 class="section-title">Active Suppressions</h2>
<span class="section-badge">{{ active | length }}</span>
</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>
</tr>
</thead>
<tbody>
{% for s in active %}
<tr id="sup-row-{{ s.id }}">
<td><span class="badge badge-info">{{ s.target_type }}</span></td>
<td>{{ s.target_name or 'all' }}</td>
<td>{{ s.target_detail or '' }}</td>
<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>
<button class="btn-sm btn-danger" onclick="removeSuppression({{ s.id }})">Remove</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="empty-state">No active suppressions.</p>
{% endif %}
</section>
<!-- ── Suppression history ────────────────────────────────────────── -->
<section class="section">
<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>
</tr>
</thead>
<tbody>
{% for s in history %}
<tr class="{% if not s.active %}row-resolved{% endif %}">
<td>{{ s.target_type }}</td>
<td>{{ s.target_name or 'all' }}</td>
<td>{{ s.target_detail or '' }}</td>
<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>
{% if s.active %}
<span class="badge badge-ok">Yes</span>
{% else %}
<span class="badge badge-neutral">No</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="empty-state">No suppression history yet.</p>
{% endif %}
</section>
<!-- ── Available targets reference ───────────────────────────────── -->
<section class="section">
<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 (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 %}
</div>
</section>
{% endblock %}
{% block scripts %}
<script>
function onTypeChange() {
const t = document.getElementById('s-type').value;
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, el) {
document.getElementById('s-expires').value = mins || '';
document.querySelectorAll('.duration-pills .pill').forEach(p => p.classList.remove('active'));
if (el) el.classList.add('active');
const hint = document.getElementById('s-dur-hint');
if (mins) {
const h = Math.floor(mins/60), m = mins%60;
hint.textContent = `Expires in ${h?h+'h ':''} ${m?m+'m':''}`.trim()+'.';
} else {
hint.textContent = 'Persists until manually removed.';
}
}
async function createSuppression(e) {
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,
expires_minutes: form.expires_minutes.value ? parseInt(form.expires_minutes.value) : null,
};
const resp = await fetch('/api/suppressions', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
});
const data = await resp.json();
if (data.success) {
showToast('Suppression applied', 'success');
setTimeout(() => location.reload(), 800);
} else {
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 data = await resp.json();
if (data.success) {
document.getElementById(`sup-row-${id}`)?.remove();
showToast('Suppression removed', 'success');
} else {
showToast(data.error || 'Failed to remove suppression', 'error');
}
}
</script>
{% endblock %}