feat(monitors): unify ping/dns/http into one Monitor entity + custom targets
Collapse the three former check types into a single core `Monitor` entity
with one management surface (/monitors), one result table (monitor_results),
and a single scheduled task. Every type can now watch a free-standing custom
destination (optional host_id) — not just a registered Host.
- models: Monitor + MonitorResult replace PingResult/DnsResult; Host loses its
ping/dns facet columns (now Monitor rows linked by host_id).
- checks: monitors/{ping,dns,http}.py pure probes + runner.run_monitor
dispatcher; one monitor_check scheduler with a per-monitor due-filter.
- status: single monitor_status_source replaces the three sources.
- UI: /monitors blueprint (type-aware add/edit/list/widget); host hub shows a
host's linked monitors + "add monitor for this host"; nav + widget registry
+ alert metric catalog rewired. http plugin folded into core and removed.
- migration 0022 merges the http branch, data-migrates host facets +
http_monitors + all three result histories, drops the old tables/columns.
Resolves the per-host ping/dns auto-attach issue (#275): monitors are now
explicit, never auto-added to every host.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Jg27rgypiW2efULXJDtMC
This commit is contained in:
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import delete
|
||||
|
||||
from steward.models.monitors import DnsResult, PingResult
|
||||
from steward.models.monitors import MonitorResult
|
||||
from steward.models.metrics import PluginMetric
|
||||
from steward.models.ansible import AnsibleRun
|
||||
|
||||
@@ -23,8 +23,7 @@ async def run_cleanup(app: "Quart") -> None:
|
||||
async with app.db_sessionmaker() as session:
|
||||
async with session.begin():
|
||||
for model, ts_col in [
|
||||
(PingResult, PingResult.probed_at),
|
||||
(DnsResult, DnsResult.resolved_at),
|
||||
(MonitorResult, MonitorResult.checked_at),
|
||||
(PluginMetric, PluginMetric.recorded_at),
|
||||
(AnsibleRun, AnsibleRun.started_at),
|
||||
]:
|
||||
|
||||
+28
-33
@@ -12,9 +12,8 @@ from email.message import EmailMessage
|
||||
from sqlalchemy import and_, case, func, select
|
||||
|
||||
from steward.models.alerts import AlertRule, AlertState, AlertStateEnum, AlertEvent
|
||||
from steward.models.hosts import Host
|
||||
from steward.models.metrics import PluginMetric
|
||||
from steward.models.monitors import PingResult, PingStatus
|
||||
from steward.models.monitors import Monitor, MonitorResult
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,32 +27,28 @@ async def build_report_data(app) -> dict:
|
||||
cutoff_24h = now - timedelta(hours=24)
|
||||
|
||||
async with app.db_sessionmaker() as db:
|
||||
# ── Uptime summary (7d) ───────────────────────────────────────────────
|
||||
host_result = await db.execute(
|
||||
select(Host).where(Host.ping_enabled.is_(True)).order_by(Host.name)
|
||||
)
|
||||
hosts = host_result.scalars().all()
|
||||
# ── Uptime summary (7d) — per enabled monitor of any type ─────────────
|
||||
monitors = (await db.execute(
|
||||
select(Monitor).where(Monitor.enabled.is_(True)).order_by(Monitor.name)
|
||||
)).scalars().all()
|
||||
|
||||
uptime_rows = await db.execute(
|
||||
select(
|
||||
PingResult.host_id,
|
||||
func.count(PingResult.id).label("total"),
|
||||
func.sum(case((PingResult.status == PingStatus.up, 1), else_=0)).label("up"),
|
||||
MonitorResult.monitor_id,
|
||||
func.count(MonitorResult.id).label("total"),
|
||||
func.sum(case((MonitorResult.is_up.is_(True), 1), else_=0)).label("up"),
|
||||
)
|
||||
.where(PingResult.probed_at >= cutoff_7d)
|
||||
.group_by(PingResult.host_id)
|
||||
.where(MonitorResult.checked_at >= cutoff_7d)
|
||||
.group_by(MonitorResult.monitor_id)
|
||||
)
|
||||
uptime_map: dict[str, float | None] = {}
|
||||
for row in uptime_rows:
|
||||
pct = round(float(row.up) / float(row.total) * 100, 2) if row.total else None
|
||||
uptime_map[row.host_id] = pct
|
||||
uptime_map[row.monitor_id] = pct
|
||||
|
||||
uptime_summary = []
|
||||
for h in hosts:
|
||||
uptime_summary.append({
|
||||
"name": h.name,
|
||||
"pct_7d": uptime_map.get(h.id),
|
||||
})
|
||||
uptime_summary = [
|
||||
{"name": m.name, "pct_7d": uptime_map.get(m.id)} for m in monitors
|
||||
]
|
||||
|
||||
# ── Active alerts ─────────────────────────────────────────────────────
|
||||
active_result = await db.execute(
|
||||
@@ -112,27 +107,27 @@ async def build_report_data(app) -> dict:
|
||||
for row in top_routers_result
|
||||
]
|
||||
|
||||
# ── Hosts currently down ──────────────────────────────────────────────
|
||||
latest_ping_subq = (
|
||||
select(PingResult.host_id, func.max(PingResult.probed_at).label("max_at"))
|
||||
.group_by(PingResult.host_id)
|
||||
# ── Monitors currently down (latest result is_up = false) ─────────────
|
||||
latest_subq = (
|
||||
select(MonitorResult.monitor_id, func.max(MonitorResult.checked_at).label("max_at"))
|
||||
.group_by(MonitorResult.monitor_id)
|
||||
.subquery()
|
||||
)
|
||||
down_result = await db.execute(
|
||||
select(Host.name)
|
||||
.join(PingResult, PingResult.host_id == Host.id)
|
||||
select(Monitor.name)
|
||||
.join(MonitorResult, MonitorResult.monitor_id == Monitor.id)
|
||||
.join(
|
||||
latest_ping_subq,
|
||||
latest_subq,
|
||||
and_(
|
||||
PingResult.host_id == latest_ping_subq.c.host_id,
|
||||
PingResult.probed_at == latest_ping_subq.c.max_at,
|
||||
MonitorResult.monitor_id == latest_subq.c.monitor_id,
|
||||
MonitorResult.checked_at == latest_subq.c.max_at,
|
||||
),
|
||||
)
|
||||
.where(
|
||||
Host.ping_enabled.is_(True),
|
||||
PingResult.status == PingStatus.down,
|
||||
Monitor.enabled.is_(True),
|
||||
MonitorResult.is_up.is_(False),
|
||||
)
|
||||
.order_by(Host.name)
|
||||
.order_by(Monitor.name)
|
||||
)
|
||||
hosts_down = [row.name for row in down_result]
|
||||
|
||||
@@ -157,7 +152,7 @@ def _render_report_text(data: dict) -> str:
|
||||
# ── Hosts currently down ──────────────────────────────────────────────────
|
||||
down = data["hosts_down"]
|
||||
if down:
|
||||
lines.append(f"⚠ {len(down)} host(s) currently DOWN: {', '.join(down)}")
|
||||
lines.append(f"⚠ {len(down)} monitor(s) currently DOWN: {', '.join(down)}")
|
||||
lines.append("")
|
||||
|
||||
# ── Uptime summary ────────────────────────────────────────────────────────
|
||||
@@ -168,7 +163,7 @@ def _render_report_text(data: dict) -> str:
|
||||
pct_str = f"{pct:.2f}%" if pct is not None else "no data"
|
||||
lines.append(f" {entry['name']:<32} {pct_str}")
|
||||
if not data["uptime_summary"]:
|
||||
lines.append(" (no ping-enabled hosts)")
|
||||
lines.append(" (no monitors configured)")
|
||||
lines.append("")
|
||||
|
||||
# ── Active alerts ─────────────────────────────────────────────────────────
|
||||
|
||||
+74
-100
@@ -2,12 +2,11 @@
|
||||
"""Unified monitor-status aggregation.
|
||||
|
||||
A single readable "is everything up?" surface (the Status page + dashboard
|
||||
widget) is assembled from heterogeneous monitor types — core ping/DNS, plus
|
||||
any plugin that contributes (e.g. the http plugin). Rather than have the core
|
||||
status page import plugin tables (plugins are optional and loaded late), each
|
||||
monitor type registers a *status source*: an async callable(db) that returns a
|
||||
list of normalised StatusEntry objects. Core registers its ping/DNS sources in
|
||||
create_app; a plugin registers its own from setup().
|
||||
widget) is assembled from heterogeneous monitors. The core unified Monitor
|
||||
entity (ping/dns/http) contributes via `monitor_status_source`; plugins may
|
||||
register their own sources from setup(). Each source is an async callable(db)
|
||||
returning normalised StatusEntry objects so the Status page never imports
|
||||
plugin tables directly.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -26,8 +25,8 @@ HEARTBEAT_COUNT = 30 # number of recent checks shown in the heartbeat bar
|
||||
@dataclass
|
||||
class StatusEntry:
|
||||
"""One monitored thing, normalised across monitor types for display."""
|
||||
kind: str # "ping" | "dns" | "http" | ...
|
||||
key: str # unique within kind (host_id / monitor_id)
|
||||
kind: str # "icmp" | "tcp" | "dns" | "http" | ...
|
||||
key: str # unique within kind (monitor id)
|
||||
name: str
|
||||
target: str = "" # address / URL shown as subtitle
|
||||
status: str = "pending" # "up" | "down" | "pending"
|
||||
@@ -98,49 +97,52 @@ def sparkline_svg(values: list[float], width: int = 80, height: int = 20,
|
||||
)
|
||||
|
||||
|
||||
# ── Shared query helpers (host-keyed result tables: ping, dns) ────────────────
|
||||
# ── Unified monitor status source ─────────────────────────────────────────────
|
||||
|
||||
async def _last_n_by_host(db, model, ts_col, host_ids: list[str],
|
||||
async def _last_n_results(db, monitor_ids: list[str],
|
||||
n: int = HEARTBEAT_COUNT) -> dict[str, list]:
|
||||
"""Return {host_id: [rows]} of the last n results per host, oldest-first."""
|
||||
if not host_ids:
|
||||
"""Return {monitor_id: [MonitorResult]} of the last n results, oldest-first."""
|
||||
from steward.models.monitors import MonitorResult
|
||||
if not monitor_ids:
|
||||
return {}
|
||||
rn = func.row_number().over(
|
||||
partition_by=model.host_id, order_by=ts_col.desc()
|
||||
partition_by=MonitorResult.monitor_id, order_by=MonitorResult.checked_at.desc()
|
||||
).label("rn")
|
||||
subq = select(model.id, rn).where(model.host_id.in_(host_ids)).subquery()
|
||||
subq = select(MonitorResult.id, rn).where(
|
||||
MonitorResult.monitor_id.in_(monitor_ids)
|
||||
).subquery()
|
||||
res = await db.execute(
|
||||
select(model)
|
||||
.join(subq, model.id == subq.c.id)
|
||||
select(MonitorResult)
|
||||
.join(subq, MonitorResult.id == subq.c.id)
|
||||
.where(subq.c.rn <= n)
|
||||
.order_by(model.host_id, ts_col.asc())
|
||||
.order_by(MonitorResult.monitor_id, MonitorResult.checked_at.asc())
|
||||
)
|
||||
out: dict[str, list] = {hid: [] for hid in host_ids}
|
||||
out: dict[str, list] = {mid: [] for mid in monitor_ids}
|
||||
for row in res.scalars():
|
||||
out[row.host_id].append(row)
|
||||
out[row.monitor_id].append(row)
|
||||
return out
|
||||
|
||||
|
||||
async def _uptime_by_host(db, model, ts_col, status_col, up_value,
|
||||
host_ids: list[str]) -> dict[str, dict]:
|
||||
"""Per-host uptime % over 24h/7d/30d in one grouped query."""
|
||||
if not host_ids:
|
||||
async def _uptime_by_monitor(db, monitor_ids: list[str]) -> dict[str, dict]:
|
||||
"""Per-monitor uptime % over 24h/7d/30d in one grouped query."""
|
||||
from steward.models.monitors import MonitorResult
|
||||
if not monitor_ids:
|
||||
return {}
|
||||
now = datetime.now(timezone.utc)
|
||||
c30, c7, c24 = now - timedelta(days=30), now - timedelta(days=7), now - timedelta(hours=24)
|
||||
res = await db.execute(
|
||||
select(
|
||||
model.host_id,
|
||||
MonitorResult.monitor_id,
|
||||
func.count().label("total_30d"),
|
||||
func.sum(case((status_col == up_value, 1), else_=0)).label("up_30d"),
|
||||
func.sum(case((ts_col >= c7, 1), else_=0)).label("total_7d"),
|
||||
func.sum(case((and_(ts_col >= c7, status_col == up_value), 1), else_=0)).label("up_7d"),
|
||||
func.sum(case((ts_col >= c24, 1), else_=0)).label("total_24h"),
|
||||
func.sum(case((and_(ts_col >= c24, status_col == up_value), 1), else_=0)).label("up_24h"),
|
||||
func.sum(case((MonitorResult.is_up.is_(True), 1), else_=0)).label("up_30d"),
|
||||
func.sum(case((MonitorResult.checked_at >= c7, 1), else_=0)).label("total_7d"),
|
||||
func.sum(case((and_(MonitorResult.checked_at >= c7, MonitorResult.is_up.is_(True)), 1), else_=0)).label("up_7d"),
|
||||
func.sum(case((MonitorResult.checked_at >= c24, 1), else_=0)).label("total_24h"),
|
||||
func.sum(case((and_(MonitorResult.checked_at >= c24, MonitorResult.is_up.is_(True)), 1), else_=0)).label("up_24h"),
|
||||
)
|
||||
.where(model.host_id.in_(host_ids))
|
||||
.where(ts_col >= c30)
|
||||
.group_by(model.host_id)
|
||||
.where(MonitorResult.monitor_id.in_(monitor_ids))
|
||||
.where(MonitorResult.checked_at >= c30)
|
||||
.group_by(MonitorResult.monitor_id)
|
||||
)
|
||||
|
||||
def _pct(up, total):
|
||||
@@ -148,7 +150,7 @@ async def _uptime_by_host(db, model, ts_col, status_col, up_value,
|
||||
|
||||
out: dict[str, dict] = {}
|
||||
for r in res:
|
||||
out[r.host_id] = {
|
||||
out[r.monitor_id] = {
|
||||
"24h": _pct(r.up_24h, r.total_24h),
|
||||
"7d": _pct(r.up_7d, r.total_7d),
|
||||
"30d": _pct(r.up_30d, r.total_30d),
|
||||
@@ -156,78 +158,50 @@ async def _uptime_by_host(db, model, ts_col, status_col, up_value,
|
||||
return out
|
||||
|
||||
|
||||
# ── Core status sources: ping + DNS ───────────────────────────────────────────
|
||||
def _heartbeat_title(mtype: str, r) -> str:
|
||||
"""Type-aware tooltip for one heartbeat cell."""
|
||||
ts = f"{r.checked_at:%H:%M:%S} UTC"
|
||||
if not r.is_up:
|
||||
return f"{(r.error_msg or 'Down')} — {ts}"
|
||||
if mtype == "http":
|
||||
code = r.status_code or "—"
|
||||
ms = f"{r.response_ms:.0f} ms" if r.response_ms is not None else ""
|
||||
return f"{code} · {ms} — {ts}".replace(" · — ", " — ")
|
||||
if mtype == "dns":
|
||||
return f"{r.resolved_ip or 'resolved'} — {ts}"
|
||||
if r.response_ms is not None:
|
||||
return f"{r.response_ms:.0f} ms — {ts}"
|
||||
return f"Up — {ts}"
|
||||
|
||||
async def ping_status_source(db) -> list[StatusEntry]:
|
||||
from steward.models.hosts import Host
|
||||
from steward.models.monitors import PingResult, PingStatus
|
||||
|
||||
hosts = (await db.execute(
|
||||
select(Host).where(Host.ping_enabled.is_(True)).order_by(Host.name)
|
||||
)).scalars().all()
|
||||
ids = [h.id for h in hosts]
|
||||
recent = await _last_n_by_host(db, PingResult, PingResult.probed_at, ids)
|
||||
uptime = await _uptime_by_host(
|
||||
db, PingResult, PingResult.probed_at, PingResult.status, PingStatus.up, ids
|
||||
)
|
||||
async def monitor_status_source(db) -> list[StatusEntry]:
|
||||
"""Contribute every enabled Monitor (all types) to the Status page."""
|
||||
from steward.models.monitors import Monitor
|
||||
|
||||
monitors = list((await db.execute(
|
||||
select(Monitor).where(Monitor.enabled.is_(True)).order_by(Monitor.name)
|
||||
)).scalars())
|
||||
ids = [m.id for m in monitors]
|
||||
recent = await _last_n_results(db, ids)
|
||||
uptime = await _uptime_by_monitor(db, ids)
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
entries: list[StatusEntry] = []
|
||||
for h in hosts:
|
||||
rows = recent.get(h.id, [])
|
||||
for m in monitors:
|
||||
rows = recent.get(m.id, [])
|
||||
latest = rows[-1] if rows else None
|
||||
heartbeat = [{
|
||||
"state": "down" if r.status == PingStatus.down else "up",
|
||||
"title": (
|
||||
f"Down — {r.probed_at:%H:%M:%S} UTC" if r.status == PingStatus.down
|
||||
else f"{r.response_time_ms:.0f} ms — {r.probed_at:%H:%M:%S} UTC"
|
||||
if r.response_time_ms is not None else f"Up — {r.probed_at:%H:%M:%S} UTC"
|
||||
),
|
||||
} for r in rows]
|
||||
status = "pending" if latest is None else (
|
||||
"down" if latest.status == PingStatus.down else "up"
|
||||
)
|
||||
status = "pending" if latest is None else ("up" if latest.is_up else "down")
|
||||
tls_days = None
|
||||
if latest and latest.tls_expires_at:
|
||||
tls_days = (latest.tls_expires_at - now).total_seconds() / 86400.0
|
||||
entries.append(StatusEntry(
|
||||
kind="ping", key=h.id, name=h.name, target=h.address, status=status,
|
||||
last_checked=latest.probed_at if latest else None,
|
||||
uptime=uptime.get(h.id, {}), heartbeat=heartbeat,
|
||||
latency_ms=latest.response_time_ms if latest else None,
|
||||
spark=[r.response_time_ms for r in rows if r.response_time_ms is not None],
|
||||
detail_url="/ping/",
|
||||
))
|
||||
return entries
|
||||
|
||||
|
||||
async def dns_status_source(db) -> list[StatusEntry]:
|
||||
from steward.models.hosts import Host
|
||||
from steward.models.monitors import DnsResult, DnsStatus
|
||||
|
||||
hosts = (await db.execute(
|
||||
select(Host).where(Host.dns_enabled.is_(True)).order_by(Host.name)
|
||||
)).scalars().all()
|
||||
ids = [h.id for h in hosts]
|
||||
recent = await _last_n_by_host(db, DnsResult, DnsResult.resolved_at, ids)
|
||||
uptime = await _uptime_by_host(
|
||||
db, DnsResult, DnsResult.resolved_at, DnsResult.status, DnsStatus.resolved, ids
|
||||
)
|
||||
|
||||
entries: list[StatusEntry] = []
|
||||
for h in hosts:
|
||||
rows = recent.get(h.id, [])
|
||||
latest = rows[-1] if rows else None
|
||||
heartbeat = [{
|
||||
"state": "up" if r.status == DnsStatus.resolved else "down",
|
||||
"title": (
|
||||
f"{r.resolved_ip} — {r.resolved_at:%H:%M:%S} UTC" if r.status == DnsStatus.resolved
|
||||
else f"Failed — {r.resolved_at:%H:%M:%S} UTC"
|
||||
),
|
||||
} for r in rows]
|
||||
status = "pending" if latest is None else (
|
||||
"up" if latest.status == DnsStatus.resolved else "down"
|
||||
)
|
||||
entries.append(StatusEntry(
|
||||
kind="dns", key=h.id, name=h.name, target=h.address, status=status,
|
||||
last_checked=latest.resolved_at if latest else None,
|
||||
uptime=uptime.get(h.id, {}), heartbeat=heartbeat,
|
||||
detail_url="/dns/",
|
||||
kind=m.type, key=m.id, name=m.name, target=m.target, status=status,
|
||||
last_checked=latest.checked_at if latest else None,
|
||||
uptime=uptime.get(m.id, {}),
|
||||
heartbeat=[{"state": "up" if r.is_up else "down",
|
||||
"title": _heartbeat_title(m.type, r)} for r in rows],
|
||||
latency_ms=latest.response_ms if latest else None,
|
||||
spark=[r.response_ms for r in rows if r.response_ms is not None],
|
||||
tls_days=tls_days, detail_url="/monitors/",
|
||||
))
|
||||
return entries
|
||||
|
||||
+43
-50
@@ -12,15 +12,50 @@
|
||||
from __future__ import annotations
|
||||
|
||||
WIDGET_REGISTRY: dict[str, dict] = {
|
||||
"ping": {
|
||||
"key": "ping",
|
||||
"label": "Ping",
|
||||
"description": "Live ping status and latency history for all monitored hosts",
|
||||
"hx_url": "/ping/rows",
|
||||
"detail_url": "/ping/",
|
||||
"monitors": {
|
||||
"key": "monitors",
|
||||
"label": "Monitors",
|
||||
"description": "Uptime checks of every type — ping, DNS, and HTTP — with up/down status and latency",
|
||||
"hx_url": "/monitors/widget",
|
||||
"detail_url": "/monitors/",
|
||||
"plugin": None,
|
||||
"poll": True,
|
||||
"params": [],
|
||||
"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",
|
||||
@@ -52,16 +87,6 @@ WIDGET_REGISTRY: dict[str, dict] = {
|
||||
"poll": False,
|
||||
"params": [],
|
||||
},
|
||||
"dns": {
|
||||
"key": "dns",
|
||||
"label": "DNS",
|
||||
"description": "DNS resolution status for all monitored hosts",
|
||||
"hx_url": "/dns/rows",
|
||||
"detail_url": "/dns/",
|
||||
"plugin": None,
|
||||
"poll": True,
|
||||
"params": [],
|
||||
},
|
||||
"traefik_routers": {
|
||||
"key": "traefik_routers",
|
||||
"label": "Traefik — Routers",
|
||||
@@ -203,38 +228,6 @@ WIDGET_REGISTRY: dict[str, dict] = {
|
||||
},
|
||||
],
|
||||
},
|
||||
"http_monitors": {
|
||||
"key": "http_monitors",
|
||||
"label": "HTTP Monitors",
|
||||
"description": "Synthetic endpoint checks — up/down status and response times",
|
||||
"hx_url": "/plugins/http/widget",
|
||||
"detail_url": "/plugins/http/",
|
||||
"plugin": "http",
|
||||
"poll": True,
|
||||
"params": [
|
||||
{
|
||||
"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"},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"docker_resources": {
|
||||
"key": "docker_resources",
|
||||
"label": "Docker — Resources",
|
||||
@@ -312,7 +305,7 @@ WIDGET_REGISTRY: dict[str, dict] = {
|
||||
# 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 = {"ping", "dns", "status_overview", "uptime_summary", "hosts_overview"}
|
||||
_CORE_WIDGETS = {"monitors", "status_overview", "uptime_summary", "hosts_overview"}
|
||||
_INTEGRATION_PLUGINS = {"traefik", "unifi"}
|
||||
GROUP_LABELS = {
|
||||
"core": "Core monitors",
|
||||
|
||||
Reference in New Issue
Block a user