The Monitors widget opens with a verdict ("1 of 5 monitors down."), then one
row per monitor under a single header: latency and uptime over 24h, 7d and
30d, down monitors first with why and since when, the name linking to the
monitor. Counts follow the type filter. Problems always show; the limit
trims only the healthy rest ("+N more up").
Uptime / SLA (uptime_summary) repeated those numbers per host; migration
0033 moves dashboards onto Monitors. Shared widget styles (.w-verdict,
.w-table, .w-sub, .w-more) carry the widget-shape ruling for the widgets
that follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
346 lines
12 KiB
Python
346 lines
12 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 (cores, 5-minute average) and their memory",
|
|
"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"},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
"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
|