Files
FabledSteward/steward/templates/hosts/overview_widget.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

48 lines
3.0 KiB
HTML

{# hosts/overview_widget.html — the dashboard Hosts widget (#5251): a verdict,
then one row per host, worst first, with its resources under one header.
Load is shown against the host's cores and graded per core. CPU, memory
and load within the host's own normal for the hour read healthy (#5742),
with what is usual on hover. Reachability
and uptime belong to the Monitors widget, so they aren't repeated here. #}
{% from "_macros.html" import status_dot %}
{% if not rows %}
<div class="w-verdict" style="color:var(--text-muted);">
No hosts yet.{% if session.user_id %} <a href="/hosts/?add=1">Add one →</a>{% endif %}
</div>
{% else %}
{% set problems = rows | selectattr("state", "in", ["down", "crit", "stale", "warn"]) | list %}
<div class="w-verdict {{ 'tone-crit' if problems and problems[0].state in ['down', 'crit'] else ('tone-warn' if problems else 'tone-ok') }}">
{%- if not problems %}All {{ rows|length }} host{{ 's' if rows|length != 1 }} healthy.
{%- elif problems|length == 1 %}{% set p = problems[0] %}{{ p.host.name }}:
{% if p.state == 'down' %}a monitor is down.{% elif p.state == 'stale' %}its agent stopped reporting.{% else %}{{ p.worst[0] }} high.{% endif %}
{%- else %}{{ problems|length }} of {{ rows|length }} hosts need a look.{% endif -%}
</div>
<table class="w-table">
<colgroup><col><col style="width:3.6rem"><col style="width:3.6rem"><col style="width:3.6rem"><col style="width:6.5rem"></colgroup>
<thead><tr><th>Host</th><th>CPU</th><th>Mem</th><th title="Root filesystem">Disk</th><th>Load</th></tr></thead>
<tbody>
{% for r in rows %}
{% set a = r.agent %}{% set fresh = a.get('fresh') %}
<tr>
<td class="w-name" title="{{ r.host.name }}">
{{- status_dot(r.state) -}}
{% if session.user_id %}<a href="/hosts/{{ r.host.id }}">{{ r.host.name }}</a>{% else %}{{ r.host.name }}{% endif %}
{%- if r.state == 'down' %}<span class="w-sub tone-crit">a monitor is down</span>
{%- elif r.state == 'stale' %}<span class="w-sub tone-warn">agent last reported {{ a.last_at|ago }}; figures are from then</span>
{%- elif not a %}<span class="w-sub">no agent</span>{% endif %}
</td>
{% for v, kind in ((a.get('cpu_pct'), 'cpu'), (a.get('mem_used_pct'), 'mem'), (a.get('disk_root'), 'disk')) %}
{% set g = (r.grades or {}).get(kind) or (none, none) %}
<td style="{{ grade_style(g[0]) }}"{% if g[1] %} title="{{ g[1]|capitalize }}"{% endif %}>{% if v is none %}—{% else %}{{ "%.0f"|format(v) }}%{% endif %}</td>
{% endfor %}
{% set g = (r.grades or {}).get('load') or (none, none) %}
<td style="{{ grade_style(g[0]) }}"
title="{% if a.get('cores') %}Load over 1 minute on {{ a.cores }} cores: {{ a.load_per_core }}% of them busy or waiting{% endif %}{% if g[1] %}; {{ g[1] }}{% endif %}">
{%- if a.get('load_1m') is none %}—{% elif a.get('cores') %}{{ "%.1f"|format(a.load_1m) }} / {{ a.cores }} cores{% else %}{{ "%.1f"|format(a.load_1m) }}{% endif -%}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}