History (#5316): - Transitions fold into episodes (steward/alerts/episodes.py): each stretch out of quiet, the worst state it reached, how long it lasted, its worst reading against the rule ('3m 21s (fires above 3m)'), who was told across its changes, and its Ansible runs. The changes themselves fold under it. - An episode cut by paging says so ('cleared later (above)', 'earlier'). - For one rule: a verdict ('Fired 4 times in the last 7d, 1h 2m in all') and no Rule column. Notified and Action show only when the range has any. Tests: - The confirm guard looks for a confirm( call, not the word in the comment that says it is gone (lesson #4365). - The Hosts viewer test looks for the add-host dialog by id, since every page now carries the confirm dialog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,51 +1,74 @@
|
||||
{# alerts/_history_page.html — one page of alert transitions, then the sentinel
|
||||
that loads the next page as the reader nears it (core.paging, pref 172). #}
|
||||
{# alerts/_history_page.html — one page of alert episodes (#5316), then the
|
||||
sentinel that loads the next page as the reader nears it (core.paging,
|
||||
pref 172). An episode is one stretch out of quiet: what it reached, for how
|
||||
long, its worst reading, who was told; its transitions fold under it. #}
|
||||
{% from "_macros.html" import status_badge, scroll_more, scroll_end %}
|
||||
{% for ev in events %}
|
||||
{% set cols = 3 + (0 if chosen else 1) + (1 if show_notified else 0) + (1 if show_action else 0) %}
|
||||
{% for ep in episodes %}
|
||||
<tr>
|
||||
<td style="font-size:0.82rem;white-space:nowrap;">{{ ev.at|ago }}</td>
|
||||
<td class="nowrap">
|
||||
{%- if ep.began_here %}{{ ep.start|ago }}{% else %}<span class="tone-dim" title="It began before this part of the list">earlier</span>{% endif -%}
|
||||
</td>
|
||||
{% if not chosen %}
|
||||
<td class="cell-name">
|
||||
<a href="/alerts/history?rule={{ ep.rule.id }}&range={{ current_range }}" title="{{ ep.rule.name }}">{{ ep.rule.name }}</a>
|
||||
<div class="cell-sub">{{ source_label(ep.rule.source_module) }} · {{ ep.rule.resource_name }}</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<a href="/alerts/history?rule={{ ev.rule.id }}&range={{ current_range }}">{{ ev.rule.name }}</a>
|
||||
<div style="font-size:0.78rem;color:var(--text-dim);">{{ source_label(ev.rule.source_module) }} · {{ ev.rule.resource_name }}</div>
|
||||
</td>
|
||||
<td style="white-space:nowrap;">
|
||||
{{ status_badge(ev.from_state) }}
|
||||
<span style="color:var(--text-dim);">→</span>
|
||||
{{ status_badge(ev.to_state) }}
|
||||
{% for n in ev.notes %}
|
||||
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.25rem;white-space:normal;">{{ n }}</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td style="font-size:0.85rem;white-space:nowrap;">
|
||||
{{ ev.value }}
|
||||
<div style="font-size:0.75rem;color:var(--text-dim);">fires when {{ ev.condition }}</div>
|
||||
</td>
|
||||
<td style="font-size:0.82rem;">
|
||||
{% if ev.muted_by %}
|
||||
<span class="badge badge-yellow" title="A maintenance window muted this notification">muted · {{ ev.muted_by }}</span>
|
||||
{% if ev.released %}<div style="color:var(--text-muted);margin-top:0.25rem;">notified when the window ended</div>{% endif %}
|
||||
{% if not ep.began_here and ep.steps|length == 1 %}
|
||||
{%- set s = ep.steps[0] %}{{ status_badge(s.from_state) }} <span class="tone-dim">→</span> {{ status_badge(s.to_state) }}
|
||||
{% else %}
|
||||
{{ status_badge(ep.worst) }}
|
||||
{%- if ep.ongoing %} <span class="tone-crit">{% if ep.began_here %}for {{ ep.duration|duration }}, {% endif %}still going</span>
|
||||
{%- elif ep.continues %} <span class="tone-muted">cleared later (above)</span>
|
||||
{%- elif ep.began_here %} for {{ ep.duration|duration }}<span class="tone-dim"> · cleared {{ ep.end|ago }}</span>
|
||||
{%- else %} <span class="tone-dim">cleared {{ ep.end|ago }}</span>{% endif %}
|
||||
{% endif %}
|
||||
{% for label, sent, error in ev.deliveries %}
|
||||
<div style="margin-top:0.15rem;">
|
||||
{% if sent %}
|
||||
<span class="badge badge-green">{{ label }} sent</span>
|
||||
{% else %}
|
||||
<span class="badge badge-red">{{ label }} failed</span>
|
||||
{% if error %}<div style="color:var(--text-muted);font-size:0.78rem;margin-top:0.15rem;max-width:28rem;word-break:break-word;">{{ error }}</div>{% endif %}
|
||||
{% for n in ep.notes %}<div class="cell-note">{{ n }}</div>{% endfor %}
|
||||
{% if ep.steps|length > 1 %}
|
||||
<details class="episode-steps">
|
||||
<summary>{{ ep.steps|length }} changes of state</summary>
|
||||
<ol>
|
||||
{% for s in ep.steps %}
|
||||
<li>{{ status_badge(s.from_state) }} <span class="tone-dim">→</span> {{ status_badge(s.to_state) }}
|
||||
<span class="tone-muted">{{ s.value }}</span> <span class="tone-dim">· {{ s.at|ago }}</span></li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</details>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="nowrap">
|
||||
{%- if ep.reading %}{{ ep.reading }} <span class="tone-dim">({{ ep.fires_when }})</span>
|
||||
{%- else %}<span class="tone-dim">no reading</span> <span class="tone-dim">({{ ep.fires_when }})</span>{% endif -%}
|
||||
</td>
|
||||
{% if show_notified %}
|
||||
<td>
|
||||
{% if ep.muted_by %}
|
||||
<span class="badge badge-yellow" title="A maintenance window muted this notification">muted · {{ ep.muted_by }}</span>
|
||||
{% if ep.released %}<div class="cell-note">notified when the window ended</div>{% endif %}
|
||||
{% endif %}
|
||||
{% for label, sent, error in ep.deliveries %}
|
||||
<div>
|
||||
{% if sent %}<span class="badge badge-green">{{ label }} sent</span>
|
||||
{% else %}<span class="badge badge-red">{{ label }} failed</span>
|
||||
{% if error %}<div class="cell-note" style="max-width:28rem;overflow-wrap:anywhere;">{{ error }}</div>{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not ev.muted_by and not ev.deliveries %}<span style="color:var(--text-dim);" title="No notification was sent for this change">—</span>{% endif %}
|
||||
{% if not ep.muted_by and not ep.deliveries %}<span class="tone-dim" title="No notification was sent for this">—</span>{% endif %}
|
||||
</td>
|
||||
<td style="font-size:0.82rem;">
|
||||
{% if ev.ansible_run_id %}
|
||||
<a href="/ansible/runs/{{ ev.ansible_run_id }}">Ansible run</a>
|
||||
{% else %}<span style="color:var(--text-dim);">—</span>{% endif %}
|
||||
{% endif %}
|
||||
{% if show_action %}
|
||||
<td>
|
||||
{% for run_id in ep.runs %}<div><a href="/ansible/runs/{{ run_id }}">Ansible run</a></div>
|
||||
{% else %}<span class="tone-dim">—</span>{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% if after %}
|
||||
{{ scroll_more("/alerts/history?before=" ~ (after|urlencode) ~ "&range=" ~ (current_range|urlencode) ~ (("&rule=" ~ (chosen.id|urlencode)) if chosen else ""), cols=6) }}
|
||||
{{ scroll_more("/alerts/history?before=" ~ (after|urlencode) ~ "&range=" ~ (current_range|urlencode) ~ (("&rule=" ~ (chosen.id|urlencode)) if chosen else ""), cols=cols) }}
|
||||
{% else %}
|
||||
{{ scroll_end(cols=6) }}
|
||||
{{ scroll_end(cols=cols) }}
|
||||
{% endif %}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
{# alerts/_history_rows.html — the history table; swapped in on a range or rule
|
||||
change. Its rows come a page at a time (alerts/_history_page.html). #}
|
||||
{% from "_macros.html" import status_badge, empty_state %}
|
||||
{% if events %}
|
||||
<div class="card-flush" style="overflow-x:auto;">
|
||||
<table class="table">
|
||||
{# alerts/_history_rows.html — the history: for one rule, how often it fired
|
||||
and for how long in all; then its episodes (#5316), swapped in on a range
|
||||
or rule change. The rule column shows only across rules, Notified and
|
||||
Action only when the range has something to put in them. Episodes come a
|
||||
page of transitions at a time (alerts/_history_page.html). #}
|
||||
{% from "_macros.html" import empty_state %}
|
||||
{% if chosen and verdict %}
|
||||
<p class="page-verdict">
|
||||
{%- if verdict.fired %}<strong class="tone-{{ 'crit' if verdict.ongoing else 'warn' }}">Fired {{ verdict.fired }} time{{ "s" if verdict.fired != 1 }}</strong> in the last {{ current_range }}
|
||||
{%- if verdict.seconds %}, {{ verdict.seconds|duration }} in all{% endif %}
|
||||
{%- if verdict.ongoing %} · <span class="tone-crit">firing now</span>{% endif %}
|
||||
{%- else %}<strong class="tone-ok">Never fired</strong> in the last {{ current_range }}{% endif %}
|
||||
<span class="tone-dim">· fires when {{ rule_condition(chosen) }} on {{ chosen.resource_name }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if episodes %}
|
||||
<div class="card-flush">
|
||||
<table class="table alert-episodes">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>When</th>
|
||||
<th>Rule</th>
|
||||
<th>Change</th>
|
||||
<th>Reading</th>
|
||||
<th>Notified</th>
|
||||
<th>Action</th>
|
||||
{% if not chosen %}<th>Rule</th>{% endif %}
|
||||
<th>What happened</th>
|
||||
<th title="The reading furthest past the rule's threshold">Worst reading</th>
|
||||
{% if show_notified %}<th>Notified</th>{% endif %}
|
||||
{% if show_action %}<th>Action</th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% block title %}Alert History — Steward{% endblock %}
|
||||
{% block breadcrumb %}{{ crumbs([("Alerts", "/alerts/"), ("History", "")]) }}{% endblock %}
|
||||
{% block content %}
|
||||
{% call page_header("Alert history", "Every change of state, newest first: the value against the rule's condition, whether the email and webhook went out, and the Ansible run an action started.") %}
|
||||
{% call page_header("Alert history", "Each time a rule left quiet, newest first: what it reached and for how long, its worst reading, and who was told.") %}
|
||||
<select id="history-rule" name="rule" aria-label="Rule" class="input" style="width:auto;"
|
||||
hx-get="/alerts/history" hx-target="#history-rows" hx-include="#time-range"
|
||||
hx-trigger="change" hx-push-url="false"
|
||||
|
||||
Reference in New Issue
Block a user