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
+39
View File
@@ -88,6 +88,45 @@ async def test_a_quiet_page_says_nothing_waits(app):
assert "Nothing to review" in page and "within 14 days" in page
async def test_a_page_narrowed_to_one_resource_says_so_and_a_review_returns_to_it(app):
here = "/alerts/anomalies?source=host_agent&resource=nas"
async with app.test_request_context(here):
session["user_id"], session["user_role"] = "u1", "operator"
page = await render_template("alerts/anomalies.html", review_days=14, past=[],
to_review=[{"v": _view(), "opens_group": True, "group": "Today"}],
source="host_agent", resource="nas", here=here)
assert "<strong>nas</strong>" in page and '<a href="/alerts/anomalies">Show every anomaly</a>' in page
assert 'name="next" value="/alerts/anomalies?source=host_agent&amp;resource=nas"' in page
async def _card(app, anomalies, base_resource="nas"):
async with app.test_request_context("/hosts/h1"):
session["user_id"], session["user_role"] = "u1", "operator"
return await render_template("_anomaly_card.html", anomalies=anomalies,
back="/hosts/h1", base_resource=base_resource)
async def test_a_resources_card_is_one_line_counting_what_waits_with_a_way_to_review(app):
ended = NOW - timedelta(minutes=5)
page = await _card(app, [_view(ended_at=ended), _view(id="a2", metric_name="load_pct", ended_at=ended),
_view(id="a3", review="normal", ended_at=ended)])
assert "<strong>2</strong> to review" in page
assert 'href="/alerts/anomalies?source=host_agent&amp;resource=nas"' in page
assert "This is normal" not in page and "Out of its normal now" not in page # reviewed there
async def test_a_card_names_what_is_out_of_its_normal_now(app):
page = await _card(app, [_view(resource_name="nas:/var", metric_name="disk_used_pct")])
assert "Out of its normal now" in page and "nas:/var" in page and "problem-card" in page
async def test_a_card_with_nothing_waiting_or_going_is_not_shown(app):
reviewed = _view(review="normal", ended_at=NOW - timedelta(minutes=5))
assert "resource-anomalies" not in await _card(app, [reviewed])
going_but_accepted = _view(review="normal") # the new normal being learned
assert "resource-anomalies" not in await _card(app, [going_but_accepted])
async def _widget(app, logged_in, anomalies, to_review):
async with app.test_request_context("/status/widget"):
if logged_in:
+1 -1
View File
@@ -70,7 +70,7 @@ async def test_the_agent_card_keeps_its_actions_under_manage(app):
app, "host_agent", "panel.html", host=host, reg=reg, reporting=True, stale=False,
target=target, ansible_available=True, managed_key_set=True)
manage = page.index(">Manage agent</summary>")
assert page.index("Full metrics →") < manage
assert "Full metrics" not in page # it heads the vitals strip, at the top of the page
for action in (">Update agent</button>", ">Rotate token</button>", ">Unregister agent</button>"):
assert page.index(action) > manage
assert ">Remove</button>" not in page
+8 -2
View File
@@ -31,8 +31,8 @@ async def _vitals(app, *, cpu=40.0, mem=50.0, stale=False, seen_min=1):
async with app.test_request_context("/plugins/host_agent/vitals/h1"):
page = await render_plugin_template(
app, "host_agent", "_host_vitals.html", reporting=True, stale=stale,
reg=SimpleNamespace(last_seen_at=datetime.now(timezone.utc) - timedelta(minutes=seen_min),
agent_version="1.9.0"),
reg=SimpleNamespace(host_id="h1", agent_version="1.9.0",
last_seen_at=datetime.now(timezone.utc) - timedelta(minutes=seen_min)),
cpu=cpu, mem=mem, disk_root=30.0, load_per_core=20,
sparks={"cpu": "", "mem": "", "disk": "", "load": ""},
psi={"cpu": None, "mem": None, "io": None}, worst=worst_reading(readings, TH))
@@ -53,6 +53,12 @@ async def test_a_quiet_host_says_so(app):
assert "Every reading is within its thresholds." in await _vitals(app)
async def test_the_strip_at_the_top_of_the_page_leads_to_the_full_metrics(app):
"""The temperatures, charts and every mount are on the full-metrics page;
its link sat in the Agent card below the fold and was missed."""
assert 'href="/plugins/host_agent/h1/"' in await _vitals(app)
async def test_a_stale_host_says_since_when_before_any_reading(app):
page = await _vitals(app, mem=93.0, stale=True, seen_min=20)
assert '<span class="badge badge-warn">Stale</span>' in page