feat(tickets): open, comment and resolve tickets from the UI and MCP; known issues on alerts and anomalies (#6351, #6352)
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:
@@ -0,0 +1,62 @@
|
||||
{# tickets/list.html — known problems, open first (tickets.service): each with
|
||||
what it is about, its area, who opened it and when, and how many comments
|
||||
it has; resolved ones say who resolved them and how. #}
|
||||
{% extends "base.html" %}
|
||||
{% from "_macros.html" import page_header, empty_state, status_badge %}
|
||||
{% block title %}Tickets — Steward{% endblock %}
|
||||
{% block content %}
|
||||
{% call page_header("Tickets", "Known problems, open until someone resolves them. Opening and resolving one goes on the timeline.") %}
|
||||
{% if session.user_role in ('operator', 'admin') %}<a class="btn btn-sm" href="/tickets/new{% if host %}?host={{ host|urlencode }}{% endif %}">Open a ticket</a>{% endif %}
|
||||
{% endcall %}
|
||||
|
||||
<p class="page-verdict">
|
||||
{%- if open_count %}<strong class="tone-warn">{{ open_count }} open</strong>{% else %}<strong class="tone-ok">No open tickets</strong>{% endif %}
|
||||
{%- if host %} · on <a href="/timeline/?host={{ host|urlencode }}">{{ host }}</a> · <a href="/tickets/?status={{ status }}">every host</a>{% endif %}
|
||||
</p>
|
||||
|
||||
<nav class="filter-bar" aria-label="Which tickets">
|
||||
{% for key, label in (("open", "Open"), ("resolved", "Resolved"), ("all", "All")) %}
|
||||
<a class="btn btn-sm {{ 'btn-ghost' if status != key }}"{% if status == key %} aria-current="page"{% endif %}
|
||||
href="/tickets/?status={{ key }}{% if host %}&host={{ host|urlencode }}{% endif %}">{{ label }}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
|
||||
{% if tickets %}
|
||||
<div class="card-flush">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ticket</th>
|
||||
<th>Where</th>
|
||||
<th>Area</th>
|
||||
<th>Opened ({{ tz_label() }})</th>
|
||||
<th>State</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in tickets %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/tickets/{{ t.id }}"><span class="tone-dim">#{{ t.id }}</span> {{ t.title }}</a>
|
||||
{%- if counts.get(t.id) %}<div class="field-hint">{{ counts[t.id] }} comment{{ "s" if counts[t.id] != 1 }}</div>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{%- for h in t.hosts %}<a href="/tickets/?host={{ h|urlencode }}&status={{ status }}">{{ h }}</a>{{ ", " if not loop.last }}{% endfor -%}
|
||||
{%- if t.subject %}{% if t.hosts %}<br>{% endif %}<span class="field-hint">{{ t.subject }}</span>{% endif -%}
|
||||
{%- if not t.hosts and not t.subject %}<span class="tone-dim">—</span>{% endif -%}
|
||||
</td>
|
||||
<td class="nowrap"><span class="chip">{{ labels.get(t.area, t.area) }}</span></td>
|
||||
<td class="nowrap">{{ t.opened_at|when }}<div class="field-hint">{{ t.opened_by }}</div></td>
|
||||
<td>
|
||||
{{ status_badge(t.status) }}
|
||||
{%- if t.status == 'resolved' %}<div class="field-hint">{{ t.resolved_by }}, {{ t.resolved_at|when }}</div>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card">{{ empty_state({"open": "No open tickets.", "resolved": "No ticket has been resolved yet.", "all": "No tickets yet."}[status]) }}</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user