Files
FabledSteward/plugins/docker/templates/docker/rows.html
T
bvandeusenandClaude Opus 5.5 0c66c1e7d3
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 55s
CI / unit (push) Failing after 1m25s
CI / publish (push) Skipped
feat(alerts): Steward's own confirm dialog, a verdict-first rules list, and a rule page in three sections (#5314, #5315, #5317)
Confirm dialog (#5314):
- Every data-confirm form now asks in one native <dialog> (base.html + static/confirm.js)
  instead of window.confirm.
- The question becomes the heading and what follows it the detail; the act's
  button is named after the form's own button; Cancel has the focus and Esc
  backs out. Optional data-confirm-title/-detail/-action/-dismiss attributes.
- All 27 existing uses work as they are. Cancelling a run says 'Keep running'.

Alerts list (#5315):
- A verdict line (what is firing, how many rules, where notifications go,
  or 'notifies nobody yet' with a link) replaces the empty Active card.
- Each rule's name opens it; rows show 'last fired' and fold fire/clear into
  the condition; History and Disable stay; Delete leaves the list.
- .docker-verdict becomes the shared .page-verdict.

Rule page (#5317):
- Named for the rule (title and crumb), with when it last fired and how many
  changes of state it holds.
- Three sections: what it watches, when it fires (checks side by side,
  seconds echoed as a duration), what happens (where notifications go, no-data,
  the playbook, whose fields stay folded until ticked).
- Delete at the foot, behind the dialog, saying its history goes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 22:58:38 -04:00

229 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{# docker/rows.html — HTMX fragment for the Docker page (#5298–#5301):
a verdict line; 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 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="page-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 %}
{%- if stale %} · <span class="tone-warn" title="No recent report from their agent: what is shown may no longer be true">{{ stale }} stale</span>{% endif %}
</p>
{# ── Needs attention ──────────────────────────────────────────────────────── #}
{% if attention or failing_services %}
<section class="card problem-card" aria-labelledby="docker-problems">
<h2 id="docker-problems" class="section-title section-count">Needs attention <span class="tone-crit">{{ problems }}</span>
{%- if unhealthy %}<span class="tone-muted"> · {{ unhealthy }} unhealthy</span>{% endif %}
{%- if restarting %}<span class="tone-muted"> · {{ restarting }} restarting</span>{% endif %}</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/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 %}
{% 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>
<a class="field-hint" href="/plugins/docker/container/{{ c.host_id }}/{{ c.name }}/logs" style="margin-left:0.4rem;">Logs →</a>
</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 %}
{# ── 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. 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">
<h2 id="docker-failures" class="section-title section-count">Failed in the last 24h <span class="tone-warn">{{ failures|length }}</span></h2>
<ul class="problem-list">
{% for x in failures %}
<li>
{{ 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">
{{- 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: 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("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 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>
{{ 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>{% 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 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>
</tr>
{% endfor %}
</tbody>
</table>
</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.
</p>
{% endif %}
</div>
{% endif %}