"""Suppression rules and per-instance preferences.""" from __future__ import annotations from typing import Any from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from sqlalchemy.orm import Session from triagelib import settings as legacy from ..auth import current_user, require_admin from ..config import get_settings from ..db import get_db from ..models import AppSetting, SuppressionRule, User from ..routers.alerts_router import engine router = APIRouter(prefix="/api/settings", tags=["settings"]) app_settings = get_settings() class RuleBody(BaseModel): id: str | None = None name: str reason: str = "" enabled: bool = True conditions: dict[str, Any] = {} class GeneralBody(BaseModel): agent_name: str | None = None chronic_days: int | None = None @router.get("") def read_settings(db: Session = Depends(get_db), user: User = Depends(current_user)): row = db.get(AppSetting, "general") general = (row.value if row else {}) or {} return { "rules": [r.to_json() for r in db.query(SuppressionRule).order_by(SuppressionRule.id).all()], "conditions": legacy.CONDITIONS, "agent_name": general.get("agent_name") or "", "chronic_days": general.get("chronic_days") or 3, "config": app_settings.public_flags(), } @router.post("/general") def save_general(body: GeneralBody, db: Session = Depends(get_db), user: User = Depends(current_user)): row = db.get(AppSetting, "general") or AppSetting(key="general", value={}) value = dict(row.value or {}) if body.agent_name is not None: value["agent_name"] = body.agent_name.strip() if body.chronic_days is not None: value["chronic_days"] = max(1, int(body.chronic_days)) row.value = value db.merge(row) db.commit() return read_settings(db, user) @router.post("/rules") def save_rule(body: RuleBody, db: Session = Depends(get_db), user: User = Depends(require_admin)): conditions = {k: v for k, v in (body.conditions or {}).items() if k in legacy.CONDITIONS and v} if not conditions: raise HTTPException(status.HTTP_400_BAD_REQUEST, "A rule needs at least one condition, otherwise it would hide everything.") rule = db.get(SuppressionRule, int(body.id)) if (body.id or "").isdigit() else None if rule is None: rule = SuppressionRule(created_by=user.email) db.add(rule) rule.name = body.name.strip() or "Untitled rule" rule.reason = body.reason.strip() rule.enabled = body.enabled rule.conditions = conditions db.commit() return read_settings(db, user) @router.delete("/rules/{rule_id}") def delete_rule(rule_id: int, db: Session = Depends(get_db), user: User = Depends(require_admin)): rule = db.get(SuppressionRule, rule_id) if rule: db.delete(rule) db.commit() return read_settings(db, user) @router.post("/rules/preview") def preview_rule(body: RuleBody, db: Session = Depends(get_db), user: User = Depends(current_user)): """Show which firing alerts a rule would hide, before it is saved.""" from triagelib import alerts as alertlib raw, _error, _age = engine.cache.get() candidates = [a for a in (alertlib.from_prometheus(x, engine.rules) for x in raw) if not alertlib.is_excluded(a) and alertlib.cx_relevant(a)] rule = legacy._normalize_rule({"name": body.name, "conditions": body.conditions}) hits = [{ "kind": a.kind, "title": a.title, "instance_name": a.instance_name, "host": a.host, "org_name": a.org_name, "region": a.region, } for a in candidates if legacy.rule_matches(rule, a)] return {"count": len(hits), "matches": hits[:60]}