fix: error-pages graded healthy, unwired sensors listed apart, Maintenance failed reads 0 (#5780, #5781, #5782)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m23s
CI / unit (push) Successful in 1m29s
CI / publish (push) Successful in 18s

- #5780 "The error-pages service is graded on other routers' errors". The
  Traefik scrape reads which services an errors middleware sends to, and
  grades them healthy: their 4xx and 5xx are the codes of the errors they
  cover for, already graded on the routers that failed.
- #5781 "The Maintenance failed rules sit at no data until a host has had
  a maintenance job". Every host with an agent reads 0 until a job fails.
- #5782 "VDNT-Util01's host page lists SYSTIN at 115 °C in red". The host
  page lists sensors that counts_as_hottest leaves out (unwired board
  inputs, 0 °C or below) on one muted line, not as heat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 21:15:44 -04:00
co-authored by Claude Opus 5.5
parent 0f604ecb4a
commit 72e754b580
9 changed files with 120 additions and 13 deletions
+26
View File
@@ -341,6 +341,32 @@ async def fetch_router_domains(base: str) -> dict[str, list[str]]:
return out
async def fetch_error_page_services(base: str) -> frozenset[str]:
"""The services Traefik's `errors` middlewares send error pages to, named
as the metrics name them ("error-pages-service@swarm"). Such a service
answers with the code of the error it covers for: its 4xx and 5xx are
other routers' errors, already graded on them."""
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(f"{base}/http/middlewares", params={"per_page": 1000})
response.raise_for_status()
return error_page_services(response.json())
def error_page_services(middlewares: list[dict]) -> frozenset[str]:
"""fetch_error_page_services' reading of /api/http/middlewares: a service
named without a provider belongs to its middleware's provider."""
out = set()
for m in middlewares:
service = (m.get("errors") or {}).get("service")
if not service:
continue
if "@" not in service:
provider = m.get("provider") or (m.get("name", "").partition("@")[2])
service = f"{service}@{provider}" if provider else service
out.add(service)
return frozenset(out)
def cert_names(cn: str | None, sans: str | None) -> set[str]:
"""Every name a certificate covers: its CN and SANs, lower-cased."""
names = {n.strip().lower() for n in (sans or "").split(",") if n.strip()}