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 -1
View File
@@ -12,6 +12,7 @@ from datetime import datetime, timedelta
from sqlalchemy import select
from steward.mcp.tools.tickets import open_tickets_for
from steward.ansible.runner import trigger_run
from steward.ansible.sources import BUILTIN_SOURCE_NAME
from steward.mcp._context import current_caller, mcp_app
@@ -390,7 +391,8 @@ async def get_container(container: str, host: str = "", range: str = "24h", even
still resolves by its exact name, or by a part of it that matches one
removed container, while Steward retains its logs or events: the reply
has `removed: true`, `last_seen_at`, its history and its events, and no
live state. A live container has `removed: false`.
live state. A live container has `removed: false`. `open_tickets` are the
open tickets about it (list_tickets).
Memory (`mem_used_bytes`, `mem_used_pct`, of its limit or of the host's
RAM when it has none) is what its processes hold: anon + shmem.
@@ -413,6 +415,8 @@ async def get_container(container: str, host: str = "", range: str = "24h", even
mem = await docker_svc.container_points(db, c.host_id, c.name, "mem_used_pct", since)
evs = await docker_svc.container_events(db, c.host_id, c.name, events)
sick = await docker_svc.unhealthy_since(db, [(c.host_id, c.name)])
tickets = await open_tickets_for(
db, subject=docker_svc.container_resource(host_names.get(c.host_id, c.host_id), c.name))
hour_ago = now - timedelta(hours=1)
recent = sum(1 for e in failures if e.at >= hour_ago)
try:
@@ -433,6 +437,7 @@ async def get_container(container: str, host: str = "", range: str = "24h", even
"cpu_pct_history": summarize(cpu),
"mem_used_pct_history": summarize(mem),
"events": _event_rows(evs, now),
"open_tickets": tickets,
"next": f'search_container_logs(container="{c.name}", host="{c.host_id}")',
}
@@ -66,6 +66,8 @@
{# ── Its CPU or memory out of its normal for the hour, and past anomalies (#5742) #}
{% with back=anomaly_back, base_resource=anomaly_resource %}{% include "_anomaly_card.html" %}{% endwith %}
{# ── Its open tickets ──────────────────────────────────────────────────────── #}
<div hx-get="/tickets/card?subject={{ anomaly_resource|urlencode }}&amp;back={{ anomaly_back|urlencode }}" hx-trigger="load" hx-swap="outerHTML"></div>
{# ── What it is set up with, and what it moved since it started ─────────── #}
<div class="card">