import asyncio import json import logging from typing import Optional from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect from app.core.auth_service import AuthService from .service import get_active_timer, get_dashboard_status, get_notifications logger = logging.getLogger(__name__) router = APIRouter() def _resolve_user_id_from_request(request: Request) -> Optional[int]: user_id = getattr(request.state, "user_id", None) if user_id is not None: try: return int(user_id) except (TypeError, ValueError): return None user_id_param = request.query_params.get("user_id") if user_id_param: try: return int(user_id_param) except (TypeError, ValueError): return None return None def _resolve_ws_payload(websocket: WebSocket) -> Optional[dict]: token = websocket.query_params.get("token") auth_header = (websocket.headers.get("authorization") or "").strip() if not token and auth_header.lower().startswith("bearer "): token = auth_header.split(" ", 1)[1].strip() payload = AuthService.verify_token(token) if token else None if not payload: access_cookie_token = (websocket.cookies.get("access_token") or "").strip() or None payload = AuthService.verify_token(access_cookie_token) if access_cookie_token else None return payload @router.get("/api/v1/dashboard/status") async def get_dashboard_status_endpoint() -> dict: return get_dashboard_status() @router.get("/api/v1/timer/active") async def get_active_timer_endpoint(request: Request) -> dict: user_id = _resolve_user_id_from_request(request) return get_active_timer(user_id) @router.get("/api/v1/notifications") async def get_notifications_endpoint(request: Request, limit: int = 20) -> dict: user_id = _resolve_user_id_from_request(request) return get_notifications(user_id, limit=limit) @router.websocket("/api/v1/bottom-bar/ws") async def bottom_bar_ws(websocket: WebSocket): payload = _resolve_ws_payload(websocket) if not payload: await websocket.close(code=1008) return try: user_id = int(payload.get("sub")) if payload.get("sub") is not None else None except (TypeError, ValueError): await websocket.close(code=1008) return await websocket.accept() initial_status = get_dashboard_status() initial_notifications = get_notifications(user_id, limit=20) await websocket.send_json({"event": "status_delta", "data": initial_status}) await websocket.send_json({"event": "notification_delta", "data": initial_notifications}) last_status_json = json.dumps(initial_status, sort_keys=True, default=str) last_notifications_json = json.dumps(initial_notifications, sort_keys=True, default=str) last_timer_elapsed = -1 status_tick = 0 try: while True: timer = get_active_timer(user_id) elapsed = int(timer.get("elapsed") or 0) if elapsed != last_timer_elapsed: await websocket.send_json({"event": "timer_tick", "data": timer}) last_timer_elapsed = elapsed status_tick += 1 if status_tick >= 5: status = get_dashboard_status() notifications = get_notifications(user_id, limit=20) status_json = json.dumps(status, sort_keys=True, default=str) if status_json != last_status_json: await websocket.send_json({"event": "status_delta", "data": status}) last_status_json = status_json notifications_json = json.dumps(notifications, sort_keys=True, default=str) if notifications_json != last_notifications_json: await websocket.send_json({"event": "notification_delta", "data": notifications}) last_notifications_json = notifications_json status_tick = 0 try: await asyncio.wait_for(websocket.receive_text(), timeout=1.0) except TimeoutError: continue except WebSocketDisconnect: logger.info("Bottom bar websocket disconnected user_id=%s", user_id) except Exception as exc: logger.warning("Bottom bar websocket error user_id=%s error=%s", user_id, exc)