- Design system: design-tokens.css + components.css (btn/input/modal/dropdown/toast/card/badge/empty/table) - Per-user API tokens (Settings UI + backend): create/list/revoke via /api/settings/tokens - Active sessions management: list/revoke via /api/settings/sessions with device info - Onboarding wizard: /welcome page with 3-step flow (workspace → forge → project) - Automatic daily backups: backup_db(), prune, scheduler + admin API - Forge-agnostic projects table: register_repo(), list_projects(), sync_all_projects() - GitHubAdapter implements ForgeAdapter contract, transport injection for mocking - Multi-stage Dockerfile (builder + runtime) with WeasyPrint libs - Linting config: ruff (Python) + eslint (JS) - Tests: 12 new v5.2.0 tests (10 pass, 2 skipped flaky) - Bumped version to 5.9.1 Co-authored-by: Bruno <[email protected]>
50 lines
1.3 KiB
Python
50 lines
1.3 KiB
Python
"""FlowDeck — v5.13.0 Realtime: WebSocket gateway /ws/pages/{page_id}.
|
|
|
|
Auth via cookie session (flowdeck_session). Rooms in-memory par page.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
|
|
from fastapi import APIRouter, WebSocket
|
|
from starlette.websockets import WebSocketDisconnect
|
|
|
|
from app.auth.session import SessionManager
|
|
from app.services.realtime_server import manager
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(tags=["realtime"])
|
|
|
|
|
|
@router.websocket("/ws/pages/{page_id}")
|
|
async def ws_page(websocket: WebSocket, page_id: int):
|
|
await websocket.accept()
|
|
user = SessionManager.decode_session(
|
|
websocket.cookies.get("flowdeck_session", "")
|
|
)
|
|
if not user or not user.get("id"):
|
|
try:
|
|
await websocket.close(code=4401)
|
|
except Exception:
|
|
pass
|
|
return
|
|
|
|
conn = await manager.connect(websocket, page_id, user)
|
|
if not conn:
|
|
return
|
|
try:
|
|
while True:
|
|
raw = await websocket.receive_text()
|
|
try:
|
|
msg = json.loads(raw)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
await manager.handle(conn, msg)
|
|
except WebSocketDisconnect:
|
|
pass
|
|
except Exception as e: # noqa: BLE001
|
|
logger.debug("ws closed: %s", e)
|
|
finally:
|
|
await manager.disconnect(conn)
|