Files
HartOMat/plan.md
T
Hartmut f5ca91ee02 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>
2026-03-07 00:09:27 +01:00

115 lines
7.0 KiB
Markdown

# Plan: Layout Hamburger + Media Browser Fixes + Retroactive Import
## Kontext
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
---
## 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" |
---
## Tasks (in Reihenfolge)
### Task 1: Layout — Hamburger-Menü + Mobile Sidebar
- **Datei**: `frontend/src/components/layout/Layout.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
### 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
---
## Migrations-Check
Keine neue Migration nötig — alle Felder bereits vorhanden.
---
## 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.
---
## 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