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
+6 -2
View File
@@ -15,6 +15,7 @@ from steward.models.users import UserRole
from ._shape import (
age_s, bounded, clamp_limit, db_session, iso, lookback, pick, rnd, summarize,
)
from .tickets import open_tickets_for
AGENT_SOURCE = "host_agent"
@@ -141,8 +142,9 @@ async def get_host(host: str, parts_limit: int = 20) -> dict:
Returns the host's monitors (latest result, error, uptime), the agent's
latest values, alerts currently pending, firing or acknowledged on this
host's resources, and the Ansible inventory target linked to it with its
groups (`ansible_target`: null when unlinked).
host's resources, its open tickets (`open_tickets`: known problems someone
recorded, list_tickets / get_ticket), and the Ansible inventory target
linked to it with its groups (`ansible_target`: null when unlinked).
The agent's values are `agent.metrics` (host-level) and its sub-resources
grouped by kind: `mounts` (fullest first), `interfaces` and `disk_io`
@@ -166,6 +168,7 @@ async def get_host(host: str, parts_limit: int = 20) -> dict:
select(AnsibleTarget).where(AnsibleTarget.host_id == h.id)
.options(selectinload(AnsibleTarget.groups))
)).scalar_one_or_none()
tickets = await open_tickets_for(db, host=h.name)
agent = None
if samples:
@@ -218,6 +221,7 @@ async def get_host(host: str, parts_limit: int = 20) -> dict:
],
"agent": agent,
"active_alerts": alerts,
"open_tickets": tickets,
"ansible_target": {
"target_id": target.id,
"target_name": target.name,