feat: v0.2.0 branding, healthz version, SSE dashboard live
Centralize APP_VERSION 0.2.0; /health and /healthz return version; startup log line; UI title Security Alert Center v.0.2.0; SSE /api/v1/stream/events for live dashboard counters.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.auth.jwt_auth import verify_access_token
|
||||
from app.database import SessionLocal
|
||||
from app.models import Event, Problem
|
||||
from app.version import APP_VERSION
|
||||
|
||||
router = APIRouter(prefix="/stream", tags=["stream"])
|
||||
|
||||
SSE_INTERVAL_SEC = 5
|
||||
|
||||
|
||||
def _dashboard_snapshot(db: Session) -> dict:
|
||||
since = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||
events_24h = db.scalar(
|
||||
select(func.count()).select_from(Event).where(Event.received_at >= since)
|
||||
) or 0
|
||||
problems_open = (
|
||||
db.scalar(select(func.count()).select_from(Problem).where(Problem.status == "open")) or 0
|
||||
)
|
||||
last_event = db.scalar(select(Event.id).order_by(Event.received_at.desc()).limit(1))
|
||||
return {
|
||||
"type": "dashboard",
|
||||
"version": APP_VERSION,
|
||||
"events_last_24h": events_24h,
|
||||
"problems_open": problems_open,
|
||||
"last_event_id": last_event,
|
||||
"at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
|
||||
|
||||
async def _sse_generator():
|
||||
while True:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
payload = _dashboard_snapshot(db)
|
||||
finally:
|
||||
db.close()
|
||||
yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
||||
await asyncio.sleep(SSE_INTERVAL_SEC)
|
||||
|
||||
|
||||
def _sse_auth(token: str = Query(..., description="JWT access_token")) -> str:
|
||||
return verify_access_token(token)
|
||||
|
||||
|
||||
@router.get("/events")
|
||||
async def stream_events(
|
||||
_user: str = Depends(_sse_auth),
|
||||
) -> StreamingResponse:
|
||||
return StreamingResponse(
|
||||
_sse_generator(),
|
||||
media_type="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
|
||||
)
|
||||
Reference in New Issue
Block a user