feat(docker): the Docker page leads with a verdict, lists services and containers as filterable tables, and each service has its own page with logs across its tasks (#5298–#5302)
- #5298 "Docker overview: verdict line; only the problem tiles that aren't zero" - #5299 "Docker overview: swarm services as a filterable, sortable table with a Stack column" - #5300 "Docker overview: failures listed once, each with a link to the failed task's logs" - #5301 "Docker overview: standalone containers in one table with a Host column" - #5302 "Service page: replicas, image, task history, resource history and logs across all its tasks" overview.py holds the rows, filters and sort as plain functions. The filter bar sits outside the polled fragment and the URL keeps its state. A service's page reads every task container Steward still holds anything for, the live ones and removed ones known by their events, so a failed task's logs are reachable after Docker has removed it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,36 @@
|
||||
{# 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 %}
|
||||
{# 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
|
||||
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 "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) }}
|
||||
{%- endmacro %}
|
||||
{% macro up(row) -%}
|
||||
{%- if row.started_at %}<span title="Started {{ row.started_at.strftime('%Y-%m-%d %H:%M UTC') }}">{{ row.started_at|ago|striptags|replace(" ago", "") }}</span>
|
||||
{%- else %}<span class="tone-dim">—</span>{% endif -%}
|
||||
{%- endmacro %}
|
||||
|
||||
{# ── Verdict ──────────────────────────────────────────────────────────────── #}
|
||||
{% set problems = attention|length + failing_services|length %}
|
||||
<p class="docker-verdict">
|
||||
{% if problems %}<strong class="tone-crit">{{ problems }} need{{ "s" if problems == 1 }} attention</strong>
|
||||
{% else %}<strong class="tone-ok">Nothing needs attention</strong>{% 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 docker_hosts %} · {{ docker_hosts }} Docker host{{ "s" if docker_hosts != 1 }}{% 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 %}
|
||||
@@ -17,13 +45,14 @@
|
||||
{% 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>
|
||||
<a href="/plugins/docker/service/{{ s.name }}">{{ 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>
|
||||
<a class="field-hint" href="/plugins/docker/service/{{ s.name }}{% if e and e.task_id %}?task={{ e.task_id }}{% endif %}#logs" style="margin-left:0.4rem;">Logs →</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% for r in attention %}
|
||||
@@ -38,6 +67,7 @@
|
||||
{%- 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>
|
||||
<a class="field-hint" href="/plugins/docker/container/{{ c.host_id }}/{{ c.name }}/logs" style="margin-left:0.4rem;">Logs →</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -60,170 +90,144 @@
|
||||
</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. ───────────────────────── #}
|
||||
someone stopped (exit 137/143) is not a failure. Each links to the logs of
|
||||
what died: a service's to its failed task's lines (#5300). ───────────── #}
|
||||
{% if failures %}
|
||||
<section id="failures" class="card" aria-labelledby="docker-failures" style="margin-bottom:1.5rem;">
|
||||
<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>
|
||||
<ul class="problem-list">
|
||||
{% for f in failures %}
|
||||
{% for x 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 %}
|
||||
{{ status_badge('warn', 'Service' if x.kind == 'service' else 'Container') }}
|
||||
{% if x.kind == 'service' %}<a href="/plugins/docker/service/{{ x.name }}">{{ x.name }}</a>
|
||||
{% else %}<a href="/plugins/docker/container/{{ x.host_id }}/{{ x.name }}">{{ x.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 -%}
|
||||
{{- x.count }} {{ "failure" if x.count == 1 else "failures" }}
|
||||
{%- if x.detail %} · last: {{ x.detail }}{% endif %}
|
||||
{%- if x.host %} · {{ x.host }}{% endif %}
|
||||
{%- if x.at %} · {{ x.at|ago }}{% endif -%}
|
||||
</span>
|
||||
{% if x.kind == 'service' %}
|
||||
<a class="field-hint" href="/plugins/docker/service/{{ x.name }}{% if x.task_id %}?task={{ x.task_id }}{% endif %}#logs" style="margin-left:0.4rem;">Logs{% if x.task_id %} of that task{% endif %} →</a>
|
||||
{% else %}
|
||||
<a class="field-hint" href="/plugins/docker/container/{{ x.host_id }}/{{ x.name }}/logs" style="margin-left:0.4rem;">Logs →</a>
|
||||
{% endif %}
|
||||
</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>
|
||||
{# ── Swarm services: one row each, problems first unless a header picked
|
||||
another order; CPU and memory add up the running replicas (#5299). ───── #}
|
||||
{% if services %}
|
||||
<section class="card-flush" aria-labelledby="docker-services">
|
||||
<h2 id="docker-services" class="table-caption">Swarm services <span class="tone-muted">{{ services|length }}{% if services|length != services_total %} of {{ services_total }}{% endif %}</span>
|
||||
<a href="/plugins/docker/swarm" class="field-hint">By node →</a></h2>
|
||||
<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>
|
||||
{{ th("name", "Service") }}
|
||||
{{ th("status", "Status", title="Problems first: a crash loop, then down, then short of replicas, then failed tasks in 24h") }}
|
||||
{{ th("stack", "Stack", title="The part of the service's name before the first _: the stack it was deployed in") }}
|
||||
<th title="The nodes running its replicas">Nodes</th>
|
||||
{{ th("cpu", "CPU", "num", "Across its running replicas; Docker counts CPU per core") }}
|
||||
{{ th("mem", "Memory", "num", "Across its running replicas") }}
|
||||
{{ th("up", "Up", "num", "Since its newest replica started") }}
|
||||
<th class="num" title="Tasks that failed in the last 24h; Swarm replaced them">Failed 24h</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 %}
|
||||
{% for r in services %}
|
||||
<tr>
|
||||
<td class="cell-name"><a href="/plugins/docker/service/{{ r.name }}" title="{{ r.name }}{% if r.image %} · {{ r.image }}{% endif %}">{{ r.name }}</a></td>
|
||||
<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>
|
||||
{{ status_dot('up' if r.state == 'healthy' else ('warn' if r.state == 'degraded' else 'down')) }}
|
||||
<span class="tone-{{ 'ok' if r.state == 'healthy' else ('warn' if r.state == 'degraded' else 'crit') }}" style="font-variant-numeric:tabular-nums;">{{ r.running }}/{{ r.desired }}</span>
|
||||
{%- if r.crash_looping %} {{ status_badge('crit', 'Crash-looping') }}{% elif r.unhealthy %} {{ status_badge('crit', 'Unhealthy') }}{% endif %}
|
||||
{%- if r.mode == 'global' %} <span class="tone-dim">global</span>{% endif %}
|
||||
</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>{% if r.stack %}<a class="chip" href="{{ page_url(stack=r.stack) }}" title="Show the {{ r.stack }} stack">{{ r.stack }}</a>{% else %}<span class="tone-dim">—</span>{% endif %}</td>
|
||||
<td class="tone-muted">
|
||||
{{- r.nodes|join(", ") or ("" if r.ghost_nodes else "—") -}}
|
||||
{%- if r.ghost_nodes %}{{ ", " if r.nodes }}<span title="Running on a node with no Steward agent: no figures for it">{{ r.ghost_nodes|join(", ") }} (no agent)</span>{% endif -%}
|
||||
</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 class="num">{% if r.cpu_cores is not none %}{{ cores(r.cpu_cores) }}{% else %}<span class="tone-dim">—</span>{% endif %}</td>
|
||||
<td class="num">{% if r.mem_bytes is not none %}{{ r.mem_bytes|bytes }}{% else %}<span class="tone-dim">—</span>{% endif %}</td>
|
||||
<td class="num">{{ up(r) }}</td>
|
||||
<td class="num">
|
||||
{%- if r.failures_24h %}<a class="tone-warn" href="/plugins/docker/service/{{ r.name }}#tasks"
|
||||
title="{% if r.last_error %}Last: {{ r.last_error.error or r.last_error.message or r.last_error.state }}{% if r.last_error.exit_code %} (exit {{ r.last_error.exit_code }}){% endif %}{% endif %}">{{ r.failures_24h }}</a>
|
||||
{%- else %}<span class="tone-dim">—</span>{% endif -%}
|
||||
</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;">
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{# ── Containers outside any swarm service, every host in one table (#5301);
|
||||
a compose project is its stack. ─────────────────────────────────────── #}
|
||||
{% if containers %}
|
||||
<section class="card-flush" aria-labelledby="docker-containers">
|
||||
<h2 id="docker-containers" class="table-caption">{{ "Other containers" if services_total else "Containers" }} <span class="tone-muted">{{ containers|length }}{% if containers|length != containers_total %} of {{ containers_total }}{% endif %}</span></h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
{{ th("name", "Container") }}
|
||||
{{ th("status", "Status", title="Problems first: unhealthy or restarting, then died with a failing exit, then stale; then running, then stopped") }}
|
||||
<th>Host</th>
|
||||
{{ th("stack", "Stack", title="Its compose project") }}
|
||||
<th>Image</th>
|
||||
{{ th("cpu", "CPU", "num", "Docker counts CPU per core: 1 core = one core fully busy") }}
|
||||
{{ th("mem", "Memory", "num", "Used, and the limit when one is set") }}
|
||||
{{ th("up", "Up", "num", "Since it started") }}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in containers %}
|
||||
<tr>
|
||||
<td class="cell-name">
|
||||
<a href="/plugins/docker/container/{{ r.host_id }}/{{ r.container_name }}" title="{{ r.container_name }}">{{ r.name }}</a>
|
||||
{%- if r.ports %}<div class="cell-sub mono">{% for p in r.ports %}{{ p.host_port }}:{{ p.container_port }}/{{ p.protocol }}{{ " " if not loop.last }}{% endfor %}</div>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ status_dot('stale' if r.stale else ('up' if r.running else ('pending' if r.status == 'paused' else 'down'))) }}
|
||||
{%- if r.health == 'unhealthy' %} {{ status_badge('crit', 'Unhealthy') }}
|
||||
{%- elif r.health == 'starting' %} {{ status_badge('pending', 'Starting') }}
|
||||
{%- else %} <span class="tone-muted">{{ r.status }}</span>{% endif %}
|
||||
{%- if not r.running and r.exit_code is not none and r.exit_code != 0 %}
|
||||
<span class="tone-{{ 'crit' if r.exit_failed else 'dim' }}" title="{{ 'last exit code' if r.exit_failed else 'stopped by a signal; not a failure' }}">exit {{ r.exit_code }}{% if r.oom_killed %} (OOM){% endif %}</span>
|
||||
{%- endif %}
|
||||
{%- if r.restart_count %} <span class="tone-warn" title="Restarted {{ r.restart_count }} times">⟳{{ r.restart_count }}</span>{% endif %}
|
||||
{%- if r.stale %} {{ status_badge('stale', title="Last report " ~ (r.scraped_at.strftime('%Y-%m-%d %H:%M UTC') if r.scraped_at else 'never') ~ "; what is shown may no longer be true") }}{% endif %}
|
||||
</td>
|
||||
<td class="tone-muted">{{ r.host }}</td>
|
||||
<td>{% if r.stack %}<a class="chip" href="{{ page_url(stack=r.stack) }}" title="Show the {{ r.stack }} stack">{{ r.stack }}</a>{% else %}<span class="tone-dim">—</span>{% endif %}</td>
|
||||
<td class="cell-image" title="{{ r.image }}">{{ r.image }}</td>
|
||||
<td class="num">{% if r.running %}{{ cpu(r.usage) }}{% else %}<span class="tone-dim">—</span>{% endif %}</td>
|
||||
<td class="num">{% if r.running %}{{ mem(r.usage) }}{% else %}<span class="tone-dim">—</span>{% endif %}</td>
|
||||
<td class="num">{{ up(r) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if not services and not containers %}
|
||||
<div class="card">
|
||||
{% if services_total or containers_total %}
|
||||
<p class="tone-muted" style="margin:0;">Nothing matches these filters. <a href="/plugins/docker/">Show everything</a></p>
|
||||
{% else %}
|
||||
<p class="tone-muted" style="margin:0;">
|
||||
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>
|
||||
its containers will appear here.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user