fix(admin): limit generate-missing-usd-masters to product-linked CadFiles

Previously the endpoint queued USD generation for ALL 295 completed CadFiles,
including 250 orphan CadFiles not linked to any product. Now filters to only
CadFiles referenced by at least one Product.cad_file_id, reducing the backfill
from ~285 to ~41 tasks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 22:23:32 +01:00
parent 8e1cd41868
commit e7b70a35ea
+34 -20
View File
@@ -12,7 +12,7 @@ from app.models.system_setting import SystemSetting
from app.models.cad_file import CadFile, ProcessingStatus from app.models.cad_file import CadFile, ProcessingStatus
from app.models.output_type import OutputType as OutputTypeModel from app.models.output_type import OutputType as OutputTypeModel
from app.schemas.user import UserOut, UserUpdate, UserCreate from app.schemas.user import UserOut, UserUpdate, UserCreate
from app.utils.auth import require_admin, hash_password from app.utils.auth import require_global_admin, hash_password
router = APIRouter(prefix="/admin", tags=["admin"]) router = APIRouter(prefix="/admin", tags=["admin"])
@@ -122,7 +122,7 @@ class SettingsUpdate(BaseModel):
@router.get("/users", response_model=list[UserOut]) @router.get("/users", response_model=list[UserOut])
async def list_users( async def list_users(
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
result = await db.execute(select(User).order_by(User.created_at.desc())) result = await db.execute(select(User).order_by(User.created_at.desc()))
@@ -132,7 +132,7 @@ async def list_users(
@router.post("/users", response_model=UserOut, status_code=status.HTTP_201_CREATED) @router.post("/users", response_model=UserOut, status_code=status.HTTP_201_CREATED)
async def create_user( async def create_user(
body: UserCreate, body: UserCreate,
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
result = await db.execute(select(User).where(User.email == body.email)) result = await db.execute(select(User).where(User.email == body.email))
@@ -155,7 +155,7 @@ async def create_user(
async def update_user( async def update_user(
user_id: uuid.UUID, user_id: uuid.UUID,
body: UserUpdate, body: UserUpdate,
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
result = await db.execute(select(User).where(User.id == user_id)) result = await db.execute(select(User).where(User.id == user_id))
@@ -173,7 +173,7 @@ async def update_user(
@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT) @router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user( async def delete_user(
user_id: uuid.UUID, user_id: uuid.UUID,
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
result = await db.execute(select(User).where(User.id == user_id)) result = await db.execute(select(User).where(User.id == user_id))
@@ -241,7 +241,7 @@ def _settings_to_out(raw: dict[str, str]) -> SettingsOut:
@router.get("/settings", response_model=SettingsOut) @router.get("/settings", response_model=SettingsOut)
async def get_settings( async def get_settings(
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
return _settings_to_out(await _load_settings(db)) return _settings_to_out(await _load_settings(db))
@@ -250,7 +250,7 @@ async def get_settings(
@router.put("/settings", response_model=SettingsOut) @router.put("/settings", response_model=SettingsOut)
async def update_settings( async def update_settings(
body: SettingsUpdate, body: SettingsUpdate,
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
if body.thumbnail_renderer is not None and body.thumbnail_renderer not in VALID_RENDERERS: if body.thumbnail_renderer is not None and body.thumbnail_renderer not in VALID_RENDERERS:
@@ -373,7 +373,7 @@ async def update_settings(
@router.post("/settings/process-unprocessed", status_code=status.HTTP_202_ACCEPTED) @router.post("/settings/process-unprocessed", status_code=status.HTTP_202_ACCEPTED)
async def process_unprocessed_steps( async def process_unprocessed_steps(
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""Queue all STEP files that are not yet completed. """Queue all STEP files that are not yet completed.
@@ -416,7 +416,7 @@ async def process_unprocessed_steps(
@router.post("/settings/regenerate-thumbnails", status_code=status.HTTP_202_ACCEPTED) @router.post("/settings/regenerate-thumbnails", status_code=status.HTTP_202_ACCEPTED)
async def regenerate_thumbnails( async def regenerate_thumbnails(
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""Re-queue completed CAD files that are linked to a product for thumbnail regeneration.""" """Re-queue completed CAD files that are linked to a product for thumbnail regeneration."""
@@ -439,7 +439,7 @@ async def regenerate_thumbnails(
@router.get("/settings/orphaned-cad-files") @router.get("/settings/orphaned-cad-files")
async def get_orphaned_cad_files( async def get_orphaned_cad_files(
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""Return count and total disk size of CadFiles not linked to any product.""" """Return count and total disk size of CadFiles not linked to any product."""
@@ -459,7 +459,7 @@ async def get_orphaned_cad_files(
@router.post("/settings/cleanup-orphaned-cad-files") @router.post("/settings/cleanup-orphaned-cad-files")
async def cleanup_orphaned_cad_files( async def cleanup_orphaned_cad_files(
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""Delete CadFile DB records and associated files on disk for all orphaned CadFiles. """Delete CadFile DB records and associated files on disk for all orphaned CadFiles.
@@ -504,7 +504,7 @@ async def cleanup_orphaned_cad_files(
@router.post("/settings/reextract-metadata", status_code=status.HTTP_202_ACCEPTED) @router.post("/settings/reextract-metadata", status_code=status.HTTP_202_ACCEPTED)
async def reextract_all_metadata( async def reextract_all_metadata(
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""Re-extract OCC metadata (dimensions, sharp edges) for all completed CAD files. """Re-extract OCC metadata (dimensions, sharp edges) for all completed CAD files.
@@ -534,7 +534,7 @@ async def reextract_all_metadata(
@router.post("/settings/generate-missing-canonical-scenes", status_code=status.HTTP_202_ACCEPTED) @router.post("/settings/generate-missing-canonical-scenes", status_code=status.HTTP_202_ACCEPTED)
async def generate_missing_canonical_scenes( async def generate_missing_canonical_scenes(
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""Queue canonical scene (geometry GLB + USD master) generation for every completed CAD file that has no gltf_geometry MediaAsset.""" """Queue canonical scene (geometry GLB + USD master) generation for every completed CAD file that has no gltf_geometry MediaAsset."""
@@ -565,14 +565,28 @@ async def generate_missing_canonical_scenes(
@router.post("/settings/generate-missing-usd-masters", status_code=status.HTTP_202_ACCEPTED) @router.post("/settings/generate-missing-usd-masters", status_code=status.HTTP_202_ACCEPTED)
async def generate_missing_usd_masters( async def generate_missing_usd_masters(
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""Queue USD master export for every completed CAD file that has no usd_master MediaAsset.""" """Queue USD master export for completed CAD files linked to a product that have no usd_master MediaAsset.
Only CadFiles referenced by at least one Product are included — orphan CadFiles
(uploaded but never linked to a product) are skipped to avoid unnecessary work.
"""
from app.domains.media.models import MediaAsset, MediaAssetType from app.domains.media.models import MediaAsset, MediaAssetType
from app.domains.products.models import Product
# Only CadFiles that are actually used by a product
product_cad_ids_result = await db.execute(
select(Product.cad_file_id).where(Product.cad_file_id.isnot(None)).distinct()
)
product_cad_ids = {row[0] for row in product_cad_ids_result.all()}
result = await db.execute( result = await db.execute(
select(CadFile).where(CadFile.processing_status == ProcessingStatus.completed) select(CadFile).where(
CadFile.processing_status == ProcessingStatus.completed,
CadFile.id.in_(product_cad_ids),
)
) )
cad_files = result.scalars().all() cad_files = result.scalars().all()
@@ -595,7 +609,7 @@ async def generate_missing_usd_masters(
@router.post("/settings/recover-stuck-processing", status_code=status.HTTP_200_OK) @router.post("/settings/recover-stuck-processing", status_code=status.HTTP_200_OK)
async def recover_stuck_processing( async def recover_stuck_processing(
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""Reset CAD files stuck in 'processing' for more than 10 minutes to 'failed'. """Reset CAD files stuck in 'processing' for more than 10 minutes to 'failed'.
@@ -629,7 +643,7 @@ async def recover_stuck_processing(
@router.post("/settings/seed-workflows", status_code=status.HTTP_200_OK) @router.post("/settings/seed-workflows", status_code=status.HTTP_200_OK)
async def seed_workflows( async def seed_workflows(
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
"""Create the standard workflow definitions if they do not already exist.""" """Create the standard workflow definitions if they do not already exist."""
@@ -685,7 +699,7 @@ async def seed_workflows(
@router.get("/settings/renderer-status") @router.get("/settings/renderer-status")
async def renderer_status( async def renderer_status(
admin: User = Depends(require_admin), admin: User = Depends(require_global_admin),
): ):
"""Check health of renderer services.""" """Check health of renderer services."""
from app.services.render_blender import find_blender, is_blender_available from app.services.render_blender import find_blender, is_blender_available
@@ -706,7 +720,7 @@ async def renderer_status(
@router.post("/import-media-assets") @router.post("/import-media-assets")
async def import_existing_media_assets( async def import_existing_media_assets(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_admin), current_user: User = Depends(require_global_admin),
): ):
"""Import existing cad thumbnails and order line renders as MediaAsset records.""" """Import existing cad thumbnails and order line renders as MediaAsset records."""
from app.domains.media.models import MediaAsset, MediaAssetType from app.domains.media.models import MediaAsset, MediaAssetType