feat: layout hamburger, media browser filters+previews, billing fixes
- Layout: mobile hamburger menu + overlay backdrop + close button; content area always full-width - Media browser: filter chips (default still+turntable); advanced toggle for GLB/STL; thumbnail_url previews for non-image types; video hover-play for turntable - Backend: asset_types multi-filter, thumbnail_url in MediaAssetOut, download proxy endpoint for MinIO/local files - Admin: "Import Existing Media" button → POST /api/admin/import-media-assets - Billing: fix invoice create 500 (MissingGreenlet — use selectinload after commit); PDF download uses axios blob instead of bare <a href> (auth header missing); fix storage.upload() accepting str|Path - SSE task logs: task_logs.py core + router, LiveRenderLog component - CadPreview: fix infinite loop when no gltf_geometry assets; loading screen before ThreeDViewer render - render-worker: add trimesh layer to Dockerfile Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import logging
|
||||
from pathlib import Path
|
||||
|
||||
from app.tasks.celery_app import celery_app
|
||||
from app.core.task_logs import log_task_event
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,6 +56,7 @@ def render_still_task(
|
||||
Returns render metadata dict on success.
|
||||
Retries up to 2 times on failure (30s countdown).
|
||||
"""
|
||||
log_task_event(self.request.id, f"Starting render_still_task: {Path(step_path).name}", "info")
|
||||
try:
|
||||
from app.services.render_blender import render_still
|
||||
result = render_still(
|
||||
@@ -86,14 +88,34 @@ def render_still_task(
|
||||
denoising_use_gpu=denoising_use_gpu,
|
||||
mesh_attributes=mesh_attributes or {},
|
||||
)
|
||||
log_task_event(self.request.id, f"Completed successfully in {result.get('total_duration_s', 0):.1f}s", "done")
|
||||
logger.info(
|
||||
"render_still_task completed: %s → %s in %.1fs",
|
||||
Path(step_path).name, Path(output_path).name,
|
||||
result.get("total_duration_s", 0),
|
||||
)
|
||||
try:
|
||||
from app.core.websocket import publish_event_sync
|
||||
publish_event_sync(None, {
|
||||
"type": "render.still.completed",
|
||||
"step_path": Path(step_path).name,
|
||||
"output": Path(output_path).name,
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
except Exception as exc:
|
||||
log_task_event(self.request.id, f"Failed: {exc}", "error")
|
||||
logger.error("render_still_task failed for %s: %s", step_path, exc)
|
||||
try:
|
||||
from app.core.websocket import publish_event_sync
|
||||
publish_event_sync(None, {
|
||||
"type": "render.still.failed",
|
||||
"step_path": Path(step_path).name,
|
||||
"error": str(exc),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
raise self.retry(exc=exc, countdown=30)
|
||||
|
||||
|
||||
@@ -136,6 +158,7 @@ def render_turntable_task(
|
||||
|
||||
Returns render metadata dict on success.
|
||||
"""
|
||||
log_task_event(self.request.id, f"Starting render_turntable_task: {Path(step_path).name}", "info")
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
@@ -211,7 +234,17 @@ def render_turntable_task(
|
||||
f"Blender turntable exited {result.returncode}:\n{result.stdout[-2000:]}"
|
||||
)
|
||||
except Exception as exc:
|
||||
log_task_event(self.request.id, f"Failed: {exc}", "error")
|
||||
logger.error("render_turntable_task failed: %s", exc)
|
||||
try:
|
||||
from app.core.websocket import publish_event_sync
|
||||
publish_event_sync(None, {
|
||||
"type": "render.turntable.failed",
|
||||
"step_path": Path(step_path).name,
|
||||
"error": str(exc),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
raise self.retry(exc=exc, countdown=60)
|
||||
|
||||
# FFmpeg composite: frames → MP4 with optional background
|
||||
@@ -224,6 +257,16 @@ def render_turntable_task(
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise RuntimeError(f"FFmpeg composite failed: {exc.stderr[-500:]}")
|
||||
|
||||
log_task_event(self.request.id, "Completed successfully", "done")
|
||||
try:
|
||||
from app.core.websocket import publish_event_sync
|
||||
publish_event_sync(None, {
|
||||
"type": "render.turntable.completed",
|
||||
"step_path": Path(step_path).name,
|
||||
"output": Path(output_mp4).name,
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return {
|
||||
"output_mp4": str(output_mp4),
|
||||
"frame_count": frame_count,
|
||||
@@ -313,8 +356,10 @@ def render_order_line_still_task(self, order_line_id: str, **params) -> dict:
|
||||
Wraps render_still_task logic but accepts order_line_id instead of step_path.
|
||||
On success, creates a MediaAsset record via publish_asset.
|
||||
"""
|
||||
log_task_event(self.request.id, f"Starting render_order_line_still_task: order_line={order_line_id}", "info")
|
||||
step_path_str, cad_file_id = _resolve_step_path_for_order_line(order_line_id)
|
||||
if not step_path_str:
|
||||
log_task_event(self.request.id, f"Failed: cannot resolve STEP path for order_line {order_line_id}", "error")
|
||||
raise RuntimeError(
|
||||
f"Cannot resolve STEP path for order_line {order_line_id}: "
|
||||
"product missing or has no linked CAD file"
|
||||
@@ -338,13 +383,32 @@ def render_order_line_still_task(self, order_line_id: str, **params) -> dict:
|
||||
str(output_path),
|
||||
render_config=result,
|
||||
)
|
||||
log_task_event(self.request.id, f"Completed successfully in {result.get('total_duration_s', 0):.1f}s", "done")
|
||||
logger.info(
|
||||
"render_order_line_still_task completed for line %s in %.1fs",
|
||||
order_line_id, result.get("total_duration_s", 0),
|
||||
)
|
||||
try:
|
||||
from app.core.websocket import publish_event_sync
|
||||
publish_event_sync(None, {
|
||||
"type": "render.order_line.completed",
|
||||
"order_line_id": order_line_id,
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
except Exception as exc:
|
||||
log_task_event(self.request.id, f"Failed: {exc}", "error")
|
||||
logger.error("render_order_line_still_task failed for %s: %s", order_line_id, exc)
|
||||
try:
|
||||
from app.core.websocket import publish_event_sync
|
||||
publish_event_sync(None, {
|
||||
"type": "render.order_line.failed",
|
||||
"order_line_id": order_line_id,
|
||||
"error": str(exc),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
raise self.retry(exc=exc, countdown=30)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user