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
+45 -1
View File
@@ -10,7 +10,7 @@ from steward.models.users import UserRole
from steward.core.settings import (
get_all_settings, set_setting,
to_smtp_cfg, to_webhook_cfg, to_ansible_cfg, to_plugins_cfg,
to_oidc_cfg, to_ldap_cfg,
to_oidc_cfg, to_ldap_cfg, to_thresholds_cfg,
)
settings_bp = Blueprint("settings", __name__, url_prefix="/settings")
@@ -72,6 +72,7 @@ async def _reload_app_config() -> None:
PLUGINS=to_plugins_cfg(fresh),
OIDC=to_oidc_cfg(fresh),
LDAP=to_ldap_cfg(fresh),
THRESHOLDS=to_thresholds_cfg(fresh),
)
@@ -113,6 +114,49 @@ async def save_general():
return redirect(url_for("settings.general"))
# ── Monitoring thresholds ─────────────────────────────────────────────────────
# (form field, setting key, is_float) for every tunable cutoff.
_THRESHOLD_FIELDS = [
("cpu_warn", "thresholds.cpu_warn", False), ("cpu_crit", "thresholds.cpu_crit", False),
("mem_warn", "thresholds.mem_warn", False), ("mem_crit", "thresholds.mem_crit", False),
("disk_warn", "thresholds.disk_warn", False), ("disk_crit", "thresholds.disk_crit", False),
("load_warn", "thresholds.load_warn", False), ("load_crit", "thresholds.load_crit", False),
("temp_warn", "thresholds.temp_warn", False), ("temp_crit", "thresholds.temp_crit", False),
("uptime_warn", "thresholds.uptime_warn", True), ("uptime_crit", "thresholds.uptime_crit", True),
("latency_warn", "ping.threshold.good_ms", False), ("latency_crit", "ping.threshold.warn_ms", False),
]
@settings_bp.get("/thresholds/")
@require_role(UserRole.admin)
async def thresholds():
async with current_app.db_sessionmaker() as db:
settings = await get_all_settings(db)
return await render_template("settings/thresholds.html", settings=settings)
@settings_bp.post("/thresholds/")
@require_role(UserRole.admin)
async def save_thresholds():
form = await request.form
async with current_app.db_sessionmaker() as db:
async with db.begin():
for field, key, is_float in _THRESHOLD_FIELDS:
raw = form.get(field, "")
if raw == "":
continue
try:
val = float(raw) if is_float else int(raw)
except (TypeError, ValueError):
continue
await set_setting(db, key, val)
await _reload_app_config()
await log_audit(current_app, session.get("user_id"), session.get("username", ""),
"settings.saved", detail={"section": "thresholds"})
return redirect(url_for("settings.thresholds"))
# ── Notifications (SMTP + Webhook) ────────────────────────────────────────────
@settings_bp.get("/notifications/")