feat(J): WebSocket live-events + replace polling + fix ffmpeg turntable timeout
- fix(render): ffmpeg overlay=0:0 -> overlay=0:0:shortest=1 to prevent hang on finite PNG sequences - feat(ws): add core/websocket.py ConnectionManager + Redis Pub/Sub subscriber loop - feat(ws): add /api/ws WebSocket endpoint with JWT query-param auth in main.py - feat(ws): emit render_complete/failed + cad_processing_complete events from step_tasks.py - feat(ws): emit order_status_change events from orders router - feat(ws): add beat_tasks.py broadcast_queue_status task (every 10s via Redis __broadcast__) - feat(frontend): add useWebSocket hook with auto-reconnect (exponential backoff, 25s ping) - feat(frontend): add WebSocketContext + WebSocketProvider wrapping App - refactor(frontend): remove polling from WorkerActivity (was 5s/3s) + OrderDetail (was 5s) - refactor(frontend): reduce polling in Layout (8s->60s) + NotificationCenter (15s->60s) - docs: add ffmpeg shortest=1 + WebSocket JWT auth learnings to LEARNINGS.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -171,6 +171,28 @@ def render_step_thumbnail(self, cad_file_id: str):
|
||||
f"Auto material population failed for cad_file {cad_file_id} (non-fatal)"
|
||||
)
|
||||
|
||||
# Broadcast WebSocket event for live UI updates
|
||||
try:
|
||||
from sqlalchemy import create_engine, select as sql_select2
|
||||
from sqlalchemy.orm import Session as _Session
|
||||
from app.config import settings as _cfg
|
||||
from app.models.cad_file import CadFile as _CadFile
|
||||
_sync_url = _cfg.database_url.replace("+asyncpg", "")
|
||||
_eng = create_engine(_sync_url)
|
||||
with _Session(_eng) as _s:
|
||||
_cad = _s.get(_CadFile, cad_file_id)
|
||||
_tid = str(_cad.tenant_id) if _cad and _cad.tenant_id else None
|
||||
_eng.dispose()
|
||||
if _tid:
|
||||
from app.core.websocket import publish_event_sync
|
||||
publish_event_sync(_tid, {
|
||||
"type": "cad_processing_complete",
|
||||
"cad_file_id": cad_file_id,
|
||||
"status": "completed",
|
||||
})
|
||||
except Exception:
|
||||
logger.debug("WebSocket publish for CAD complete skipped (non-fatal)")
|
||||
|
||||
|
||||
@celery_app.task(bind=True, name="app.tasks.step_tasks.generate_stl_cache", queue="thumbnail_rendering")
|
||||
def generate_stl_cache(self, cad_file_id: str, quality: str):
|
||||
@@ -559,6 +581,22 @@ def render_order_line_task(self, order_line_id: str):
|
||||
else:
|
||||
emit(order_line_id, f"Render failed after {elapsed:.1f}s", "error")
|
||||
|
||||
# Broadcast WebSocket event for live UI updates
|
||||
try:
|
||||
from app.core.websocket import publish_event_sync
|
||||
_tenant_id = str(line.product.cad_file.tenant_id) if (
|
||||
line.product and line.product.cad_file and line.product.cad_file.tenant_id
|
||||
) else None
|
||||
if _tenant_id:
|
||||
publish_event_sync(_tenant_id, {
|
||||
"type": "render_complete" if success else "render_failed",
|
||||
"order_line_id": order_line_id,
|
||||
"order_id": str(line.order_id),
|
||||
"status": new_status,
|
||||
})
|
||||
except Exception:
|
||||
logger.debug("WebSocket publish skipped (non-fatal)")
|
||||
|
||||
# Notify order creator about render result
|
||||
try:
|
||||
from app.models.order import Order as OrderModel
|
||||
|
||||
Reference in New Issue
Block a user