feat: per-position camera settings, material alias dialog, product delete, media browser links
- Per-render-position focal_length_mm/sensor_width_mm (DB → pipeline → Blender)
- FOV-based camera distance with min clamp fix for wide-angle lenses
- Unmapped materials blocking dialog on "Dispatch Renders" with batch alias creation
- Material check endpoint (GET /orders/{id}/check-materials)
- Batch alias endpoint (POST /materials/batch-aliases)
- Quick-map "No alias" badges on Materials page
- Full product hard-delete with storage cleanup (MinIO + disk files + orphaned CadFile)
- Delete button on ProductDetail page with confirmation
- Clickable product names in Media Browser (links to product page)
- Single-line render dispatch/retry (POST /orders/{id}/lines/{id}/dispatch-render)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -825,3 +825,60 @@ async def import_existing_media_assets(
|
||||
await db.commit()
|
||||
return {"created": created, "skipped": skipped}
|
||||
|
||||
|
||||
@router.delete("/settings/purge-render-media", status_code=status.HTTP_200_OK)
|
||||
async def purge_render_media(
|
||||
admin: User = Depends(require_global_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Delete all still and turntable MediaAsset records and their backing files.
|
||||
|
||||
This removes rendered images and animations but leaves thumbnails, GLBs,
|
||||
STLs, and USD masters intact.
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from app.config import settings
|
||||
from app.core.storage import get_storage
|
||||
from app.domains.media.models import MediaAsset, MediaAssetType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
storage = get_storage()
|
||||
|
||||
result = await db.execute(
|
||||
select(MediaAsset).where(
|
||||
MediaAsset.asset_type.in_([MediaAssetType.still, MediaAssetType.turntable])
|
||||
)
|
||||
)
|
||||
assets = result.scalars().all()
|
||||
|
||||
deleted_db = 0
|
||||
deleted_files = 0
|
||||
freed_bytes = 0
|
||||
|
||||
for asset in assets:
|
||||
# Delete backing file
|
||||
key = asset.storage_key
|
||||
try:
|
||||
candidate = Path(key) if Path(key).is_absolute() else Path(settings.upload_dir) / key
|
||||
if candidate.exists():
|
||||
freed_bytes += candidate.stat().st_size
|
||||
candidate.unlink()
|
||||
deleted_files += 1
|
||||
elif hasattr(storage, 'delete'):
|
||||
storage.delete(key)
|
||||
deleted_files += 1
|
||||
except Exception as exc:
|
||||
logger.warning("Could not delete file for asset %s (%s): %s", asset.id, key, exc)
|
||||
|
||||
await db.delete(asset)
|
||||
deleted_db += 1
|
||||
|
||||
await db.commit()
|
||||
return {
|
||||
"deleted_records": deleted_db,
|
||||
"deleted_files": deleted_files,
|
||||
"freed_mb": round(freed_bytes / 1024 / 1024, 1),
|
||||
"message": f"Purged {deleted_db} still/turntable asset(s), freed {round(freed_bytes / 1024 / 1024, 1)} MB",
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user