fix(docker): the overview's failed count sits in the heading of the list it counts, no tile row beside it; stale moves into the verdict (#5214)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 55s
CI / unit (push) Failing after 1m19s
CI / publish (push) Skipped

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 20:38:04 -04:00
co-authored by Claude Opus 5.5
parent 2ecf4d57cb
commit 429629be61
3 changed files with 18 additions and 17 deletions
+7 -12
View File
@@ -1,10 +1,10 @@
{# docker/rows.html — HTMX fragment for the Docker page (#5298–#5301):
a verdict line with only the problem counts that aren't zero; what needs
a verdict line; what needs
attention (service.needs_attention, the ranking the MCP tools use); hosts
whose Docker is not answering; what failed in 24h, each with its logs; then
the swarm services and the standalone containers as two tables that share
the filter bar's search, filters and sort (overview.py). #}
{% from "_macros.html" import stat_tile, status_badge, status_dot, sort_th %}
{% from "_macros.html" import status_badge, status_dot, sort_th %}
{% from "docker/_usage.html" import cpu, mem, cores %}
{% macro th(key, label, cls=none, title=none) -%}
{{ sort_th(label, sort_url(key), active=f.sort == key, desc=f.dir == 'desc', cls=cls, title=title) }}
@@ -22,20 +22,15 @@
{%- if services_total %} · <span class="tone-{{ 'ok' if services_healthy == services_total else 'warn' }}">{{ services_healthy }}/{{ services_total }}</span> services healthy{% endif %}
{%- if containers_total %} · {{ running }} container{{ "s" if running != 1 }} running{% if stopped %}, {{ stopped }} stopped{% endif %}{% if services_total %} outside services{% endif %}{% endif %}
{%- if docker_hosts %} · {{ docker_hosts }} Docker host{{ "s" if docker_hosts != 1 }}{% endif %}
{%- if stale %} · <span class="tone-warn" title="No recent report from their agent: what is shown may no longer be true">{{ stale }} stale</span>{% endif %}
</p>
{% if unhealthy or restarting or failures or stale %}
<div class="stat-strip">
{% if unhealthy %}{{ stat_tile("Unhealthy", unhealthy, state="crit") }}{% endif %}
{% if restarting %}{{ stat_tile("Restarting", restarting, state="crit", hint="restarting or crash-looping") }}{% endif %}
{% if failures %}{{ stat_tile("Failed (24h)", failures|length, state="warn", hint="listed below, with their logs", href="#failures") }}{% endif %}
{% if stale %}{{ stat_tile("Stale", stale, state="stale", hint="no recent report") }}{% endif %}
</div>
{% endif %}
{# ── Needs attention ──────────────────────────────────────────────────────── #}
{% if attention or failing_services %}
<section class="card problem-card" aria-labelledby="docker-problems">
<h2 id="docker-problems" class="section-title" style="margin-bottom:0.6rem;">Needs attention</h2>
<h2 id="docker-problems" class="section-title section-count">Needs attention <span class="tone-crit">{{ problems }}</span>
{%- if unhealthy %} <span class="tone-muted">· {{ unhealthy }} unhealthy</span>{% endif %}
{%- if restarting %} <span class="tone-muted">· {{ restarting }} restarting</span>{% endif %}</h2>
<ul class="problem-list">
{# A swarm service in trouble: Swarm replaces its failed tasks, so the
service, not any one container, is what is failing. #}
@@ -96,7 +91,7 @@
what died: a service's to its failed task's lines (#5300). ───────────── #}
{% if failures %}
<section id="failures" class="card" aria-labelledby="docker-failures">
<h2 id="docker-failures" class="section-title" style="margin-bottom:0.6rem;">Failed in the last 24h</h2>
<h2 id="docker-failures" class="section-title section-count">Failed in the last 24h <span class="tone-warn">{{ failures|length }}</span></h2>
<ul class="problem-list">
{% for x in failures %}
<li>
+3
View File
@@ -209,6 +209,9 @@ strong, b, th { font-weight: 500; }
.table td.cell-image { max-width: 14rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.8rem; color: var(--text-muted); }
/* The Docker page's one-line answer, above its tiles (#5298). */
.docker-verdict { margin: 0 0 0.75rem; font-size: 0.95rem; }
/* A problem section's heading carries its own count, so no tile repeats it (#5214). */
.section-count { margin-bottom: 0.6rem; }
.section-count > span:first-child { margin-left: 0.25rem; }
/* A service's resource history: CPU and memory side by side, each a figure
over a line that spans its half (docker/_service_history.html). */
.history-pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 1.25rem; }
+8 -5
View File
@@ -72,10 +72,10 @@ async def test_a_crash_loop_comes_first_and_a_stale_row_says_so(app):
page = await _rows(app, attention=attention, restarting=1, stale=1, running=1, stopped=1, docker_hosts=1,
containers=[_crow(loop, exit_failed=True), _crow(old, stale=True)], containers_total=2)
# #5298: the verdict first; only the tiles that aren't zero.
# #5298: the verdict first; each problem section carries its own count (#5214).
assert page.index('class="docker-verdict"') < page.index("Needs attention")
assert "1 needs attention" in page and "1 container running, 1 stopped" in page
assert "Unhealthy</span>" not in page and "Failed (24h)" not in page
assert "unhealthy</span>" not in page and "Failed in the last 24h" not in page and "stat-strip" not in page
card = page[page.index("Needs attention"):page.index("</section>")]
assert '<span class="badge badge-crit">Restarting</span>' in card
assert 'href="/plugins/docker/container/h1/api" title="api">api</a>' in card
@@ -84,8 +84,9 @@ async def test_a_crash_loop_comes_first_and_a_stale_row_says_so(app):
assert "cache" not in card
assert page.index("Needs attention") < page.index("<table")
assert '<span class="stat-tile-label">Restarting</span><span class="stat-tile-value">1</span>' in page
assert '<span class="stat-tile-label">Stale</span><span class="stat-tile-value">1</span>' in page
assert ('Needs attention <span class="tone-crit">1</span> '
'<span class="tone-muted">· 1 restarting</span></h2>') in card
assert '">1 stale</span>' in page[:page.index("</p>")] # stale sits in the verdict
assert 'class="badge badge-warn" title="Last report' in page # the stale row
# CPU in cores, toned by its share of the host's CPUs through Settings → Thresholds.
assert ('<span class="tone-crit" title="Docker counts CPU per core: 95% of one core, '
@@ -149,7 +150,9 @@ async def test_a_failing_service_leads_and_the_failures_are_listed_once_with_the
assert "9 failures · last: task: non-zero exit (1) · nas" in listed
assert f'/plugins/docker/service/codex_db?task={"t" * 25}#logs"' in listed and "Logs of that task" in listed
assert 'href="/plugins/docker/container/h1/backup/logs"' in listed
assert '<span class="stat-tile-label">Failed (24h)</span><span class="stat-tile-value">2</span>' in page
# The count sits in the heading of the list it counts: one widget, not a tile beside it.
assert 'Failed in the last 24h <span class="tone-warn">2</span></h2>' in page
assert "stat-tile" not in page
async def test_services_and_containers_are_tables_that_sort_and_filter(app):