#5210 "Problems first, with since-time, across every list" (milestone 477), first batch. - Alerts: the Active list reads firing, no data, acknowledged, pending, the longest-running first, and re-polls itself. An acknowledged alert says how long it has been breaching (from the alert's events; acknowledging moves last_transition_at) and, apart from that, who acknowledged it and when. Rules show what each is doing now; disabled rules sort last. The current value reads the latest-value table (#5208) instead of plugin_metrics. - Monitors: a down monitor says "Down 12m · connection refused" in the list. - Hosts: sorted down, agent gone quiet, pending, healthy. The agent column shows "—" for a missing reading (was 0%) and a Stale badge with its age. agent_metrics_by_host is gone; the list reads agent_overview_by_host. - Schedules: failing first (failed or interrupted run, or a firing that could not start one), then running, then the rest, paused last; status badge from the shared states. - Maintenance windows: active (ending soonest), then upcoming (starting soonest), with expired folded away; each says when it ends/starts/ended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
3.9 KiB
HTML
75 lines
3.9 KiB
HTML
{# HTMX fragment — the Monitors list: counts, then every monitor, problems first
|
||
(service.problems_first). Colours come from Settings → Thresholds. #}
|
||
{% from "_macros.html" import stat_tile, status_badge, status_dot, icon_button, empty_state %}
|
||
<div class="stat-strip">
|
||
{{ stat_tile("Down", counts.down, state="down" if counts.down else "inactive") }}
|
||
{{ stat_tile("Up", counts.up, state="up" if counts.up else "inactive",
|
||
hint=(counts.degraded ~ " degraded") if counts.degraded else none) }}
|
||
{{ stat_tile("Pending", counts.pending, state="pending" if counts.pending else "inactive") }}
|
||
{{ stat_tile("Paused", counts.paused, state="paused") }}
|
||
</div>
|
||
|
||
{% if monitor_data %}
|
||
<div class="card ping-card">
|
||
{% set can_edit = session.user_role in ['operator', 'admin'] %}
|
||
{% for d in monitor_data %}
|
||
{% set m = d.monitor %}
|
||
{% set latest = d.latest %}
|
||
<div class="ping-row{% if d.state == 'paused' %} ping-row-paused{% endif %}">
|
||
<div class="ping-meta" style="min-width:160px;max-width:260px;">
|
||
<div style="display:flex;align-items:baseline;gap:0.4rem;">
|
||
{{ status_dot(d.state) }}
|
||
<a class="ping-name" href="/monitors/{{ m.id }}">{{ m.name }}</a>
|
||
</div>
|
||
<span class="ping-addr" title="{{ m.target }}">{{ m.type|monitor_type }} · {{ m.target }}</span>
|
||
{% if d.state == 'down' %}
|
||
<span class="tone-crit" style="font-size:0.75rem;overflow-wrap:anywhere;">
|
||
Down{% if d.down_since %} {{ d.down_since|ago|replace(' ago', '') }}{% endif %}{% if d.down_reason %} · {{ d.down_reason }}{% endif %}
|
||
</span>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="ping-pills">
|
||
{% for _ in range([30 - d.heartbeat|length, 0]|max) %}<span class="pill pill-empty" title="No data"></span>{% endfor %}
|
||
{% for h in d.heartbeat %}<span class="pill pill-{{ h.state }}" title="{{ h.title }}"></span>{% endfor %}
|
||
</div>
|
||
|
||
<div class="ping-cur">
|
||
{% if d.state in ('down', 'paused', 'pending') %}{{ status_badge(d.state) }}
|
||
{% elif latest.response_ms is not none %}
|
||
<span class="tone-{{ status_class(latest.response_ms, 'latency') }}">{{ "%.0f"|format(latest.response_ms) }} ms</span>
|
||
{% elif m.type == 'dns' and latest.resolved_ip %}<span class="tone-ok">{{ latest.resolved_ip }}</span>
|
||
{% else %}{{ status_badge('up') }}{% endif %}
|
||
</div>
|
||
|
||
<div style="min-width:4.5rem;text-align:right;font-size:0.78rem;font-variant-numeric:tabular-nums;">
|
||
{% if d.uptime_pct is not none %}<span class="tone-{{ status_class(d.uptime_pct, 'uptime') }}">{{ "%.1f"|format(d.uptime_pct) }}%</span>{% else %}<span class="tone-unknown">—</span>{% endif %}
|
||
<span class="tone-dim" style="font-size:0.7rem;margin-left:0.15rem;">{{ range_key }}</span>
|
||
</div>
|
||
|
||
{% if d.tls_days is not none %}
|
||
<div style="min-width:4rem;text-align:right;font-size:0.72rem;">
|
||
{% if d.tls_days < 0 %}<span class="tone-crit" title="Certificate expired">cert expired</span>
|
||
{% else %}<span class="tone-{{ status_class(d.tls_days, 'tls_days') }}" title="TLS certificate expires in {{ d.tls_days|round|int }} days">cert {{ d.tls_days|round|int }}d</span>{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if can_edit %}
|
||
<div class="row-actions">
|
||
{{ icon_button("✎", "Edit " ~ m.name, href="/monitors/" ~ m.id ~ "/edit") }}
|
||
<form method="post" action="/monitors/{{ m.id }}/toggle">{{ csrf_field() }}
|
||
{{ icon_button("⏸" if m.enabled else "▶", ("Pause " if m.enabled else "Resume ") ~ m.name, type="submit") }}
|
||
</form>
|
||
<form method="post" action="/monitors/{{ m.id }}/delete">{{ csrf_field() }}
|
||
{{ icon_button("×", "Delete " ~ m.name, type="submit", confirm="Delete monitor " ~ m.name ~ "?") }}
|
||
</form>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div class="card">{{ empty_state("No monitors yet. Add one, or attach checks to a host from its page.",
|
||
"Add monitor" if session.user_role in ['operator', 'admin'] else none, "/monitors/new") }}</div>
|
||
{% endif %}
|