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
+36 -1
View File
@@ -201,7 +201,7 @@ WIDGET_REGISTRY: dict[str, dict] = {
"docker_resources": {
"key": "docker_resources",
"label": "Docker — Busiest containers",
"description": "The containers using the most CPU (cores, 5-minute average) and their memory",
"description": "The containers using the most CPU and memory, with bars of the last few hours",
"hx_url": "/plugins/docker/widget/resources",
"detail_url": "/plugins/docker/",
"plugin": "docker",
@@ -218,6 +218,41 @@ WIDGET_REGISTRY: dict[str, dict] = {
{"value": 20, "label": "20 containers"},
],
},
{
"key": "hours",
"label": "Bars cover",
"type": "select",
"default": 6,
"options": [
{"value": 1, "label": "Last hour"},
{"value": 6, "label": "Last 6 hours"},
{"value": 24, "label": "Last 24 hours"},
],
},
{
"key": "cpu_floor",
"label": "CPU bars scale to at least",
"type": "select",
"default": 1.0, # a float: the add form keeps fractions
"options": [
{"value": 0.5, "label": "0.5 cores"},
{"value": 1.0, "label": "1 core"},
{"value": 2.0, "label": "2 cores"},
{"value": 4.0, "label": "4 cores"},
],
},
{
"key": "mem_floor",
"label": "Memory bars scale to at least",
"type": "select",
"default": 1.0, # GiB
"options": [
{"value": 0.25, "label": "256 MiB"},
{"value": 1.0, "label": "1 GiB"},
{"value": 4.0, "label": "4 GiB"},
{"value": 16.0, "label": "16 GiB"},
],
},
],
},
"snmp_devices": {
+9 -1
View File
@@ -409,7 +409,15 @@ body.dash-editing .widget-drawer { transform:translateY(0); }
tone: the bar never stands without the figure it measures. */
.w-meter { background:linear-gradient(90deg, color-mix(in srgb, currentColor 20%, transparent) var(--pct, 0%), transparent var(--pct, 0%)); border-radius:3px; }
.w-note { font-size:0.7rem; color:var(--text-dim); margin-top:0.3rem; }
@container (max-width: 26rem) { .w-wide-only { display:none; } }
.w-table col.w-barcol { width:9.5rem; }
/* Bars of a figure's recent past, inline before it (Busiest Containers, #5214):
the row's tone, faint, with the newest bar solid. A gap is a slice with no
samples. Heights are set per bar as a % of the row's roof. */
.w-bars { display:inline-flex; align-items:flex-end; gap:1px; height:0.95rem; width:4.3rem; margin-right:0.45rem; vertical-align:-0.15rem; }
.w-bars i { flex:1; min-height:1px; background:currentColor; opacity:0.4; border-radius:1px 1px 0 0; }
.w-bars i:last-child { opacity:1; }
.w-bars i.gap { background:none; }
@container (max-width: 26rem) { .w-wide-only { display:none; } .w-table col.w-barcol { width:5.5rem; } }
/* Time range selector */