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:
PTah
2026-05-27 13:20:39 +10:00
parent ea221db3c7
commit c657a65970
15 changed files with 197 additions and 25 deletions
+15 -2
View File
@@ -3,12 +3,12 @@ from sqlalchemy import text
from sqlalchemy.orm import Session
from app.database import get_db
from app.version import APP_NAME, APP_VERSION
router = APIRouter(tags=["health"])
@router.get("/health")
def health(db: Session = Depends(get_db)) -> dict:
def build_health_payload(db: Session) -> dict:
db_status = "ok"
try:
db.execute(text("SELECT 1"))
@@ -18,4 +18,17 @@ def health(db: Session = Depends(get_db)) -> dict:
"status": "ok" if db_status == "ok" else "degraded",
"database": db_status,
"service": "security-alert-center",
"name": APP_NAME,
"version": APP_VERSION,
}
@router.get("/health")
def health(db: Session = Depends(get_db)) -> dict:
return build_health_payload(db)
@router.get("/healthz")
def healthz(db: Session = Depends(get_db)) -> dict:
"""Kubernetes-style alias; same payload as /health."""
return build_health_payload(db)
+2 -1
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter
from app.api.v1 import auth, dashboards, events, health, hosts, problems
from app.api.v1 import auth, dashboards, events, health, hosts, problems, stream
api_router = APIRouter()
api_router.include_router(health.router)
@@ -9,3 +9,4 @@ api_router.include_router(events.router)
api_router.include_router(hosts.router)
api_router.include_router(problems.router)
api_router.include_router(dashboards.router)
api_router.include_router(stream.router)
+62
View File
@@ -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"},
)