feat(dashboard): Monitors widget carries uptime; Uptime / SLA retired (#5250)

The Monitors widget opens with a verdict ("1 of 5 monitors down."), then one
row per monitor under a single header: latency and uptime over 24h, 7d and
30d, down monitors first with why and since when, the name linking to the
monitor. Counts follow the type filter. Problems always show; the limit
trims only the healthy rest ("+N more up").

Uptime / SLA (uptime_summary) repeated those numbers per host; migration
0033 moves dashboards onto Monitors. Shared widget styles (.w-verdict,
.w-table, .w-sub, .w-more) carry the widget-shape ruling for the widgets
that follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 13:12:58 -04:00
co-authored by Claude Opus 5.5
parent 4dcf958022
commit b5fcccba5f
10 changed files with 149 additions and 126 deletions
+39 -39
View File
@@ -1,44 +1,44 @@
{# monitors/widget.html — dashboard widget: unified monitor summary #}
{% if not monitor_data and up == 0 and down == 0 and pending == 0 %}
<div style="color:var(--text-muted);font-size:0.85rem;padding:0.5rem 0;">
No monitors configured. <a href="/monitors/new">Add a monitor →</a>
{# monitors/widget.html — the dashboard Monitors widget (#5250): a verdict,
then one row per monitor, problems first with why and since when, under one
header of latency and uptime over 24h / 7d / 30d. It replaced the separate
Uptime / SLA widget, so uptime has one home. #}
{% from "_macros.html" import status_dot %}
{% if not total %}
<div class="w-verdict" style="color:var(--text-muted);">
No monitors configured.{% if session.user_id %} <a href="/monitors/new">Add a monitor →</a>{% endif %}
</div>
{% else %}
<div style="display:flex;gap:1rem;margin-bottom:0.65rem;flex-wrap:wrap;">
{% if up %}
<div><span style="font-size:1.4rem;font-weight:500;line-height:1;color:var(--green);">{{ up }}</span>
<span style="font-size:0.78rem;color:var(--text-muted);margin-left:0.25rem;">up</span></div>
{% endif %}
{% if down %}
<div><span style="font-size:1.4rem;font-weight:500;line-height:1;color:var(--red);">{{ down }}</span>
<span style="font-size:0.78rem;color:var(--text-muted);margin-left:0.25rem;">down</span></div>
{% endif %}
{% if pending %}
<div><span style="font-size:1.4rem;font-weight:500;line-height:1;color:var(--text-dim);">{{ pending }}</span>
<span style="font-size:0.78rem;color:var(--text-muted);margin-left:0.25rem;">pending</span></div>
{% endif %}
<div class="w-verdict {{ 'tone-crit' if counts.down else ('tone-warn' if counts.degraded or counts.pending else 'tone-ok') }}">
{%- if counts.down %}{{ counts.down }} of {{ total }} monitor{{ 's' if total != 1 }} down{% if counts.degraded %}, {{ counts.degraded }} degraded{% endif %}.
{%- elif counts.degraded %}{{ counts.degraded }} of {{ total }} monitor{{ 's' if total != 1 }} degraded, the rest up.
{%- elif counts.pending %}{{ counts.pending }} not checked yet, {{ counts.up }} up.
{%- else %}All {{ counts.up }} monitor{{ 's' if counts.up != 1 }} up{% if counts.paused %}, {{ counts.paused }} paused{% endif %}.{% endif -%}
</div>
<div style="display:grid;gap:3px;">
{% for d in monitor_data %}
{% set latest = d.latest %}
<div style="display:flex;align-items:center;gap:0.5rem;font-size:0.82rem;padding:0.15rem 0;">
{% if not latest %}<span class="dot dot-dim"></span>
{% elif latest.is_up %}<span class="dot dot-up"></span>
{% else %}<span class="dot dot-down"></span>{% endif %}
{% if d.monitor.host_id %}
<a href="/hosts/{{ d.monitor.host_id }}" style="flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:inherit;text-decoration:none;">{{ d.monitor.name }}</a>
{% else %}
<span style="flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ d.monitor.name }}</span>
{% endif %}
{% if latest and latest.response_ms is not none %}
<span style="font-size:0.72rem;font-family:ui-monospace,monospace;color:var(--text-muted);flex-shrink:0;">{{ "%.0f"|format(latest.response_ms) }}ms</span>
{% elif latest and not latest.is_up %}
<span style="font-size:0.72rem;color:var(--red);flex-shrink:0;">{{ latest.status_code or "down" }}</span>
{% endif %}
</div>
{% if problems or rest %}
<table class="w-table">
<colgroup><col><col style="width:4.5rem"><col style="width:4.5rem"><col style="width:4.5rem"><col style="width:4.5rem"></colgroup>
<thead><tr><th>Monitor</th><th>Latency</th><th>24h</th><th>7d</th><th>30d</th></tr></thead>
<tbody>
{% for d in problems + rest %}
{% set m = d.monitor %}{% set latest = d.latest %}
<tr>
<td class="w-name" title="{{ m.name }}">
{{- status_dot(d.state) -}}
{% if session.user_id %}<a href="/monitors/{{ m.id }}">{{ m.name }}</a>{% else %}{{ m.name }}{% endif %}
{%- if d.state == 'down' %}<span class="w-sub tone-crit">went down {{ d.down_since|ago if d.down_since else '' }}{% if d.down_reason %} · {{ d.down_reason }}{% endif %}</span>
{%- elif d.state == 'degraded' %}<span class="w-sub tone-warn">{% if latest and status_class(latest.response_ms, 'latency') == 'crit' %}slow replies{% else %}checks failing now and then{% endif %}</span>
{%- elif d.state == 'pending' %}<span class="w-sub">not checked yet</span>{% endif %}
</td>
<td style="{{ threshold_style(latest.response_ms, 'latency') if latest and latest.is_up and latest.response_ms is not none }}">
{%- if latest and latest.is_up and latest.response_ms is not none %}{{ "%.0f"|format(latest.response_ms) }} ms{% else %}—{% endif -%}
</td>
{% for k in ("24h", "7d", "30d") %}{% set u = d.uptime.get(k) %}
<td style="{{ threshold_style(u, 'uptime') if u is not none }}">{% if u is none %}—{% else %}{{ "%.2f"|format(u) }}%{% endif %}</td>
{% endfor %}
</tr>
{% endfor %}
</div>
</tbody>
</table>
{% endif %}
{% if more %}<div class="w-more">+{{ more }} more up{% if session.user_id %} · <a href="/monitors/">all monitors</a>{% endif %}</div>{% endif %}
{% endif %}