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>
This commit is contained in:
@@ -2,10 +2,11 @@
|
||||
{% block title %}Alerts — Steward{% endblock %}
|
||||
{% from "_macros.html" import page_header, status_badge, empty_state %}
|
||||
{% block content %}
|
||||
{% call page_header("Alerts") %}
|
||||
{% 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>
|
||||
<a class="btn" href="/alerts/rules/new">New rule</a>
|
||||
{% if can_edit %}<a class="btn" href="/alerts/rules/new">New rule</a>{% endif %}
|
||||
{% endcall %}
|
||||
|
||||
{% if active %}
|
||||
@@ -15,30 +16,29 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>State</th>
|
||||
<th>What is wrong</th>
|
||||
<th>Rule</th>
|
||||
<th>Resource</th>
|
||||
<th>Metric</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for state, rule in active %}
|
||||
{% for row in active %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ status_badge(state.state) }}
|
||||
{% if state.no_data_since %}
|
||||
{{ status_badge('no_data', title="No sample since " ~ (state.last_sample_at.strftime('%Y-%m-%d %H:%M UTC') if state.last_sample_at else 'the rule started watching')) }}
|
||||
<div style="font-size:0.72rem;color:var(--text-dim);margin-top:0.2rem;">
|
||||
since {{ state.no_data_since|ago }}
|
||||
</div>
|
||||
{{ 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>{{ rule.name }}</td>
|
||||
<td style="color:var(--text-muted);">{{ rule.resource_name }}</td>
|
||||
<td style="color:var(--text-muted);">{{ rule.metric_name }} {{ rule.operator.value }} {{ rule.threshold }}</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 state.state.value == 'firing' %}
|
||||
<form method="post" action="/alerts/{{ rule.id }}/acknowledge" style="display:inline;">{{ csrf_field() }}
|
||||
{% 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 %}
|
||||
@@ -82,16 +82,20 @@
|
||||
<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 %}
|
||||
@@ -99,6 +103,6 @@
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card">{{ empty_state("No alert rules yet.", "New rule", "/alerts/rules/new") }}</div>
|
||||
<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 %}
|
||||
|
||||
Reference in New Issue
Block a user