feat(backend): alerts come enriched and paged with an unseen count and a live alert frame

This commit is contained in:
hh committed 2026-10-19 02:57:27 +00:00
1 parent 4469938f47
commit 2bb8ca45fb
7 files changed
+682 -65

No files matched your search

+69 -21
View File
@@ -6,55 +6,88 @@ from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from utils.read import watches
from utils.read.models import DEFAULT_LIMIT, AlertView, Page, WatchView
from utils.read.models import AlertPage, WatchItem, WatchView
from utils.watch_rules import WatchParamsError, clean_name, normalize_params
router = APIRouter(prefix="/api", tags=["watches"], route_class=DishkaRoute)
class WatchCreate(BaseModel):
class WatchUpdate(BaseModel):
kind: str | None = None
params: dict[str, Any] = {}
name: str | None = None
enabled: bool = True
class WatchCreate(WatchUpdate):
account_id: int
kind: str
params: dict[str, Any] = {}
enabled: bool = True
class WatchUpdate(BaseModel):
params: dict[str, Any] = {}
enabled: bool = True
class SeenAll(BaseModel):
account_id: int
up_to: int | None = None
def _params(kind: str, params: dict[str, Any]) -> dict[str, Any]:
try:
return normalize_params(kind, params)
except WatchParamsError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
async def _item(pool: asyncpg.Pool, watch: WatchView) -> WatchItem:
return (await watches.describe_watches(pool, watch.account_id, [watch]))[0]
@router.get("/watches")
async def list_watches(
pool: FromDishka[asyncpg.Pool], account_id: Annotated[int, Query()]
) -> list[WatchView]:
return await watches.list_watches(pool, account_id)
) -> list[WatchItem]:
rows = await watches.list_watches(pool, account_id)
return await watches.describe_watches(pool, account_id, rows)
@router.post("/watches", status_code=201)
async def create_watch(pool: FromDishka[asyncpg.Pool], body: WatchCreate) -> WatchView:
return await watches.create_watch(
pool, body.account_id, body.kind, body.params, enabled=body.enabled
async def create_watch(pool: FromDishka[asyncpg.Pool], body: WatchCreate) -> WatchItem:
watch = await watches.create_watch(
pool,
body.account_id,
body.kind,
_params(body.kind, body.params),
name=clean_name(body.name),
enabled=body.enabled,
)
return await _item(pool, watch)
@router.get("/watches/{watch_id}")
async def get_watch(pool: FromDishka[asyncpg.Pool], watch_id: int) -> WatchView:
async def get_watch(pool: FromDishka[asyncpg.Pool], watch_id: int) -> WatchItem:
watch = await watches.get_watch(pool, watch_id)
if watch is None:
raise HTTPException(status_code=404, detail="watch not found")
return watch
return await _item(pool, watch)
@router.put("/watches/{watch_id}")
async def update_watch(
pool: FromDishka[asyncpg.Pool], watch_id: int, body: WatchUpdate
) -> WatchView:
) -> WatchItem:
current = await watches.get_watch(pool, watch_id)
if current is None:
raise HTTPException(status_code=404, detail="watch not found")
kind = body.kind or current.kind
watch = await watches.update_watch(
pool, watch_id, body.params, enabled=body.enabled
pool,
watch_id,
kind,
_params(kind, body.params),
name=clean_name(body.name),
enabled=body.enabled,
)
if watch is None:
raise HTTPException(status_code=404, detail="watch not found")
return watch
return await _item(pool, watch)
@router.delete("/watches/{watch_id}", status_code=204)
@@ -68,14 +101,29 @@ async def list_alerts(
pool: FromDishka[asyncpg.Pool],
account_id: Annotated[int, Query()],
seen: Annotated[bool | None, Query()] = None,
limit: Annotated[int, Query()] = DEFAULT_LIMIT,
offset: Annotated[int, Query()] = 0,
) -> list[AlertView]:
before: Annotated[int | None, Query()] = None,
limit: Annotated[int, Query()] = watches.ALERT_PAGE,
) -> AlertPage:
return await watches.list_alerts(
pool, account_id, Page(limit=limit, offset=offset), seen=seen
pool, account_id, seen=seen, before=before, limit=limit
)
@router.get("/alerts/unseen")
async def unseen_alerts(
pool: FromDishka[asyncpg.Pool], account_id: Annotated[int, Query()]
) -> dict[str, int]:
return {"unseen": await watches.unseen_count(pool, account_id)}
@router.post("/alerts/seen")
async def mark_all_seen(
pool: FromDishka[asyncpg.Pool], body: SeenAll
) -> dict[str, int]:
marked = await watches.mark_all_seen(pool, body.account_id, body.up_to)
return {"marked": marked}
@router.post("/alerts/{alert_id}/seen", status_code=204)
async def mark_alert_seen(pool: FromDishka[asyncpg.Pool], alert_id: int) -> None:
if not await watches.mark_alert_seen(pool, alert_id):