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: