Backend: - Notification model + Alembic migration - Notification service: CRUD, mark read, unread count, pending scheduled - WebSocket manager singleton for real-time push - WebSocket endpoint /ws/notifications with JWT auth via query param - APScheduler integration: periodic notification sender (every 60s), daily proactive health review job (8 AM) - AI tool: schedule_notification (immediate or scheduled) - Health review worker: analyzes user memory via Claude, creates ai_generated notifications with WebSocket push Frontend: - Notification API client + Zustand store - WebSocket hook with auto-reconnect (exponential backoff) - Notification bell in header with unread count badge + dropdown - Notifications page with type badges, mark read, mark all read - WebSocket initialized in AppLayout for app-wide real-time updates - Enabled notifications nav in sidebar - English + Russian translations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
56 lines
1.7 KiB
Python
56 lines
1.7 KiB
Python
import uuid
|
|
|
|
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.security import decode_access_token
|
|
from app.database import async_session_factory
|
|
from app.models.user import User
|
|
from app.services.ws_manager import manager
|
|
from app.services.notification_service import get_unread_count
|
|
|
|
router = APIRouter(tags=["websocket"])
|
|
|
|
|
|
async def _authenticate_ws(token: str) -> uuid.UUID | None:
|
|
payload = decode_access_token(token)
|
|
user_id = payload.get("sub")
|
|
if not user_id:
|
|
return None
|
|
try:
|
|
uid = uuid.UUID(user_id)
|
|
except ValueError:
|
|
return None
|
|
|
|
async with async_session_factory() as db:
|
|
result = await db.execute(select(User).where(User.id == uid, User.is_active == True)) # noqa: E712
|
|
user = result.scalar_one_or_none()
|
|
if not user:
|
|
return None
|
|
return uid
|
|
|
|
|
|
@router.websocket("/ws/notifications")
|
|
async def ws_notifications(websocket: WebSocket, token: str = Query(...)):
|
|
user_id = await _authenticate_ws(token)
|
|
if not user_id:
|
|
await websocket.close(code=4001, reason="Unauthorized")
|
|
return
|
|
|
|
await manager.connect(user_id, websocket)
|
|
|
|
try:
|
|
# Send initial unread count
|
|
async with async_session_factory() as db:
|
|
count = await get_unread_count(db, user_id)
|
|
await websocket.send_json({"type": "unread_count", "count": count})
|
|
|
|
# Keep alive - wait for disconnect
|
|
while True:
|
|
await websocket.receive_text()
|
|
except WebSocketDisconnect:
|
|
pass
|
|
finally:
|
|
manager.disconnect(user_id, websocket)
|