- Overview: a verdict line when something is wrong ('1 alert firing, 1
monitor down.'); its picker description now says what it lists (alerts,
monitors down, anomalies). Host figures have their home in Hosts.
Comments no longer name the Status page that was folded into Monitors.
- Monitors widget: a monitor not checked yet says when it was added.
- Host page: filesystems fullest first, by the worse of space and inodes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
396 lines
14 KiB
Python
396 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", or "host" for a live list of hosts
|
|
# options_from - async (db) -> options, read when the picker opens (a plugin's
|
|
# targets, its templates); "empty" is said when it returns none
|
|
# 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: firing alerts, monitors down and metric anomalies, each with since when",
|
|
"hx_url": "/status/widget",
|
|
"detail_url": "/monitors/",
|
|
"plugin": None,
|
|
"poll": True,
|
|
"params": [],
|
|
},
|
|
"alerts": {
|
|
"key": "alerts",
|
|
"label": "Alerts",
|
|
"description": "What is firing or silent, its reading and since when, then what someone has acknowledged; says when alert email is failing",
|
|
"hx_url": "/alerts/widget",
|
|
"detail_url": "/alerts/",
|
|
"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,
|
|
"poll_seconds": 300, # buckets are minutes wide; each refresh redraws the chart
|
|
"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 (the
|
|
# Overview, and beside it the Alerts, #5510), 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", 6, 4, {}),
|
|
("alerts", 6, 4, {}),
|
|
("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/prometheus are discrete integrations.
|
|
_CORE_WIDGETS = {"monitors", "status_overview", "alerts", "hosts_overview"}
|
|
_INTEGRATION_PLUGINS = {"traefik", "unifi", "prometheus"}
|
|
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
|