#5209 "Docker views: readable names, CPU/memory in cores and bytes, failures listed with since-time" (milestone 477 "UI problems first"). - A swarm task reads as its service and slot ("codex_db · 1", or the node for a global service), with the raw name in the title: Docker page, host panel, busiest widget, container detail. - CPU reads in cores (Docker's 100% = one core), toned by its share of the host's CPUs from the agent's per-core series. Memory reads in bytes, against a limit only when one is really set (Docker reports host RAM otherwise). - Exit 137/143 without an OOM kill is a stop, not a failure: no exited_nonzero, no crash-loop death, no "failed (24h)", dim exit code. - "Failed (24h)" is a list (Docker page #failures, linked from the widget and the tile): plain containers by their deaths, swarm services by Swarm's failed tasks; a second manager's copy of a task death counts once. - Failing swarm services lead Needs attention; services sort problems first with since-times on the page, the widget and the Swarm page; nodes that are down or drained sort first. - Busiest widget orders by a 5-minute CPU average so the list holds still. - Host panel: a table, problems first with what is wrong and since when; replaced swarm tasks hidden and counted. - Container detail: node hostname, I/O labelled "since start", Restarts hidden for swarm tasks with a note on where task failures are. - Widget names: "Docker — Services & failures", "Docker — Busiest containers". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
230 lines
14 KiB
HTML
230 lines
14 KiB
HTML
{# docker/rows.html — HTMX fragment for the Docker page: what needs attention
|
|
first (service.needs_attention, the ranking the MCP tools use), the counts,
|
|
then swarm services and each host's containers. #}
|
|
{% from "_macros.html" import stat_tile, status_badge, status_dot %}
|
|
{% from "docker/_usage.html" import cpu, mem, cores %}
|
|
|
|
{# ── 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>
|
|
<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. #}
|
|
{% for s in failing_services %}
|
|
{% set e = s.last_error %}
|
|
<li>
|
|
{% if s.crash_looping %}{{ status_badge('crit', 'Crash-looping') }}
|
|
{% elif s.state == 'down' %}{{ status_badge('crit', 'Down') }}
|
|
{% else %}{{ status_badge('warn', 'Degraded') }}{% endif %}
|
|
<a href="/plugins/docker/swarm">{{ s.name }}</a>
|
|
<span class="problem-detail">
|
|
{{- s.running }} of {{ s.desired }} running
|
|
{%- if s.crash_looping %} · {{ s.failures_1h }} tasks failed in the last hour{% endif %}
|
|
{%- if e %} · last task {{ e.state }}: {{ e.error or e.message or e.state }}{% if e.exit_code %} (exit {{ e.exit_code }}){% endif %}{% if e.node %} on {{ e.node }}{% endif %}{% endif %}
|
|
{%- if s.trouble_since %} · since {{ s.trouble_since|ago }}{% endif -%}
|
|
</span>
|
|
</li>
|
|
{% endfor %}
|
|
{% for r in attention %}
|
|
{% set c = r.container %}
|
|
{% set worst = r.problems[0] %}
|
|
<li>
|
|
{{ status_badge('crit' if severity[worst] == 'critical' else 'warn', worst|replace('_', ' ')|capitalize) }}
|
|
<a href="/plugins/docker/container/{{ c.host_id }}/{{ c.name }}" title="{{ c.name }}">{{ r.label }}</a>
|
|
<span class="problem-detail">
|
|
{%- for p in r.problems %}{{ says[p] }}{{ " and " if not loop.last }}{% endfor %}
|
|
{%- if c.exit_code and worst in ('exited_nonzero', 'crash_looping', 'restarting') %} · exit {{ c.exit_code }}{% endif %}
|
|
{%- if r.since %} · since {{ r.since|ago }}{% endif %}
|
|
{%- if r.stale %} · {{ status_badge('stale') }} last report {{ c.scraped_at|ago if c.scraped_at else "never" }}{% endif -%}
|
|
</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{# ── Hosts whose Docker is not answering (agent ≥ 1.9.0) ──────────────────── #}
|
|
{% if docker_down %}
|
|
<section class="card problem-card" aria-labelledby="docker-unreachable">
|
|
<h2 id="docker-unreachable" class="section-title" style="margin-bottom:0.6rem;">Docker not answering</h2>
|
|
<ul class="problem-list">
|
|
{% for d in docker_down %}
|
|
<li>
|
|
{{ status_badge('crit', 'Error') if d.status == 'error' else status_badge('stale', 'No Docker') }}
|
|
<a href="/hosts/{{ d.host_id }}">{{ d.host_name }}</a>
|
|
<span class="problem-detail">{{ d.detail or "no reason given" }} · since {{ d.since|ago }} · its containers below are what Docker last reported</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{# ── Counts ──────────────────────────────────────────────────────────────
|
|
Swarm services are counted as services (a task's container comes and goes);
|
|
the container counts are the containers outside any swarm service. ────── #}
|
|
<div class="stat-strip">
|
|
{{ stat_tile("Needs attention", attention|length + failing_services|length, state="crit" if attention or failing_services else "inactive") }}
|
|
{{ stat_tile("Unhealthy", unhealthy, state="crit" if unhealthy else "inactive") }}
|
|
{{ stat_tile("Restarting", restarting, state="crit" if restarting else "inactive", hint="restarting or crash-looping") }}
|
|
{{ stat_tile("Failed (24h)", failures|length, state="warn" if failures else "inactive", hint="containers and services; listed below", href="#failures" if failures else none) }}
|
|
{% if swarm_services %}{{ stat_tile("Swarm services", services_healthy ~ "/" ~ swarm_services|length, state="up" if services_healthy == swarm_services|length else "warn", hint="healthy", href="/plugins/docker/swarm") }}{% endif %}
|
|
{{ stat_tile("Containers running", running, state="up" if running else "inactive", hint="outside swarm services" if swarm_services else none) }}
|
|
{{ stat_tile("Containers stopped", stopped, state="inactive") }}
|
|
{% if stale %}{{ stat_tile("Stale", stale, state="stale", hint="no recent report") }}{% endif %}
|
|
{{ stat_tile("Docker hosts", docker_hosts) }}
|
|
</div>
|
|
|
|
{# ── What failed in the last 24h: what the dashboard's "failed (24h)" counts.
|
|
A plain container's death, or a swarm service's failed tasks; a container
|
|
someone stopped (exit 137/143) is not a failure. ───────────────────────── #}
|
|
{% if failures %}
|
|
<section id="failures" class="card" aria-labelledby="docker-failures" style="margin-bottom:1.5rem;">
|
|
<h2 id="docker-failures" class="section-title" style="margin-bottom:0.6rem;">Failed in the last 24h</h2>
|
|
<ul class="problem-list">
|
|
{% for f in failures %}
|
|
<li>
|
|
{{ status_badge('warn', 'Service' if f.kind == 'service' else 'Container') }}
|
|
{% if f.kind == 'service' %}<a href="/plugins/docker/swarm">{{ f.name }}</a>
|
|
{% else %}<a href="/plugins/docker/container/{{ f.host_id }}/{{ f.name }}">{{ f.name }}</a>{% endif %}
|
|
<span class="problem-detail">
|
|
{{- f.count }} {{ "failure" if f.count == 1 else "failures" }}
|
|
{%- if f.detail %} · last: {{ f.detail }}{% endif %}
|
|
{%- if f.host %} · {{ f.host }}{% endif %}
|
|
{%- if f.at %} · {{ f.at|ago }}{% endif -%}
|
|
</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{# ── Swarm services: collapsed per-service, each current replica labelled with
|
|
its host (tasks Swarm replaced are left out). Ghost replicas (tasks on nodes
|
|
with no Steward agent) come from the managers' placement data so the picture
|
|
is cluster-complete. A failing service names Swarm's last task error. ───── #}
|
|
{% if swarm_services %}
|
|
<div class="card-flush" style="margin-bottom:1.5rem;">
|
|
<div style="display:flex;align-items:baseline;gap:0.6rem;padding:0.5rem 0.75rem;border-bottom:1px solid var(--border);">
|
|
<span style="font-weight:500;font-size:0.95rem;">Swarm services</span>
|
|
<a href="/plugins/docker/swarm" style="font-size:0.78rem;color:var(--text-muted);">Topology →</a>
|
|
</div>
|
|
<div style="padding:0.5rem 0.75rem;display:grid;gap:0.85rem;">
|
|
{% for s in swarm_services %}
|
|
<div>
|
|
<div style="display:flex;align-items:center;gap:0.55rem;flex-wrap:wrap;margin-bottom:0.35rem;">
|
|
<span class="dot {% if s.state == 'healthy' %}dot-up{% elif s.state == 'degraded' %}dot-warn{% else %}dot-down{% endif %}"></span>
|
|
<span style="font-weight:500;font-size:0.9rem;">{{ s.name }}</span>
|
|
<span style="font-size:0.72rem;color:var(--text-dim);">{{ s.mode }}</span>
|
|
<span class="tone-{{ 'ok' if s.state == 'healthy' else ('warn' if s.state == 'degraded' else 'crit') }}" style="font-size:0.78rem;font-variant-numeric:tabular-nums;">
|
|
{{ s.running }}/{{ s.desired }} running{% if s.state != 'healthy' and s.trouble_since %} · since {{ s.trouble_since|ago }}{% endif %}
|
|
</span>
|
|
</div>
|
|
{% with style="padding-left:1.1rem;margin-bottom:0.35rem;" %}{% include "docker/_swarm_failures.html" %}{% endwith %}
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:0.4rem;padding-left:1.1rem;">
|
|
{% for r in s.replicas %}
|
|
{% if r.ghost %}
|
|
<div style="display:flex;align-items:center;gap:0.4rem;font-size:0.8rem;background:var(--bg-elevated);border:1px dashed var(--border-mid);border-radius:5px;padding:0.3rem 0.55rem;" title="Running on a node with no Steward agent — detail unavailable">
|
|
<span class="dot dot-warn" style="opacity:0.6;"></span>
|
|
<span style="font-weight:500;">{{ r.host }}</span>
|
|
<span style="color:var(--text-dim);margin-left:auto;">{{ r.count }} · no agent</span>
|
|
</div>
|
|
{% else %}
|
|
<div style="display:flex;align-items:center;gap:0.4rem;font-size:0.8rem;background:var(--bg-elevated);border:1px solid var(--border);border-radius:5px;padding:0.3rem 0.55rem;">
|
|
<span class="dot {% if r.status == 'running' %}dot-up{% elif r.status == 'paused' %}dot-warn{% else %}dot-down{% endif %}"></span>
|
|
<a href="/plugins/docker/container/{{ r.host_id }}/{{ r.name }}" style="font-weight:500;color:inherit;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="{{ r.name }}">{{ r.host }}</a>
|
|
{% if r.cpu_pct is not none %}<span style="color:var(--text-muted);font-family:ui-monospace,monospace;margin-left:auto;" title="Docker counts CPU per core">{{ cores(r.cpu_pct / 100) }}</span>{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# ── Container tables, one per host ───────────────────────────────────────── #}
|
|
{% if host_groups %}
|
|
{% for g in host_groups %}
|
|
<div class="card-flush" style="margin-bottom:1.5rem;">
|
|
<div style="display:flex;align-items:baseline;gap:0.6rem;padding:0.5rem 0.75rem;border-bottom:1px solid var(--border);">
|
|
{% if g.host %}
|
|
<a href="/hosts/{{ g.host.id }}" style="font-weight:500;font-size:0.95rem;color:inherit;text-decoration:none;">{{ g.host.name }}</a>
|
|
{% else %}
|
|
<span style="font-weight:500;font-size:0.95rem;">{{ g.host_name }}</span>
|
|
{% endif %}
|
|
<span style="font-size:0.78rem;color:var(--text-muted);">
|
|
{{ g.running }} running{% if g.stopped %} · {{ g.stopped }} stopped{% endif %}
|
|
</span>
|
|
</div>
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Container</th>
|
|
<th>Image</th>
|
|
<th>Ports</th>
|
|
<th title="Docker counts CPU per core: 1 core = one core fully busy">CPU</th>
|
|
<th title="Used, and the limit when one is set">Memory</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for sub in g.subgroups %}
|
|
{% if g.grouped and sub.label %}
|
|
<tr><td colspan="5" style="padding:0.4rem 0.75rem 0.2rem;font-size:0.72rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;font-weight:500;">{{ sub.label }}</td></tr>
|
|
{% endif %}
|
|
{% for item in sub.containers %}
|
|
{% set c = item.container %}
|
|
<tr>
|
|
<td>
|
|
<div style="display:flex;align-items:center;gap:0.5rem;">
|
|
{{ status_dot('stale' if item.stale else ('up' if c.status == 'running' else ('pending' if c.status == 'paused' else 'down')), 'Stale' if item.stale else c.status|capitalize) }}
|
|
<div>
|
|
<div style="font-weight:500;font-size:0.9rem;">
|
|
<a href="/plugins/docker/container/{{ c.host_id }}/{{ c.name }}" style="color:inherit;text-decoration:none;border-bottom:1px dotted var(--border-mid);">{{ c.name }}</a>
|
|
{% if c.health == 'unhealthy' %} {{ status_badge('crit', 'Unhealthy') }}
|
|
{% elif c.health == 'starting' %} {{ status_badge('pending', 'Starting') }}{% endif %}
|
|
{% if item.stale %} {{ status_badge('stale', title="Last report " ~ (c.scraped_at.strftime('%Y-%m-%d %H:%M UTC') if c.scraped_at else 'never') ~ "; what is shown may no longer be true") }}{% endif %}
|
|
</div>
|
|
<div style="font-size:0.73rem;color:var(--text-muted);">
|
|
{{ c.status }}{% if item.uptime %} · up {{ item.uptime }}{% endif %}
|
|
{% if c.status != 'running' and c.exit_code is not none and c.exit_code != 0 %}
|
|
· <span class="tone-{{ 'crit' if item.exit_failed else 'dim' }}" title="{{ 'last exit code' if item.exit_failed else 'stopped by a signal; not a failure' }}">exit {{ c.exit_code }}{% if c.oom_killed %} (OOM){% endif %}</span>
|
|
{% endif %}
|
|
{% if c.restart_count %} · <span class="tone-warn" title="Restarted {{ c.restart_count }} times">⟳{{ c.restart_count }}</span>{% endif %}
|
|
</div>
|
|
{% if c.service_name or c.compose_project %}
|
|
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:0.1rem;">
|
|
{{ c.service_name or c.compose_project }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td style="font-size:0.82rem;color:var(--text-muted);max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
|
|
{{ c.image }}
|
|
</td>
|
|
<td style="font-size:0.78rem;font-family:ui-monospace,monospace;">
|
|
{% for p in item.ports %}
|
|
<div style="color:var(--text-muted);">{{ p.host_port }}:{{ p.container_port }}/{{ p.protocol }}</div>
|
|
{% endfor %}
|
|
</td>
|
|
<td style="font-family:ui-monospace,monospace;font-size:0.88rem;white-space:nowrap;">{{ cpu(item.usage) }}</td>
|
|
<td style="font-family:ui-monospace,monospace;font-size:0.88rem;white-space:nowrap;">{{ mem(item.usage) }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endfor %}
|
|
{% elif not swarm_services %}
|
|
<div class="card" style="text-align:center;padding:3rem;">
|
|
<div style="color:var(--text-muted);font-size:0.9rem;">
|
|
No containers reported yet. Containers are collected by the Steward host agent —
|
|
deploy the agent to a host running Docker (Hosts → the host → agent panel) and
|
|
its containers will appear here under that host.
|
|
</div>
|
|
</div>
|
|
{% endif %}
|