# 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