Files
FabledSteward/steward/core/widgets.py
T
bvandeusenandClaude Opus 5.5 05eaca3f2f
CI / lint (push) Successful in 4s
CI / integration (push) Failing after 59s
CI / unit (push) Failing after 1m27s
CI / publish (push) Skipped
feat(docker): Busiest Containers shows bars of the last hours beside each figure, scaled to the row's peak with a floor (#5214)
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>
2026-10-07 15:05:06 -04:00

381 lines
14 KiB
Python

# steward/core/widgets.py
#
# Central widget registry. Each entry describes one widget type that can be
# placed on a dashboard. Plugin widgets declare which plugin must be enabled.
#
# "params" is a list of configurable parameters for the widget. Each param:
# key - form field name (stored in config_json)
# label - human-readable label
# type - "select" (only type currently supported)
# default - default value (int or str determines how it's parsed from the form)
# options - list of {value, label} dicts
#
# "poll" refreshes the cell every monitor poll interval; "poll_seconds"
# overrides that cadence for a widget whose data moves slowly.
from __future__ import annotations
WIDGET_REGISTRY: dict[str, dict] = {
"monitors": {
"key": "monitors",
"label": "Monitors",
"description": "Every uptime check, problems first: latency and uptime over 24h, 7d and 30d",
"hx_url": "/monitors/widget",
"detail_url": "/monitors/",
"plugin": None,
"poll": True,
"params": [
{
"key": "type_filter",
"label": "Type",
"type": "select",
"default": "all",
"options": [
{"value": "all", "label": "All types"},
{"value": "icmp", "label": "Ping (ICMP)"},
{"value": "tcp", "label": "Ping (TCP)"},
{"value": "dns", "label": "DNS"},
{"value": "http", "label": "HTTP"},
],
},
{
"key": "show_down_only",
"label": "Display filter",
"type": "select",
"default": "no",
"options": [
{"value": "no", "label": "All monitors"},
{"value": "yes", "label": "Failing only"},
],
},
{
"key": "limit",
"label": "Max shown",
"type": "select",
"default": 10,
"options": [
{"value": 5, "label": "5 monitors"},
{"value": 10, "label": "10 monitors"},
{"value": 20, "label": "20 monitors"},
],
},
],
},
"status_overview": {
"key": "status_overview",
"label": "Overview",
"description": "What needs attention across Steward: hosts, monitors and firing alerts, problems listed with since-times",
"hx_url": "/status/widget",
"detail_url": "/status/",
"plugin": None,
"poll": True,
"params": [],
},
"hosts_overview": {
"key": "hosts_overview",
"label": "Hosts — Overview",
"description": "One row per host: monitor status, uptime, and agent CPU, memory, disk and load, linking to the host page",
"hx_url": "/hosts/overview/widget",
"detail_url": "/hosts/",
"plugin": None,
"poll": True,
"params": [],
},
"traefik_routers": {
"key": "traefik_routers",
"label": "Traefik — Routers",
"description": "Top routers by request rate with error highlights and expiring certs",
"hx_url": "/plugins/traefik/widget",
"detail_url": "/plugins/traefik/",
"plugin": "traefik",
"poll": True,
"params": [],
},
"traefik_access_log": {
"key": "traefik_access_log",
"label": "Traefik — Access Log",
"description": "Traffic origin summary — top IPs, countries, and request distribution",
"hx_url": "/plugins/traefik/widget/access_log",
"detail_url": "/plugins/traefik/",
"plugin": "traefik",
"poll": True,
"params": [
{
"key": "ip_filter",
"label": "IP Filter",
"type": "select",
"default": "all",
"options": [
{"value": "all", "label": "All traffic"},
{"value": "internal", "label": "Internal only"},
{"value": "external", "label": "External only"},
],
},
{
"key": "limit",
"label": "Top IPs shown",
"type": "select",
"default": 10,
"options": [
{"value": 10, "label": "10 entries"},
{"value": 20, "label": "20 entries"},
{"value": 50, "label": "50 entries"},
],
},
],
},
"traefik_request_chart": {
"key": "traefik_request_chart",
"label": "Traefik — Request Chart",
"description": "Request rate and error percentage over time (Chart.js)",
"hx_url": "/plugins/traefik/widget/request_chart",
"detail_url": "/plugins/traefik/",
"plugin": "traefik",
"poll": True,
"params": [
{
"key": "hours",
"label": "Time range",
"type": "select",
"default": 6,
"options": [
{"value": 1, "label": "Last 1 hour"},
{"value": 6, "label": "Last 6 hours"},
{"value": 24, "label": "Last 24 hours"},
],
},
],
},
"unifi_overview": {
"key": "unifi_overview",
"label": "UniFi — Overview",
"description": "WAN status, client counts, offline devices, and active alarms",
"hx_url": "/plugins/unifi/widget",
"detail_url": "/plugins/unifi/",
"plugin": "unifi",
"poll": True,
"params": [],
},
"unifi_clients": {
"key": "unifi_clients",
"label": "UniFi — Top Clients",
"description": "Active wireless and wired clients sorted by bandwidth",
"hx_url": "/plugins/unifi/widget/clients",
"detail_url": "/plugins/unifi/",
"plugin": "unifi",
"poll": True,
"params": [
{
"key": "limit",
"label": "Clients shown",
"type": "select",
"default": 5,
"options": [
{"value": 5, "label": "5 clients"},
{"value": 10, "label": "10 clients"},
{"value": 20, "label": "20 clients"},
],
},
],
},
"docker_containers": {
"key": "docker_containers",
"label": "Docker — Services & failures",
"description": "What Docker is running and what failed: swarm services and containers, problems first, with the last 24h's failures",
"hx_url": "/plugins/docker/widget",
"detail_url": "/plugins/docker/",
"plugin": "docker",
"poll": True,
"params": [
{
"key": "show_stopped",
"label": "Show stopped",
"type": "select",
"default": "no",
"options": [
{"value": "no", "label": "Running only"},
{"value": "yes", "label": "All containers"},
],
},
],
},
"docker_resources": {
"key": "docker_resources",
"label": "Docker — Busiest containers",
"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",
"poll": True,
"params": [
{
"key": "limit",
"label": "Containers shown",
"type": "select",
"default": 10,
"options": [
{"value": 5, "label": "5 containers"},
{"value": 10, "label": "10 containers"},
{"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": {
"key": "snmp_devices",
"label": "SNMP — Devices",
"description": "Latest OID readings for all configured SNMP devices",
"hx_url": "/plugins/snmp/widget",
"detail_url": "/plugins/snmp/",
"plugin": "snmp",
"poll": True,
"params": [],
},
"host_resource_history": {
"key": "host_resource_history",
"label": "Host Agent — History Graph",
"description": "CPU / memory / disk time-series graph for one host (the host-view chart, on your dashboard)",
"hx_url": "/plugins/host_agent/widget/history",
"detail_url": "/hosts/",
"plugin": "host_agent",
"poll": True,
"params": [
# type "host" → the edit form renders a live dropdown of hosts.
{
"key": "host_id",
"label": "Host",
"type": "host",
"default": "",
},
{
"key": "hours",
"label": "Time range",
"type": "select",
"default": 6,
"options": [
{"value": 1, "label": "Last 1 hour"},
{"value": 6, "label": "Last 6 hours"},
{"value": 24, "label": "Last 24 hours"},
],
},
],
},
}
# Widgets that were merged into another (#5213): migration 0032 moved every
# placement of the old key onto the new one, so nothing here renders them.
RETIRED = {
"host_resources": "hosts_overview", # Hosts — Overview carries cpu/mem/disk/load per host
"unifi_devices": "unifi_overview", # UniFi — Overview lists the offline devices
"uptime_summary": "monitors", # Monitors shows 24h/7d/30d uptime per monitor (#5250)
}
# The recommended dashboard (#5213): what needs attention first, then the
# fleet, then each integration. (key, width, height, config) on the 12-column
# grid, one cell 70px tall; a widget whose plugin is off is skipped and the
# rest close up. Seeded into the first dashboard and offered by
# "Reset to the recommended layout" in edit mode.
RECOMMENDED: list[tuple[str, int, int, dict]] = [
("status_overview", 12, 3, {}),
("hosts_overview", 6, 7, {}),
("monitors", 6, 7, {"show_down_only": "no", "limit": 10}),
("docker_containers", 6, 6, {}),
("docker_resources", 6, 6, {"limit": 10}),
("traefik_routers", 6, 5, {}),
("unifi_overview", 6, 4, {}),
("snmp_devices", 6, 4, {}),
("traefik_request_chart", 12, 4, {}),
]
def recommended_layout(plugins_cfg: dict) -> list[dict]:
"""RECOMMENDED placed on the grid for what is enabled: left to right,
a new row when the next one does not fit, each row as tall as its
tallest widget. [{key, x, y, w, h, config}]."""
available = {w["key"] for w in get_available_widgets(plugins_cfg)}
out, x, y, row_h = [], 0, 0, 0
for key, w, h, config in RECOMMENDED:
if key not in available:
continue
if x + w > 12:
x, y, row_h = 0, y + row_h, 0
out.append({"key": key, "x": x, "y": y, "w": w, "h": h, "config": config})
x, row_h = x + w, max(row_h, h)
return out
# Group each widget for the picker (Core monitors / Monitoring capabilities /
# Integrations), mirroring the Settings → Plugins taxonomy. Capability plugins
# are host/monitoring facets; traefik/unifi are discrete vendor integrations.
_CORE_WIDGETS = {"monitors", "status_overview", "hosts_overview"}
_INTEGRATION_PLUGINS = {"traefik", "unifi"}
GROUP_LABELS = {
"core": "Core monitors",
"capability": "Monitoring capabilities",
"integration": "Integrations",
}
GROUP_ORDER = ["core", "capability", "integration"]
def _group_for(w: dict) -> str:
if w["key"] in _CORE_WIDGETS:
return "core"
if w.get("plugin") in _INTEGRATION_PLUGINS:
return "integration"
return "capability"
for _w in WIDGET_REGISTRY.values():
_w["group"] = _group_for(_w)
def register_widget(definition: dict) -> None:
"""Register a widget at runtime (used by external plugins)."""
definition.setdefault("group", _group_for(definition))
WIDGET_REGISTRY[definition["key"]] = definition
def get_available_widgets(plugins_cfg: dict) -> list[dict]:
"""Return widgets whose plugin dependency is satisfied."""
result = []
for w in WIDGET_REGISTRY.values():
if w["plugin"] is None:
result.append(w)
elif plugins_cfg.get(w["plugin"], {}).get("enabled", False):
result.append(w)
return result