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
+19 -1
View File
@@ -213,8 +213,25 @@ async def send_report(app) -> tuple[bool, str]:
return False, f"Send failed: {exc}"
def report_due(now: datetime, day: int, hour: int, tz) -> bool:
"""Whether `now` falls in the scheduled hour, read in the schedule's zone."""
local = now.astimezone(tz)
return local.weekday() == day and local.hour == hour
def schedule_in(day: int, hour: int, saved_tz, view_tz, now: datetime | None = None) -> tuple[int, int]:
"""The schedule's (weekday, hour) as a reader in `view_tz` sees it: the next
time it falls due in `saved_tz`, read on their clock."""
local = (now or datetime.now(timezone.utc)).astimezone(saved_tz)
due = (local + timedelta(days=(day - local.weekday()) % 7)).replace(
hour=hour, minute=0, second=0, microsecond=0)
seen = due.astimezone(view_tz)
return seen.weekday(), seen.hour
async def check_and_send(app) -> None:
"""Hourly scheduler hook: send if day/hour match and not sent recently."""
from steward.core.formatting import zone
from steward.core.settings import get_setting, set_setting
async with app.db_sessionmaker() as db:
@@ -224,10 +241,11 @@ async def check_and_send(app) -> None:
schedule_day = int(await get_setting(db, "reports.schedule_day") or 6)
schedule_hour = int(await get_setting(db, "reports.schedule_hour") or 8)
schedule_tz = zone(await get_setting(db, "reports.schedule_tz"))
last_sent_str = await get_setting(db, "reports.last_sent_at") or ""
now = datetime.now(timezone.utc)
if now.weekday() != schedule_day or now.hour != schedule_hour:
if not report_due(now, schedule_day, schedule_hour, schedule_tz):
return
if last_sent_str:
+13 -1
View File
@@ -83,6 +83,10 @@ DEFAULTS: dict[str, Any] = {
"ansible.max_concurrent_runs": 3,
"ping.threshold.good_ms": 50,
"ping.threshold.warn_ms": 200,
# A Traefik router serving more than this share of 5xx is failing; more
# than this share of 4xx is worth a look (#5380).
"traefik.threshold.error_5xx_pct": 0.1,
"traefik.threshold.error_4xx_pct": 1.0,
# Degraded/critical cutoffs for metric coloring (warn=amber, crit=red).
# cpu/mem/disk/load are percentages (load is load-per-core %); temp in °C.
# uptime is "higher is better" so its warn/crit are floors. Ping latency
@@ -156,7 +160,10 @@ DEFAULTS: dict[str, Any] = {
# Scheduled reports
"reports.enabled": False,
"reports.schedule_day": 6, # 0=Monday … 6=Sunday
"reports.schedule_hour": 8, # UTC hour
"reports.schedule_hour": 8, # an hour in reports.schedule_tz
# The zone of whoever last saved the schedule (an IANA name; blank is UTC),
# so "Sunday 08:00" means 08:00 where they are (#5378).
"reports.schedule_tz": "",
"reports.last_sent_at": "",
}
@@ -452,6 +459,11 @@ def to_thresholds_cfg(settings: dict[str, Any]) -> dict:
"tls_days": {"warn": g("thresholds.tls_warn_days", 14), "crit": g("thresholds.tls_crit_days", 3), "dir": "low"},
"clock": {"warn": g("thresholds.clock_warn_s", 30), "crit": g("thresholds.clock_crit_s", 300), "dir": "high"},
"crash_loop": {"warn": g("thresholds.crash_loop_deaths", 3), "crit": g("thresholds.crash_loop_deaths", 3), "dir": "high"},
# One cutoff each: a router's 5xx share past it is crit, a 4xx share warn.
"error_5xx": {"warn": g("traefik.threshold.error_5xx_pct", 0.1),
"crit": g("traefik.threshold.error_5xx_pct", 0.1), "dir": "high"},
"error_4xx": {"warn": g("traefik.threshold.error_4xx_pct", 1.0),
"crit": g("traefik.threshold.error_4xx_pct", 1.0), "dir": "high"},
}