From 5c7a4cf9a0c20eb01fcad3c119c313efda5bef7b Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 15 Oct 2026 10:09:25 -0400 Subject: [PATCH] feat(widgets): a troubled container on the Docker widget says since when (#5810) Its problems and since come from the same container_problems and problem_since the Docker page uses, read from the day's deaths and health changes; troubled containers sort worst problem first, then name. Co-Authored-By: Claude Opus 5.5 --- plugins/docker/routes.py | 19 ++++++++++++++++++- plugins/docker/templates/docker/widget.html | 3 ++- tests/plugins/docker/test_docker_page.py | 10 ++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/plugins/docker/routes.py b/plugins/docker/routes.py index 5cb6d78..3b636f0 100644 --- a/plugins/docker/routes.py +++ b/plugins/docker/routes.py @@ -237,6 +237,10 @@ async def widget(): {c.host_id for c in all_containers} | {n.host_id for n in nodes} | {s.host_id for s in services}, ) + deaths = await docker_svc.recent_failures_by_container(db, now - timedelta(hours=24)) + sick = await docker_svc.unhealthy_since( + db, [(c.host_id, c.name) for c in all_containers + if not c.service_name and (c.health or "") == "unhealthy"]) # Swarm tasks collapse into per-service rows (each replica tagged with its # host); non-swarm containers stay grouped by host. @@ -260,7 +264,19 @@ async def widget(): # The widget-shape order (UI & Design ruling): what is wrong first, then # the healthy rest one line each, collapsing past HEALTHY_SHOWN. - bad_containers = sorted((c for c in standalone if troubled(c)), key=lambda c: c.name) + # Worst problem first, each with since when, as the Docker page has them. + hour_ago, crash = now - timedelta(hours=1), docker_svc.crash_loop_deaths(current_app.config) + trouble = {} + for c in standalone: + if troubled(c): + died = deaths.get((c.host_id, c.name), []) + problems = docker_svc.container_problems( + c, sum(1 for e in died if e.at >= hour_ago), bool(died), crash) + trouble[(c.host_id, c.name)] = ( + docker_svc.PROBLEMS.index(problems[0]) if problems else len(docker_svc.PROBLEMS), + docker_svc.problem_since(problems, died, hour_ago, sick.get((c.host_id, c.name)))) + bad_containers = sorted((c for c in standalone if troubled(c)), + key=lambda c: (trouble[(c.host_id, c.name)][0], c.name)) healthy = ([{"kind": "service", "s": s} for s in swarm_services if s["state"] == "healthy"] + [{"kind": "container", "c": c} for c in sorted(running, key=lambda c: c.name) if not troubled(c)] @@ -271,6 +287,7 @@ async def widget(): "docker/widget.html", bad_services=[s for s in swarm_services if s["state"] != "healthy"], bad_containers=bad_containers, + bad_since={key: since for key, (_rank, since) in trouble.items()}, healthy=healthy[:HEALTHY_SHOWN], healthy_more=max(0, len(healthy) - HEALTHY_SHOWN), services_total=len(swarm_services), services_healthy=sum(s["state"] == "healthy" for s in swarm_services), diff --git a/plugins/docker/templates/docker/widget.html b/plugins/docker/templates/docker/widget.html index 395ca89..93a9e5e 100644 --- a/plugins/docker/templates/docker/widget.html +++ b/plugins/docker/templates/docker/widget.html @@ -35,7 +35,8 @@ {%- if session.user_id %}{{ c.name }}{% else %}{{ c.name }}{% endif %} - {%- if c.health == 'unhealthy' %}unhealthy{% elif c.status == 'restarting' %}restarting{% else %}exited {{ c.exit_code }}{% if c.oom_killed %}, out of memory{% endif %}{% endif %} · {{ host_name.get(c.host_id, c.host_id) }} + {%- if c.health == 'unhealthy' %}unhealthy{% elif c.status == 'restarting' %}restarting{% else %}exited {{ c.exit_code }}{% if c.oom_killed %}, out of memory{% endif %}{% endif %} + {%- set since = (bad_since or {}).get((c.host_id, c.name)) %}{% if since %} since {{ since|ago }}{% endif %} · {{ host_name.get(c.host_id, c.host_id) }} diff --git a/tests/plugins/docker/test_docker_page.py b/tests/plugins/docker/test_docker_page.py index ab59639..9b5b1ab 100644 --- a/tests/plugins/docker/test_docker_page.py +++ b/tests/plugins/docker/test_docker_page.py @@ -299,6 +299,16 @@ async def test_the_services_widget_lists_what_is_wrong_then_packs_the_healthy(ap assert not hex_colours(page), hex_colours(page) +async def test_a_troubled_container_says_since_when_on_the_widget(app): + """#5810: a container in trouble carries its since, as the Docker page does.""" + sick = _c("backup_cron", host_id="h1", status="running", health="unhealthy") + page = await _services_widget( + app, bad_containers=[sick], + bad_since={("h1", "backup_cron"): datetime.now(timezone.utc) - timedelta(minutes=40)}) + page = " ".join(page.split()) + assert "unhealthy since