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>
90 lines
4.9 KiB
HTML
90 lines
4.9 KiB
HTML
{# 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 %}
|