feat(hosts): a host's page stays short — Docker folds past the busiest, monitors add on their own form, agent actions under Manage (#5276, #5277, #5278)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 59s
CI / unit (push) Successful in 1m17s
CI / publish (push) Successful in 1m3s

Docker panel (#5276): problems first, then the 8 busiest running
containers by CPU; the other running and the stopped fold behind one
"+N more running · M stopped" line. Containers that are not swarm tasks
read the way the Docker page reads them: CI jobs as "CI: workflow · job",
buildx builders as "BuildKit builder · name".

Monitors card (#5277): the edit/delete icon buttons and the inline add
form are gone. Each row links to its monitor's page; "Add monitor" opens
the add form filed under this host, starting from its address, with the
host in its breadcrumbs and Cancel back to it.

Agent card (#5278): Full metrics stays in view; Update, Rotate token,
Unregister agent (was "Remove") and Re-provision sit under "Manage agent".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 17:08:46 -04:00
co-authored by Claude Opus 5.5
parent 6788ce60f1
commit d4974ddf6c
10 changed files with 224 additions and 126 deletions
+24 -5
View File
@@ -369,19 +369,38 @@ async def host_panel(host_id: str):
"usage": docker_svc.usage(c, *capacity.get(host_id, (None, None))),
"uptime": _uptime(c.started_at) if c.status == "running" else None,
})
rows.sort(key=lambda r: (
docker_svc.PROBLEMS.index(r["problems"][0]) if r["problems"] else len(docker_svc.PROBLEMS),
r["c"].status != "running", r["label"].lower()))
shown, folded = host_panel_rows(rows)
running = sum(1 for r in rows if r["c"].status == "running")
return await render_template(
"docker/host_panel.html",
rows=rows, says=docker_svc.PROBLEM_SAYS, severity=docker_svc.PROBLEM_SEVERITY,
rows=shown, folded=folded, says=docker_svc.PROBLEM_SAYS, severity=docker_svc.PROBLEM_SEVERITY,
running=running, stopped=len(rows) - running,
problems=sum(1 for r in rows if r["problems"]),
replaced=len(replaced),
replaced=len(replaced), busiest=HOST_PANEL_BUSIEST,
)
# Running containers shown before the rest fold away on a host's page (#5276).
HOST_PANEL_BUSIEST = 8
def host_panel_rows(rows: list) -> tuple[list, list]:
"""(shown, folded) for a host's Docker panel: every container with a
problem, worst first; then the busiest running ones by CPU. The other
running containers (busiest first) and the stopped ones (by name) fold
away, so a host with dozens of containers keeps a short card."""
def cpu(r):
return getattr(r["c"], "cpu_pct", None) or 0.0
problems = sorted((r for r in rows if r["problems"]), key=lambda r: (
docker_svc.PROBLEMS.index(r["problems"][0]), r["label"].lower()))
running = sorted((r for r in rows if not r["problems"] and r["c"].status == "running"),
key=lambda r: (-cpu(r), r["label"].lower()))
stopped = sorted((r for r in rows if not r["problems"] and r["c"].status != "running"),
key=lambda r: r["label"].lower())
return (problems + running[:HOST_PANEL_BUSIEST],
running[HOST_PANEL_BUSIEST:] + stopped)
# Glyph + colour per lifecycle event, so the timeline reads at a glance.
_EVENT_STYLE = {
"start": ("▲", "var(--green)"),