From 9615f9abcdfbce20adeda9e0a3f943cb706c4900 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 18 Jun 2026 22:01:12 -0400 Subject: [PATCH] feat(docker): group containers by compose/swarm + enrich widget (milestone 77 #942) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Container list now sub-groups each host's containers by compose project (or swarm service) with a small subheading, preserving the running-first order; hosts with no such labels render flat as before. The dashboard status widget links each container to its detail page and surfaces enriched state inline — health dot (healthy/unhealthy), restart count, and last non-zero exit code for stopped containers. Completes the #942 UI surface. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC --- plugins/docker/routes.py | 17 +++++++++++++++++ plugins/docker/templates/docker/rows.html | 7 ++++++- plugins/docker/templates/docker/widget.html | 9 ++++++--- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/plugins/docker/routes.py b/plugins/docker/routes.py index 98d10a8..3dc9f55 100644 --- a/plugins/docker/routes.py +++ b/plugins/docker/routes.py @@ -175,6 +175,23 @@ async def rows(): else: g["stopped"] += 1 + # Sub-group each host's containers by compose project (or swarm service), + # preserving the running-first ordering. Containers with neither label fall + # into an unlabelled bucket rendered flat, so plain hosts look unchanged. + for g in groups.values(): + subs: dict[str, dict] = {} + order: list[str] = [] + for item in g["containers"]: + c = item["container"] + label = c.compose_project or c.service_name or None + key = label or "" + if key not in subs: + subs[key] = {"label": label, "containers": []} + order.append(key) + subs[key]["containers"].append(item) + g["subgroups"] = [subs[k] for k in order] + g["grouped"] = any(s["label"] for s in g["subgroups"]) + host_groups = sorted(groups.values(), key=lambda g: g["host_name"].lower()) running = sum(g["running"] for g in host_groups) total = len(containers) diff --git a/plugins/docker/templates/docker/rows.html b/plugins/docker/templates/docker/rows.html index 66088c0..9d3b058 100644 --- a/plugins/docker/templates/docker/rows.html +++ b/plugins/docker/templates/docker/rows.html @@ -47,7 +47,11 @@ - {% for item in g.containers %} + {% for sub in g.subgroups %} + {% if g.grouped and sub.label %} + {{ sub.label }} + {% endif %} + {% for item in sub.containers %} {% set c = item.container %} @@ -105,6 +109,7 @@ {{ item.sparkline_mem | safe }} {% endfor %} + {% endfor %} diff --git a/plugins/docker/templates/docker/widget.html b/plugins/docker/templates/docker/widget.html index 9a8a5a4..cd66b22 100644 --- a/plugins/docker/templates/docker/widget.html +++ b/plugins/docker/templates/docker/widget.html @@ -24,9 +24,12 @@ {% for c in g.containers %}
- - {{ c.name }} - + + {{ c.name }}{% if c.health == 'unhealthy' %}{% elif c.health == 'healthy' %}{% endif %}{% if c.restart_count %} ⟳{{ c.restart_count }}{% endif %} + + {% if c.status != 'running' and c.exit_code is not none and c.exit_code != 0 %} + exit {{ c.exit_code }} + {% endif %} {% if c.cpu_pct is not none %} {{ "%.1f" | format(c.cpu_pct) }}%