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>
89 lines
5.0 KiB
HTML
89 lines
5.0 KiB
HTML
{# Metric anomalies (#5740), drawn the same on Alerts → Anomalies, a resource's
|
||
own page and the overview widget. Import with:
|
||
{% from "_anomaly.html" import anomaly_row, anomaly_when with context %}
|
||
(with context: the review buttons read session.user_role). #}
|
||
{% from "_macros.html" import status_badge %}
|
||
|
||
{# When it happened: "since …" while going, "from – to" once ended; a repeat
|
||
says how often and when the latest was. #}
|
||
{% macro anomaly_when(v) -%}
|
||
{%- set a = v.anomaly -%}
|
||
{%- if a.occurrences > 1 -%}
|
||
{{ a.occurrences }} times since {{ a.started_at|when }}, last
|
||
{% if v.ongoing %}since {{ a.last_started_at|when }}, ongoing{% else %}{{ a.last_started_at|when }} – {{ a.ended_at|when }}{% endif %}
|
||
{%- elif v.ongoing -%}
|
||
Ongoing since {{ a.started_at|when }}
|
||
{%- else -%}
|
||
{{ a.started_at|when }} – {{ a.ended_at|when }}
|
||
{%- endif -%}
|
||
{%- endmacro %}
|
||
|
||
{# Its highest level against what is usual for that hour. #}
|
||
{% macro anomaly_level(a) -%}
|
||
Held {{ metric_value(a.metric_name, a.peak_level) }}; usual for that hour
|
||
{{ metric_value(a.metric_name, a.usual_hold) }}, peaking at {{ metric_value(a.metric_name, a.usual_peak) }}
|
||
{%- endmacro %}
|
||
|
||
{# The review buttons, or how it was reviewed. `back` is the page to come
|
||
back to after marking. #}
|
||
{% macro anomaly_review(a, back=none) -%}
|
||
{%- set can_review = session.user_role in ('operator', 'admin') -%}
|
||
{%- if a.review == "normal" -%}
|
||
<span>Marked normal by {{ a.reviewed_by or "someone" }}, {{ a.reviewed_at|when }}.</span>
|
||
{%- elif a.review == "real" -%}
|
||
<span>Real problem, marked by {{ a.reviewed_by or "someone" }}, {{ a.reviewed_at|when }}{% if a.note %}: {{ a.note }}{% else %}.{% endif %}</span>
|
||
{%- elif a.review == "lapsed" -%}
|
||
<span>Not reviewed within the review window; lapsed {{ a.reviewed_at|when }}. Its hours are not learned.</span>
|
||
{%- endif -%}
|
||
{%- if can_review and a.review in ("to_review", "lapsed") %}
|
||
<form method="post" action="/alerts/anomalies/{{ a.id }}/review"
|
||
style="display:flex;gap:0.4rem;flex-wrap:wrap;align-items:center;margin-top:0.35rem;">
|
||
{{ csrf_field() }}
|
||
{% if back %}<input type="hidden" name="next" value="{{ back }}">{% endif %}
|
||
<input class="input" name="note" placeholder="Note (optional)" aria-label="Note"
|
||
style="max-width:220px;font-size:0.8rem;padding:0.25rem 0.5rem;">
|
||
<button class="btn btn-ghost btn-sm" name="review" value="normal"
|
||
title="Its hours are learned as normal for that time">This is normal</button>
|
||
<button class="btn btn-sm" name="review" value="real"
|
||
title="Kept out of what is learned">Real problem</button>
|
||
</form>
|
||
{%- endif -%}
|
||
{%- endmacro %}
|
||
|
||
{# One anomaly as a row: badge, resource and metric, when, level, review, and
|
||
the open tickets that make it a known issue (or a link to open one). #}
|
||
{% macro anomaly_row(v, back=none, show_resource=true) -%}
|
||
{%- set a = v.anomaly -%}
|
||
<div class="anomaly-row" style="padding:0.55rem 0;border-bottom:1px solid var(--border);">
|
||
<div style="display:flex;gap:0.5rem;align-items:baseline;flex-wrap:wrap;">
|
||
{{ status_badge("anomaly") }}
|
||
{% if a.review != "to_review" %}{{ status_badge(a.review) }}{% endif %}
|
||
{% if show_resource %}
|
||
{% if v.link %}<a href="{{ v.link }}" style="font-weight:500;">{{ a.resource_name }}</a>
|
||
{% else %}<span style="font-weight:500;">{{ a.resource_name }}</span>{% endif %}
|
||
<span style="color:var(--text-dim);font-size:0.8rem;">{{ source_label(a.source_module) }}</span>
|
||
{% endif %}
|
||
<span>{{ metric_label(a.metric_name) }}</span>
|
||
</div>
|
||
<div style="font-size:0.8rem;color:var(--text-muted);margin-top:0.2rem;">
|
||
{{ anomaly_when(v) }} · {{ anomaly_level(a) }}
|
||
</div>
|
||
<div style="font-size:0.8rem;color:var(--text-muted);margin-top:0.2rem;">{{ anomaly_review(a, back) }}</div>
|
||
{%- if v.tickets %}
|
||
<div style="font-size:0.8rem;margin-top:0.2rem;">{{ status_badge("known_issue") }}
|
||
{% for t in v.tickets %}<a href="/tickets/{{ t.id }}">#{{ t.id }} {{ t.title }}</a>{{ ", " if not loop.last }}{% endfor %}</div>
|
||
{%- elif session.user_role in ('operator', 'admin') and a.review in ('to_review', 'real') %}
|
||
<div style="font-size:0.8rem;margin-top:0.2rem;"><a href="/tickets/new?anomaly={{ a.id|urlencode }}{% if back %}&back={{ back|urlencode }}{% endif %}">Open a ticket</a></div>
|
||
{%- endif %}
|
||
{%- if "syslog" in enabled_plugins and v.last_span %}
|
||
{#- The device's own log around its latest occurrence (#6002), read only when opened. #}
|
||
<details style="font-size:0.8rem;margin-top:0.3rem;"
|
||
hx-get="/plugins/syslog/around?source={{ a.source_module|urlencode }}&resource={{ a.resource_name|urlencode }}&since={{ v.last_span.started_at.isoformat()|urlencode }}{% if v.last_span.ended_at %}&until={{ v.last_span.ended_at.isoformat()|urlencode }}{% endif %}"
|
||
hx-trigger="toggle once" hx-target="find .anomaly-syslog" hx-swap="innerHTML">
|
||
<summary style="cursor:pointer;color:var(--text-muted);">Syslog around it</summary>
|
||
<div class="anomaly-syslog"><p class="field-hint" style="margin:0.3rem 0 0;">Reading…</p></div>
|
||
</details>
|
||
{%- endif %}
|
||
</div>
|
||
{%- endmacro %}
|