@@ -616,28 +644,32 @@ export default function ProductDetailPage() {
Geometry
- {mesh_attrs.volume_mm3 != null && (
+ {dims != null && (
+ <>
+
Dimensions
+
{dims.x.toFixed(1)} × {dims.y.toFixed(1)} × {dims.z.toFixed(1)} mm
+ >
+ )}
+ {dims == null && bbox != null && (
+ <>
+
BBox
+
+ {bbox.x?.toFixed(1)} × {bbox.y?.toFixed(1)} × {bbox.z?.toFixed(1)} mm
+
+ >
+ )}
+ {(mesh_attrs.volume_mm3 as number | undefined) != null && (
<>
Volume
{((mesh_attrs.volume_mm3 as number) / 1000).toFixed(2)} cm³
>
)}
- {mesh_attrs.surface_area_mm2 != null && (
+ {(mesh_attrs.surface_area_mm2 as number | undefined) != null && (
<>
Surface
{((mesh_attrs.surface_area_mm2 as number) / 100).toFixed(1)} cm²
>
)}
- {mesh_attrs.bbox != null && (
- <>
-
BBox
-
- {(mesh_attrs.bbox as { x?: number; y?: number; z?: number }).x?.toFixed(1)} ×{' '}
- {(mesh_attrs.bbox as { x?: number; y?: number; z?: number }).y?.toFixed(1)} ×{' '}
- {(mesh_attrs.bbox as { x?: number; y?: number; z?: number }).z?.toFixed(1)} mm
-
- >
- )}
{mesh_attrs.suggested_smooth_angle !== undefined && (
<>
Sharp angle
diff --git a/plan.md b/plan.md
index 6d79f78..35711fa 100644
--- a/plan.md
+++ b/plan.md
@@ -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
+`

` fails silently: the download endpoint requires JWT auth, but `
![]()
` 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
` — 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 `