24df8458ea
The container list and dashboard widget listed each swarm task as its own cryptic row (svc.1.<taskid>) and could only show tasks on nodes that run a Steward agent. Make both views service-centric and cluster-complete. - swarm_view.build_swarm_services(): model-free builder that merges real container rows (collected local-per-node, so host_id = the node a task runs on) with the managers' placement (DockerSwarmService.placement_json). Where placement counts exceed the local rows on a node — i.e. nodes with no agent — it synthesizes "ghost" replicas so the replica list matches the cluster. Non-swarm containers pass through grouped by host. - /rows + rows.html: a Swarm-services panel collapses each service to one block, every replica a host chip (status + cpu); ghosts render dashed "N · no agent". Non-swarm/compose containers keep the per-host table below. New Services stat. - /widget + widget.html: same service collapse with host chips; standalone containers stay grouped by host. - Unit tests for the builder (real+ghost merge, full coverage, partial agent-less, synthesized service, down state). No migration — node_id, placement_json and swarm-node hostnames are already stored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
175 lines
9.5 KiB
HTML
175 lines
9.5 KiB
HTML
{# docker/rows.html — HTMX fragment for the Docker main page, grouped by host #}
|
|
|
|
{# ── Summary strip ─────────────────────────────────────────────────────────── #}
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:0.75rem;margin-bottom:1.5rem;">
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Running</div>
|
|
<span class="stat-val" style="color:var(--green);">{{ running }}</span>
|
|
</div>
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Stopped</div>
|
|
<span class="stat-val" style="{% if stopped %}color:var(--text-muted){% endif %};">{{ stopped }}</span>
|
|
</div>
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Total</div>
|
|
<span class="stat-val">{{ total }}</span>
|
|
</div>
|
|
{% if swarm_services %}
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Services</div>
|
|
<span class="stat-val">{{ swarm_services | length }}</span>
|
|
</div>
|
|
{% endif %}
|
|
<div class="card" style="margin-bottom:0;">
|
|
<div class="section-title" style="margin-bottom:0.35rem;">Hosts</div>
|
|
<span class="stat-val">{{ host_groups | length }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Swarm services: collapsed per-service, each replica labelled with its host.
|
|
Ghost replicas (tasks on nodes with no Steward agent) come from the managers'
|
|
placement data so the picture is cluster-complete. ──────────────────────── #}
|
|
{% if swarm_services %}
|
|
<div class="card-flush" style="margin-bottom:1.5rem;">
|
|
<div style="display:flex;align-items:baseline;gap:0.6rem;padding:0.5rem 0.75rem;border-bottom:1px solid var(--border);">
|
|
<span style="font-weight:600;font-size:0.95rem;">Swarm services</span>
|
|
<a href="/plugins/docker/swarm" style="font-size:0.78rem;color:var(--text-muted);">Topology →</a>
|
|
</div>
|
|
<div style="padding:0.5rem 0.75rem;display:grid;gap:0.85rem;">
|
|
{% for s in swarm_services %}
|
|
<div>
|
|
<div style="display:flex;align-items:center;gap:0.55rem;flex-wrap:wrap;margin-bottom:0.35rem;">
|
|
<span class="dot {% if s.state == 'healthy' %}dot-up{% elif s.state == 'degraded' %}dot-warn{% else %}dot-down{% endif %}"></span>
|
|
<span style="font-weight:600;font-size:0.9rem;">{{ s.name }}</span>
|
|
<span style="font-size:0.72rem;color:var(--text-dim);">{{ s.mode }}</span>
|
|
<span style="font-size:0.78rem;color:{% if s.state == 'healthy' %}var(--green){% elif s.state == 'degraded' %}var(--orange){% else %}var(--red){% endif %};font-variant-numeric:tabular-nums;">
|
|
{{ s.running }}/{{ s.desired }} running
|
|
</span>
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:0.4rem;padding-left:1.1rem;">
|
|
{% for r in s.replicas %}
|
|
{% if r.ghost %}
|
|
<div style="display:flex;align-items:center;gap:0.4rem;font-size:0.8rem;background:var(--bg-elevated);border:1px dashed var(--border-mid);border-radius:5px;padding:0.3rem 0.55rem;" title="Running on a node with no Steward agent — detail unavailable">
|
|
<span class="dot dot-warn" style="opacity:0.6;"></span>
|
|
<span style="font-weight:500;">{{ r.host }}</span>
|
|
<span style="color:var(--text-dim);margin-left:auto;">{{ r.count }} · no agent</span>
|
|
</div>
|
|
{% else %}
|
|
<div style="display:flex;align-items:center;gap:0.4rem;font-size:0.8rem;background:var(--bg-elevated);border:1px solid var(--border);border-radius:5px;padding:0.3rem 0.55rem;">
|
|
<span class="dot {% if r.status == 'running' %}dot-up{% elif r.status == 'paused' %}dot-warn{% else %}dot-down{% endif %}"></span>
|
|
<a href="/plugins/docker/container/{{ r.host_id }}/{{ r.name }}" style="font-weight:500;color:inherit;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;" title="{{ r.name }}">{{ r.host }}</a>
|
|
{% if r.cpu_pct is not none %}<span style="color:var(--text-muted);font-family:ui-monospace,monospace;margin-left:auto;">{{ "%.0f" | format(r.cpu_pct) }}%</span>{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# ── Container tables, one per host ───────────────────────────────────────── #}
|
|
{% if host_groups %}
|
|
{% for g in host_groups %}
|
|
<div class="card-flush" style="margin-bottom:1.5rem;">
|
|
<div style="display:flex;align-items:baseline;gap:0.6rem;padding:0.5rem 0.75rem;border-bottom:1px solid var(--border);">
|
|
{% if g.host %}
|
|
<a href="/hosts/{{ g.host.id }}" style="font-weight:600;font-size:0.95rem;color:inherit;text-decoration:none;">{{ g.host.name }}</a>
|
|
{% else %}
|
|
<span style="font-weight:600;font-size:0.95rem;">{{ g.host_name }}</span>
|
|
{% endif %}
|
|
<span style="font-size:0.78rem;color:var(--text-muted);">
|
|
{{ g.running }} running{% if g.stopped %} · {{ g.stopped }} stopped{% endif %}
|
|
</span>
|
|
</div>
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Container</th>
|
|
<th>Image</th>
|
|
<th>Ports</th>
|
|
<th>CPU %</th>
|
|
<th style="min-width:100px;">CPU history</th>
|
|
<th>Mem %</th>
|
|
<th style="min-width:100px;">Mem history</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for sub in g.subgroups %}
|
|
{% if g.grouped and sub.label %}
|
|
<tr><td colspan="7" style="padding:0.4rem 0.75rem 0.2rem;font-size:0.72rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;font-weight:600;">{{ sub.label }}</td></tr>
|
|
{% endif %}
|
|
{% for item in sub.containers %}
|
|
{% set c = item.container %}
|
|
<tr>
|
|
<td>
|
|
<div style="display:flex;align-items:center;gap:0.5rem;">
|
|
<span class="dot {% if c.status == 'running' %}dot-up{% elif c.status == 'paused' %}dot-warn{% else %}dot-down{% endif %}"></span>
|
|
<div>
|
|
<div style="font-weight:500;font-size:0.9rem;">
|
|
<a href="/plugins/docker/container/{{ c.host_id }}/{{ c.name }}" style="color:inherit;text-decoration:none;border-bottom:1px dotted var(--border-mid);">{{ c.name }}</a>
|
|
{% if c.health == 'healthy' %}<span title="healthy" style="color:var(--green);font-size:0.7rem;">●</span>
|
|
{% elif c.health == 'unhealthy' %}<span title="unhealthy" style="color:var(--red);font-size:0.7rem;">●</span>
|
|
{% elif c.health == 'starting' %}<span title="health: starting" style="color:var(--orange);font-size:0.7rem;">◐</span>{% endif %}
|
|
</div>
|
|
<div style="font-size:0.73rem;color:var(--text-muted);">
|
|
{{ c.status }}{% if item.uptime %} · up {{ item.uptime }}{% endif %}
|
|
{% if c.status != 'running' and c.exit_code is not none and c.exit_code != 0 %}
|
|
· <span style="color:var(--red);">exit {{ c.exit_code }}{% if c.oom_killed %} (OOM){% endif %}</span>
|
|
{% endif %}
|
|
{% if c.restart_count %} · <span title="restart count" style="color:var(--orange);">⟳{{ c.restart_count }}</span>{% endif %}
|
|
</div>
|
|
{% if c.service_name or c.compose_project %}
|
|
<div style="font-size:0.68rem;color:var(--text-dim);margin-top:0.1rem;">
|
|
{{ c.service_name or c.compose_project }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td style="font-size:0.82rem;color:var(--text-muted);max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">
|
|
{{ c.image }}
|
|
</td>
|
|
<td style="font-size:0.78rem;font-family:ui-monospace,monospace;">
|
|
{% for p in item.ports %}
|
|
<div style="color:var(--text-muted);">{{ p.host_port }}:{{ p.container_port }}/{{ p.protocol }}</div>
|
|
{% endfor %}
|
|
</td>
|
|
<td style="font-family:ui-monospace,monospace;font-size:0.88rem;">
|
|
{% if c.cpu_pct is not none %}
|
|
<span style="color:{% if c.cpu_pct > 80 %}var(--red){% elif c.cpu_pct > 50 %}var(--orange){% else %}var(--text){% endif %};">
|
|
{{ "%.1f" | format(c.cpu_pct) }}%
|
|
</span>
|
|
{% else %}
|
|
<span style="color:var(--text-dim);">—</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ item.sparkline_cpu | safe }}</td>
|
|
<td style="font-family:ui-monospace,monospace;font-size:0.88rem;">
|
|
{% if c.mem_pct is not none %}
|
|
<span style="color:{% if c.mem_pct > 90 %}var(--red){% elif c.mem_pct > 70 %}var(--orange){% else %}var(--text){% endif %};">
|
|
{{ "%.1f" | format(c.mem_pct) }}%
|
|
</span>
|
|
{% else %}
|
|
<span style="color:var(--text-dim);">—</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ item.sparkline_mem | safe }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endfor %}
|
|
{% elif not swarm_services %}
|
|
<div class="card" style="text-align:center;padding:3rem;">
|
|
<div style="color:var(--text-muted);font-size:0.9rem;">
|
|
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 under that host.
|
|
</div>
|
|
</div>
|
|
{% endif %}
|