feat(ui): audit, alert rules and the rule form say things in words (#5212)
#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:
@@ -3,12 +3,10 @@
|
||||
{% from "_macros.html" import status_badge, scroll_more, scroll_end %}
|
||||
{% for ev in events %}
|
||||
<tr>
|
||||
<td style="font-family:ui-monospace,monospace;font-size:0.82rem;white-space:nowrap;">
|
||||
{{ ev.at.strftime("%Y-%m-%d %H:%M:%S") }}
|
||||
</td>
|
||||
<td style="font-size:0.82rem;white-space:nowrap;">{{ ev.at|ago }}</td>
|
||||
<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);">{{ ev.rule.source_module }}/{{ ev.rule.resource_name }}</div>
|
||||
<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) }}
|
||||
@@ -18,9 +16,9 @@
|
||||
<div style="font-size:0.78rem;color:var(--text-muted);margin-top:0.25rem;white-space:normal;">{{ n }}</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td style="font-family:ui-monospace,monospace;font-size:0.85rem;white-space:nowrap;">
|
||||
{{ ev.rule.metric_name }} = {{ ev.value }}
|
||||
<div style="font-size:0.75rem;color:var(--text-dim);">rule: {{ ev.condition }}</div>
|
||||
<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 %}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>When (UTC)</th>
|
||||
<th>When</th>
|
||||
<th>Rule</th>
|
||||
<th>Change</th>
|
||||
<th>Value</th>
|
||||
<th>Reading</th>
|
||||
<th>Notified</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{# alerts/_rule_fields.html — HTMX partial: resource + metric selects for a given source_module #}
|
||||
{# alerts/_rule_fields.html — the resource and metric pickers for one source;
|
||||
the rule form includes it and swaps it in again when the source changes. #}
|
||||
<div class="form-group">
|
||||
<label>Resource Name</label>
|
||||
<label>Resource</label>
|
||||
{% if resources %}
|
||||
<select name="resource_name" required>
|
||||
{% if not current_resource %}<option value="" disabled selected>— pick a resource —</option>{% endif %}
|
||||
@@ -10,20 +11,24 @@
|
||||
</select>
|
||||
{% else %}
|
||||
<select name="resource_name" required disabled>
|
||||
<option value="">No data yet — run monitors first</option>
|
||||
<option value="">Nothing has reported from this source yet</option>
|
||||
</select>
|
||||
<p class="field-hint">
|
||||
Resource names populate from live metric data. Start the scheduler and try again.
|
||||
The list fills from readings this source has recorded. Come back once it has reported.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Metric Name</label>
|
||||
<select name="metric_name" required>
|
||||
<label>Metric</label>
|
||||
<select name="metric_name" required data-metric-picker>
|
||||
{% if not current_metric %}<option value="" disabled selected>— pick a metric —</option>{% endif %}
|
||||
{% for m in metrics %}
|
||||
<option value="{{ m }}" {% if m == current_metric %}selected{% endif %}>{{ m }}</option>
|
||||
<option value="{{ m }}" data-unit="{{ metric_unit(m) }}" data-about="{{ metric_about(m) }}" {% if m in ("is_up", "is_healthy") %}data-binary{% endif %}
|
||||
{% if m == current_metric %}selected{% endif %}>{{ metric_label(m) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% if current_metric and metric_about(current_metric) %}
|
||||
<p class="field-hint" data-metric-about>{{ metric_about(current_metric) }}</p>
|
||||
{% else %}<p class="field-hint" data-metric-about hidden></p>{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -44,10 +44,8 @@
|
||||
{%- else %}<span class="tone-ok" style="font-size:0.8rem;" title="Within its threshold">quiet</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>
|
||||
{{ rule.resource_name }} {{ rule_condition(rule) }}
|
||||
<div style="font-size:0.75rem;color:var(--text-dim);">{{ source_label(rule.source_module) }}</div>
|
||||
</td>
|
||||
<td style="color:var(--text-muted);text-align:center;">{{ rule.consecutive_failures_required }} / {{ rule.consecutive_ok_required }}</td>
|
||||
<td class="td-actions">
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
{% block title %}Maintenance Windows — Steward{% endblock %}
|
||||
{% block breadcrumb %}{{ crumbs([("Alerts", "/alerts/"), ("Maintenance", "")]) }}{% endblock %}
|
||||
{% block content %}
|
||||
{% set can_edit = session.user_role in ('operator', 'admin') %}
|
||||
{% call page_header("Maintenance windows", "During an active window, alerts in its scope still change state and show on the Alerts page, but send no notifications and run no Ansible actions. Each event a window mutes says so in the alert's history. An alert that is still firing when the window ends notifies then, and runs its action.") %}
|
||||
<a class="btn" href="/alerts/maintenance/new">New window</a>
|
||||
{% if can_edit %}<a class="btn" href="/alerts/maintenance/new">New window</a>{% endif %}
|
||||
{% endcall %}
|
||||
|
||||
{% macro window_table(rows, kind) %}
|
||||
@@ -28,9 +29,9 @@
|
||||
{% if w.scope_source is none %}
|
||||
<span title="All alert rules">All alerts</span>
|
||||
{% elif w.scope_resource is none %}
|
||||
<span>{{ w.scope_source }}/*</span>
|
||||
<span>Every alert on {{ source_label(w.scope_source) }}</span>
|
||||
{% else %}
|
||||
<span>{{ w.scope_source }}/{{ w.scope_resource }}</span>
|
||||
<span>{{ w.scope_resource }}</span> <span class="field-hint">{{ source_label(w.scope_source) }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="font-size:0.85rem;font-family:ui-monospace,monospace;">
|
||||
@@ -45,10 +46,12 @@
|
||||
{% else %}{{ status_badge('inactive', 'Expired') }}<div class="field-hint">ended {{ w.end_at|ago }}</div>{% endif %}
|
||||
</td>
|
||||
<td class="td-actions">
|
||||
{% if session.user_role in ('operator', 'admin') %}
|
||||
<form method="post" action="/alerts/maintenance/{{ w.id }}/delete" style="display:inline;">{{ csrf_field() }}
|
||||
<button type="submit" class="btn btn-sm btn-danger"
|
||||
data-confirm="Delete window "{{ w.name }}"?">Delete</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -76,6 +79,6 @@
|
||||
</details>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="card">{{ empty_state("No maintenance windows yet.", "New window", "/alerts/maintenance/new") }}</div>
|
||||
<div class="card">{% if can_edit %}{{ empty_state("No maintenance windows yet.", "New window", "/alerts/maintenance/new") }}{% else %}{{ empty_state("No maintenance windows yet.") }}{% endif %}</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -24,68 +24,49 @@
|
||||
hx-target="#rule-fields"
|
||||
hx-include="[name='source_module']">
|
||||
{% for src in source_modules %}
|
||||
<option value="{{ src }}" {% if src == initial_source %}selected{% endif %}>
|
||||
{{ src }}
|
||||
</option>
|
||||
<option value="{{ src }}" {% if src == initial_source %}selected{% endif %}>{{ source_label(src) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# ── Dynamic resource + metric ────────────────────────────────────────── #}
|
||||
<div id="rule-fields">
|
||||
{# Inline the same partial logic so the page is fully usable without JS #}
|
||||
<div class="form-group">
|
||||
<label>Resource Name</label>
|
||||
{% if resources %}
|
||||
<select name="resource_name" required>
|
||||
{% if not (rule and rule.resource_name) %}<option value="" disabled selected>— pick a resource —</option>{% endif %}
|
||||
{% for r in resources %}
|
||||
<option value="{{ r }}" {% if rule and r == rule.resource_name %}selected{% endif %}>{{ r }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% else %}
|
||||
<select name="resource_name" required disabled>
|
||||
<option value="">No data yet — run monitors first</option>
|
||||
</select>
|
||||
<p class="field-hint">
|
||||
Resource names populate from live metric data. Start the scheduler and try again.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Metric Name</label>
|
||||
<select name="metric_name" required>
|
||||
{% if not (rule and rule.metric_name) %}<option value="" disabled selected>— pick a metric —</option>{% endif %}
|
||||
{% for m in metrics %}
|
||||
<option value="{{ m }}" {% if rule and m == rule.metric_name %}selected{% endif %}>{{ m }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% with current_resource = rule.resource_name if rule else "", current_metric = rule.metric_name if rule else "" %}
|
||||
{% include "alerts/_rule_fields.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{# ── Condition ───────────────────────────────────────────────────────── #}
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
|
||||
{# ── Condition: a threshold in the metric's unit, or for an up/down metric
|
||||
the word it fires on (sent as binary_when; the route turns it into
|
||||
< 0.5 or >= 0.5). The script swaps the two as the metric changes. #}
|
||||
{% set m0 = rule.metric_name if rule else "" %}
|
||||
{% set binary = m0 in ("is_up", "is_healthy") %}
|
||||
{% set fires_down = not rule or rule.operator.value in ("<", "<=") %}
|
||||
<div data-cond-binary {% if not binary %}hidden{% endif %} class="form-group">
|
||||
<label>Fires when it is</label>
|
||||
<select name="binary_when" {% if not binary %}disabled{% endif %}>
|
||||
<option value="down" {% if fires_down %}selected{% endif %}>down (unhealthy)</option>
|
||||
<option value="up" {% if not fires_down %}selected{% endif %}>up (healthy)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div data-cond-numeric {% if binary %}hidden{% endif %} style="display:grid;grid-template-columns:1fr 1fr;gap:1rem;">
|
||||
<div class="form-group">
|
||||
<label>Operator</label>
|
||||
<select name="operator">
|
||||
<label>Fires when the reading is</label>
|
||||
<select name="operator" {% if binary %}disabled{% endif %}>
|
||||
{% for val, label in operators %}
|
||||
<option value="{{ val }}" {% if rule and rule.operator.value == val %}selected{% endif %}>
|
||||
{{ label }}
|
||||
</option>
|
||||
<option value="{{ val }}" {% if rule and rule.operator.value == val %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Threshold</label>
|
||||
<input type="number" name="threshold" step="any" required
|
||||
value="{{ rule.threshold if rule else '' }}"
|
||||
placeholder="0.5">
|
||||
<label>Threshold <span data-threshold-unit style="color:var(--text-muted);font-weight:normal;">{% if metric_unit(m0) %}({{ metric_unit(m0) }}){% endif %}</span></label>
|
||||
<input type="number" name="threshold" step="any" required {% if binary %}disabled{% endif %}
|
||||
value="{{ rule.threshold if rule and not binary else '' }}" placeholder="90">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Consecutive failures before FIRING</label>
|
||||
<label>Breaching checks in a row before it fires</label>
|
||||
<input type="number" name="consecutive_failures_required" min="1"
|
||||
value="{{ rule.consecutive_failures_required if rule else 1 }}">
|
||||
</div>
|
||||
@@ -114,44 +95,16 @@
|
||||
{# ── Metric reference ────────────────────────────────────────────────── #}
|
||||
<details style="margin-bottom:1rem;">
|
||||
<summary style="cursor:pointer;font-size:0.82rem;color:var(--text-muted);user-select:none;">
|
||||
Metric reference
|
||||
What each metric measures
|
||||
</summary>
|
||||
<div style="margin-top:0.75rem;display:grid;gap:0.5rem;">
|
||||
{% set descs = {
|
||||
"icmp": [("is_up", "1.0 = reachable, 0.0 = unreachable"),
|
||||
("response_ms", "round-trip latency in milliseconds")],
|
||||
"tcp": [("is_up", "1.0 = port open, 0.0 = unreachable"),
|
||||
("response_ms", "TCP connect time in milliseconds")],
|
||||
"dns": [("is_up", "1.0 = resolved (and matches expected IP), 0.0 = failed"),
|
||||
("response_ms", "resolution time in milliseconds")],
|
||||
"traefik": [("request_rate", "requests/sec for this router"),
|
||||
("error_rate_4xx_pct", "% of responses that are 4xx"),
|
||||
("error_rate_5xx_pct", "% of responses that are 5xx"),
|
||||
("latency_p50_ms", "median response time ms"),
|
||||
("latency_p95_ms", "p95 response time ms"),
|
||||
("latency_p99_ms", "p99 response time ms"),
|
||||
("response_bytes_rate", "bytes/sec of responses"),
|
||||
("cert_expiry_days", "days until TLS cert expires")],
|
||||
"unifi": [("is_up", "1.0 = WAN up, 0.0 = down"),
|
||||
("latency_ms", "WAN latency in ms"),
|
||||
("total_clients", "number of connected clients")],
|
||||
"docker": [("cpu_pct", "container CPU usage %"),
|
||||
("mem_pct", "container memory usage %")],
|
||||
"http": [("is_up", "1.0 = check passed, 0.0 = failed"),
|
||||
("response_ms", "HTTP response time in ms")],
|
||||
"host_agent": [("seconds_since_seen", "seconds since the agent last reported; > 180 = offline"),
|
||||
("cpu_pct", "host CPU usage %"),
|
||||
("mem_used_pct", "host memory usage %"),
|
||||
("disk_used_pct_worst", "fullest disk, % used"),
|
||||
("disk_used_pct", "one mount, % used (resource host:mount)")],
|
||||
} %}
|
||||
{% for src, pairs in descs.items() %}
|
||||
{% for src, names in catalog.items() if names %}
|
||||
<div>
|
||||
<div style="font-size:0.78rem;font-weight:500;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:0.2rem;">{{ src }}</div>
|
||||
{% for metric, desc in pairs %}
|
||||
<div style="display:grid;grid-template-columns:160px 1fr;gap:0.5rem;font-size:0.78rem;padding:0.15rem 0;">
|
||||
<code>{{ metric }}</code>
|
||||
<span style="color:var(--text-muted);">{{ desc }}</span>
|
||||
<div style="font-size:0.78rem;font-weight:500;color:var(--text-dim);margin-bottom:0.2rem;">{{ source_label(src) }}</div>
|
||||
{% for m in names %}
|
||||
<div style="display:grid;grid-template-columns:200px 1fr;gap:0.5rem;font-size:0.78rem;padding:0.15rem 0;">
|
||||
<span>{{ metric_label(m) }}{% if metric_unit(m) %} <span style="color:var(--text-dim);">({{ metric_unit(m) }})</span>{% endif %}</span>
|
||||
<span style="color:var(--text-muted);">{{ metric_about(m) }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -210,19 +163,29 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not rule %}
|
||||
<script>
|
||||
// "Agent offline" is seconds_since_seen > 180 (the agent's stale window).
|
||||
// The condition follows the metric: an up/down metric asks which word
|
||||
// fires, any other a threshold in its unit. "Agent offline" is
|
||||
// seconds_since_seen > 180 (the agent's stale window).
|
||||
document.addEventListener("change", function (e) {
|
||||
if (e.target.name !== "metric_name" || e.target.value !== "seconds_since_seen") return;
|
||||
var form = e.target.form, threshold = form.elements["threshold"];
|
||||
if (threshold.value === "") {
|
||||
if (e.target.name !== "metric_name") return;
|
||||
var form = e.target.form, opt = e.target.selectedOptions[0];
|
||||
var binary = opt.hasAttribute("data-binary"), unit = opt.dataset.unit || "";
|
||||
form.querySelector("[data-cond-binary]").hidden = !binary;
|
||||
form.querySelector("[data-cond-numeric]").hidden = binary;
|
||||
form.elements["binary_when"].disabled = !binary;
|
||||
form.elements["operator"].disabled = binary;
|
||||
form.elements["threshold"].disabled = binary;
|
||||
form.querySelector("[data-threshold-unit]").textContent = unit ? "(" + unit + ")" : "";
|
||||
var about = form.querySelector("[data-metric-about]");
|
||||
if (about) { about.textContent = opt.dataset.about || ""; about.hidden = !opt.dataset.about; }
|
||||
var threshold = form.elements["threshold"];
|
||||
if (opt.value === "seconds_since_seen" && threshold.value === "") {
|
||||
form.elements["operator"].value = ">";
|
||||
threshold.value = "180";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<div style="display:flex;gap:1rem;margin-top:1.5rem;">
|
||||
<button type="submit" class="btn">{% if rule %}Save Changes{% else %}Create Rule{% endif %}</button>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
{% from "_macros.html" import scroll_more, scroll_end %}
|
||||
{% for item in events %}
|
||||
{% set e = item.event %}
|
||||
{% set d = item.detail %}
|
||||
<tr>
|
||||
<td style="white-space:nowrap;" title="{{ e.timestamp.strftime('%Y-%m-%d %H:%M:%S UTC') }}">{{ e.timestamp|ago }}</td>
|
||||
<td style="font-size:0.85rem;">{{ e.username }}</td>
|
||||
<td><span class="mono">{{ e.action }}</span></td>
|
||||
<td title="{{ e.action }}">{{ item.action or e.action }}</td>
|
||||
<td>
|
||||
{% if item.entity %}
|
||||
<span class="field-hint">{{ e.entity_type|replace('_', ' ') }}</span>
|
||||
@@ -16,13 +15,11 @@
|
||||
{% elif e.entity_type %}<span class="field-hint">{{ e.entity_type|replace('_', ' ') }}</span>{% endif %}
|
||||
</td>
|
||||
<td style="font-size:0.8rem;color:var(--text-muted);max-width:340px;">
|
||||
{% if d %}
|
||||
{% for k, v in d.items() %}
|
||||
<span style="color:var(--text-dim);">{{ k }}:</span>
|
||||
<span class="mono">{{ v }}</span>
|
||||
{% for f in item.facts %}
|
||||
<span style="color:var(--text-dim);">{{ f.label }}</span>
|
||||
{% if f.url %}<a href="{{ f.url }}">{{ f.text }}</a>{% else %}{{ f.text }}{% endif %}
|
||||
{% if not loop.last %} · {% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
{% block content %}
|
||||
{% call page_header("Audit log", "Who changed what, newest first.") %}
|
||||
<form method="get" style="display:flex;gap:0.5rem;align-items:center;">
|
||||
<input type="text" name="action" value="{{ action_filter }}"
|
||||
placeholder="Filter by action…" aria-label="Filter by action"
|
||||
style="width:200px;font-size:0.85rem;">
|
||||
<button type="submit" class="btn btn-ghost btn-sm">Filter</button>
|
||||
{% if action_filter %}
|
||||
<a href="/audit/" class="btn btn-ghost btn-sm">Clear</a>
|
||||
{% endif %}
|
||||
<select name="action" aria-label="Show only" style="font-size:0.85rem;" onchange="this.form.submit()">
|
||||
<option value="">Everything</option>
|
||||
{% for prefix, label in areas %}
|
||||
<option value="{{ prefix }}" {% if action_filter == prefix %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<noscript><button type="submit" class="btn btn-ghost btn-sm">Filter</button></noscript>
|
||||
</form>
|
||||
{% endcall %}
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
<tr>
|
||||
<th>When</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Entity</th>
|
||||
<th>Did</th>
|
||||
<th>What</th>
|
||||
<th>Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<li>
|
||||
{{ status_badge('no_data' if state.no_data_since and state.state.value == 'inactive' else state.state) }}
|
||||
<a href="/alerts/history?rule={{ rule.id }}">{{ rule.name }}</a>
|
||||
<span class="problem-detail">{{ rule.resource_name }} · {{ rule.metric_name }} {{ rule.operator.value }} {{ rule.threshold }} · since {{ (state.no_data_since if state.state.value == 'inactive' else state.last_transition_at)|ago }}</span>
|
||||
<span class="problem-detail">{{ rule.resource_name }} {{ rule_condition(rule) }} · since {{ (state.no_data_since if state.state.value == 'inactive' else state.last_transition_at)|ago }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user