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

7.0 KiB

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
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):
    # 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