f5ca91ee02
- 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>
7.0 KiB
7.0 KiB
Plan: Layout Hamburger + Media Browser Fixes + Retroactive Import
Kontext
Vier unabhängige Bereiche:
- Layout: Sidebar hat kein Mobile-Support, kein Hamburger-Menü → Content füllt nicht volle Breite auf kleinen Screens
- Media Browser Previews: glTF-Assets zeigen nur Icon-Placeholder; CadFile-Thumbnails wären als Preview nutzbar
- Media Browser Filter-Defaults: Aktuell kein Default-Filter → alle Types (inkl. GLB/STL) sichtbar; gewünscht: Default nur still + turntable
- Retroactive Import: Bestehende
cad_files.thumbnail_pathundorder_lines.result_pathsind nicht alsmedia_assetserfasst
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 |
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:falseauf mobile,trueauf desktop via window.innerWidth) - Hamburger-Button (
Menu-Icon aus lucide) in einem mobilen Header-Bar (nur sichtbar< md, alsomd:hidden) - Sidebar: auf mobile
fixed left-0 top-0 h-full z-40 transform transition-transform, beisidebarOpen:translate-x-0, sonst-translate-x-full; auf Desktop immer sichtbar (md:relative md:translate-x-0) - Overlay-Backdrop: halbtransparentes
divhinter 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-0damit er immer volle restliche Breite nutzt
- State
- 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_assetsEndpoint: Zusätzlichen Query-Paramasset_types: list[MediaAssetType] = Query(default=[])hinzufügen- Filter-Logik: wenn
asset_typesnicht leer →WHERE asset_type IN (asset_types); sonst wennasset_typegesetzt → wie bisher MediaAssetOut: neues Feldthumbnail_url: str | None = Noneservice.py: neue Funktionget_thumbnail_url(asset) -> str | None— gibt/api/cad/{cad_file_id}/thumbnailzurück wenncad_file_idgesetzt (unabhängig von asset_type)- In
list_assetsundget_asset:a.thumbnail_url = service.get_thumbnail_url(a)setzen (analog zudownload_url)
- Akzeptanzkriterium:
GET /api/media/?asset_types=still&asset_types=turntablegibt nur still+turntable zurück; jedes Asset mitcad_file_idhatthumbnail_urlgesetzt
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[](stattasset_type);getMediaAssetssendetasset_typesals repeated params;MediaAssetbekommtthumbnail_url: string | nullMediaBrowser.tsx:- State:
activeTypes: Set<MediaAssetType>— Default:new Set(['still', 'turntable']) - Filter-UI: Chip-Grid mit allen Types;
still/turntable/thumbnailin der Hauptreihe;gltf_geometry/gltf_production/blend_production/stl_low/stl_highhinter "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: wennisImageAsset(type)→download_url; wennthumbnail_urlvorhanden →thumbnail_urlals Preview; sonst Icon- Video-Assets (
turntable): Video-Poster viathumbnail_url(falls vorhanden) mit<video>-Tag anzeigen oder Bild
- State:
- 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):# 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}
- De-dup:
- 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_urlfür GLBs zeigt immer das CadFile-Thumbnail — das ist korrekt (kein spezifisches Render vorhanden)result_pathbei OrderLines kann Pfad zu PNG oder MP4 sein — kein Media-Type prüfen, einfach MIME aus Extension ableiten- Bestehende
thumbnail_pathWerte sind absolute Paths (/app/uploads/...) — gleicher Proxy-Mechanismus wie bei GLBs nötig (der download endpoint kann damit umgehen) - Video-Preview (turntable):
<video>Tag mitthumbnail_urlals Poster +download_urlals src — falls download_url MP4 ist