feat(ui): audit log, alert history and Ansible runs load as you scroll (#5211)
#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>
This commit is contained in:
@@ -5,6 +5,7 @@ from steward.core.audit import log_audit
|
||||
from sqlalchemy import select
|
||||
from steward.auth.middleware import require_role
|
||||
from steward.alerts import service as alert_svc
|
||||
from steward.core.paging import PAGE_SIZE, next_page
|
||||
from steward.core.time_range import parse_range
|
||||
from steward.ansible import sources as src_module
|
||||
from steward.models.alerts import AlertRule, AlertOperator, AlertState, MaintenanceWindow
|
||||
@@ -117,9 +118,6 @@ async def active_fragment():
|
||||
return await render_template("alerts/_active.html", active=active)
|
||||
|
||||
|
||||
HISTORY_LIMIT = 200
|
||||
|
||||
|
||||
def _history_row(event, rule) -> dict:
|
||||
"""One transition as the history page shows it."""
|
||||
from steward.core.alerts import MUTED, RELEASED
|
||||
@@ -147,15 +145,20 @@ def _history_row(event, rule) -> dict:
|
||||
@alerts_bp.get("/history")
|
||||
@require_role(UserRole.viewer)
|
||||
async def alert_history():
|
||||
"""Every alert transition in a range, newest first; one rule's with ?rule=."""
|
||||
"""Every alert transition in a range, newest first; one rule's with ?rule=.
|
||||
A page at a time as the reader scrolls: ?before=<cursor> is the next
|
||||
page's rows alone (core.paging)."""
|
||||
since, range_key = parse_range(request.args.get("range") or "7d")
|
||||
before = request.args.get("before")
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
rules = await alert_svc.list_rules(db)
|
||||
chosen = next((r for r in rules if r.id == request.args.get("rule")), None)
|
||||
rows = await alert_svc.history(db, since, chosen.id if chosen else None, HISTORY_LIMIT)
|
||||
ctx = {"events": [_history_row(e, r) for e, r in rows[:HISTORY_LIMIT]],
|
||||
"truncated": len(rows) > HISTORY_LIMIT, "limit": HISTORY_LIMIT,
|
||||
rows = await alert_svc.history(db, since, chosen.id if chosen else None, PAGE_SIZE, before)
|
||||
rows, after = next_page(rows, lambda row: (row[0].transitioned_at, row[0].id))
|
||||
ctx = {"events": [_history_row(e, r) for e, r in rows], "after": after,
|
||||
"rules": rules, "chosen": chosen, "current_range": range_key}
|
||||
if before:
|
||||
return await render_template("alerts/_history_page.html", **ctx)
|
||||
if request.headers.get("HX-Request"):
|
||||
return await render_template("alerts/_history_rows.html", **ctx)
|
||||
return await render_template("alerts/history.html", **ctx)
|
||||
|
||||
@@ -241,17 +241,18 @@ async def list_rules(db) -> list[AlertRule]:
|
||||
|
||||
|
||||
async def history(db, since: datetime, rule_id: str | None = None,
|
||||
limit: int = 200) -> list[tuple[AlertEvent, AlertRule]]:
|
||||
limit: int = 200, before: str | None = None) -> list[tuple[AlertEvent, AlertRule]]:
|
||||
"""Alert transitions since `since`, newest first, with their rule; one
|
||||
rule's when `rule_id` is given. Returns up to limit+1 rows so a caller can
|
||||
tell the list was cut."""
|
||||
rule's when `rule_id` is given; only those older than the `before` cursor
|
||||
when given (core.paging). Returns up to limit+1 rows so a caller can tell
|
||||
the list was cut."""
|
||||
from steward.core.paging import keyset
|
||||
stmt = (select(AlertEvent, AlertRule)
|
||||
.join(AlertRule, AlertEvent.rule_id == AlertRule.id)
|
||||
.where(AlertEvent.transitioned_at >= since)
|
||||
.order_by(AlertEvent.transitioned_at.desc())
|
||||
.limit(limit + 1))
|
||||
.where(AlertEvent.transitioned_at >= since))
|
||||
if rule_id:
|
||||
stmt = stmt.where(AlertEvent.rule_id == rule_id)
|
||||
stmt = keyset(stmt, AlertEvent.transitioned_at, AlertEvent.id, before, limit)
|
||||
return [tuple(row) for row in (await db.execute(stmt)).all()]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user