feat(widgets): Routers and Hosts problem rows say since when (#5810)

Routers: a router in trouble reads 'started 2h ago' or 'all of the last
24 h', from problem_since over the day's history, read for the troubled
routers only (a certificate's problem carries its own date). Hosts: a
down host reads 'a monitor is down for 3h'; host_glance keeps the
down_since host_status already had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 10:08:38 -04:00
co-authored by Claude Opus 5.5
parent 423493e2bf
commit c113cadbdd
6 changed files with 47 additions and 5 deletions
+13
View File
@@ -496,10 +496,21 @@ async def widget():
r["cert"]["grade"] = grade
if _GRADE_ORDER.get(grade, 2) < _GRADE_ORDER[r["state"]]:
left = cert_left(r["cert"]["days"]) # "12 days", "expires today", "expired …"
r["by_cert"] = True
r["state"], r["why"] = grade, (
f"certificate expires in {left}" if left[0].isdigit() else f"certificate {left}")
problems, healthy = split_routers(current)
healthy.sort(key=lambda r: -(r["latest"].request_rate or 0))
# Since when, from the day's history, for problems the traffic shows (a
# certificate's says its own date). Only the routers in trouble are read.
traffic = [r for r in problems if not r.get("by_cert")]
if traffic:
since = now - WIDGET_SINCE
async with current_app.db_sessionmaker() as db:
history = await router_history(db, [r["name"] for r in traffic], since,
bucket_seconds(since))
for r in traffic:
r["since"], r["since_before"] = problem_since(history[r["name"]], thresholds)
# With no router domains to go on, certificates can't sit on their
# routers: the expiring ones are listed after the routers instead.
loose = [] if domains is not None else sorted(
@@ -516,6 +527,8 @@ async def widget():
# Healthy routers on the dashboard widget (the busiest); the page lists all.
ROUTERS_SHOWN = 6
# How far back the widget looks for when a router's problem began.
WIDGET_SINCE = timedelta(hours=24)
@traefik_bp.get("/widget/access_log")
@@ -24,7 +24,8 @@
<td class="w-name" title="{{ r.name }}">
{{- status_dot(r.state) -}}
{{ r.name.split('@')[0] }}
{%- if r.why and r.state != "ok" %}<span class="w-sub tone-{{ r.state }}">{{ r.why }}</span>{% endif %}
{%- if r.why and r.state != "ok" %}<span class="w-sub tone-{{ r.state }}">{{ r.why }}
{%- if r.since_before %}, all of the last 24 h{% elif r.since %}, started {{ r.since|ago }}{% endif %}</span>{% endif %}
</td>
<td>{% if m.request_rate is none %}—{% else %}{{ "%.2f"|format(m.request_rate) }}{% endif %}</td>
<td class="w-wide-only">{% if m.response_bytes_rate is none %}—{% else %}{{ m.response_bytes_rate|rate }}{% endif %}</td>