feat(widgets): a troubled container on the Docker widget says since when (#5810)
CI / lint (push) Successful in 3s
CI / integration (push) Successful in 1m23s
CI / unit (push) Successful in 1m33s
CI / publish (push) Successful in 1m7s

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 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 10:09:25 -04:00
co-authored by Claude Opus 5.5
parent c113cadbdd
commit 5c7a4cf9a0
3 changed files with 30 additions and 2 deletions
+18 -1
View File
@@ -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),
+2 -1
View File
@@ -35,7 +35,8 @@
<span class="w-label" title="{{ c.name }}">
{%- if session.user_id %}<a href="/plugins/docker/container/{{ c.host_id }}/{{ c.name }}">{{ c.name }}</a>{% else %}{{ c.name }}{% endif %}
<span class="w-sub tone-crit">
{%- 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) }}
</span>
</span>
</div>
+10
View File
@@ -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 <time" in page and "40m ago" in page and "· VDNT-Docker02" in page
async def test_the_services_widget_says_when_all_is_well(app):
page = await _services_widget(app, services_total=2, services_healthy=2, running_count=1,
healthy=[{"kind": "service", "s": _widget_svc("a")}])