Files
FabledSteward/steward/templates/alerts/list.html
T
bvandeusenandClaude Opus 5.5 4fbe93bdf7
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 45s
CI / unit (push) Failing after 1m7s
CI / publish (push) Skipped
feat(alerts): a sentence per alert, a link to what it watches, a count in the sidebar (#5124)
The Alerts page's active list now says each alert in one line around the
resource it watches, e.g. "cpu_pct on web-1 is 94% (> 90%), firing for 12m",
with web-1 linking to its host, monitor or container page (or the plugin's
page for Traefik, UniFi and SNMP). Values take their unit from the metric
name through formatting.metric_value. A silent series reads "No cpu_pct from
web-1 for 20m"; an up/down metric says the word.

Actions follow the routes' roles: acknowledge, edit, toggle and New rule for
operators and admins, Delete for admins only, nothing for viewers.

The sidebar shows how many alerts need attention beside Alerts, from an
async context processor that skips htmx fragments, logged-out pages and the
database-down page, and renders without the count if the query fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 18:12:50 -04:00

109 lines
5.1 KiB
HTML

{% extends "base.html" %}
{% block title %}Alerts — Steward{% endblock %}
{% from "_macros.html" import page_header, status_badge, empty_state %}
{% block content %}
{% set can_edit = session.user_role in ('operator', 'admin') %}
{% call page_header("Alerts", "What is breaching now, in a sentence each, then every rule.") %}
<a class="btn btn-ghost" href="/alerts/history">History</a>
<a class="btn btn-ghost" href="/alerts/maintenance">Maintenance</a>
{% if can_edit %}<a class="btn" href="/alerts/rules/new">New rule</a>{% endif %}
{% endcall %}
{% if active %}
<h2 class="section-title" style="margin-bottom:1rem;">Active</h2>
<div class="card-flush" style="margin-bottom:2rem;">
<table class="table">
<thead>
<tr>
<th>State</th>
<th>What is wrong</th>
<th>Rule</th>
<th></th>
</tr>
</thead>
<tbody>
{% for row in active %}
<tr>
<td>
{{ status_badge(row.label, title=("No sample since " ~ (row.state.last_sample_at.strftime('%Y-%m-%d %H:%M UTC') if row.state.last_sample_at else 'the rule started watching')) if row.label == 'no_data' else none) }}
{% if row.state.no_data_since and row.label != 'no_data' %}
{{ status_badge('no_data', title="Silent since " ~ row.state.no_data_since.strftime('%Y-%m-%d %H:%M UTC')) }}
{% endif %}
</td>
<td>
{{- row.says.lead -}}
{%- if row.url %}<a href="{{ row.url }}">{{ row.says.resource }}</a>{% else %}<strong>{{ row.says.resource }}</strong>{% endif -%}
{{- row.says.tail -}}
</td>
<td style="color:var(--text-muted);"><a href="/alerts/history?rule={{ row.rule.id }}">{{ row.rule.name }}</a></td>
<td class="td-actions">
{% if can_edit and row.state.state.value == 'firing' %}
<form method="post" action="/alerts/{{ row.rule.id }}/acknowledge" style="display:inline;">{{ csrf_field() }}
<button type="submit" class="btn btn-sm btn-warn">Acknowledge</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="card" style="margin-bottom:2rem;">{{ empty_state("No active alerts. Every rule is quiet.") }}</div>
{% endif %}
<h2 class="section-title" style="margin-bottom:1rem;">Rules</h2>
{% if rules %}
<div class="card-flush">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Condition</th>
<th style="text-align:center;" title="Checks in a row to fire / to clear">Fire / clear</th>
<th></th>
</tr>
</thead>
<tbody>
{% for rule in rules %}
<tr {% if not rule.enabled %}style="opacity:0.55;"{% endif %}>
<td>
{{ rule.name }}
{% if not rule.enabled %}
<span style="font-size:0.72rem;color:var(--text-dim);margin-left:0.35rem;">disabled</span>
{% endif %}
</td>
<td style="color:var(--text-muted);font-size:0.85rem;">
<span style="color:var(--text-dim);">{{ rule.source_module }}/</span>{{ rule.resource_name }}
<span style="font-family:ui-monospace,monospace;font-size:0.8rem;">
:{{ rule.metric_name }} {{ rule.operator.value }} {{ rule.threshold }}
</span>
</td>
<td style="color:var(--text-muted);text-align:center;">{{ rule.consecutive_failures_required }} / {{ rule.consecutive_ok_required }}</td>
<td class="td-actions">
<a href="/alerts/history?rule={{ rule.id }}" class="btn btn-sm btn-ghost">History</a>
{% if can_edit %}
<a href="/alerts/rules/{{ rule.id }}/edit" class="btn btn-sm btn-ghost">Edit</a>
<form method="post" action="/alerts/rules/{{ rule.id }}/toggle" style="display:inline;">{{ csrf_field() }}
<button type="submit" class="btn btn-sm btn-ghost">
{% if rule.enabled %}Disable{% else %}Enable{% endif %}
</button>
</form>
{% endif %}
{% if session.user_role == 'admin' %}
<form method="post" action="/alerts/rules/{{ rule.id }}/delete" style="display:inline;">{{ csrf_field() }}
<button type="submit" class="btn btn-sm btn-danger"
data-confirm="Delete rule {{ rule.name }}?">Delete</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="card">{{ empty_state("No alert rules yet.", "New rule" if can_edit else none, "/alerts/rules/new" if can_edit else none) }}</div>
{% endif %}
{% endblock %}