feat(ui): audit, alert rules and the rule form say things in words (#5212)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 51s
CI / unit (push) Failing after 1m14s
CI / publish (push) Skipped

#5212 "Plain labels: no raw ids, scopes, metric keys or unexplained jargon",
batch 1:

- Audit log: each action reads as a sentence ("paused monitor"), with the
  code as its tooltip. Detail shows flags as yes/no, lists joined, a run's
  scope by name, spans as spans, and no ids the entity column already gives.
  The filter is now a picker of areas.
- Alert rules, history and Status say the condition in words: "is down",
  "Fullest disk >= 85%". Readings use metric_value, and history times use
  |ago.
- Rule form: sources and metrics by name, the threshold's unit, "fires
  when it is down/up" for an up/down metric instead of 0.5, and a metric
  reference built from the catalog.
- Schedules and Maintenance: viewers no longer see Run now, Pause, Edit,
  Delete or New. Intervals and next runs read as "every 2h" and "in 3h";
  a scope reads "Every alert on Host agent".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 00:23:02 -04:00
co-authored by Claude Opus 5.5
parent 7275fad767
commit 3a772716ae
21 changed files with 487 additions and 156 deletions
+10 -5
View File
@@ -5,10 +5,11 @@
{% macro fmt_interval(secs) %}
{%- set m = {300: "every 5 min", 3600: "hourly", 21600: "every 6h", 43200: "every 12h", 86400: "daily", 604800: "weekly"} -%}
{{ m.get(secs, secs ~ "s") }}
{{ m.get(secs) or ("every " ~ (secs|duration)) }}
{% endmacro %}
{% block content %}
{% set can_edit = session.user_role in ('operator', 'admin') %}
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.25rem;gap:1rem;flex-wrap:wrap;">
<h1 class="page-title" style="margin-bottom:0;">Ansible Schedules</h1>
<div style="display:flex;gap:0.5rem;">
@@ -19,7 +20,7 @@
{# ── Existing schedules ───────────────────────────────────────────────────── #}
{% if not rows %}
<div class="card empty">No schedules yet. Create one below to run a playbook on a recurring interval.</div>
<div class="card empty">No schedules yet.{% if can_edit %} Create one below to run a playbook on a recurring interval.{% endif %}</div>
{% else %}
<div class="card-flush">
<table class="table">
@@ -46,7 +47,7 @@
{# With a run beside it, the last firing was skipped because that run was
still going (#5158); without one, the firing couldn't start a run. #}
<div style="color:{{ 'var(--text-muted)' if row.last_run else 'var(--red)' }};">
{{ s.last_error }} · {{ s.last_run_at.strftime("%m-%d %H:%M") }}
{{ s.last_error }} · {{ s.last_run_at|ago }}
</div>
{% elif not row.last_run %}
<span style="color:var(--text-dim);">never</span>
@@ -54,10 +55,11 @@
</td>
<td style="font-size:0.82rem;color:var(--text-muted);">
{% if not s.enabled %}<span style="color:var(--text-dim);">paused</span>
{% elif row.next_run %}{{ row.next_run.strftime("%m-%d %H:%M") }}
{% elif row.next_run %}{{ row.next_run|ago }}
{% else %}due now{% endif %}
</td>
<td class="td-actions" style="white-space:nowrap;">
{% if can_edit %}
<form method="post" action="/ansible/schedules/{{ s.id }}/run-now" style="display:inline;">{{ csrf_field() }}
<button class="btn btn-sm btn-ghost" type="submit">Run now</button>
</form>
@@ -69,6 +71,7 @@
data-confirm="Delete schedule {{ s.name }}?">{{ csrf_field() }}
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
@@ -77,7 +80,8 @@
</div>
{% endif %}
{# ── Create / edit form ───────────────────────────────────────────────────── #}
{# ── Create / edit form (operators and admins) ───────────────────────────── #}
{% if can_edit %}
{% set p = (editing.params or {}) if editing else {} %}
{% set saved_vars = p.get('extra_vars_map') or {} %}
{% set declared_names = (edit_variables or []) | map(attribute='name') | list %}
@@ -174,4 +178,5 @@
</div>
</form>
</div>
{% endif %}
{% endblock %}