from datetime import UTC, datetime from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import func, select from sqlalchemy.orm import Session from app.auth.dependencies import get_current_user, require_role from app.db.session import get_db from app.models import AlertRule, Asset, CheckResult, Incident, Monitor, User from app.schemas.core import CheckResultRead, MonitorCreate, MonitorRead, MonitorUpdate, WebsiteMonitorCreate router = APIRouter(prefix="/monitors", tags=["monitors"]) @router.get("", response_model=list[MonitorRead]) def list_monitors(_: User = Depends(get_current_user), db: Session = Depends(get_db)) -> list[Monitor]: return list(db.scalars(select(Monitor).order_by(Monitor.name)).all()) @router.post("", response_model=MonitorRead) def create_monitor( payload: MonitorCreate, _: User = Depends(require_role("admin")), db: Session = Depends(get_db), ) -> Monitor: monitor = Monitor(**payload.model_dump()) db.add(monitor) db.commit() db.refresh(monitor) return monitor @router.post("/website", response_model=MonitorRead) def create_website_monitor( payload: WebsiteMonitorCreate, _: User = Depends(require_role("admin")), db: Session = Depends(get_db), ) -> Monitor: asset_id: int | None = None if payload.create_asset: asset = Asset(name=payload.name, asset_type="website", address=payload.url, status="unknown", extra={}) db.add(asset) db.flush() asset_id = asset.id monitor = Monitor( asset_id=asset_id, name=payload.name, monitor_type="http", target=payload.url, config={ "expected_status": payload.expected_status, "expected_text": payload.expected_text, "unexpected_text": payload.unexpected_text, "timeout_seconds": payload.timeout_seconds, }, interval_seconds=payload.interval_seconds, status="unknown", ) db.add(monitor) db.flush() if payload.alert_enabled: db.add( AlertRule( monitor_id=monitor.id, name=f"{payload.name} website failure", severity=payload.alert_severity, condition={"type": "status_not_up"}, failure_threshold=payload.failure_threshold, cooldown_seconds=300, is_enabled=True, ) ) db.commit() db.refresh(monitor) return monitor @router.get("/{monitor_id}", response_model=MonitorRead) def get_monitor(monitor_id: int, _: User = Depends(get_current_user), db: Session = Depends(get_db)) -> Monitor: monitor = db.get(Monitor, monitor_id) if monitor is None: raise HTTPException(status_code=404, detail="Monitor not found") return monitor @router.patch("/{monitor_id}", response_model=MonitorRead) def update_monitor( monitor_id: int, payload: MonitorUpdate, _: User = Depends(require_role("admin")), db: Session = Depends(get_db), ) -> Monitor: monitor = db.get(Monitor, monitor_id) if monitor is None: raise HTTPException(status_code=404, detail="Monitor not found") for field, value in payload.model_dump(exclude_unset=True).items(): setattr(monitor, field, value) db.commit() db.refresh(monitor) return monitor @router.delete("/{monitor_id}", status_code=204) def delete_monitor( monitor_id: int, cleanup_orphan_website_asset: bool = True, _: User = Depends(require_role("admin")), db: Session = Depends(get_db), ) -> None: monitor = db.get(Monitor, monitor_id) if monitor is None: raise HTTPException(status_code=404, detail="Monitor not found") asset_id = monitor.asset_id now = datetime.now(UTC) open_incidents = db.scalars(select(Incident).where(Incident.monitor_id == monitor_id, Incident.status == "open")).all() for incident in open_incidents: incident.status = "resolved" incident.resolved_at = now incident.details = {**(incident.details or {}), "resolution_reason": "monitor_deleted"} db.delete(monitor) db.flush() if cleanup_orphan_website_asset and asset_id is not None: remaining = db.scalar(select(func.count(Monitor.id)).where(Monitor.asset_id == asset_id)) asset = db.get(Asset, asset_id) if remaining == 0 and asset is not None and asset.asset_type == "website": db.delete(asset) db.commit() @router.get("/{monitor_id}/results", response_model=list[CheckResultRead]) def list_monitor_results( monitor_id: int, limit: int = 20, _: User = Depends(get_current_user), db: Session = Depends(get_db), ) -> list[CheckResult]: monitor = db.get(Monitor, monitor_id) if monitor is None: raise HTTPException(status_code=404, detail="Monitor not found") return list( db.scalars( select(CheckResult) .where(CheckResult.monitor_id == monitor_id) .order_by(CheckResult.observed_at.desc()) .limit(min(limit, 100)) ).all() )