feat(translate): add WebSocket endpoint for real-time run progress
Backend:
- Add /ws/translate/run/{run_id} WebSocket endpoint in app.py
- Streams structured run status every second (total_records,
successful_records, failed_records, progressPct, batch_count, etc.)
- Auto-detects terminal states (COMPLETED, FAILED, CANCELLED) and closes
- Authenticated via JWT/API key token query param
Frontend:
- Add getTranslateRunWsUrl() URL builder in api.js
- Update translationRunStore to use WebSocket instead of log stream
- WebSocket receives structured status JSON, updates store reactively
- Falls back to polling silently if WebSocket fails
- Keep polling as fallback for insert phase
This commit is contained in:
@@ -624,6 +624,55 @@ async def dataset_websocket_endpoint(websocket: WebSocket, env_id: str):
|
||||
task_manager.unsubscribe_dataset_events(env_id, queue)
|
||||
logger.reflect("Released dataset event subscription", extra={"env_id": env_id})
|
||||
# #endregion dataset_websocket_endpoint
|
||||
# #region translate_run_websocket [C:3] [TYPE Function]
|
||||
# @BRIEF WebSocket endpoint for translation run progress — streams structured status updates.
|
||||
# @PRE run_id must be a valid translation run ID. WebSocket authenticated via `token` query param.
|
||||
# @POST Streams run status JSON every second until terminal state or disconnect.
|
||||
# @SIDE_EFFECT Queries DB each tick for current run status via TranslationOrchestrator.
|
||||
# @UX_STATE Streaming -> Terminal (completed/failed/cancelled) -> Close
|
||||
# @UX_FEEDBACK Client receives {status, total_records, successful_records, failed_records, progressPct, ...}
|
||||
@app.websocket("/ws/translate/run/{run_id}")
|
||||
async def translate_run_websocket(websocket: WebSocket, run_id: str):
|
||||
seed_trace_id()
|
||||
if not await _authenticate_websocket(websocket, "ws/translate/run"):
|
||||
await websocket.close(code=4001, reason="Authentication required")
|
||||
return
|
||||
await websocket.accept()
|
||||
logger.reason("Accepted translate run WebSocket", extra={"run_id": run_id})
|
||||
try:
|
||||
config_manager = get_config_manager()
|
||||
while True:
|
||||
try:
|
||||
from sqlalchemy.orm import Session
|
||||
from .core.database import SessionLocal
|
||||
from .plugins.translate.orchestrator_aggregator import TranslationResultAggregator
|
||||
from .core.event_log import EventLog
|
||||
db = SessionLocal()
|
||||
try:
|
||||
event_log = EventLog(db)
|
||||
aggregator = TranslationResultAggregator(db, event_log)
|
||||
status = aggregator.get_run_status(run_id)
|
||||
total = status.get("total_records", 0) or 0
|
||||
done = (status.get("successful_records", 0) or 0) + (status.get("failed_records", 0) or 0) + (status.get("skipped_records", 0) or 0)
|
||||
progress_pct = round((done / total) * 100) if total > 0 else 0
|
||||
status["progressPct"] = progress_pct
|
||||
await websocket.send_json(status)
|
||||
if status.get("status") in ("COMPLETED", "FAILED", "CANCELLED"):
|
||||
await asyncio.sleep(2)
|
||||
break
|
||||
finally:
|
||||
db.close()
|
||||
except Exception as tick_err:
|
||||
logger.explore("Translate run WS tick error", extra={"run_id": run_id, "error": str(tick_err)})
|
||||
await websocket.send_json({"error": str(tick_err)})
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
except WebSocketDisconnect:
|
||||
logger.reason("Translate run WS disconnected", extra={"run_id": run_id})
|
||||
except Exception as exc:
|
||||
logger.explore("Translate run WS error", extra={"run_id": run_id, "error": str(exc)})
|
||||
logger.reflect("Translate run WS closed", extra={"run_id": run_id})
|
||||
# #endregion translate_run_websocket
|
||||
# #region StaticFiles [C:1] [TYPE Mount] [SEMANTICS static, frontend, spa]
|
||||
# @BRIEF Mounts the frontend build directory to serve static assets.
|
||||
frontend_path = project_root / "frontend" / "build"
|
||||
|
||||
Reference in New Issue
Block a user