Files
FabledSteward/steward/templates/monitors/rows.html
T
bvandeusenandClaude Opus 5.5 f3433fe3ce
CI / lint (push) Successful in 2s
CI / unit (push) Failing after 1m26s
CI / integration (push) Failing after 1m27s
CI / publish (push) Skipped
feat(norms): hosts, containers and monitor response times are graded against their own normal (#5742)
Host CPU, memory and load per core, container CPU and memory, and every
monitor's response time learn a normal per hour (milestone 513, System 182).
A reading past its fixed cutoff but within its normal reads healthy and says
what is usual at this hour; one well out of it keeps its grade and is a
metric anomaly. Disk, a monitor down and an agent offline keep their cutoffs.

- Each normal's floor, the smallest departure that counts, is a setting:
  Settings -> Thresholds -> Normals (norms.floor.*). Defaults: host CPU 10
  points, host memory 5, load per core 10, container CPU 10 points of one
  core, container memory 5, monitor response 100 ms; Traefik's 2-point 4xx
  and 50 ms p95 floors move there from constants. learn_normal takes a
  floor function (floor_setting) read at each pass.
- learn_normal(parts=False): a host learns, its cores don't.
- Ingest records load_pct (load_1m / cores x 100), the figure pages grade.
- Shared grading: anomalies.against / graded, worst_reading(normals=),
  hosts' host_grades, docker usage(thresholds=, norms=), monitors'
  latency_grade. Applied to host vitals, the Hosts widget, MCP overview,
  Docker usage, and the monitor list, widget and page.
- The monitor and container pages list their anomalies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:17:04 -04:00

90 lines
4.9 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{# HTMX fragment — the Monitors list: counts, then every monitor, problems first
(service.problems_first), under one header: the last 30 checks, latest
response with its trend, uptime over 24h / 7d / 30d and TLS days. This is
the one home for monitor state; the Status page it replaced is gone (#5270).
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 %}
{% set can_edit = session.user_role in ['operator', 'admin'] %}
{# TLS only when some monitor has a certificate: a column of dashes says nothing. #}
{% set any_tls = monitor_data | selectattr("tls_days", "ne", none) | list | length > 0 %}
<div class="card ping-card">
<div class="ping-row mon-head" aria-hidden="true">
<span class="ping-meta mon-col-name">Monitor</span>
<span class="ping-pills mon-pills">Last 30 checks</span>
<span class="mon-latest">Latest</span>
<span class="mon-up">24h</span><span class="mon-up">7d</span><span class="mon-up">30d</span>
{% if any_tls %}<span class="mon-tls">TLS</span>{% endif %}
{% if can_edit %}<span class="mon-act"></span>{% endif %}
</div>
{% 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 mon-col-name">
<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 mon-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="mon-latest">
{% if d.state in ('down', 'paused', 'pending') %}{{ status_badge(d.state) }}
{% elif latest.response_ms is not none %}
{% if d.spark %}<span class="mon-spark" title="Response time over the last 30 checks">{{ d.spark|safe }}</span>{% endif %}
<span class="tone-{{ d.latency }}"{% if d.latency_usual %} title="{{ d.latency_usual|capitalize }}"{% endif %}>{{ "%.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>
{% for k in ("24h", "7d", "30d") %}{% set u = d.uptime.get(k) %}
<div class="mon-up">{% if u is none %}<span class="tone-unknown">—</span>{% else %}<span class="tone-{{ status_class(u, 'uptime') }}">{{ "%.2f"|format(u) }}%</span>{% endif %}</div>
{% endfor %}
{% if any_tls %}
<div class="mon-tls">
{% if d.tls_days is none %}<span class="tone-unknown">—</span>
{% elif d.tls_days < 0 %}<span class="tone-crit" title="Certificate expired">expired</span>
{% else %}<span class="tone-{{ status_class(d.tls_days, 'tls_days') }}" title="TLS certificate expires in {{ d.tls_days|round|int }} days">{{ d.tls_days|round|int }}d</span>{% endif %}
</div>
{% endif %}
{% if can_edit %}
<div class="row-actions mon-act">
{{ 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>
<p class="field-hint" style="margin-top:0.6rem;">Last 30 checks: newest on the right; hover one for its result. Uptime is over the rolling window.{% if any_tls %} TLS is days until the certificate expires (HTTPS monitors).{% endif %}</p>
{% 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 %}