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>
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
# Plan: UI-Vollständigkeit + Workflows — Phase O
|
||||
# Plan: Layout Hamburger + Media Browser Fixes + Retroactive Import
|
||||
|
||||
**Ziel**: Alle implementierten Backend-Features im UI zugänglich machen + v3-Workflows vollständig verdrahten.
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
@@ -8,232 +14,101 @@
|
||||
|
||||
| Datei | Änderung |
|
||||
|-------|----------|
|
||||
| `frontend/src/components/layout/Layout.tsx` | Upload-Link hinzufügen |
|
||||
| `frontend/src/pages/Admin.tsx` | OutputType-Tabelle: Workflow-Dropdown |
|
||||
| `frontend/src/pages/AssetLibrary.tsx` | NEU: Asset Library Management UI |
|
||||
| `frontend/src/api/asset_libraries.ts` | NEU: API-Client |
|
||||
| `frontend/src/pages/ProductDetail.tsx` | Mesh-Attribute-Anzeige |
|
||||
| `frontend/src/pages/Upload.tsx` | Sanity-Check-Dialog nach Import |
|
||||
| `frontend/src/api/imports.ts` | NEU: import_validation API |
|
||||
| `frontend/src/App.tsx` | Route /asset-libraries |
|
||||
| `backend/app/api/routers/notification_configs.py` | NEU: notification_configs CRUD |
|
||||
| `backend/app/main.py` | notification_configs router registrieren |
|
||||
| `backend/app/api/routers/orders.py` | dispatch_renders → dispatch_render_with_workflow |
|
||||
| `backend/app/api/routers/output_types.py` | workflow_definition_id im PATCH |
|
||||
| `backend/app/schemas/output_type.py` | workflow_definition_id im Schema |
|
||||
| `backend/app/domains/rendering/tasks.py` | K3: apply_asset_library_materials_task |
|
||||
| `backend/app/tasks/step_tasks.py` | OCC sharp edge extraction in render_step_thumbnail |
|
||||
| `render-worker/scripts/still_render.py` | mark_sharp / UV seams support |
|
||||
| `render-worker/scripts/blender_render.py` | mark_sharp / UV seams support |
|
||||
| `backend/app/services/step_processor.py` | extract_mesh_edge_data() für sharp edges |
|
||||
| `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
|
||||
## Tasks (in Reihenfolge)
|
||||
|
||||
### Task 1: Upload-Link in Sidebar [QUICK WIN]
|
||||
### Task 1: Layout — Hamburger-Menü + Mobile Sidebar
|
||||
- **Datei**: `frontend/src/components/layout/Layout.tsx`
|
||||
- **Was**: `Upload`-Icon + NavLink zu `/upload` in der Sidebar für alle eingeloggten User
|
||||
- **Akzeptanzkriterium**: Upload-Link sichtbar in Sidebar
|
||||
|
||||
### Task 2: notification_configs Backend-Router [Phase I]
|
||||
- **Datei**: `backend/app/api/routers/notification_configs.py` (NEU), `backend/app/main.py`
|
||||
- **Was**: REST-Endpoints für `notification_configs` Tabelle (044 bereits migriert):
|
||||
- `GET /api/notification-configs` — gibt configs für aktuellen User zurück (mit Defaults falls keine Zeilen)
|
||||
- `PUT /api/notification-configs/{event_type}/{channel}` — setzt enabled=true/false
|
||||
- `POST /api/notification-configs/reset` — löscht alle configs des Users → Defaults gelten wieder
|
||||
- Response: `[{event_type, channel, enabled}]`
|
||||
- Auth: `get_current_user` (jeder kann seine eigenen Configs verwalten)
|
||||
- **Akzeptanzkriterium**: NotificationSettings.tsx zeigt Toggle-Matrix und speichert korrekt
|
||||
|
||||
### Task 3: OutputType → WorkflowDefinition — Schema + API
|
||||
- **Datei**: `backend/app/schemas/output_type.py`, `backend/app/api/routers/output_types.py`
|
||||
- **Was**:
|
||||
- `OutputTypeOut` + `OutputTypePatch`: `workflow_definition_id: uuid.UUID | None` hinzufügen
|
||||
- PATCH-Handler: `workflow_definition_id` setzen wenn in body
|
||||
- `OutputTypeOut` soll `workflow_name: str | None` als convenience field enthalten
|
||||
- **Akzeptanzkriterium**: `PATCH /api/output-types/{id}` mit `{"workflow_definition_id": "..."}` funktioniert
|
||||
- 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 4: Workflow-Dispatch Integration
|
||||
- **Datei**: `backend/app/api/routers/orders.py`
|
||||
- **Was**: In `dispatch_renders()` (Zeile 910):
|
||||
- Statt `dispatch_order_line_render.delay(str(line.id))` aufrufen:
|
||||
- `from app.domains.rendering.dispatch_service import dispatch_render_with_workflow`
|
||||
- `dispatch_render_with_workflow(str(line.id))` aufrufen
|
||||
- Das dispatch_service lädt OutputType.workflow_definition_id und nutzt Celery Canvas falls verknüpft; fällt auf Legacy zurück wenn nicht.
|
||||
- **Akzeptanzkriterium**: Dispatch nutzt neuen Pfad; Legacy-Fallback bleibt erhalten
|
||||
|
||||
### Task 5: Asset Library API-Client (Frontend)
|
||||
- **Datei**: `frontend/src/api/asset_libraries.ts` (NEU)
|
||||
### 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**:
|
||||
```typescript
|
||||
export interface AssetLibrary { id, name, description, original_filename, catalog: {materials: string[], node_groups: string[]}, is_active, created_at }
|
||||
export async function listAssetLibraries(): Promise<AssetLibrary[]>
|
||||
export async function uploadAssetLibrary(name: string, file: File, description?: string): Promise<AssetLibrary>
|
||||
export async function refreshLibraryCatalog(id: string): Promise<AssetLibrary>
|
||||
export async function deleteAssetLibrary(id: string): Promise<void>
|
||||
export async function updateAssetLibrary(id: string, data: Partial<AssetLibrary>): Promise<AssetLibrary>
|
||||
```
|
||||
- **Akzeptanzkriterium**: TypeScript kompiliert fehlerfrei
|
||||
- `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 6: Asset Library Management Page (K2)
|
||||
- **Datei**: `frontend/src/pages/AssetLibrary.tsx` (NEU)
|
||||
- **Was**: Seite `/asset-libraries` (admin/PM):
|
||||
- Liste der Asset Libraries als Karten: Name, Filename, Badge-Grid mit Materialien/Node-Groups aus `catalog`
|
||||
- Upload-Button: Datei-Input für `.blend` + Name-Feld → `uploadAssetLibrary()`
|
||||
- "Refresh Catalog" Button je Library → `refreshLibraryCatalog(id)` → Toast
|
||||
- Toggle `is_active` → `updateAssetLibrary()`
|
||||
- Delete-Button → `deleteAssetLibrary()`
|
||||
- Leer-Zustand: "No asset libraries yet — upload a .blend file"
|
||||
- **Akzeptanzkriterium**: Libraries hochladen, Katalog anzeigen, löschen
|
||||
|
||||
### Task 7: Asset Library Route + Sidebar-Link
|
||||
- **Datei**: `frontend/src/App.tsx`, `frontend/src/components/layout/Layout.tsx`
|
||||
### Task 3: Frontend — Media Browser Filter-Chips + Previews
|
||||
- **Datei**: `frontend/src/pages/MediaBrowser.tsx`, `frontend/src/api/media.ts`
|
||||
- **Was**:
|
||||
- App.tsx: Route `/asset-libraries` → `<AssetLibraryPage />` (AdminRoute)
|
||||
- Layout.tsx: Sidebar-Link "Asset Libraries" mit `Library`-Icon (admin/PM)
|
||||
- **Abhängigkeiten**: Task 6
|
||||
- `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 8: OutputType Workflow-Dropdown (Frontend)
|
||||
- **Datei**: `frontend/src/pages/Admin.tsx` (OutputTypeTable-Bereich)
|
||||
- **Was**: In der OutputType-Tabelle eine neue Spalte "Workflow":
|
||||
- Dropdown mit allen WorkflowDefinitions (aus `GET /api/workflows`) + "— None —"
|
||||
- Bei Änderung: `PATCH /api/output-types/{id}` mit `{workflow_definition_id: ...}`
|
||||
- Wenn kein Workflow: zeige "Legacy" Badge; wenn Workflow: zeige Workflow-Name als grünes Badge
|
||||
- **Akzeptanzkriterium**: Workflow kann pro OutputType zugewiesen werden
|
||||
|
||||
### Task 9: Excel Sanity-Check Backend (Phase H)
|
||||
- **Datei**: `backend/app/domains/imports/sanity_check.py` (NEU), `backend/app/domains/imports/router.py`
|
||||
- **Was**:
|
||||
- Sync-Funktion `run_sanity_check(import_validation_id: str)`:
|
||||
- Lädt ImportValidation-Record
|
||||
- Iteriert über `rows` (ParsedRows aus Excel)
|
||||
- Für jede Zeile: prüft ob `name_cad_modell` eine CadFile zugeordnet hat (`cad_files.original_name ILIKE`)
|
||||
- Prüft ob `cad_part_materials` alle Materialien in `materials`-Tabelle (via Alias-Lookup) auflösbar sind
|
||||
- Erstellt `summary: {total_rows, rows_with_cad, rows_without_cad, material_gaps: [{product, missing_material}]}`
|
||||
- Status → 'completed'
|
||||
- Celery-Task `validate_excel_import_task(import_validation_id)` Queue `step_processing`
|
||||
- Endpoint `GET /api/imports/{id}/validation` — gibt ImportValidation zurück
|
||||
- Endpoint `POST /api/imports/{id}/add-alias` — schnell einen Alias hinzufügen (part_name → material)
|
||||
- ImportValidation DB-Zugriif: sync SQLAlchemy (Celery-kompatibel)
|
||||
- **Akzeptanzkriterium**: Nach Excel-Upload wird Import-Validierung automatisch gequeuet; `summary` liefert Material-Lücken
|
||||
|
||||
### Task 10: Upload.tsx — Sanity-Check-Dialog (Phase H)
|
||||
- **Datei**: `frontend/src/pages/Upload.tsx`
|
||||
- **Was**: Nach erfolgreichem Excel-Upload:
|
||||
- `GET /api/imports/{id}/validation` pollen (alle 3s, max 30s)
|
||||
- Wenn status='completed': Ampel-Dialog anzeigen:
|
||||
- Grün-Badge: "X Produkte mit STEP-Datei"
|
||||
- Gelb-Badge: "Y Produkte ohne STEP-Datei"
|
||||
- Rote Liste: Material-Lücken (Part-Name → fehlendes Material, mit "Add Alias" Button)
|
||||
- "Proceed" Button schließt Dialog
|
||||
- Import API erweitern: `api/imports.ts` mit `getImportValidation(id)`, `addMaterialAlias()`
|
||||
- **Akzeptanzkriterium**: Nach Upload erscheint Dialog mit Produktions-Readiness
|
||||
|
||||
### Task 11: Mesh-Attribute Anzeige in ProductDetail (Phase D)
|
||||
- **Datei**: `frontend/src/pages/ProductDetail.tsx`
|
||||
- **Was**: Im CAD-File-Bereich, nach dem Status-Badge:
|
||||
- Wenn `product.cad_file.mesh_attributes` vorhanden: kleine Info-Karte
|
||||
- Felder: `volume_cm3` (aus `mesh_attributes.volume_mm3 / 1000` → "12.5 cm³"),
|
||||
`surface_area_cm2`, `bounding_box` ("W×H×D mm"), `sharp_angle_deg` (aus `suggested_smooth_angle`)
|
||||
- Label "Geometry" mit `Ruler`-Icon
|
||||
- **API-Änderung**: Product-API gibt `cad_file.mesh_attributes` zurück (prüfen ob vorhanden)
|
||||
- **Akzeptanzkriterium**: Volumen, Oberfläche, BBox in ProductDetail sichtbar (wenn vorhanden)
|
||||
|
||||
### Task 12: OCC Edge-Analyse → mesh_attributes (Sharp/Seam)
|
||||
- **Datei**: `backend/app/services/step_processor.py`
|
||||
- **Was**: Neue Funktion `extract_mesh_edge_data(step_path: str) -> dict`:
|
||||
- Öffnet STEP via OCC
|
||||
- Iteriert über alle Faces und deren Edges
|
||||
- Berechnet Winkel zwischen adjazenten Faces per Edge (Dihedralwinkel)
|
||||
- Sammelt:
|
||||
- `suggested_smooth_angle`: Median-Winkel aller Kanten wo Winkel > 5° (typisch 30–60°)
|
||||
- `has_mechanical_edges`: bool (True wenn mehrere Kanten mit Winkel > 60° → Lagerkante)
|
||||
- `sharp_edge_midpoints`: Liste von `[x,y,z]` mm-Koordinaten der scharfen Kanten-Mittelpunkte (max 500 Stück, für Winkel > 45°)
|
||||
- Integriert in `extract_cad_metadata()`: nach `_extract_step_objects()` aufrufen, Ergebnis in `mesh_attributes` mergen
|
||||
- Fallback: bei OCC-Fehler gracefully `{}` zurückgeben
|
||||
- **Akzeptanzkriterium**: `cad_files.mesh_attributes` enthält `suggested_smooth_angle` nach Verarbeitung
|
||||
|
||||
### Task 13: Blender-Scripts — mark_sharp + UV-Seams
|
||||
- **Dateien**: `render-worker/scripts/still_render.py`, `render-worker/scripts/blender_render.py`
|
||||
- **Was**: Nach STL-Import, vor dem Render:
|
||||
1. Wenn `mesh_attributes.suggested_smooth_angle` vorhanden: diesen Winkel statt globalem `smooth_angle` nutzen
|
||||
2. Neue Funktion `_mark_sharp_edges(obj, smooth_angle_deg, sharp_edge_midpoints=None)`:
|
||||
- Setzt `obj.data.auto_smooth_angle = math.radians(smooth_angle_deg)`
|
||||
- Wählt Kanten aus: `bpy.ops.mesh.edges_select_sharp(sharpness=math.radians(smooth_angle_deg))`
|
||||
- Ruft `bpy.ops.mesh.mark_sharp()` auf
|
||||
- Wenn `sharp_edge_midpoints` vorhanden: KD-Tree matching → zusätzliche Kanten markieren
|
||||
3. Neue Funktion `_create_uv_seams_from_sharps(obj)`:
|
||||
- Startet Edit-Mode
|
||||
- Selektiert alle Sharp-Kanten: `[e for e in mesh.edges if e.use_edge_sharp]`
|
||||
- Markiert diese als Seams: `edge.use_seam = True`
|
||||
- Ruft `bpy.ops.uv.smart_project(angle_limit=math.radians(smooth_angle_deg))` auf
|
||||
4. Beide Funktionen nach `_import_stl()` aufrufen (Mode A + Mode B)
|
||||
- **Akzeptanzkriterium**: Gerenderte Bilder zeigen korrekte Kanten für Lager (30° Winkel scharf sichtbar)
|
||||
|
||||
### Task 14: K3 — apply_asset_library_materials_task
|
||||
- **Datei**: `backend/app/domains/rendering/tasks.py`
|
||||
- **Was**: Neuer Celery-Task:
|
||||
### 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
|
||||
@celery_app.task(name="...apply_asset_library_materials_task", queue="thumbnail_rendering")
|
||||
def apply_asset_library_materials_task(order_line_id: str, asset_library_id: str) -> dict:
|
||||
# Lädt OrderLine, CadFile, AssetLibrary
|
||||
# Prüft ob asset_library.blend_file_path existiert
|
||||
# Ruft Blender subprocess auf mit asset_library.py:
|
||||
# blender --background --python asset_library.py -- --stl_path X --asset_library_blend Y --material_map '{...}'
|
||||
# Returns {'status': 'applied', 'materials_count': N}
|
||||
```
|
||||
Skript `render-worker/scripts/asset_library.py` existiert bereits.
|
||||
- **Akzeptanzkriterium**: Task läuft ohne Fehler wenn Blender verfügbar
|
||||
# 1. CadFiles mit thumbnail_path + status='completed'
|
||||
SELECT id, thumbnail_path FROM cad_files
|
||||
WHERE thumbnail_path IS NOT NULL AND status = 'completed'
|
||||
|
||||
### Task 15: K4/K5 — export_gltf + export_blend via Blender
|
||||
- **Datei**: `backend/app/domains/rendering/tasks.py`
|
||||
- **Was**: `export_gltf_for_order_line_task` und `export_blend_for_order_line_task` überarbeiten:
|
||||
- Statt trimesh: Blender subprocess mit `export_gltf.py` / `export_blend.py`
|
||||
- Asset Library path aus LinkedAssetLibrary (via OutputType) übergeben falls vorhanden
|
||||
- GLB → MinIO `production-exports/{cad_file_id}/{order_line_id}.glb`
|
||||
- .blend → MinIO `production-exports/{cad_file_id}/{order_line_id}.blend`
|
||||
- MediaAsset erstellen mit `gltf_production` / `blend_production` type
|
||||
- **Akzeptanzkriterium**: Export-Tasks produzieren GLB/BLEND-Dateien in MinIO
|
||||
# 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
|
||||
|
||||
---
|
||||
|
||||
## Abhängigkeiten
|
||||
|
||||
```
|
||||
Sofort (parallel):
|
||||
Task 1 (Upload Link)
|
||||
Task 2 (Notification Config Backend)
|
||||
Task 3 (OutputType Schema)
|
||||
Task 5 (Asset Library API)
|
||||
Task 9 (Sanity Check Backend)
|
||||
Task 12 (OCC Edge Analyse)
|
||||
|
||||
Nach Task 3:
|
||||
Task 4 (Dispatch Integration)
|
||||
Task 8 (OutputType Workflow Dropdown)
|
||||
|
||||
Nach Task 5+6:
|
||||
Task 6 (Asset Library Page) — braucht Task 5
|
||||
Task 7 (Route + Sidebar) — braucht Task 6
|
||||
|
||||
Nach Task 9:
|
||||
Task 10 (Upload Sanity Dialog)
|
||||
|
||||
Nach Task 11:
|
||||
Task 11 (Mesh Display) — unabhängig
|
||||
|
||||
Nach Task 12:
|
||||
Task 13 (Blender Scripts)
|
||||
|
||||
Nach Task 14:
|
||||
Task 15 (K4/K5 Exports)
|
||||
```
|
||||
|
||||
## Migrations-Check
|
||||
Alle benötigten Migrationen existieren bereits:
|
||||
- 043: import_validations ✅
|
||||
- 044: notification_configs ✅
|
||||
- 045: asset_libraries ✅
|
||||
|
||||
Keine neue Migration nötig.
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user