feat(alerts): Steward's own confirm dialog, a verdict-first rules list, and a rule page in three sections (#5314, #5315, #5317)
Confirm dialog (#5314): - Every data-confirm form now asks in one native <dialog> (base.html + static/confirm.js) instead of window.confirm. - The question becomes the heading and what follows it the detail; the act's button is named after the form's own button; Cancel has the focus and Esc backs out. Optional data-confirm-title/-detail/-action/-dismiss attributes. - All 27 existing uses work as they are. Cancelling a run says 'Keep running'. Alerts list (#5315): - A verdict line (what is firing, how many rules, where notifications go, or 'notifies nobody yet' with a link) replaces the empty Active card. - Each rule's name opens it; rows show 'last fired' and fold fire/clear into the condition; History and Disable stay; Delete leaves the list. - .docker-verdict becomes the shared .page-verdict. Rule page (#5317): - Named for the rule (title and crumb), with when it last fired and how many changes of state it holds. - Three sections: what it watches, when it fires (checks side by side, seconds echoed as a duration), what happens (where notifications go, no-data, the playbook, whose fields stay folded until ticked). - Delete at the foot, behind the dialog, saying its history goes with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,13 +2,13 @@ from __future__ import annotations
|
||||
from datetime import datetime, timezone
|
||||
from quart import Blueprint, render_template, request, redirect, url_for, current_app, session
|
||||
from steward.core.audit import log_audit
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy import func, 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
|
||||
from steward.models.alerts import AlertEvent, AlertRule, AlertOperator, AlertState, MaintenanceWindow
|
||||
from steward.models.users import UserRole
|
||||
|
||||
alerts_bp = Blueprint("alerts", __name__, url_prefix="/alerts")
|
||||
@@ -102,29 +102,41 @@ def _parse_ansible_action(form) -> tuple[dict | None, str | None]:
|
||||
return action, None
|
||||
|
||||
|
||||
def _notify() -> list[str]:
|
||||
"""Where alerts' notifications go now, from the settings the app runs with."""
|
||||
return alert_svc.notify_targets(current_app.config.get("SMTP", {}), current_app.config.get("WEBHOOK", {}))
|
||||
|
||||
|
||||
@alerts_bp.get("/")
|
||||
@require_role(UserRole.viewer)
|
||||
async def list_alerts():
|
||||
"""The verdict and what is active, then every rule with what it is doing
|
||||
now and when it last fired (#5315)."""
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
active = await alert_svc.active_rows(db, datetime.now(timezone.utc))
|
||||
rules = await alert_svc.list_rules(db)
|
||||
states = {s.rule_id: s for s in (await db.execute(select(AlertState))).scalars()}
|
||||
fired = await alert_svc.last_fired(db)
|
||||
|
||||
# Every rule with what it is doing now; disabled rules last.
|
||||
rules.sort(key=lambda r: (not r.enabled, r.name.lower()))
|
||||
rule_state = {r.id: alert_svc.alert_label(states[r.id]) for r in rules if r.id in states}
|
||||
return await render_template(
|
||||
"alerts/list.html", active=active, rules=rules, rule_state=rule_state,
|
||||
operators=_OPERATORS, poll_interval=current_app.config.get("MONITORS_POLL_INTERVAL", 60))
|
||||
"alerts/list.html", active=active, rules=rules, rule_state=rule_state, last_fired=fired,
|
||||
notify=_notify(), operators=_OPERATORS,
|
||||
poll_interval=current_app.config.get("MONITORS_POLL_INTERVAL", 60))
|
||||
|
||||
|
||||
@alerts_bp.get("/active")
|
||||
@require_role(UserRole.viewer)
|
||||
async def active_fragment():
|
||||
"""The Active section alone, which the alerts page polls."""
|
||||
"""The verdict and Active section alone, which the alerts page polls."""
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
active = await alert_svc.active_rows(db, datetime.now(timezone.utc))
|
||||
return await render_template("alerts/_active.html", active=active)
|
||||
rules = await alert_svc.list_rules(db)
|
||||
return await render_template(
|
||||
"alerts/_active.html", active=active, notify=_notify(), rules_total=len(rules),
|
||||
rules_off=sum(1 for r in rules if not r.enabled))
|
||||
|
||||
|
||||
def _history_row(event, rule) -> dict:
|
||||
@@ -215,6 +227,7 @@ async def new_rule():
|
||||
metrics=alert_svc.metric_catalog().get(first_source, []),
|
||||
catalog=alert_svc.metric_catalog(),
|
||||
ansible_sources=_ansible_source_names(),
|
||||
notify=_notify(),
|
||||
)
|
||||
|
||||
|
||||
@@ -227,9 +240,12 @@ async def edit_rule(rule_id: str):
|
||||
if rule is None:
|
||||
return "Not found", 404
|
||||
resources = await alert_svc.resources_for(db, rule.source_module)
|
||||
history_count = (await db.execute(
|
||||
select(func.count()).select_from(AlertEvent).where(AlertEvent.rule_id == rule.id))).scalar_one()
|
||||
fired = (await alert_svc.last_fired(db, rule.id)).get(rule.id)
|
||||
return await render_template(
|
||||
"alerts/rules_form.html",
|
||||
rule=rule,
|
||||
rule=rule, history_count=history_count, fired=fired, notify=_notify(),
|
||||
operators=_OPERATORS,
|
||||
source_modules=alert_svc.source_modules(),
|
||||
initial_source=rule.source_module,
|
||||
|
||||
Reference in New Issue
Block a user