Files
FabledSteward/steward/templates/_anomaly.html
T
bvandeusenandClaude Opus 5.5 521d84f318
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m29s
CI / integration (push) Successful in 1m37s
CI / publish (push) Successful in 1m9s
feat(syslog): lines beside the readings: around an anomaly, in get_anomaly, and marked on host and SNMP charts (#6002)
get_anomaly's circumstances gain the host's or SNMP device's syslog
warnings and worse (mcp_events / mcp_resource_events). An anomaly row opens
"Syslog around it", read only when opened. A shared Chart.js plugin
(chart-marks.js) draws a dashed line at each syslog error on the host
metrics charts and an SNMP device's charts, with a line saying how many.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 06:36:31 -04:00

82 lines
4.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{# 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. #}
{% 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 "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 }}&amp;resource={{ a.resource_name|urlencode }}&amp;since={{ v.last_span.started_at.isoformat()|urlencode }}{% if v.last_span.ended_at %}&amp;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 %}