#5211 "Lists load as you scroll: audit, alert history, Ansible runs" (milestone 477), per preference 172. - core/paging.py: keyset paging over (time, id), newest first, with an opaque "<iso>|<id>" cursor; a page reads one row past itself to know whether another follows. No OFFSET, so rows added while someone scrolls never shift the pages after them. - _macros.html scroll_more / scroll_end and static/scroll-more.js, written once: an IntersectionObserver with a 600px margin fetches the next page before the reader reaches the end; "Loading more…" while in flight, "End of the list." at the end, and a failed page says so with a Retry button instead of re-firing on every scroll. - Audit log: no "Load more" link and no 500-row cap. - Alert history: no 200-row stop with "pick a shorter range"; alert_svc.history takes the cursor, so the MCP tool and the page share one query. - Ansible runs: no silent cap of 50. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
94 lines
4.7 KiB
Python
94 lines
4.7 KiB
Python
"""Integration: /alerts/history (#5066) against real Postgres. A viewer can
|
|
read it; it shows the transition, value against the condition, delivery
|
|
outcome with its error, and a maintenance mute; ?rule= narrows it to one
|
|
rule, an HTMX request gets only the rows, and ?before= the next page."""
|
|
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_a_viewer_reads_the_alert_history(app):
|
|
from steward.core.alerts import MUTED
|
|
from steward.models.alerts import AlertEvent, AlertOperator, AlertRule
|
|
from steward.models.users import User, UserRole
|
|
|
|
async def _go():
|
|
tag = uuid.uuid4().hex[:8]
|
|
uid = str(uuid.uuid4())
|
|
now = datetime.now(timezone.utc)
|
|
async with app.db_sessionmaker() as s:
|
|
async with s.begin():
|
|
s.add(User(id=uid, username=f"hist-{tag}", email=f"hist-{tag}@test",
|
|
password_hash="x", role=UserRole.viewer, is_active=True))
|
|
await s.flush()
|
|
hot = AlertRule(name=f"hot {tag}", source_module="host_agent", resource_name=f"box-{tag}",
|
|
metric_name="cpu_pct", operator=AlertOperator(">"), threshold=90,
|
|
created_by=uid)
|
|
cold = AlertRule(name=f"cold {tag}", source_module="host_agent", resource_name=f"box-{tag}",
|
|
metric_name="cpu_pct", operator=AlertOperator("<"), threshold=1,
|
|
created_by=uid)
|
|
s.add_all([hot, cold])
|
|
await s.flush()
|
|
s.add_all([
|
|
AlertEvent(rule_id=hot.id, from_state="inactive", to_state="firing", metric_value=97.5,
|
|
transitioned_at=now - timedelta(minutes=5), email_sent=False,
|
|
email_error=f"mx-{tag}: timed out", webhook_sent=True),
|
|
AlertEvent(rule_id=hot.id, from_state="firing", to_state="resolved", metric_value=None,
|
|
transitioned_at=now - timedelta(minutes=2), note=f"{MUTED} patch-{tag}"),
|
|
AlertEvent(rule_id=cold.id, from_state="inactive", to_state="firing", metric_value=0.5,
|
|
transitioned_at=now - timedelta(minutes=1), note=f"cold-note-{tag}"),
|
|
# Outside a 1h range.
|
|
AlertEvent(rule_id=hot.id, from_state="pending", to_state="firing", metric_value=95,
|
|
transitioned_at=now - timedelta(hours=3), note=f"old-{tag}"),
|
|
])
|
|
hot_id = hot.id
|
|
|
|
client = app.test_client()
|
|
async with client.session_transaction() as sess:
|
|
sess.update(user_id=uid, user_role="viewer", username=f"hist-{tag}", login_at=time.time())
|
|
|
|
resp = await client.get("/alerts/history?range=1h")
|
|
assert resp.status_code == 200
|
|
page = await resp.get_data(as_text=True)
|
|
assert "Alert History" in page and 'href="/alerts/"' in page # crumbs back to Alerts
|
|
assert f"hot {tag}" in page and f"cold-note-{tag}" in page and f"old-{tag}" not in page
|
|
assert "cpu_pct = 97.5" in page and "rule: > 90" in page
|
|
assert "email failed" in page and f"mx-{tag}: timed out" in page and "webhook sent" in page
|
|
assert f"muted · patch-{tag}" in page and "cpu_pct = no data" in page
|
|
|
|
resp = await client.get(f"/alerts/history?range=24h&rule={hot_id}", headers={"HX-Request": "true"})
|
|
rows = await resp.get_data(as_text=True)
|
|
assert "<html" not in rows and "<table" in rows
|
|
assert f"old-{tag}" in rows and f"cold-note-{tag}" not in rows
|
|
|
|
# The next page as the reader scrolls: only older rows, without the table
|
|
# around them, and the list says where it ends (#5211).
|
|
from steward.core.paging import cursor
|
|
resp = await client.get("/alerts/history", query_string={
|
|
"range": "24h", "rule": hot_id, "before": cursor(now - timedelta(minutes=3), "")})
|
|
older = await resp.get_data(as_text=True)
|
|
assert resp.status_code == 200 and "<table" not in older
|
|
assert "cpu_pct = 97.5" in older and f"old-{tag}" in older and f"patch-{tag}" not in older
|
|
assert "End of the list." in older and "data-scroll-more" not in older
|
|
|
|
# The rules list links each rule to its history.
|
|
assert f'/alerts/history?rule={hot_id}' in await (await client.get("/alerts/")).get_data(as_text=True)
|
|
|
|
asyncio.run(_go())
|