Files
FabledSteward/plugins/docker/templates/docker/container_detail.html
T
bvandeusenandClaude Opus 5.5 f3433fe3ce
CI / lint (push) Successful in 2s
CI / unit (push) Failing after 1m26s
CI / integration (push) Failing after 1m27s
CI / publish (push) Skipped
feat(norms): hosts, containers and monitor response times are graded against their own normal (#5742)
Host CPU, memory and load per core, container CPU and memory, and every
monitor's response time learn a normal per hour (milestone 513, System 182).
A reading past its fixed cutoff but within its normal reads healthy and says
what is usual at this hour; one well out of it keeps its grade and is a
metric anomaly. Disk, a monitor down and an agent offline keep their cutoffs.

- Each normal's floor, the smallest departure that counts, is a setting:
  Settings -> Thresholds -> Normals (norms.floor.*). Defaults: host CPU 10
  points, host memory 5, load per core 10, container CPU 10 points of one
  core, container memory 5, monitor response 100 ms; Traefik's 2-point 4xx
  and 50 ms p95 floors move there from constants. learn_normal takes a
  floor function (floor_setting) read at each pass.
- learn_normal(parts=False): a host learns, its cores don't.
- Ingest records load_pct (load_1m / cores x 100), the figure pages grade.
- Shared grading: anomalies.against / graded, worst_reading(normals=),
  hosts' host_grades, docker usage(thresholds=, norms=), monitors'
  latency_grade. Applied to host vitals, the Hosts widget, MCP overview,
  Docker usage, and the monitor list, widget and page.
- The monitor and container pages list their anomalies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 16:17:04 -04:00

130 lines
7.1 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), with its metric
anomalies above when it has any; 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 %}
{# ── Its CPU or memory out of its normal for the hour, and past anomalies (#5742) #}
{% with back=anomaly_back, base_resource=anomaly_resource %}{% include "_anomaly_card.html" %}{% endwith %}
{# ── 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 %}