from __future__ import annotations from datetime import datetime, timezone from quart import Blueprint, render_template, request, redirect, url_for, current_app, session from sqlalchemy import select from fablednetmon.auth.middleware import require_role from fablednetmon.models.alerts import AlertRule, AlertState, AlertStateEnum, AlertOperator from fablednetmon.models.users import UserRole alerts_bp = Blueprint("alerts", __name__, url_prefix="/alerts") _OPERATORS = [(op.value, op.value) for op in AlertOperator] @alerts_bp.get("/") @require_role(UserRole.viewer) async def list_alerts(): async with current_app.db_sessionmaker() as db: result = await db.execute( select(AlertState, AlertRule) .join(AlertRule, AlertState.rule_id == AlertRule.id) .where(AlertState.state.in_([ AlertStateEnum.firing, AlertStateEnum.acknowledged, AlertStateEnum.pending ])) .order_by(AlertState.last_transition_at.desc()) ) active = result.all() rules_result = await db.execute( select(AlertRule).order_by(AlertRule.name) ) rules = rules_result.scalars().all() return await render_template("alerts/list.html", active=active, rules=rules, operators=_OPERATORS) @alerts_bp.get("/rules/new") @require_role(UserRole.operator) async def new_rule(): return await render_template("alerts/rules_form.html", rule=None, operators=_OPERATORS) @alerts_bp.post("/rules") @require_role(UserRole.operator) async def create_rule(): form = await request.form user_id = session.get("user_id") now = datetime.now(timezone.utc) rule = AlertRule( name=form["name"].strip(), source_module=form["source_module"].strip(), resource_name=form["resource_name"].strip(), metric_name=form["metric_name"].strip(), operator=AlertOperator(form["operator"]), threshold=float(form["threshold"]), consecutive_failures_required=int(form.get("consecutive_failures_required") or 1), enabled=True, created_by=user_id, ) state = AlertState( rule_id=rule.id, state=AlertStateEnum.inactive, last_transition_at=now, ) async with current_app.db_sessionmaker() as db: async with db.begin(): db.add(rule) await db.flush() state.rule_id = rule.id db.add(state) return redirect(url_for("alerts.list_alerts")) @alerts_bp.post("/rules//delete") @require_role(UserRole.admin) async def delete_rule(rule_id: str): async with current_app.db_sessionmaker() as db: async with db.begin(): result = await db.execute(select(AlertRule).where(AlertRule.id == rule_id)) rule = result.scalar_one_or_none() if rule: await db.delete(rule) return redirect(url_for("alerts.list_alerts")) @alerts_bp.post("//acknowledge") @require_role(UserRole.operator) async def acknowledge(rule_id: str): user_id = session.get("user_id") now = datetime.now(timezone.utc) async with current_app.db_sessionmaker() as db: async with db.begin(): result = await db.execute( select(AlertState).where(AlertState.rule_id == rule_id) ) state = result.scalar_one_or_none() if state is None: return "Not found", 404 if state.state != AlertStateEnum.firing: return "Conflict: alert is not in FIRING state", 409 state.state = AlertStateEnum.acknowledged state.acknowledged_by = user_id state.acknowledged_at = now state.last_transition_at = now return redirect(url_for("alerts.list_alerts"))