feat(settings): Settings brought into line with the rest of Steward (milestone 493)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 56s
CI / unit (push) Successful in 1m24s
CI / publish (push) Successful in 1m9s

The last page of #5214's walk. Settings still described Steward as it was
before monitors were unified and SNMP was rebuilt, drew its tabs below the
breadcrumb, and showed the report schedule in UTC.

- Tabs (#5377): settings/_tabs.html is a settings_tabs() macro called in
  each page's section_tabs block, above the trail, as Ansible's are. The
  breadcrumb guard also fails any page that {% include %}s a tab strip.
- Report time (#5378): reports.schedule_tz keeps the zone of whoever saved
  the schedule; check_and_send compares in it (report_due), and the form shows
  the day and hour on the reader's clock (schedule_in) under their zone's
  name. "Last sent" uses |when.
- Wording (#5379): the report contents list what the report holds (monitors,
  not ping-enabled hosts); "Ping latency" is "Response time" and says what it
  grades; plugin detail says enabling loads now and disabling needs a
  restart; the monitor interval says it applies on the next check (the
  scheduler reads it every pass); the dead list-config branch is gone.
- Traefik (#5380): the 5xx and 4xx cutoffs are settings
  (traefik.threshold.error_5xx_pct 0.1, error_4xx_pct 1.0) in a "Traefik
  routers" group on Thresholds while the plugin runs, read by router_state
  through error_cutoffs(). The Traefik page has a Thresholds button for
  admins, to /settings/thresholds/#traefik.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 13:28:43 -04:00
co-authored by Claude Opus 5.5
parent 263560cc94
commit 24700ee608
18 changed files with 262 additions and 86 deletions
+17 -11
View File
@@ -42,24 +42,29 @@ def _sparkline(values: list[float], width: int = 80, height: int = 20) -> str:
)
# A router serving any real share of 5xx is failing; a 4xx share above this
# is worth a look (clients hitting missing routes, auth failures).
ERROR_5XX_PCT = 0.1
ERROR_4XX_PCT = 1.0
_GRADE_ORDER = {"crit": 0, "warn": 1, "ok": 2}
def error_cutoffs(thresholds: dict) -> tuple[float, float]:
"""(5xx, 4xx): the error shares, in percent, past which a router is failing
or worth a look. Settings → Thresholds, Traefik routers (#5380)."""
return (float(thresholds.get("error_5xx", {}).get("crit", 0.1)),
float(thresholds.get("error_4xx", {}).get("warn", 1.0)))
def router_state(reading, thresholds: dict, over: str = "") -> tuple[str, str | None]:
"""(grade, why) for a router's reading (recent_window's, or one scrape's):
crit while it serves 5xx, warn on a p95 past the latency critical cutoff
(Settings → Thresholds) or a high 4xx share, otherwise ok with nothing to
say. `over` names the span the reading covers, for the why."""
if (reading.error_rate_5xx_pct or 0) > ERROR_5XX_PCT:
crit while it serves 5xx past its cutoff, warn on a p95 past the latency
critical cutoff or a 4xx share past its cutoff (all Settings → Thresholds),
otherwise ok with nothing to say. `over` names the span the reading
covers, for the why."""
error_5xx, error_4xx = error_cutoffs(thresholds)
if (reading.error_rate_5xx_pct or 0) > error_5xx:
return "crit", f"{reading.error_rate_5xx_pct:.1f}% of responses are 5xx{over}"
p95 = reading.latency_p95_ms
if p95 is not None and status_class(p95, "latency", thresholds) == "crit":
return "warn", f"p95 latency is {p95:.0f} ms{over}"
if (reading.error_rate_4xx_pct or 0) > ERROR_4XX_PCT:
if (reading.error_rate_4xx_pct or 0) > error_4xx:
return "warn", f"{reading.error_rate_4xx_pct:.1f}% of responses are 4xx{over}"
return "ok", None
@@ -298,13 +303,14 @@ async def rows():
old_certs = [cert_row(c) for c in old]
problems, healthy = split_routers(current)
phases.mark("cert_rows")
error_5xx, error_4xx = error_cutoffs(thresholds)
page = await render_template(
"traefik/rows.html",
problems=problems,
healthy=healthy,
gone=gone,
error_4xx=ERROR_4XX_PCT,
error_5xx=ERROR_5XX_PCT,
error_4xx=error_4xx,
error_5xx=error_5xx,
global_stat=global_stat,
certs=certs,
old_certs=old_certs,
+6 -1
View File
@@ -7,7 +7,12 @@
<h1 class="page-title" style="margin-bottom:0;">Traefik Services</h1>
<span style="color:var(--text-muted);font-size:0.85rem;">polling every {{ poll_interval }}s</span>
</div>
{% include "_time_range.html" %}
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;">
{% if session.user_role == 'admin' %}
<a href="/settings/thresholds/#traefik" class="btn btn-ghost btn-sm">Thresholds</a>
{% endif %}
{% include "_time_range.html" %}
</div>
</div>
<div id="traefik-rows"