feat(ui): audit, alert rules and the rule form say things in words (#5212)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 51s
CI / unit (push) Failing after 1m14s
CI / publish (push) Skipped

#5212 "Plain labels: no raw ids, scopes, metric keys or unexplained jargon",
batch 1:

- Audit log: each action reads as a sentence ("paused monitor"), with the
  code as its tooltip. Detail shows flags as yes/no, lists joined, a run's
  scope by name, spans as spans, and no ids the entity column already gives.
  The filter is now a picker of areas.
- Alert rules, history and Status say the condition in words: "is down",
  "Fullest disk >= 85%". Readings use metric_value, and history times use
  |ago.
- Rule form: sources and metrics by name, the threshold's unit, "fires
  when it is down/up" for an up/down metric instead of 0.5, and a metric
  reference built from the catalog.
- Schedules and Maintenance: viewers no longer see Run now, Pause, Edit,
  Delete or New. Intervals and next runs read as "every 2h" and "in 3h";
  a scope reads "Every alert on Host agent".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 00:23:02 -04:00
co-authored by Claude Opus 5.5
parent 7275fad767
commit 3a772716ae
21 changed files with 487 additions and 156 deletions
+82
View File
@@ -184,3 +184,85 @@ def metric_value(name: str, value: Any) -> str:
if n.endswith("_days"):
return f"{value:.0f} days"
return f"{value:g}"
# What each alertable metric is called on a page, and what it measures. The
# key is the name a source records; a name not listed reads as itself with
# its underscores spaced (SNMP's OID labels are the operator's own words).
METRICS: dict[str, tuple[str, str]] = {
"is_up": ("Up or down", "whether the check passed"),
"is_healthy": ("Health check", "the container's own HEALTHCHECK result"),
"response_ms": ("Response time", "how long the check took"),
"open_fds": ("Open files", "files Steward itself has open"),
"open_fds_pct": ("Open files (% of limit)", "files Steward has open, against its limit"),
"request_rate": ("Requests per second", "requests through this router"),
"error_rate_4xx_pct": ("4xx responses", "share of responses that were client errors"),
"error_rate_5xx_pct": ("5xx responses", "share of responses that were server errors"),
"latency_p50_ms": ("Response time (median)", "half of requests were faster than this"),
"latency_p95_ms": ("Response time (95th percentile)", "95% of requests were faster than this"),
"latency_p99_ms": ("Response time (99th percentile)", "99% of requests were faster than this"),
"response_bytes_rate": ("Response throughput", "bytes per second sent back"),
"cert_expiry_days": ("Certificate expires in", "days until the TLS certificate expires"),
"latency_ms": ("WAN latency", "round trip from the gateway"),
"total_clients": ("Connected clients", "devices on the network"),
"cpu_pct": ("CPU", "CPU in use; for a container, 100% is one core"),
"mem_pct": ("Memory (% of limit)", "memory in use, against the container's limit or the host's RAM"),
"restart_count": ("Restarts", "times the container has restarted, in total"),
"mem_used_pct": ("Memory used", "memory in use"),
"mem_available_bytes": ("Memory available", "memory free for new work"),
"swap_used_bytes": ("Swap used", "memory paged out to disk"),
"disk_used_pct_worst": ("Fullest disk", "the most-full mount on the host"),
"disk_used_pct": ("Disk used", "one mount (resource host:mount)"),
"load_1m": ("Load (1 min)", "runnable processes, averaged over a minute"),
"load_5m": ("Load (5 min)", "runnable processes, averaged over 5 minutes"),
"load_15m": ("Load (15 min)", "runnable processes, averaged over 15 minutes"),
"uptime_secs": ("Uptime", "time since the host booted"),
"seconds_since_seen": ("Time since the agent reported", "over 3 minutes means the agent is offline"),
}
# Where a rule's metric comes from, as the picker and the rules list say it.
SOURCES: dict[str, str] = {
"icmp": "Ping monitors", "tcp": "TCP monitors", "dns": "DNS monitors",
"http": "HTTP monitors", "steward": "Steward itself", "host_agent": "Host agent",
"docker": "Docker containers", "traefik": "Traefik routers", "unifi": "UniFi",
"snmp": "SNMP devices",
}
def metric_label(name: str) -> str:
entry = METRICS.get(name or "")
return entry[0] if entry else (name or "").replace("_", " ")
def metric_about(name: str) -> str:
entry = METRICS.get(name or "")
return entry[1] if entry else ""
def source_label(source: str) -> str:
return SOURCES.get(source or "", (source or "").replace("_", " "))
def metric_unit(name: str) -> str:
"""The unit a threshold for this metric is written in, from the same
suffixes metric_value reads; "" for a count or a plain number."""
n = name or ""
if n in ("is_up", "is_healthy"):
return ""
if n.endswith("_pct") or "_pct_" in n:
return "%"
if n.endswith("_ms"):
return "ms"
if n.endswith("_bytes"):
return "bytes"
if n.endswith(("_bps", "bytes_rate")):
return "bytes/s"
if n.endswith("_secs") or n.startswith("seconds_"):
return "seconds"
if n.endswith("_c"):
return "°C"
if n.endswith("_days"):
return "days"
if n == "request_rate":
return "req/s"
return ""