feat(settings): configurable monitoring thresholds
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 2m19s
CI / publish (push) Successful in 1m3s

Move the hardcoded warn/crit cutoffs into Settings -> Thresholds (DB-backed,
live, no restart). New thresholds.* keys + to_thresholds_cfg() + a
threshold_style(value, kind) jinja global that reads them; latency reuses the
existing ping good/warn keys, uptime is direction-aware (floors).

Replace the _macros metric_style/uptime_style macros (now removed) with the
global across Hosts-Overview, host_agent fleet + panel, Uptime/SLA widget, and
the ping page uptime column — all now honor the configured cutoffs. Uptime keeps
its green 'good' look when not degraded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 20:52:58 -04:00
parent eefa38cc75
commit 591706bd39
11 changed files with 187 additions and 50 deletions
+4 -5
View File
@@ -1,5 +1,4 @@
{# Per-host agent panel — embedded into /hosts/<id> via HTMX. Self-contained. #}
{% from "_macros.html" import metric_style %}
{% set scope = "steward:target:" ~ target.id if target else "" %}
<div class="card">
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-bottom:0.75rem;gap:1rem;flex-wrap:wrap;">
@@ -22,20 +21,20 @@
<div style="display:flex;gap:1.5rem;flex-wrap:wrap;margin-bottom:0.6rem;">
<div title="Average CPU utilization across all cores">
<div style="{{ lbl }}">CPU</div>
<div style="font-weight:600;{{ metric_style(cpu) }}">{{ '%.0f%%'|format(cpu) if cpu is not none else '—' }}</div>
<div style="font-weight:600;{{ threshold_style(cpu, 'cpu') }}">{{ '%.0f%%'|format(cpu) if cpu is not none else '—' }}</div>
{{ sparks.cpu | safe }}</div>
<div title="RAM in use (total minus available; cache/buffers count as free)">
<div style="{{ lbl }}">Memory</div>
<div style="font-weight:600;{{ metric_style(mem) }}">{{ '%.0f%%'|format(mem) if mem is not none else '—' }}</div>
<div style="font-weight:600;{{ threshold_style(mem, 'mem') }}">{{ '%.0f%%'|format(mem) if mem is not none else '—' }}</div>
{{ sparks.mem | safe }}</div>
<div title="Root filesystem (/) usage — see Full metrics for every mount">
<div style="{{ lbl }}">Disk /</div>
<div style="font-weight:600;{{ metric_style(disk_root) }}">{{ '%.0f%%'|format(disk_root) if disk_root is not none else '—' }}</div>
<div style="font-weight:600;{{ threshold_style(disk_root, 'disk') }}">{{ '%.0f%%'|format(disk_root) if disk_root is not none else '—' }}</div>
{{ sparks.disk | safe }}</div>
{# Load /core: 100% = run queue matches CPU capacity, so warn 80 / crit 100. #}
<div title="1-minute load average ÷ {{ cores or '?' }} CPU cores (100% = run queue matches capacity). Raw 1m load: {{ '%.2f'|format(load1) if load1 is not none else '—' }}.">
<div style="{{ lbl }}">Load /core</div>
<div style="font-weight:600;{{ metric_style(load_per_core, warn=80, crit=100) }}">{{ '%d%%'|format(load_per_core) if load_per_core is not none else ('%.2f'|format(load1) if load1 is not none else '—') }}</div>
<div style="font-weight:600;{{ threshold_style(load_per_core, 'load') }}">{{ '%d%%'|format(load_per_core) if load_per_core is not none else ('%.2f'|format(load1) if load1 is not none else '—') }}</div>
{{ sparks.load | safe }}</div>
</div>
{% if psi.cpu is not none or psi.mem is not none or psi.io is not none %}
@@ -3,7 +3,6 @@
left, metric cells (cpu/mem/disk/load) each with a trend sparkline laid out
horizontally on the right. The name wraps (never hard-truncated) but the row
stays horizontal. #}
{% from "_macros.html" import metric_style %}
{% if rows %}
{% macro metric_cell(label, value, fmt, svg, style="", title="") %}
<div style="min-width:62px;flex:0 0 auto;" title="{{ title }}">
@@ -54,9 +53,9 @@
</div>
{# Right — metric cells with value + trend, laid out horizontally #}
<div style="display:flex;flex-wrap:wrap;gap:0.4rem 1rem;justify-content:flex-end;">
{{ metric_cell("cpu", r.cpu_pct, "%.0f%%", r.sparks.cpu, metric_style(r.cpu_pct), "Average CPU utilization") }}
{{ metric_cell("mem", r.mem_used_pct, "%.0f%%", r.sparks.mem, metric_style(r.mem_used_pct), "Memory in use") }}
{{ metric_cell("disk /", r.disk_root, "%.0f%%", r.sparks.disk, metric_style(r.disk_root), "Root filesystem (/) usage") }}
{{ metric_cell("cpu", r.cpu_pct, "%.0f%%", r.sparks.cpu, threshold_style(r.cpu_pct, 'cpu'), "Average CPU utilization") }}
{{ metric_cell("mem", r.mem_used_pct, "%.0f%%", r.sparks.mem, threshold_style(r.mem_used_pct, 'mem'), "Memory in use") }}
{{ metric_cell("disk /", r.disk_root, "%.0f%%", r.sparks.disk, threshold_style(r.disk_root, 'disk'), "Root filesystem (/) usage") }}
{{ metric_cell("load", r.load_1m, "%.2f", r.sparks.load, "", "Load average (1m)") }}
{# Extra agent metrics — shown only when the host reports them (VMs/containers
often have no temp sensor, etc.) so absent data doesn't clutter the row. #}
@@ -67,7 +66,7 @@
{{ io_cell("disk i/o", "rd", r.disk_read_bps, "wr", r.disk_write_bps, "Disk I/O (read / write)") }}
{% endif %}
{% if r.temp_max is not none %}
{{ metric_cell("temp", r.temp_max, "%.0f°C", "", metric_style(r.temp_max, warn=70, crit=85), "Hottest sensor") }}
{{ metric_cell("temp", r.temp_max, "%.0f°C", "", threshold_style(r.temp_max, 'temp'), "Hottest sensor") }}
{% endif %}
</div>
</div>