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): {# 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 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 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 swarm services and the standalone containers as two tables that share
the filter bar's search, filters and sort (overview.py). #} 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 %} {% from "docker/_usage.html" import cpu, mem, cores %}
{% macro th(key, label, cls=none, title=none) -%} {% 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) }} {{ 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 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 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 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> </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 ──────────────────────────────────────────────────────── #} {# ── Needs attention ──────────────────────────────────────────────────────── #}
{% if attention or failing_services %} {% if attention or failing_services %}
<section class="card problem-card" aria-labelledby="docker-problems"> <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"> <ul class="problem-list">
{# A swarm service in trouble: Swarm replaces its failed tasks, so the {# A swarm service in trouble: Swarm replaces its failed tasks, so the
service, not any one container, is what is failing. #} 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). ───────────── #} what died: a service's to its failed task's lines (#5300). ───────────── #}
{% if failures %} {% if failures %}
<section id="failures" class="card" aria-labelledby="docker-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"> <ul class="problem-list">
{% for x in failures %} {% for x in failures %}
<li> <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); } .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). */ /* The Docker page's one-line answer, above its tiles (#5298). */
.docker-verdict { margin: 0 0 0.75rem; font-size: 0.95rem; } .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 /* 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). */ 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; } .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, 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) 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 page.index('class="docker-verdict"') < page.index("Needs attention")
assert "1 needs attention" in page and "1 container running, 1 stopped" in page 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>")] card = page[page.index("Needs attention"):page.index("</section>")]
assert '<span class="badge badge-crit">Restarting</span>' in card assert '<span class="badge badge-crit">Restarting</span>' in card
assert 'href="/plugins/docker/container/h1/api" title="api">api</a>' 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 "cache" not in card
assert page.index("Needs attention") < page.index("<table") 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 ('Needs attention <span class="tone-crit">1</span> '
assert '<span class="stat-tile-label">Stale</span><span class="stat-tile-value">1</span>' in page '<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 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. # 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, ' 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 "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 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 '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): async def test_services_and_containers_are_tables_that_sort_and_filter(app):