Files
FabledSteward/plugins/docker/templates/docker/index.html
T
bvandeusenandClaude Opus 5.5 8566c8f78e
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 54s
CI / unit (push) Successful in 1m21s
CI / publish (push) Skipped
feat(docker): Docker disk is recorded over time per host, the Disk page shows each host's growth and folds images under their repository, and the overview has one disk line per host (#5304–#5306)
- #5304 "Docker disk totals recorded over time per host, with a growth chart and alertable metrics"
- #5305 "Disk page: images grouped by repository, versions folded under it"
- #5306 "Docker overview: one disk line per host linking to the Disk page"

Each `docker system df` report is now also recorded as metrics, with the host
as the resource: disk_total_bytes, its parts, and disk_reclaimable_bytes. An
alert rule can watch them. The Disk page starts with the hosts side by side,
showing how much each grew over the selected range with a line of it, then
gives each host its own section. An image's versions fold under its
repository, which is charged only for the layers its versions don't share.

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

82 lines
4.2 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.
{% extends "base.html" %}
{% from "_macros.html" import page_header %}
{# The Docker page. The filter bar sits outside the polled fragment, so a poll
never resets what is being typed; the fragment reads the bar on every poll,
and a change re-fetches it and puts the filters in the URL (#5299). #}
{% block title %}Docker — Steward{% endblock %}
{% block content %}
{% call page_header("Docker", "What needs attention first, then every service and container.") %}
{% if has_swarm %}<a class="btn btn-sm btn-ghost" href="/plugins/docker/swarm">Swarm</a>{% endif %}
{% if has_disk %}<a class="btn btn-sm btn-ghost" href="/plugins/docker/disk">Disk</a>{% endif %}
{% endcall %}
<form id="docker-filters" class="filter-bar" method="get" action="/plugins/docker/" role="search">
<input type="search" id="docker-q" name="q" value="{{ f.q }}" class="input"
placeholder="Search name, image or stack" aria-label="Search services and containers" autocomplete="off">
{% if choices.stacks %}
<select name="stack" aria-label="Stack">
<option value="">All stacks</option>
{% for s in choices.stacks %}<option value="{{ s }}"{% if f.stack == s %} selected{% endif %}>{{ s }}</option>{% endfor %}
<option value="{{ no_stack }}"{% if f.stack == no_stack %} selected{% endif %}>No stack</option>
</select>
{% endif %}
{% if choices.hosts|length > 1 %}
<select name="host" aria-label="Host or node">
<option value="">All hosts</option>
{% for h in choices.hosts %}<option value="{{ h }}"{% if f.host == h %} selected{% endif %}>{{ h }}</option>{% endfor %}
</select>
{% endif %}
<select name="state" aria-label="Status">
<option value="">Any status</option>
{% for key, label in states.items() %}<option value="{{ key }}"{% if f.state == key %} selected{% endif %}>{{ label }}</option>{% endfor %}
</select>
{% if f.sort != 'status' %}<input type="hidden" name="sort" value="{{ f.sort }}"><input type="hidden" name="dir" value="{{ f.dir }}">{% endif %}
<noscript><button type="submit" class="btn btn-sm">Filter</button></noscript>
</form>
<div id="docker-rows"
hx-get="/plugins/docker/rows"
hx-include="#docker-filters"
hx-trigger="load, every {{ poll_interval }}s, change from:#docker-filters, input changed delay:250ms from:#docker-q, search from:#docker-q"
hx-swap="innerHTML">
<div class="card"><p class="field-hint">Loading…</p></div>
</div>
{% if disk %}
{# What Docker keeps on each host's disk, and its growth over a week (#5306). #}
<p class="docker-disk-line">
<a href="/plugins/docker/disk" class="section-title">Disk</a>
{% for d in disk %}
<span class="docker-disk-host"><a href="/plugins/docker/disk#host-{{ d.host_id }}">{{ d.host_name }}</a>
{{ d.total|bytes }}
{%- if d.growth and d.growth.change %} <span class="tone-{{ 'warn' if d.growth.pct is not none and d.growth.pct >= 10 else 'muted' }}"
title="Since {{ d.growth.since.strftime('%Y-%m-%d %H:%M UTC') }}">{{ "+" if d.growth.change > 0 else "−" }}{{ (d.growth.change|abs)|bytes }} in 7d</span>{% endif -%}
{%- if d.reclaimable %} <span class="tone-dim">· {{ d.reclaimable|bytes }} reclaimable</span>{% endif %}</span>
{% endfor %}
</p>
{% endif %}
<script>
(function () {
var rows = document.getElementById('docker-rows');
var form = document.getElementById('docker-filters');
// The filters live in the URL too, so a filtered view can be bookmarked and
// Back returns to it.
function keepInUrl() {
var q = new URLSearchParams(new FormData(form));
Array.from(q.keys()).forEach(function (k) { if (!q.get(k)) q.delete(k); });
var s = q.toString();
history.replaceState(null, '', location.pathname + (s ? '?' + s : '') + location.hash);
}
form.addEventListener('change', keepInUrl);
form.addEventListener('input', keepInUrl);
// A link to a section (the dashboard's "failed (24h)" → #failures) lands
// before the rows exist; scroll to it once, when they first arrive.
if (!location.hash || !rows) return;
rows.addEventListener('htmx:afterSwap', function once() {
rows.removeEventListener('htmx:afterSwap', once);
var target = document.getElementById(location.hash.slice(1));
if (target) target.scrollIntoView({ block: 'start' });
});
})();
</script>
{% endblock %}