The Monitors list now opens with a count strip (down, up with how many degraded, pending, paused) and sorts down, degraded, pending, up, paused, by name within each. Degraded is up but worth a look: uptime at or below its warning floor, or the latest response past its critical cutoff, both from Settings → Thresholds; a slow-but-tolerable reply is coloured, not sorted. A monitor had no page of its own: every link went to the list. Now /monitors/<id> shows its state and since when, latency, uptime for 24h/7d/ 30d, TLS days, the last checks, recent failures with their errors, and its settings (HTTP header names, never their values). Status links to it, and adding or editing returns to it. Adding is its own page (/monitors/new, form.html shared with edit) behind an "Add monitor" button; a refused add re-renders the form with the error and what was typed instead of a bare 400 text. Row actions are labelled icon buttons, type names read Ping/TCP/DNS/HTTP (|monitor_type), and the heartbeat uses pill-up/pill-down, so HEX_CEILING drops to 56. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
6.4 KiB
HTML
109 lines
6.4 KiB
HTML
{# status/rows.html — what is wrong first (down monitors, active alerts), then
|
|
every monitor; refreshed on poll #}
|
|
{% from "_macros.html" import status_badge, status_dot, stat_tile, empty_state %}
|
|
{% macro uptime_cell(pct) -%}
|
|
{%- if pct is none -%}<span class="tone-unknown">—</span>
|
|
{%- else -%}<span class="tone-{{ status_class(pct, 'uptime') }}">{{ "%.2f"|format(pct) }}%</span>{%- endif -%}
|
|
{%- endmacro %}
|
|
{% macro since(e) -%}
|
|
{%- if e.since -%}{{ "for at least " if e.since_at_least else "since " }}{{ e.since|ago }}{%- endif -%}
|
|
{%- endmacro %}
|
|
|
|
{# ── What is wrong ───────────────────────────────────────────────────────── #}
|
|
{% if problems or alerts %}
|
|
<section class="card problem-card" aria-labelledby="status-problems">
|
|
<h2 id="status-problems" class="section-title" style="margin-bottom:0.6rem;">Needs attention</h2>
|
|
<ul class="problem-list">
|
|
{% for e in problems %}
|
|
<li>
|
|
{{ status_badge('down') }}
|
|
{% if e.detail_url %}<a href="{{ e.detail_url }}">{{ e.name }}</a>{% else %}<strong>{{ e.name }}</strong>{% endif %}
|
|
<span class="problem-detail">{{ e.kind|upper }} · {{ e.target }} · down {{ since(e) }}</span>
|
|
</li>
|
|
{% endfor %}
|
|
{% for state, rule in alerts %}
|
|
<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>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{# ── Counts ──────────────────────────────────────────────────────────────── #}
|
|
<div class="stat-strip">
|
|
{{ stat_tile("Down", down, state="down" if down else "inactive") }}
|
|
{{ stat_tile("Active alerts", alerts|length, state="firing" if alerts else "inactive", href="/alerts/") }}
|
|
{{ stat_tile("Pending", pending, state="pending" if pending else "inactive") }}
|
|
{{ stat_tile("Up", up, state="up") }}
|
|
</div>
|
|
|
|
{% if not entries %}
|
|
<div class="card">{{ empty_state("No monitors yet. Add a host (ping or DNS) or an HTTP monitor to fill the board.", "Add a monitor", "/monitors/new") }}</div>
|
|
{% else %}
|
|
<div class="card-flush">
|
|
{# header row #}
|
|
<div style="display:flex;align-items:center;gap:1rem;padding:0.55rem 1rem;border-bottom:1px solid var(--border-mid);font-size:0.72rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.04em;">
|
|
<span style="width:11rem;flex-shrink:0;">Monitor</span>
|
|
<span style="flex:1;min-width:0;">Recent checks</span>
|
|
<span style="width:92px;text-align:right;flex-shrink:0;">Latest</span>
|
|
<span style="width:54px;text-align:center;flex-shrink:0;">24h</span>
|
|
<span style="width:54px;text-align:center;flex-shrink:0;">7d</span>
|
|
<span style="width:54px;text-align:center;flex-shrink:0;">30d</span>
|
|
<span style="width:64px;text-align:right;flex-shrink:0;">TLS</span>
|
|
</div>
|
|
|
|
{% for e in entries %}
|
|
<div style="display:flex;align-items:center;gap:1rem;padding:0.55rem 1rem;border-bottom:1px solid var(--border);">
|
|
{# name + kind #}
|
|
<div style="width:11rem;flex-shrink:0;min-width:0;">
|
|
<div style="display:flex;align-items:baseline;gap:0.4rem;">
|
|
{{ status_dot(e.status) }}
|
|
{% if e.detail_url %}<a href="{{ e.detail_url }}" style="font-size:0.85rem;font-weight:500;color:var(--text);overflow-wrap:anywhere;">{{ e.name }}</a>
|
|
{% else %}<span style="font-size:0.85rem;font-weight:500;overflow-wrap:anywhere;">{{ e.name }}</span>{% endif %}
|
|
</div>
|
|
<div style="color:var(--text-dim);font-size:0.7rem;overflow-wrap:anywhere;padding-left:0.85rem;">
|
|
<span style="text-transform:uppercase;letter-spacing:0.04em;">{{ e.kind }}</span> · {{ e.target }}
|
|
</div>
|
|
</div>
|
|
|
|
{# heartbeat bar #}
|
|
<div style="flex:1;min-width:0;display:flex;gap:2px;align-items:center;overflow:hidden;">
|
|
{% set pad = 30 - e.heartbeat|length %}
|
|
{% if pad > 0 %}{% for _ in range(pad) %}<span class="pill pill-empty" title="No data"></span>{% endfor %}{% endif %}
|
|
{% for hb in e.heartbeat %}
|
|
<span class="pill pill-{{ 'down' if hb.state == 'down' else 'up' }}" title="{{ hb.title }}"></span>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
{# latest latency + sparkline #}
|
|
<div style="width:92px;text-align:right;flex-shrink:0;font-size:0.8rem;font-variant-numeric:tabular-nums;">
|
|
{% if e.status == 'down' %}{{ status_badge('down') }}
|
|
{% elif e.latency_ms is not none %}
|
|
{% if e.spark_svg %}<span style="opacity:0.8;">{{ e.spark_svg|safe }}</span><br>{% endif %}
|
|
<span style="color:var(--text-muted);">{{ "%.0f"|format(e.latency_ms) }} ms</span>
|
|
{% elif e.status == 'up' %}{{ status_badge('up') }}
|
|
{% else %}<span style="color:var(--text-dim);">—</span>{% endif %}
|
|
</div>
|
|
|
|
{# uptime windows #}
|
|
<span style="width:54px;text-align:center;flex-shrink:0;font-size:0.8rem;font-family:ui-monospace,monospace;">{{ uptime_cell(e.uptime.get('24h')) }}</span>
|
|
<span style="width:54px;text-align:center;flex-shrink:0;font-size:0.8rem;font-family:ui-monospace,monospace;">{{ uptime_cell(e.uptime.get('7d')) }}</span>
|
|
<span style="width:54px;text-align:center;flex-shrink:0;font-size:0.8rem;font-family:ui-monospace,monospace;">{{ uptime_cell(e.uptime.get('30d')) }}</span>
|
|
|
|
{# TLS expiry countdown #}
|
|
<span style="width:64px;text-align:right;flex-shrink:0;font-size:0.78rem;font-variant-numeric:tabular-nums;">
|
|
{% if e.tls_days is none %}<span class="tone-unknown">—</span>
|
|
{% elif e.tls_days < 0 %}<span class="tone-crit" title="Certificate expired">expired</span>
|
|
{% else %}<span class="tone-{{ status_class(e.tls_days, 'tls_days') }}" title="TLS certificate expires in {{ e.tls_days|round|int }} days">{{ e.tls_days|round|int }}d</span>{% endif %}
|
|
</span>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
<p style="color:var(--text-dim);font-size:0.74rem;margin-top:0.75rem;">
|
|
Heartbeat shows the last 30 checks (newest on the right). Uptime % is over the rolling window. TLS countdown applies to HTTPS monitors.
|
|
</p>
|
|
{% endif %}
|