feat(tickets): open, comment and resolve tickets from the UI and MCP; known issues on alerts and anomalies (#6351, #6352)
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m37s
CI / integration (push) Successful in 1m51s
CI / publish (push) Successful in 1m16s

A ticket records a known problem: title, area, hosts, subject, description,
links to alert rules and anomalies, and a log of comments, edits, resolving
and reopening. Opening, resolving (with the resolution as the note) and
reopening go on the timeline; every step is audited, and the audit rows stay
off the timeline.

An alert or anomaly an open ticket covers (by link, by subject, or a
host-wide ticket on the host itself) shows a Known issue badge, and offers
"Open a ticket" prefilled from its rule or anomaly when none does. Hosts,
monitors and containers get a Known issues card.

MCP: list_tickets, get_ticket (viewer); open_ticket, update_ticket,
comment_ticket, resolve_ticket, reopen_ticket (operator). list_alerts and
list_anomalies carry known_issue; get_overview, get_host, get_monitor and
get_container carry open_tickets. Also update_change and delete_change for
changes recorded by hand (author or admin).

Tickets are left out of backups, like the timeline (2026-10-15 ruling).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-24 22:06:43 -04:00
co-authored by Claude Opus 5.5
parent 1ea86c0330
commit 2a561210c3
40 changed files with 1786 additions and 35 deletions
+7 -1
View File
@@ -17,7 +17,7 @@ What the operator ruled (System 182 "Metric Norms and Anomalies"):
"""
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from sqlalchemy import ARRAY, String, bindparam, case, delete, func, or_, select, text, update
@@ -276,6 +276,8 @@ class AnomalyView:
anomaly: MetricAnomaly
last_span: MetricAnomalySpan | None
href: str | None = None
# Open tickets that make it a known issue (tickets.service.known_issues).
tickets: list = field(default_factory=list)
@property
def ongoing(self) -> bool:
@@ -317,6 +319,10 @@ async def views(session, rows: list[MetricAnomaly], *, links: bool = True) -> li
if links and key not in hrefs:
hrefs[key] = await resource_url_for(session, *key)
out.append(AnomalyView(a, latest.get(a.id), hrefs.get(key)))
from steward.tickets.service import known_issues
known = await known_issues(session, {a.id: ([("anomaly", a.id)], a.resource_name) for a in rows})
for v in out:
v.tickets = known.get(v.anomaly.id, [])
return out
+5
View File
@@ -215,6 +215,11 @@ STATES: dict[str, tuple[str, str]] = {
"normal": ("ok", "Marked normal"),
"real": ("crit", "Real problem"),
"lapsed": ("dim", "Not reviewed"),
# A ticket (steward.tickets): open is a known problem, so amber; an
# alert or anomaly a ticket covers is a known issue, drawn blue like an
# acknowledged alert.
"open": ("warn", "Open"),
"known_issue": ("info", "Known issue"),
}