infra/modules/kubernetes/ebook2audiobook/audiblez-web/backend/api/websocket.py
Viktor Barzin bcad200a23 chore: add untracked stacks, scripts, and agent configs
- New stacks: beads-server, hermes-agent
- Terragrunt tiers.tf for infra, phpipam, status-page
- Secrets symlinks for vault, phpipam, hermes-agent
- Scripts: cluster_manager, image_pull, containerd pullthrough setup
- Frigate config, audiblez-web app source, n8n workflows dir
- Claude agent: service-upgrade, reference: upgrade-config.json
- Removed: claudeception skill, excalidraw empty submodule, temp listings

[ci skip]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 09:33:06 +00:00

101 lines
3.4 KiB
Python

from fastapi import WebSocket, WebSocketDisconnect, HTTPException
from services.converter import job_manager
from models.schemas import JobProgress
from api.auth import sanitize_user_id
class ConnectionManager:
"""Manages WebSocket connections for job progress updates."""
def __init__(self):
self.active_connections: dict[str, list[WebSocket]] = {}
async def connect(self, job_id: str, websocket: WebSocket):
"""Connect a websocket for a specific job."""
await websocket.accept()
if job_id not in self.active_connections:
self.active_connections[job_id] = []
self.active_connections[job_id].append(websocket)
def disconnect(self, job_id: str, websocket: WebSocket):
"""Disconnect a websocket."""
if job_id in self.active_connections:
if websocket in self.active_connections[job_id]:
self.active_connections[job_id].remove(websocket)
if not self.active_connections[job_id]:
del self.active_connections[job_id]
async def send_progress(self, job_id: str, progress: JobProgress):
"""Send progress update to all connected clients for a job."""
if job_id in self.active_connections:
disconnected = []
for connection in self.active_connections[job_id]:
try:
await connection.send_json(progress.model_dump())
except:
disconnected.append(connection)
# Remove disconnected clients
for conn in disconnected:
self.disconnect(job_id, conn)
manager = ConnectionManager()
def get_user_from_websocket(websocket: WebSocket) -> str | None:
"""
Extract user ID from websocket headers.
WebSocket connections receive HTTP headers during the upgrade handshake.
"""
# Try various header name formats
uid = websocket.headers.get("x-authentik-uid")
if not uid:
uid = websocket.headers.get("X-Authentik-Uid")
if not uid:
uid = websocket.headers.get("x-authentik-userid")
if not uid:
uid = websocket.headers.get("remote-user")
if uid:
try:
return sanitize_user_id(uid)
except ValueError:
return None
return None
async def websocket_endpoint(websocket: WebSocket, job_id: str):
"""WebSocket endpoint for job progress updates."""
# Extract user from headers
user_id = get_user_from_websocket(websocket)
# Verify job exists and user has access
job = job_manager.get_job(job_id, user_id)
if not job:
# Close connection if job not found or not owned by user
await websocket.close(code=4004, reason="Job not found or access denied")
return
await manager.connect(job_id, websocket)
# Register progress callback
async def progress_callback(progress: JobProgress):
await manager.send_progress(job_id, progress)
job_manager.register_progress_callback(job_id, progress_callback)
try:
# Send initial status
await websocket.send_json({
"progress": job.progress,
"status": job.status,
})
# Wait for messages (keep-alive)
while True:
await websocket.receive_text()
except WebSocketDisconnect:
manager.disconnect(job_id, websocket)
job_manager.unregister_progress_callback(job_id, progress_callback)