feat(ui): a host's "Full metrics" heads its page, and a resource's anomalies are one line with a way to review them (#6797)
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 1m38s
CI / integration (push) Successful in 2m0s
CI / publish (push) Successful in 1m10s

The host page's link to the full metrics sat in the Agent card, under the
anomaly card and the vitals strip, and was missed. It now heads the vitals
strip at the top of the page.

The anomaly card on the host, monitor, container and Traefik pages listed
every anomaly of the last 30 days with its review form, reviewed ones
included, and took over the top of the page. It is now one line: how many
wait for review, which are out of their normal now, and a Review button to
Alerts → Anomalies narrowed to that resource (?source=&resource=), where a
review returns. It is hidden when nothing waits and nothing is going on.
The callers fetch up to 100 rows so the count isn't capped at 10 or 20.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-03 14:09:18 -05:00
co-authored by Claude Opus 5.5
parent 12a5f83c67
commit c38fd68e48
13 changed files with 108 additions and 28 deletions
+13 -4
View File
@@ -284,15 +284,24 @@ async def alert_history():
@require_role(UserRole.viewer)
async def anomalies():
"""Metric anomalies (#5740): those to review, newest occurrence first,
then the reviewed and lapsed ones under day dividers."""
then the reviewed and lapsed ones under day dividers. `source` and
`resource` narrow it to one resource's, its parts included: a resource's
page links here with them."""
from urllib.parse import urlencode
from steward.audit.service import mark_day_groups
from steward.core import anomalies as anom
from steward.core import formatting as fmt
source = request.args.get("source", "").strip() or None
resource = request.args.get("resource", "").strip() or None
only = {"source": source, "resource": resource}
async with current_app.db_sessionmaker() as db:
to_review = await anom.views(db, await anom.list_anomalies(db, (anom.TO_REVIEW,)))
to_review = await anom.views(db, await anom.list_anomalies(db, (anom.TO_REVIEW,), **only))
past = await anom.views(db, await anom.list_anomalies(
db, (anom.NORMAL, anom.REAL, anom.LAPSED), limit=PAGE_SIZE * 4))
db, (anom.NORMAL, anom.REAL, anom.LAPSED), limit=PAGE_SIZE * 4, **only))
days = int(await get_setting(db, "anomalies.review_days") or 14)
here = "/alerts/anomalies"
if source or resource:
here += "?" + urlencode({k: v for k, v in only.items() if v})
tz = fmt.zone(session.get("tz"))
to_review = [{"v": v} for v in to_review]
past = [{"v": v} for v in past]
@@ -300,7 +309,7 @@ async def anomalies():
mark_day_groups(to_review, tz, at=at)
mark_day_groups(past, tz, at=at)
return await render_template("alerts/anomalies.html", to_review=to_review, past=past,
review_days=days)
review_days=days, source=source, resource=resource, here=here)
@alerts_bp.post("/anomalies/<anomaly_id>/review")