Files
FabledSteward/steward/templates/_anomaly.html
T
bvandeusenandClaude Opus 5.5 5569f9934e
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 1m24s
CI / unit (push) Successful in 1m37s
CI / publish (push) Skipped
feat(anomalies): a metric out of its normal is an anomaly to review (#5740)
#5740 "A metric that departs from its normal is recorded as an anomaly to
review, and is findable after it ends", milestone 513.

- Every minute each learned key's last 15 minutes is judged against its
  hour's normal: above the usual hold x1.25 and by the metric's floor opens
  an occurrence, which ends when the level comes back.
- metric_anomalies holds one row per item to review; a repeat while it
  still waits joins it as an occurrence (metric_anomaly_spans).
- An ended row nobody reviews within anomalies.review_days (14, Settings ->
  Thresholds) lapses; an ongoing one never does.
- Learning skips every hour an occurrence covers unless its anomaly was
  marked normal: no normal is learned without review.
- Alerts -> Anomalies lists those to review first, then the reviewed and
  lapsed under day dividers; marking is operator/admin and audited.
- The Overview widget and get_overview warn of open ones and count those
  to review; the host page shows its own; the weekly report lists the
  week's and the lapsed; MCP list_anomalies and review_anomaly.
- Reviewed and lapsed rows are pruned at data.retention_days, and kept
  in backups like alert history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:41:57 -04:00

73 lines
3.7 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>
</div>
{%- endmacro %}