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>
229 lines
14 KiB
HTML
229 lines
14 KiB
HTML
{# 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 %}
|