158 lines
4.8 KiB
Python
158 lines
4.8 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("/{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
|