feat(docker): Busiest Containers shows bars of the last hours beside each figure, scaled to the row's peak with a floor (#5214)
CI / lint (push) Successful in 4s
CI / integration (push) Failing after 59s
CI / unit (push) Failing after 1m27s
CI / publish (push) Skipped

The fill bars were a share of the host's capacity, a sliver on hosts with
80 cores. Each figure now sits beside 24 bars of the last 6 hours (1h/24h
per widget), each row scaled to its own peak but never below 1 core or
1 GiB (configurable per widget), so a 0.05-core container reads flat and
a bar's height means real load. One grouped date_bin query feeds them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 15:05:06 -04:00
co-authored by Claude Opus 5.5
parent 97c864bf42
commit 05eaca3f2f
7 changed files with 242 additions and 36 deletions
+35 -9
View File
@@ -269,10 +269,16 @@ CPU_AVERAGE_MINUTES = 5
async def widget_resources():
"""HTMX dashboard widget: the containers using the most CPU, averaged over
the last few minutes so the order holds still between polls, with their
CPU in cores and memory in bytes."""
limit = max(1, min(20, int(request.args.get("limit", 10) or 10)))
CPU in cores and memory in bytes, each beside bars of the last few hours
scaled to the row's own peak (never below a floor, #5214)."""
limit = max(1, min(20, _arg_number("limit", 10, int)))
hours = max(1, min(24, _arg_number("hours", 6, int)))
cpu_floor = max(0.1, _arg_number("cpu_floor", 1.0, float))
mem_floor = max(0.1, _arg_number("mem_floor", 1.0, float))
widget_id = request.args.get("wid", "0")
since = datetime.now(timezone.utc) - timedelta(minutes=CPU_AVERAGE_MINUTES)
now = datetime.now(timezone.utc)
since = now - timedelta(minutes=CPU_AVERAGE_MINUTES)
slots, width = docker_svc.bar_slots(now, hours)
async with current_app.db_sessionmaker() as db:
# Dedup before the limit, else swarm tasks reported by both managers can
@@ -284,28 +290,48 @@ async def widget_resources():
.where(DockerMetric.scraped_at >= since)
.group_by(DockerMetric.host_id, DockerMetric.container_name)
)).all()}
hosts = await docker_svc.host_map(db, {c.host_id for c in running})
capacity = await docker_svc.host_capacity(db, {c.host_id for c in running})
def cpu_of(c):
v = average.get((c.host_id, c.name))
return c.cpu_pct if v is None else float(v)
def cpu_of(c):
v = average.get((c.host_id, c.name))
return c.cpu_pct if v is None else float(v)
busiest = sorted(running, key=lambda c: (-(cpu_of(c) or 0), c.name))[:limit]
history = await docker_svc.recent_usage(
db, {(c.host_id, c.name) for c in busiest}, slots[0], width)
hosts = await docker_svc.host_map(db, {c.host_id for c in busiest})
capacity = await docker_svc.host_capacity(db, {c.host_id for c in busiest})
def bars(c, i, scale, floor):
seen = history.get((c.host_id, c.name), {})
values = [None if seen.get(t) is None or seen[t][i] is None else seen[t][i] * scale
for t in slots]
return docker_svc.scaled_bars(values, floor)
busiest = sorted(running, key=lambda c: (-(cpu_of(c) or 0), c.name))[:limit]
rows_data = [{
"c": c,
"label": docker_svc.display_name(c),
"host_name": hosts[c.host_id].name if c.host_id in hosts else c.host_id,
"usage": docker_svc.usage(c, *capacity.get(c.host_id, (None, None)), cpu_pct=cpu_of(c)),
"cpu_bars": bars(c, 0, 0.01, cpu_floor), # cpu_pct → cores
"mem_bars": bars(c, 1, 1.0, mem_floor * 2**30), # bytes; floor in GiB
} for c in busiest]
return await render_template(
"docker/widget_resources.html",
rows=rows_data,
minutes=CPU_AVERAGE_MINUTES,
hours=hours, cpu_floor=cpu_floor, mem_floor=mem_floor * 2**30,
widget_id=widget_id,
)
def _arg_number(key, default, kind):
"""A numeric query argument, or `default` when missing or not a number."""
try:
return kind(request.args.get(key, default))
except (TypeError, ValueError):
return default
@docker_bp.get("/host/<host_id>")
@require_role(UserRole.viewer)
async def host_panel(host_id: str):