utp_service/backend/app/routers/alerts.py
2026-05-06 15:05:00 +03:00

194 lines
5.9 KiB
Python

from datetime import datetime, timezone
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.dependencies import get_current_user, get_db
from app.models.alert import Alert
from app.models.device import Device
from app.models.user import User
from app.schemas.alert import AlertRead
from app.services.sse import SSEEvent, sse_manager
router = APIRouter(prefix="/api/v1/alerts", tags=["alerts"])
# SSE channel for all monitor/alert events
MONITOR_CHANNEL = "__monitor__"
def _to_read(alert: Alert) -> AlertRead:
return AlertRead(
id=alert.id,
device_id=alert.device_id,
device_ip=alert.device.ip if alert.device else None,
device_hostname=alert.device.hostname if alert.device else None,
object_id=alert.device.object_id if alert.device else None,
object_name=(
alert.device.object.name
if alert.device and alert.device.object
else None
),
status=alert.status,
message=alert.message,
created_at=alert.created_at,
acknowledged_at=alert.acknowledged_at,
acknowledged_by_username=(
alert.acknowledger.username if alert.acknowledger else None
),
resolved_at=alert.resolved_at,
)
async def _load_alert(db: AsyncSession, alert_id: int) -> Alert:
result = await db.execute(
select(Alert)
.options(
selectinload(Alert.device).selectinload(Device.object),
selectinload(Alert.acknowledger),
)
.where(Alert.id == alert_id)
)
alert = result.scalar_one_or_none()
if alert is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Alert not found")
return alert
@router.get("", response_model=list[AlertRead])
async def list_alerts(
alert_status: Optional[str] = Query(None, alias="status"),
object_id: Optional[int] = Query(None),
device_id: Optional[int] = Query(None),
limit: int = Query(100, ge=1, le=500),
skip: int = Query(0, ge=0),
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
query = (
select(Alert)
.options(
selectinload(Alert.device).selectinload(Device.object),
selectinload(Alert.acknowledger),
)
.order_by(Alert.created_at.desc())
.offset(skip)
.limit(limit)
)
if alert_status and alert_status != "all":
query = query.where(Alert.status == alert_status)
if device_id is not None:
query = query.where(Alert.device_id == device_id)
if object_id is not None:
query = query.join(Device, Alert.device_id == Device.id).where(
Device.object_id == object_id
)
result = await db.execute(query)
return [_to_read(a) for a in result.scalars().all()]
@router.post("/acknowledge-by-object", response_model=list[AlertRead])
async def acknowledge_by_object(
object_id: int = Query(...),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
result = await db.execute(
select(Alert)
.join(Device, Alert.device_id == Device.id)
.where(Device.object_id == object_id, Alert.status == "open")
)
alerts = result.scalars().all()
now = datetime.now(timezone.utc)
for alert in alerts:
alert.status = "acknowledged"
alert.acknowledged_at = now
alert.acknowledged_by = current_user.id
await db.flush()
alert_ids = [a.id for a in alerts]
loaded = []
for aid in alert_ids:
loaded.append(await _load_alert(db, aid))
for alert in loaded:
await sse_manager.publish(
SSEEvent(
type="alert_updated",
task_id=MONITOR_CHANNEL,
data={"alert_id": alert.id, "status": "acknowledged", "device_id": alert.device_id},
)
)
return [_to_read(a) for a in loaded]
@router.post("/{alert_id}/acknowledge", response_model=AlertRead)
async def acknowledge_alert(
alert_id: int,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
alert = await _load_alert(db, alert_id)
if alert.status not in ("open",):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Cannot acknowledge alert with status '{alert.status}'",
)
alert.status = "acknowledged"
alert.acknowledged_at = datetime.now(timezone.utc)
alert.acknowledged_by = current_user.id
await db.flush()
await db.refresh(alert)
# Reload relationships after flush
alert = await _load_alert(db, alert_id)
result = _to_read(alert)
await sse_manager.publish(
SSEEvent(
type="alert_updated",
task_id=MONITOR_CHANNEL,
data={
"alert_id": alert_id,
"status": "acknowledged",
"device_id": alert.device_id,
},
)
)
return result
@router.post("/{alert_id}/resolve", response_model=AlertRead)
async def resolve_alert(
alert_id: int,
db: AsyncSession = Depends(get_db),
_: User = Depends(get_current_user),
):
alert = await _load_alert(db, alert_id)
if alert.status == "resolved":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Alert is already resolved",
)
alert.status = "resolved"
alert.resolved_at = datetime.now(timezone.utc)
await db.flush()
alert = await _load_alert(db, alert_id)
result = _to_read(alert)
await sse_manager.publish(
SSEEvent(
type="alert_updated",
task_id=MONITOR_CHANNEL,
data={
"alert_id": alert_id,
"status": "resolved",
"device_id": alert.device_id,
},
)
)
return result