Service page: - The header carries the verdict, mode, stack, image and when it was reported. 'In trouble since' shows only while the service is in trouble; once recovered, 'Last failed X ago · its logs'. - The failure card shows only while the service is down, degraded or crash-looping. - The tile strip and the kv card are gone. - One Tasks table: running tasks first with CPU, memory and since when, then Swarm's record. The node is named as its Steward host throughout, and 'N failed in 24h' sits in the caption. Container page: - Same shape: header verdict, a 'Last exit' card only while it is stopped after a failing exit, set-up and I/O in one kv card, and lifecycle as a table. - A swarm task's breadcrumb goes through its service. Both pages: - Resource history is the one fragment (_usage_history.html): now and peak in cores and bytes, drawn with a floor of 1 core / 1 GiB (sparkline_svg floor=), so idle noise draws flat. - The container page's old % sparklines, its private _sparkline and the unused bucket()/container_history() helpers are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
126 lines
6.8 KiB
HTML
126 lines
6.8 KiB
HTML
{# docker/container_detail.html — one container (#5310), shaped like a
|
|
service's page: a header that says whether it is well and what it is; why
|
|
it stopped, only while it has; what it moved since it started; its CPU and
|
|
memory over time (the one home for what it uses now); its lifecycle.
|
|
A swarm task is titled by its service and slot, the raw name under it. #}
|
|
{% extends "base.html" %}
|
|
{% from "_macros.html" import crumbs, status_badge %}
|
|
{% block title %}{{ label or name }} — Docker — Steward{% endblock %}
|
|
{% block breadcrumb %}
|
|
{%- if container is not none and container.service_name -%}
|
|
{{ crumbs([("Docker", "/plugins/docker/"), (container.service_name, "/plugins/docker/service/" ~ container.service_name), (label, "")]) }}
|
|
{%- else -%}
|
|
{{ crumbs([("Docker", "/plugins/docker/"), (label or name, "")]) }}
|
|
{%- endif -%}
|
|
{% endblock %}
|
|
{% block content %}
|
|
|
|
{% if container is none %}
|
|
<div class="card empty-state">
|
|
<strong>Container not found</strong>
|
|
<span>No container named <code>{{ name }}</code> is reported for this host now. It may have been
|
|
removed, or the host agent has not reported recently. <a href="/plugins/docker/">Back to Docker</a></span>
|
|
</div>
|
|
{% else %}
|
|
{% set c = container %}
|
|
|
|
{# ── Header: the verdict, then what it is ────────────────────────────────── #}
|
|
<div class="page-header">
|
|
<div style="min-width:0;">
|
|
<h1 class="page-title" style="overflow-wrap:anywhere;">{{ label }}</h1>
|
|
<p class="page-subtitle">
|
|
{% if c.status == 'running' %}{{ status_badge('ok', 'Running') }}
|
|
{% elif c.status == 'restarting' %}{{ status_badge('crit', 'Restarting') }}
|
|
{% elif c.status == 'paused' %}{{ status_badge('warn', 'Paused') }}
|
|
{% elif exit_failed %}{{ status_badge('crit', c.status|capitalize) }}
|
|
{% else %}{{ status_badge('dim', c.status|capitalize) }}{% endif %}
|
|
{%- if c.health == 'unhealthy' %} {{ status_badge('crit', 'Unhealthy') }}
|
|
{%- elif c.health == 'starting' %} {{ status_badge('pending', 'Starting') }}
|
|
{%- elif c.health == 'healthy' %} {{ status_badge('ok', 'Healthy') }}{% endif %}
|
|
{%- if uptime %} · up {{ uptime }}{% endif %}
|
|
{%- if host %} · on <a href="/hosts/{{ host.id }}">{{ host.name }}</a>{% endif %}
|
|
{%- if c.service_name %} · task of <a href="/plugins/docker/service/{{ c.service_name }}">{{ c.service_name }}</a>
|
|
{%- elif c.compose_project %} · compose <span class="chip">{{ c.compose_project }}</span>{% endif %}
|
|
{%- if c.image %} · <span class="mono" style="overflow-wrap:anywhere;" title="{{ c.image }}">{{ c.image.split("@")[0] }}</span>{% endif %}
|
|
</p>
|
|
<p class="page-subtitle tone-dim" style="margin-top:0.2rem;">
|
|
{%- if label != c.name %}<span class="mono" style="overflow-wrap:anywhere;">{{ c.name }}</span> · {% endif -%}
|
|
{%- if c.scraped_at %}reported {{ c.scraped_at|ago }}{% endif -%}
|
|
</p>
|
|
</div>
|
|
<div class="page-actions"><a class="btn btn-sm btn-ghost" href="/plugins/docker/container/{{ host_id }}/{{ name }}/logs">Logs</a></div>
|
|
</div>
|
|
|
|
{# ── Why it stopped, only while it has ───────────────────────────────────── #}
|
|
{% if c.status != 'running' and exit_failed %}
|
|
<section class="card problem-card" aria-labelledby="ctr-exit">
|
|
<h2 id="ctr-exit" class="section-title" style="margin-bottom:0.4rem;">Last exit</h2>
|
|
<p style="margin:0;">
|
|
Exited <span class="tone-crit">{{ c.exit_code }}</span>{% if c.oom_killed %}: killed for running out of memory{% endif %}
|
|
{%- if died_at %} · {{ died_at|ago }}{% endif %}
|
|
· <a href="/plugins/docker/container/{{ host_id }}/{{ name }}/logs">its logs →</a>
|
|
</p>
|
|
</section>
|
|
{% endif %}
|
|
|
|
{# ── What it is set up with, and what it moved since it started ─────────── #}
|
|
<div class="card">
|
|
<dl class="kv">
|
|
{% if not c.service_name %}
|
|
{#- Swarm never restarts a task's container: it replaces the task, so a
|
|
task's restart count is always 0; its service's Tasks table is the
|
|
count that means something. #}
|
|
<dt>Restarts</dt><dd>{% if c.restart_count %}<span class="tone-warn">{{ c.restart_count }}</span>{% else %}0{% endif %}
|
|
{%- if c.exit_code is not none and c.status == 'running' and c.restart_count %} <span class="tone-dim">· last exit {{ c.exit_code }}</span>{% endif %}</dd>
|
|
{% endif %}
|
|
<dt>Memory limit</dt><dd>{% if usage.mem_limit %}{{ usage.mem_limit|bytes }}{% else %}<span class="tone-dim">none set</span>{% endif %}</dd>
|
|
{% if ports %}
|
|
<dt>Ports</dt><dd class="mono">{% for p in ports %}{{ p.host_port }}:{{ p.container_port }}/{{ p.protocol }}{% if not loop.last %}, {% endif %}{% endfor %}</dd>
|
|
{% endif %}
|
|
{% if c.node_id %}
|
|
<dt>Swarm node</dt><dd title="node id {{ c.node_id }}">{{ node_name or c.node_id[:12] }}</dd>
|
|
{% endif %}
|
|
<dt>Network</dt><dd>{{ net_rx }} in · {{ net_tx }} out <span class="tone-dim">since it started</span></dd>
|
|
<dt>Disk</dt><dd>{{ blk_read }} read · {{ blk_write }} written <span class="tone-dim">since it started</span></dd>
|
|
</dl>
|
|
</div>
|
|
|
|
{# ── Resource history: the one home for what it uses ────────────────────── #}
|
|
<section class="card" aria-labelledby="ctr-history">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:0.75rem;flex-wrap:wrap;">
|
|
<h2 id="ctr-history" class="section-title" style="margin:0;">Resource history</h2>
|
|
{% include "_time_range.html" %}
|
|
</div>
|
|
<div id="container-history"
|
|
hx-get="/plugins/docker/container/{{ host_id }}/{{ name }}/history"
|
|
hx-trigger="load, rangeChange from:body"
|
|
hx-include="#time-range"
|
|
hx-swap="innerHTML">
|
|
<p class="field-hint">Loading…</p>
|
|
</div>
|
|
</section>
|
|
|
|
{# ── Lifecycle, newest first ─────────────────────────────────────────────── #}
|
|
<section class="card-flush" aria-labelledby="ctr-lifecycle">
|
|
<h2 id="ctr-lifecycle" class="table-caption">Lifecycle <span class="tone-muted">newest first</span></h2>
|
|
{% if events %}
|
|
<table class="table">
|
|
<thead><tr><th>Event</th><th>Detail</th><th class="num">When</th></tr></thead>
|
|
<tbody>
|
|
{% for e in events %}
|
|
<tr>
|
|
<td><span class="tone-{{ e.tone }}" aria-hidden="true">{{ e.glyph }}</span> {{ e.event|replace("_", " ") }}</td>
|
|
<td class="tone-muted" style="overflow-wrap:anywhere;">{{ e.detail or "" }}</td>
|
|
<td class="num">{{ e.at|ago }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<p class="tone-muted" style="margin:0;padding:1rem;">No lifecycle events recorded yet. Starts, stops and health changes appear here as the agent reports them.</p>
|
|
{% endif %}
|
|
</section>
|
|
|
|
{% endif %}
|
|
{% endblock %}
|