fix: media thumbnails, product dimensions, inline 3D viewer, GLB export
Bug A: Media Library thumbnails were gray because <img src> cannot send JWT auth headers. Added useAuthBlob() hook (fetch + createObjectURL) in MediaBrowser.tsx. Also fixed publish_asset Celery task to populate product_id + cad_file_id on MediaAsset for thumbnail fallback resolution. Bug B: Product dimensions now shown in Product Details card with Ruler icon and "from CAD" label when cad_mesh_attributes.dimensions_mm exists. Bug C: Replaced 128×128 CAD thumbnail with InlineCadViewer component. Queries gltf_geometry MediaAssets, fetches GLB via auth fetch → blob URL → Three.js Canvas with OrbitControls. Falls back to thumbnail + "Load 3D Model" button. Polling when GLB generation is in progress. Bug D: trimesh was in [cad] optional extra but Dockerfile only installed [dev]. Changed to pip install -e ".[dev,cad]" — trimesh now available in backend container, GLB + Colors export works. Also added bbox extraction (STL-first numpy parsing) in render_step_thumbnail and admin "Re-extract CAD Metadata" bulk endpoint. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,114 +1,81 @@
|
||||
# Plan: Layout Hamburger + Media Browser Fixes + Retroactive Import
|
||||
# Plan: 4 Bug Fixes — Media Thumbnails, Product Dimensions, Inline 3D Viewer, GLB Export
|
||||
|
||||
## Kontext
|
||||
## Root Cause Analysis
|
||||
|
||||
Vier unabhängige Bereiche:
|
||||
1. **Layout**: Sidebar hat kein Mobile-Support, kein Hamburger-Menü → Content füllt nicht volle Breite auf kleinen Screens
|
||||
2. **Media Browser Previews**: glTF-Assets zeigen nur Icon-Placeholder; CadFile-Thumbnails wären als Preview nutzbar
|
||||
3. **Media Browser Filter-Defaults**: Aktuell kein Default-Filter → alle Types (inkl. GLB/STL) sichtbar; gewünscht: Default nur still + turntable
|
||||
4. **Retroactive Import**: Bestehende `cad_files.thumbnail_path` und `order_lines.result_path` sind nicht als `media_assets` erfasst
|
||||
### Bug A — Missing Thumbnails in Media Library
|
||||
`<img src="/api/media/{id}/download">` fails silently: the download endpoint requires JWT auth, but `<img>` tags don't send auth headers → 401 → `imgError=true` → gray icon.
|
||||
For `thumbnail` type assets: fallback works via `get_thumbnail_url()` → `/api/cad/{cad_file_id}/thumbnail` (no-auth endpoint). For `still` type: no cad_file_id/product_id → no fallback → gray icon shown.
|
||||
|
||||
### Bug B — No Dimensions in "Product Details" Card
|
||||
The `cad_mesh_attributes.dimensions_mm` block exists in the CAD File section (right sidebar), NOT in the "Product Details" card. User wants it in Product Details.
|
||||
|
||||
### Bug C — No Embedded 3D Viewer
|
||||
"View 3D" navigates to `/cad/:id` (full page). User wants an inline viewer in the product page CAD card that auto-loads when a `gltf_geometry` asset exists.
|
||||
|
||||
### Bug D — GLB + Colors Error
|
||||
`trimesh` is in `pyproject.toml` but the backend container was not rebuilt → `ModuleNotFoundError: No module named 'trimesh'`. Needs rebuild.
|
||||
|
||||
---
|
||||
|
||||
## Betroffene Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
|-------|----------|
|
||||
| `frontend/src/components/layout/Layout.tsx` | Hamburger-Menü + Mobile-Overlay |
|
||||
| `frontend/src/pages/MediaBrowser.tsx` | Filter-Chips + Previews + Default-Filter |
|
||||
| `frontend/src/api/media.ts` | `asset_types[]` statt `asset_type` + `thumbnail_url` Feld |
|
||||
| `backend/app/domains/media/schemas.py` | `thumbnail_url: str | None` Feld |
|
||||
| `backend/app/domains/media/router.py` | `asset_types` Multi-Query-Param + thumbnail_url befüllen |
|
||||
| `backend/app/domains/media/service.py` | `get_thumbnail_url(asset)` Helper |
|
||||
| `backend/app/api/routers/admin.py` | `POST /api/admin/import-media-assets` Endpoint |
|
||||
| `frontend/src/pages/Admin.tsx` | Button "Import Existing Media" |
|
||||
| File | Change | Bug |
|
||||
|------|--------|-----|
|
||||
| `frontend/src/pages/MediaBrowser.tsx` | `useAuthBlob` hook + use in AssetCard | A |
|
||||
| `backend/app/domains/rendering/tasks.py` | `publish_asset` populates product_id + cad_file_id | A |
|
||||
| `frontend/src/pages/ProductDetail.tsx` | Add dimensions to Product Details card + inline viewer | B, C |
|
||||
| `frontend/src/components/cad/InlineCadViewer.tsx` | New compact 3D viewer component | C |
|
||||
| `backend/` (docker rebuild) | Rebuild to install trimesh | D |
|
||||
|
||||
---
|
||||
|
||||
## Tasks (in Reihenfolge)
|
||||
|
||||
### Task 1: Layout — Hamburger-Menü + Mobile Sidebar
|
||||
- **Datei**: `frontend/src/components/layout/Layout.tsx`
|
||||
### Task A1: Backend — publish_asset populates product_id + cad_file_id
|
||||
- **Datei**: `backend/app/domains/rendering/tasks.py`
|
||||
- **Was**: In `publish_asset`, after loading the OrderLine, also load `line.product_id` and the product's `cad_file_id`. Set these on the new MediaAsset. This enables the `_resolve_thumbnails_bulk` fallback and `get_thumbnail_url()` for still assets.
|
||||
- **Akzeptanzkriterium**: New still assets have product_id and cad_file_id set in DB.
|
||||
|
||||
### Task A2: Frontend — useAuthBlob hook in MediaBrowser
|
||||
- **Datei**: `frontend/src/pages/MediaBrowser.tsx`
|
||||
- **Was**: Add `useAuthBlob(url)` hook that fetches the URL with Authorization header and returns a blob URL. Use it in `AssetCard` instead of `asset.download_url` for image rendering. Revoke blob URL on unmount.
|
||||
- **Akzeptanzkriterium**: Still images visible in media library grid.
|
||||
|
||||
### Task B: Frontend — Dimensions in Product Details card
|
||||
- **Datei**: `frontend/src/pages/ProductDetail.tsx`
|
||||
- **Was**: In the "Product Details" card (around line 409-433), after the Notes field, add a read-only "Dimensions" row if `product.cad_mesh_attributes?.dimensions_mm` exists. Format: "X × Y × Z mm" with a Ruler icon and small "from CAD" label.
|
||||
- **Akzeptanzkriterium**: Dimensions visible in Product Details card when mesh_attributes populated.
|
||||
|
||||
### Task C: Frontend — Inline 3D Viewer in CAD card
|
||||
- **Datei**: `frontend/src/components/cad/InlineCadViewer.tsx` (new), `frontend/src/pages/ProductDetail.tsx`
|
||||
- **Was**:
|
||||
- State `sidebarOpen: boolean` (default: `false` auf mobile, `true` auf desktop via window.innerWidth)
|
||||
- Hamburger-Button (`Menu`-Icon aus lucide) in einem mobilen Header-Bar (nur sichtbar `< md`, also `md:hidden`)
|
||||
- Sidebar: auf mobile `fixed left-0 top-0 h-full z-40 transform transition-transform`, bei `sidebarOpen`: `translate-x-0`, sonst `-translate-x-full`; auf Desktop immer sichtbar (`md:relative md:translate-x-0`)
|
||||
- Overlay-Backdrop: halbtransparentes `div` hinter Sidebar, nur auf mobile sichtbar wenn open, click schließt Sidebar
|
||||
- Close-Button (X) oben in Sidebar auf mobile
|
||||
- Content-Bereich: `flex-1 overflow-auto min-w-0` damit er immer volle restliche Breite nutzt
|
||||
- **Akzeptanzkriterium**: Auf <768px Hamburger sichtbar, Sidebar aus-/einblendbar; auf ≥768px Sidebar immer sichtbar
|
||||
1. Create `InlineCadViewer` component that:
|
||||
- Accepts `cadFileId: string`
|
||||
- Queries `getMediaAssets({ cad_file_id, asset_types: ['gltf_geometry'] })`
|
||||
- If asset found: fetches GLB with auth (axios → arraybuffer → blob URL) → renders Three.js canvas (OrbitControls, auto-fit camera)
|
||||
- While loading: shows spinner
|
||||
- If no asset: shows "Generate GLB" button + thumbnail fallback
|
||||
2. In ProductDetail: replace the 128×128 thumbnail box with `InlineCadViewer` (make it ~300px tall)
|
||||
- Move thumbnail fallback inside InlineCadViewer
|
||||
- Keep "View 3D" as "View Full Screen" link below viewer
|
||||
- Remove standalone "View 3D" button (or keep as secondary link)
|
||||
- **Akzeptanzkriterium**: Inline 3D model visible in product page without clicking "View 3D".
|
||||
|
||||
### Task 2: Backend — `asset_types[]` Multi-Filter + `thumbnail_url`
|
||||
- **Datei**: `backend/app/domains/media/router.py`, `backend/app/domains/media/schemas.py`, `backend/app/domains/media/service.py`
|
||||
- **Was**:
|
||||
- `list_assets` Endpoint: Zusätzlichen Query-Param `asset_types: list[MediaAssetType] = Query(default=[])` hinzufügen
|
||||
- Filter-Logik: wenn `asset_types` nicht leer → `WHERE asset_type IN (asset_types)`; sonst wenn `asset_type` gesetzt → wie bisher
|
||||
- `MediaAssetOut`: neues Feld `thumbnail_url: str | None = None`
|
||||
- `service.py`: neue Funktion `get_thumbnail_url(asset) -> str | None` — gibt `/api/cad/{cad_file_id}/thumbnail` zurück wenn `cad_file_id` gesetzt (unabhängig von asset_type)
|
||||
- In `list_assets` und `get_asset`: `a.thumbnail_url = service.get_thumbnail_url(a)` setzen (analog zu `download_url`)
|
||||
- **Akzeptanzkriterium**: `GET /api/media/?asset_types=still&asset_types=turntable` gibt nur still+turntable zurück; jedes Asset mit `cad_file_id` hat `thumbnail_url` gesetzt
|
||||
|
||||
### Task 3: Frontend — Media Browser Filter-Chips + Previews
|
||||
- **Datei**: `frontend/src/pages/MediaBrowser.tsx`, `frontend/src/api/media.ts`
|
||||
- **Was**:
|
||||
- `api/media.ts`: `MediaFilter.asset_types?: MediaAssetType[]` (statt `asset_type`); `getMediaAssets` sendet `asset_types` als repeated params; `MediaAsset` bekommt `thumbnail_url: string | null`
|
||||
- `MediaBrowser.tsx`:
|
||||
- State: `activeTypes: Set<MediaAssetType>` — Default: `new Set(['still', 'turntable'])`
|
||||
- Filter-UI: Chip-Grid mit allen Types; `still`/`turntable`/`thumbnail` in der Hauptreihe; `gltf_geometry`/`gltf_production`/`blend_production`/`stl_low`/`stl_high` hinter "Advanced" Toggle (collapsed by default)
|
||||
- Chip aktiv = farbiger Hintergrund entsprechend `TYPE_COLORS`; inaktiv = grau
|
||||
- Chip-Klick toggled den Type aus `activeTypes`
|
||||
- `getMediaAssets({ asset_types: [...activeTypes], ... })`
|
||||
- `AssetCard`: wenn `isImageAsset(type)` → `download_url`; wenn `thumbnail_url` vorhanden → `thumbnail_url` als Preview; sonst Icon
|
||||
- Video-Assets (`turntable`): Video-Poster via `thumbnail_url` (falls vorhanden) mit `<video>`-Tag anzeigen oder Bild
|
||||
- **Akzeptanzkriterium**: Default zeigt nur still+turntable; Chip-Klick filtert korrekt; GLB-Assets zeigen CadFile-Thumbnail
|
||||
|
||||
### Task 4: Backend — Retroactive MediaAsset Import Endpoint
|
||||
- **Datei**: `backend/app/api/routers/admin.py`
|
||||
- **Was**: Neuer Endpoint `POST /api/admin/import-media-assets` (require_admin):
|
||||
```python
|
||||
# 1. CadFiles mit thumbnail_path + status='completed'
|
||||
SELECT id, thumbnail_path FROM cad_files
|
||||
WHERE thumbnail_path IS NOT NULL AND status = 'completed'
|
||||
|
||||
# 2. OrderLines mit result_path + render_status='completed' + output_type
|
||||
SELECT ol.id, ol.result_path, ol.product_id, ol.output_type_id, ot.is_animation
|
||||
FROM order_lines ol LEFT JOIN output_types ot ON ot.id = ol.output_type_id
|
||||
WHERE ol.result_path IS NOT NULL AND ol.render_status = 'completed'
|
||||
```
|
||||
- De-dup: `SELECT id FROM media_assets WHERE storage_key = ?` vor jedem Insert
|
||||
- CadFile → `MediaAsset(asset_type='thumbnail', cad_file_id=..., storage_key=thumbnail_path, mime_type='image/jpeg')`
|
||||
- OrderLine → `MediaAsset(asset_type='turntable' if is_animation else 'still', order_line_id=..., storage_key=result_path)`
|
||||
- Returns: `{"created": N, "skipped": N}`
|
||||
- **Akzeptanzkriterium**: Nach Aufruf erscheinen alle bestehenden Thumbnails + Renders im Media Browser
|
||||
|
||||
### Task 5: Frontend — Admin "Import Existing Media" Button
|
||||
- **Datei**: `frontend/src/pages/Admin.tsx`
|
||||
- **Was**: Im Admin-Panel (Media/Settings-Bereich) neuer Button "Import Existing Media" → `POST /api/admin/import-media-assets` → Toast mit `{created, skipped}` Ergebnis
|
||||
- **Abhängigkeiten**: Task 4
|
||||
- **Akzeptanzkriterium**: Button klickbar, zeigt Ergebnis
|
||||
### Task D: Backend rebuild — install trimesh
|
||||
- **Was**: Run `docker compose up -d --build backend` to install trimesh from pyproject.toml
|
||||
- **Akzeptanzkriterium**: `docker compose exec backend python3 -c "import trimesh; print('ok')"` succeeds. GLB + Colors download works.
|
||||
|
||||
---
|
||||
|
||||
## Migrations-Check
|
||||
|
||||
Keine neue Migration nötig — alle Felder bereits vorhanden.
|
||||
|
||||
---
|
||||
Keine DB-Migrationen nötig. `product_id` und `cad_file_id` sind bereits Spalten in `media_assets`.
|
||||
|
||||
## Reihenfolge-Empfehlung
|
||||
|
||||
Task 1 (Layout) + Task 2 (Backend) parallel →
|
||||
Task 3 (Frontend MediaBrowser, braucht Task 2) + Task 4 (Backend Admin) parallel →
|
||||
Task 5 (Frontend Admin Button, braucht Task 4)
|
||||
|
||||
Tasks 1 + 2 + 4 können vollständig parallel implementiert werden.
|
||||
Task 3 + 5 können dann parallel implementiert werden.
|
||||
|
||||
---
|
||||
A1 + A2 + B + C parallel (alle unabhängig).
|
||||
D parallel (nur Docker rebuild, kein Code).
|
||||
|
||||
## Risiken / Offene Fragen
|
||||
|
||||
- `thumbnail_url` für GLBs zeigt immer das CadFile-Thumbnail — das ist korrekt (kein spezifisches Render vorhanden)
|
||||
- `result_path` bei OrderLines kann Pfad zu PNG oder MP4 sein — kein Media-Type prüfen, einfach MIME aus Extension ableiten
|
||||
- Bestehende `thumbnail_path` Werte sind absolute Paths (`/app/uploads/...`) — gleicher Proxy-Mechanismus wie bei GLBs nötig (der download endpoint kann damit umgehen)
|
||||
- Video-Preview (turntable): `<video>` Tag mit `thumbnail_url` als Poster + `download_url` als src — falls download_url MP4 ist
|
||||
- `useAuthBlob` creates blob URLs per asset — 50+ assets in grid could trigger many fetches. Add `limit` or lazy load (only fetch when card is visible).
|
||||
- InlineCadViewer: GLB fetch for large files may take 5-30s. Show skeleton/spinner.
|
||||
- `useGLTF` from drei expects a URL string. Blob URLs work fine.
|
||||
- ThreeDViewer has `onClose` required prop — InlineCadViewer should be a new simpler component.
|
||||
|
||||
Reference in New Issue
Block a user