Files
FabledSteward/plugins/docker/templates/docker/service.html
T
bvandeusenandClaude Opus 5.5 29be9e3590
CI / lint (push) Successful in 4s
CI / integration (push) Successful in 1m33s
CI / unit (push) Failing after 1m59s
CI / publish (push) Skipped
feat(docker): the service and container pages say each fact once (#5310)
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>
2026-10-07 21:33:25 -04:00

161 lines
9.4 KiB
HTML

{# docker/service.html — one swarm service (#5302, #5310): a header that says
whether it is well and what it runs; its last failure only while it is in
trouble; its CPU and memory over time; one table of its tasks, the running
ones first with what they use; and the logs of every task it has run.
A failure's "Logs →" lands on #logs with ?task= picked. #}
{% extends "base.html" %}
{% from "_macros.html" import crumbs, status_badge %}
{% from "docker/_usage.html" import cpu, mem %}
{% block title %}{{ name }} — Docker — Steward{% endblock %}
{% block breadcrumb %}{{ crumbs([("Docker", "/plugins/docker/"), (name, "")]) }}{% endblock %}
{% block content %}
{% if service is none %}
<div class="card empty-state">
<strong>{{ "No longer running" if task_choices else "Service not found" }}</strong>
{% if task_choices %}
<span>No swarm service named <code>{{ name }}</code> is reported now; it may have been removed.
The logs Steward still keeps from its tasks are below.</span>
{% else %}
<span>No swarm service named <code>{{ name }}</code> is reported, and Steward holds no logs or
events for one. It may have been removed. <a href="/plugins/docker/">Back to Docker</a></span>
{% endif %}
</div>
{% else %}
{# ── Header: the verdict, then what it is (#5310). Each fact has one home:
what it uses is in Resource history, its tasks and failures in Tasks. #}
{% set troubled = row.crash_looping or row.state != 'healthy' %}
{% set tone = 'ok' if row.state == 'healthy' else ('warn' if row.state == 'degraded' else 'crit') %}
{% set image = (svc.image if svc and svc.image else row.image) %}
{% set e = row.last_error %}
<div class="page-header">
<div style="min-width:0;">
<h1 class="page-title" style="overflow-wrap:anywhere;">{{ name }}</h1>
<p class="page-subtitle">
{% if row.crash_looping %}{{ status_badge('crit', 'Crash-looping') }}
{% elif row.state == 'down' %}{{ status_badge('crit', 'Down') }}
{% elif row.state == 'degraded' %}{{ status_badge('warn', 'Degraded') }}
{% else %}{{ status_badge('ok', 'Healthy') }}{% endif %}
<span class="tone-{{ tone }}">{{ row.running }} of {{ row.desired }} running</span>
{%- if troubled and row.trouble_since %} · <span class="tone-{{ tone }}">in trouble since {{ row.trouble_since|ago }}</span>{% endif %}
· {{ row.mode }}
{%- if stack %} · stack <a class="chip" href="/plugins/docker/?stack={{ stack }}">{{ stack }}</a>{% endif %}
{%- if image %} · <span class="mono" style="overflow-wrap:anywhere;" title="{{ image }}">{{ image.split("@")[0] }}</span>{% endif %}
</p>
{% if (not troubled and e) or svc %}
<p class="page-subtitle tone-dim" style="margin-top:0.2rem;">
{%- if not troubled and e %}Last failed {% if e.age_s is not none %}{{ e.age_s|duration }} ago{% endif %}
{%- if e.exit_code %} (exit {{ e.exit_code }}){% endif %}
{%- if e.task_id %} · <a href="?task={{ e.task_id }}#logs">its logs</a>{% endif %} · {% endif -%}
{%- if svc %}reported {{ svc.updated_at|ago }} by the manager's agent{% endif -%}
</p>
{% endif %}
</div>
<div class="page-actions"><a class="btn btn-sm btn-ghost" href="#logs">Logs</a></div>
</div>
{# ── What is wrong, only while it is (#5310) ──────────────────────────────── #}
{% if troubled and e %}
<section class="card problem-card" aria-labelledby="svc-last-error">
<h2 id="svc-last-error" class="section-title" style="margin-bottom:0.4rem;">Last failed task</h2>
<p style="margin:0;">
{{ e.error or e.message or e.state }}{% if e.exit_code %} <span class="tone-crit">(exit {{ e.exit_code }})</span>{% endif %}
{%- if e.node %} · on {{ e.node }}{% endif %}{% if e.age_s is not none %} · {{ e.age_s|duration }} ago{% endif %}
{%- if e.task_id %} · <a href="?task={{ e.task_id }}#logs">its logs →</a>{% endif %}
</p>
</section>
{% endif %}
{# ── Resource history, summed across replicas ────────────────────────────── #}
<section class="card" aria-labelledby="svc-history">
<div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-bottom:0.75rem;flex-wrap:wrap;">
<h2 id="svc-history" class="section-title" style="margin:0;">Resource history <span class="tone-dim">across running replicas</span></h2>
{% include "_time_range.html" %}
</div>
<div hx-get="/plugins/docker/service/{{ name }}/history"
hx-trigger="load, rangeChange from:body"
hx-include="#time-range"
hx-swap="innerHTML">
<p class="field-hint">Loading…</p>
</div>
</section>
{# ── Tasks: the running ones with what they use, then Swarm's record ─────── #}
<section id="tasks" class="card-flush" aria-labelledby="svc-tasks">
<h2 id="svc-tasks" class="table-caption">Tasks
<span class="tone-muted">running first, then Swarm's most recent</span>
{%- if row.failures_24h %} <span class="tone-warn">· {{ row.failures_24h }} failed in 24h</span>{% endif %}</h2>
{% if tasks %}
<table class="table">
<thead><tr>
<th title="The replica slot this task filled">Slot</th><th>Task</th><th>Node</th><th>State</th>
<th class="num" title="Docker counts CPU per core">CPU</th><th class="num">Memory</th>
<th class="num">Exit</th><th>Why</th><th class="num" title="When it started, for a running task; else when it reached its state">When</th>
</tr></thead>
<tbody>
{% for t in tasks %}
<tr>
<td>{{ t.slot or "—" }}</td>
<td class="mono" title="{{ t.container or t.task_id }}">
{%- if t.task_id %}<a href="?task={{ t.task_id }}#logs" title="Its logs">{{ t.task_id[:10] }}</a>
{%- if t.container and t.host_id %} <a class="tone-dim" href="/plugins/docker/container/{{ t.host_id }}/{{ t.container }}" title="The container's own page">↗</a>{% endif %}
{%- elif t.container and t.host_id %}<a href="/plugins/docker/container/{{ t.host_id }}/{{ t.container }}">{{ t.container|truncate(24, true, "…") }}</a>
{%- else %}<span class="tone-dim">—</span>{% endif -%}
</td>
<td>{% if t.host_id %}<a href="/hosts/{{ t.host_id }}">{{ t.node }}</a>{% else %}{{ t.node }}{% endif %}</td>
<td>
{%- if t.failed %}{{ status_badge('crit', t.state|capitalize) }}
{%- elif t.current %}{{ status_badge('ok', 'Running') }}
{%- else %}<span class="tone-muted">{{ t.state }}</span>{% endif %}
{%- if t.health == 'unhealthy' %} {{ status_badge('crit', 'Unhealthy') }}{% elif t.health == 'starting' %} {{ status_badge('pending', 'Starting') }}{% endif %}
{%- if t.desired and t.desired != t.state and not t.current %} <span class="tone-dim" title="What Swarm wants of it">→ {{ t.desired }}</span>{% endif -%}
</td>
<td class="num">{% if t.usage %}{{ cpu(t.usage) }}{% else %}<span class="tone-dim">—</span>{% endif %}</td>
<td class="num">{% if t.usage %}{{ mem(t.usage) }}{% else %}<span class="tone-dim">—</span>{% endif %}</td>
<td class="num">{% if t.exit_code %}<span class="tone-{{ 'crit' if t.failed else 'dim' }}">{{ t.exit_code }}</span>{% else %}<span class="tone-dim">—</span>{% endif %}</td>
<td style="overflow-wrap:anywhere;">{{ t.why or "" }}</td>
<td class="num">{% if t.at %}{{ t.at|ago }}{% else %}<span class="tone-dim">—</span>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="tone-muted" style="margin:0;padding:1rem;">No task is running, and Swarm's record of its tasks has not been reported yet (the manager's agent reports it from version 1.8.0).</p>
{% endif %}
</section>
{% endif %}
{# ── Logs across every task, newest first ─────────────────────────────────
Shown for a removed service too, while its lines are still kept. #}
{% if service is not none or task_choices %}
<section id="logs" class="card-flush" aria-labelledby="svc-logs">
<h2 id="svc-logs" class="table-caption">Logs <span class="tone-muted">every task, newest first</span></h2>
<form id="svc-log-controls" class="filter-bar" onsubmit="return false;" style="padding:0.75rem 1rem 0;">
<input type="search" name="q" class="input" placeholder="Filter lines…" autocomplete="off" aria-label="Filter log lines">
{% if task_choices|length > 1 %}
<select name="task" aria-label="Task">
<option value="">All tasks</option>
{% for tid, label in task_choices %}<option value="{{ tid }}"{% if tid == focus_task %} selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
{% elif focus_task %}<input type="hidden" name="task" value="{{ focus_task }}">{% endif %}
<select name="stream" aria-label="Stream">
<option value="all">stdout and stderr</option>
<option value="stdout">stdout</option>
<option value="stderr">stderr</option>
</select>
</form>
<div id="svc-log-lines"
hx-get="/plugins/docker/service/{{ name }}/logs/lines"
hx-trigger="load, every 5s, change from:#svc-log-controls, keyup changed delay:400ms from:#svc-log-controls"
hx-include="#svc-log-controls"
hx-swap="innerHTML"
role="log" aria-live="polite" tabindex="0"
style="max-height:60vh;overflow:auto;padding:0.5rem 1rem 0.75rem;font-family:var(--fs-font-mono);font-size:0.8rem;line-height:1.5;">
<p class="field-hint">Loading…</p>
</div>
</section>
{% endif %}
{% endblock %}