feat(alerts): pick the host, then its part, and see what the metric reads (#5475)
CI / lint (push) Successful in 2s
CI / integration (push) Successful in 1m8s
CI / unit (push) Failing after 1m29s
CI / publish (push) Skipped

#5475 "Rule form: pick the host, then its part; metrics that part reports;
a link to groups; a preview of the metric", from the operator's review of
the New rule page: hosts and their parts shared one long picker.

- Host agent rules pick a Host, then a Part: "The whole host", then its
  mounts, CPU cores, disks, interfaces and sensors under a heading each,
  cores in number order. A part that reports nothing a rule can watch
  (an interface's traffic) isn't offered.
- The Metric list holds what the picked resource reports, in catalog order.
- Under it, a preview: the reading now, and the last 24 h low to high with
  an hourly sparkline drawn from zero (UI ruling 8), read from one series.
- The group picker links to Ansible > Inventory to make a group, in a new
  tab, and re-reads the groups when the tab comes back into view.
- Picking a host or part swaps the pickers alone (/alerts/api/rule_target);
  picking a metric swaps the preview (/alerts/api/metric_preview).
- A rule's own resource and metric stay on offer after they go quiet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 09:54:08 -04:00
co-authored by Claude Opus 5.5
parent d5601575d8
commit d19df51ffb
12 changed files with 453 additions and 73 deletions
+67
View File
@@ -0,0 +1,67 @@
"""Integration: the rule form's host and part pickers and the metric preview
(#5475), through their routes against Postgres. A host's parts come grouped
under it, and a picked metric shows what it reads now and over the last day."""
from __future__ import annotations
import asyncio
import os
import time
import uuid
from datetime import datetime, timedelta, timezone
import pytest
pytestmark = pytest.mark.integration
@pytest.fixture
def app():
if not os.environ.get("STEWARD_DATABASE_URL"):
pytest.skip("needs Postgres")
from steward.app import create_app
return create_app(testing=False)
def test_the_pickers_and_the_preview_read_what_the_host_reports(app):
from steward.core.alerts import record_metrics
from steward.models.metrics import PluginMetric
from steward.models.users import User, UserRole
async def _go():
tag = uuid.uuid4().hex[:8]
host = f"rf-{tag}"
now = datetime.now(timezone.utc)
async with app.db_sessionmaker() as s:
async with s.begin():
user = User(username=f"rf-{tag}", email=f"rf-{tag}@test", password_hash="x",
role=UserRole.operator, is_active=True)
s.add(user)
# Disk use climbing over the last few hours, then cpu and a mount.
await record_metrics(s, [
PluginMetric(source_module="host_agent", resource_name=f"{host}:/data",
metric_name="disk_used_pct", value=v, recorded_at=now - timedelta(hours=h))
for h, v in ((5, 61.0), (3, 70.0), (1, 82.0))])
await record_metrics(s, [
PluginMetric(source_module="host_agent", resource_name=host,
metric_name="cpu_pct", value=12.0, recorded_at=now)])
uid = user.id
client = app.test_client()
async with client.session_transaction() as sess:
sess.update(user_id=uid, user_role="operator", username=f"rf-{tag}",
_csrf="test-token", login_at=time.time())
resp = await client.get("/alerts/api/rule_target", query_string={
"source_module": "host_agent", "pick_host": host})
page = " ".join((await resp.get_data(as_text=True)).split())
assert resp.status_code == 200
assert f'<option value="{host}" selected>The whole host</option>' in page
assert f'<optgroup label="Mounts"> <option value="{host}:/data" >/data</option>' in page
resp = await client.get("/alerts/api/metric_preview", query_string={
"source_module": "host_agent", "pick_host": host, "resource_name": f"{host}:/data",
"metric_name": "disk_used_pct"})
page = " ".join((await resp.get_data(as_text=True)).split())
assert "Now <strong>82%</strong>" in page
assert "last 24 h</span> 61% – 82%" in page and "<polyline" in page
asyncio.run(_go())