feat(O): UI-Vollständigkeit + v3-Workflows + OCC-Kantenanalyse

Backend:
- Phase I: notification_configs router (GET/PUT/{event}/{channel}/POST reset)
  war bereits in notifications.py — add-alias endpoint in uploads.py ergänzt
- OutputType schema: workflow_definition_id + workflow_name fields;
  PATCH unterstützt Workflow-Zuweisung; _enrich_workflow_names() batch query
- Dispatch-Integration: orders.py dispatch_renders() → dispatch_render_with_workflow()
  mit Legacy-Fallback; neues Logging
- uploads.py: POST /validations/{id}/add-alias für Material-Lücken

Pipeline:
- step_processor.py: extract_mesh_edge_data() via OCC — berechnet Dihedralwinkel
  aller Kanten, liefert suggested_smooth_angle + sharp_edge_midpoints
  Integriert in extract_cad_metadata() und process_cad_file()
- domains/rendering/tasks.py: apply_asset_library_materials_task (K3),
  export_gltf_for_order_line_task → Blender export_gltf.py (K4),
  export_blend_for_order_line_task → export_blend.py fix (K5)
- render-worker/scripts/still_render.py: _mark_sharp_and_seams() mit
  OCC midpoint KD-tree matching + UV-Seam-Markierung
- render-worker/scripts/blender_render.py: identische Funktion + mesh_attributes parsing

Frontend:
- Layout.tsx: Upload-Link in Sidebar (alle User); Asset Libraries Link (admin/PM)
- App.tsx: /asset-libraries Route
- AssetLibrary.tsx: neue Seite (Upload, Catalog-Anzeige, Refresh, Toggle, Delete)
- OutputTypeTable.tsx: Workflow-Dropdown + Legacy/Workflow Badge
- ProductDetail.tsx: Geometry-Karte (Volumen, Surface, BBox, Sharp-Winkel)
- api/outputTypes.ts + api/products.ts: neue Felder
- api/imports.ts: ImportValidation API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 23:20:55 +01:00
parent f15b035b88
commit 382a18fd02
18 changed files with 1222 additions and 355 deletions
+208 -334
View File
@@ -1,16 +1,6 @@
# Plan: Phase N — Workflow-Pipeline, 3D-Viewer Production-Modus, Worker-Management, QC-Tests
# Plan: UI-Vollständigkeit + Workflows — Phase O
## Kontext
Vier offene Bereiche aus dem PLAN.md müssen abgeschlossen werden:
1. **Workflow-Pipeline verdrahten**: `workflow_builder.py` enthält nur defekte Stubs. `_build_still` übergibt `order_line_id` als `step_path` an `render_still_task` → würde crashen. Der neue `still_with_exports`-Workflow (still + gltf_export + blend_export) ist nicht implementiert. Die Celery-Tasks für export_gltf/export_blend fehlen in `domains/rendering/tasks.py`.
2. **K6: 3D-Viewer Production-Modus**: `ThreeDViewer.tsx` hat keinen Mode-Toggle, Wireframe, Env-Preset oder Download-Buttons. Für Testdaten wird `POST /api/cad/{id}/generate-gltf-geometry` benötigt (trimesh STL→GLB, kein Blender nötig).
3. **L3: Worker-Management UI**: `WorkerManagement.tsx` fehlt. Backend braucht `/celery-workers` (Celery inspect) und `/scale` (docker compose subprocess). Backend-Container bekommt Docker-Socket-Mount.
4. **M: QC-Tests**: `pytest` ist im Backend-Container nicht installiert. Dockerfile: `pip install -e ".[dev]"`. Neue Service-Tests für rendering und orders domains. 2 neue Vitest-Dateien.
**Ziel**: Alle implementierten Backend-Features im UI zugänglich machen + v3-Workflows vollständig verdrahten.
---
@@ -18,348 +8,232 @@ Vier offene Bereiche aus dem PLAN.md müssen abgeschlossen werden:
| Datei | Änderung |
|-------|----------|
| `backend/app/domains/rendering/tasks.py` | 3 neue Tasks: `render_order_line_still_task`, `export_gltf_for_order_line_task`, `export_blend_for_order_line_task` |
| `backend/app/domains/rendering/workflow_builder.py` | Stubs ersetzen durch order-line-aware Tasks, `still_with_exports` hinzufügen |
| `backend/app/api/routers/cad.py` | `POST /{id}/generate-gltf-geometry` Endpoint |
| `backend/app/api/routers/worker.py` | `GET /celery-workers`, `POST /scale` Endpoints |
| `backend/Dockerfile` | `pip install -e ".[dev]"` |
| `docker-compose.yml` | Backend + Worker: Docker-Socket + Compose-File-Mount |
| `frontend/src/components/cad/ThreeDViewer.tsx` | Mode-Toggle, Wireframe, Env-Preset, Download-Buttons |
| `frontend/src/pages/WorkerManagement.tsx` | NEU: Worker-Liste, Queue-Stats, Scale-Button |
| `frontend/src/api/worker.ts` | Neue Interfaces + API-Funktionen |
| `frontend/src/App.tsx` | Route für /workers |
| `frontend/src/components/layout/Layout.tsx` | Sidebar-Link Workers |
| `backend/tests/domains/test_rendering_service.py` | NEU: ≥5 Tests für Rendering-Tasks und Workflow-Builder |
| `backend/tests/domains/test_orders_service.py` | NEU: ≥5 Tests für Orders-Endpoints |
| `frontend/src/__tests__/pages/WorkerActivity.test.tsx` | NEU: Vitest-Tests |
| `frontend/src/__tests__/pages/WorkerManagement.test.tsx` | NEU: Vitest-Tests |
| `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 |
---
## Tasks (in Reihenfolge)
## Tasks
### Task 1: Backend — Neue order-line-aware Rendering-Tasks
- **Datei**: `backend/app/domains/rendering/tasks.py`
- **Was**: Drei neue Celery-Tasks hinzufügen (UNTER den bestehenden Tasks):
### Task 1: Upload-Link in Sidebar [QUICK WIN]
- **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
**`render_order_line_still_task(order_line_id, **params)`** — Queue `thumbnail_rendering`:
- Lädt OrderLine + CadFile via sync SQLAlchemy (wie `publish_asset`)
- Setzt `render_status = 'processing'`
- Ruft `render_still()` aus `app.services.render_blender` auf
- Setzt `render_status = 'completed'`, speichert `render_log`
- Bei Fehler: `render_status = 'failed'`
- Returns dict mit `output_path`
### 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
**`export_gltf_for_order_line_task(order_line_id)`** — Queue `thumbnail_rendering`:
- Lädt OrderLine + CadFile sync
- Sucht STL-Cache (`{step_stem}_low.stl`)
- Ruft Blender subprocess mit `export_gltf.py` auf: `blender --background --python export_gltf.py -- --stl_path X --output_path Y`
- Lädt GLB nach MinIO `production-exports/{cad_file_id}/{order_line_id}.glb`
- Erstellt `MediaAsset(asset_type=gltf_production, storage_key=...)`
- Returns `storage_key`
**`export_blend_for_order_line_task(order_line_id)`** — Queue `thumbnail_rendering`:
- Analog zu export_gltf, aber mit `export_blend.py`
- MediaAsset type: `blend_production`
- **Akzeptanzkriterium**: Tasks in `domains/rendering/tasks.py` vorhanden, keine Import-Fehler
- **Abhängigkeiten**: keine
### Task 2: Backend — workflow_builder.py reparieren + still_with_exports
- **Datei**: `backend/app/domains/rendering/workflow_builder.py`
### 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
- `_build_still`: Nutzt `render_order_line_still_task` statt `render_still_task`
- `_build_turntable`: Bleibt vorerst mit `render_turntable_task` (file-path-basiert, funktioniert via legacy path)
- `_build_multi_angle`: Nutzt `render_order_line_still_task` mit `camera_angle` param
- **NEU** `_build_still_with_exports(order_line_id, params)`:
```python
from celery import chain, group
return chain(
render_order_line_still_task.si(order_line_id, **params),
group(
export_gltf_for_order_line_task.si(order_line_id),
export_blend_for_order_line_task.si(order_line_id),
)
)
```
- `dispatch_workflow()`: `"still_with_exports"` zu `builders` hinzufügen
### 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
- **Akzeptanzkriterium**: `dispatch_workflow("still_with_exports", order_line_id)` löst keine Exception aus
- **Abhängigkeiten**: Task 1
### Task 3: Backend — generate-gltf-geometry Endpoint (Testdaten für K6)
- **Datei**: `backend/app/api/routers/cad.py`
- **Was**: Neuer Endpoint `POST /api/cad/{id}/generate-gltf-geometry` (require_admin_or_pm):
- Prüft ob CadFile existiert + STL-Cache vorhanden (`{step_dir}/{stem}_low.stl`)
- Queut neuen Celery-Task `generate_gltf_geometry_task.delay(str(cad_file.id))`
- Returns `{"task_id": ..., "message": "GLB generation queued"}`
Neuer Task `generate_gltf_geometry_task` in `domains/rendering/tasks.py` (Queue `thumbnail_rendering`):
- Lädt CadFile sync, findet STL-Cache
- **Nutzt trimesh** (kein Blender): `import trimesh; mesh = trimesh.load(stl_path); mesh.export(glb_path)`
→ Warum trimesh: Schnell, kein Blender nötig, läuft auf worker-Container (trimesh in pyproject.toml cad-extras)
- Lädt GLB nach MinIO `uploads/{cad_file_id}/geometry.glb`
- Erstellt/aktualisiert `MediaAsset(asset_type=gltf_geometry, storage_key=..., cad_file_id=...)`
→ `MediaAsset` braucht `cad_file_id` FK — prüfen ob vorhanden
**Wichtig**: Prüfen ob `media_assets.cad_file_id` existiert. Falls nicht: Migration 047 notwendig.
- **Akzeptanzkriterium**: `POST /api/cad/{id}/generate-gltf-geometry` gibt 202 zurück, nach Task-Ausführung existiert MediaAsset mit type=gltf_geometry
- **Abhängigkeiten**: Task 1
### Task 4: Migration 047 — media_assets.cad_file_id (wenn nötig)
- **Datei**: `backend/alembic/versions/047_media_assets_cad_file_id.py`
- **Was**: Nullable FK `cad_file_id UUID REFERENCES cad_files(id) ON DELETE SET NULL` auf `media_assets`
- **Prüfen**: `grep -n "cad_file_id" backend/app/domains/media/models.py` — falls schon vorhanden: Task überspringen
- **Akzeptanzkriterium**: `alembic upgrade head` erfolgreich
- **Abhängigkeiten**: keine
### Task 5: ThreeDViewer.tsx — Production-Modus, Wireframe, Env-Preset, Downloads
- **Datei**: `frontend/src/components/cad/ThreeDViewer.tsx`
- **Was**: Props erweitern + Toolbar-Erweiterung:
```typescript
interface ThreeDViewerProps {
cadFileId: string
onClose: () => void
productionGltfUrl?: string // wenn vorhanden: Mode-Toggle anzeigen
downloadUrls?: { glb?: string; blend?: string }
}
```
**Neuer State:**
- `mode: 'geometry' | 'production'` (default: 'geometry')
- `wireframe: boolean` (default: false)
- `envPreset: 'city' | 'studio' | 'sunset'` (default: 'city')
**Toolbar** (neu, rechts vom "Capture Angle"-Button):
- Mode-Toggle (nur wenn `productionGltfUrl` gesetzt): Button-Gruppe "Geometry | Production"
- Wireframe-Toggle: Button
- Env-Preset-Dropdown: `<select>` mit city/studio/sunset
- Download-Buttons (wenn `downloadUrls` gesetzt): Download-Icon + "GLB" + optional "BLEND"
**Canvas-Änderungen:**
- `Environment preset={envPreset}` (jetzt konfigurierbar, bisher hardcoded "city")
- `WireframeToggle`-Komponente: setzt `material.wireframe = wireframe` auf allen Mesh-Children
- Model-URL: `mode === 'production' && productionGltfUrl ? productionGltfUrl : modelUrl`
**GltfErrorBoundary**: Reset bei mode-Wechsel (key prop ändern)
- **Akzeptanzkriterium**: Mode-Toggle erscheint wenn `productionGltfUrl` vorhanden, Wireframe-Toggle schaltet um, Env-Preset ändert Beleuchtung
- **Abhängigkeiten**: keine
### Task 6: CadPreview.tsx — Production-Asset-URLs übergeben
- **Datei**: `frontend/src/pages/CadPreview.tsx`
- **Was**: Beim Öffnen des ThreeDViewers:
- `GET /api/media-assets?cad_file_id={id}&asset_type=gltf_geometry` (oder gltf_production falls vorhanden)
- Download-URLs für GLB + BLEND laden
- `<ThreeDViewer productionGltfUrl={...} downloadUrls={...} />`
- "Generate GLB" Button (admin/PM): ruft `POST /api/cad/{id}/generate-gltf-geometry` auf + Toast + Reload
- **Akzeptanzkriterium**: Vorhandene MediaAssets werden als Production-URLs übergeben
- **Abhängigkeiten**: Task 3, Task 5
### Task 7: Media-API — assets by cad_file_id Query-Parameter
- **Datei**: `backend/app/domains/media/router.py`
- **Was**: `GET /api/media-assets?cad_file_id={uuid}` — Query-Param zu `list_assets` hinzufügen (optional, nullable)
- `list_media_assets(db, cad_file_id=...)` in service.py erweitern
- **Akzeptanzkriterium**: `GET /api/media-assets?cad_file_id=abc` gibt nur Assets dieses CadFile zurück
- **Abhängigkeiten**: Task 4
### Task 8: Frontend API — media.ts + cad.ts erweitern
- **Datei**: `frontend/src/api/media.ts`, `frontend/src/api/cad.ts`
- **Was**:
- `media.ts`: `listMediaAssets(params: {cad_file_id?: string, asset_type?: string}): Promise<MediaAsset[]>`
- `cad.ts`: `generateGltfGeometry(cadFileId: string): Promise<{task_id: string}>`
- Interface `MediaAsset` um `cad_file_id?: string` ergänzen (falls noch nicht vorhanden)
- **Akzeptanzkriterium**: TypeScript-Kompilierung fehlerfrei
- **Abhängigkeiten**: Task 7
### Task 9: Backend — Worker-Management Endpoints
- **Datei**: `backend/app/api/routers/worker.py`
- **Was**: Zwei neue Endpoints (require_admin):
**`GET /api/worker/celery-workers`**:
```python
from app.tasks.celery_app import celery_app
inspect = celery_app.control.inspect()
active = inspect.active() or {}
stats = inspect.stats() or {}
# Aggregiere: worker_name, hostname, active_tasks_count, queues
```
Response: `list[CeleryWorkerInfo]` mit Feldern: `worker_name, hostname, active_tasks, status`
**`POST /api/worker/scale`** (Body: `{service: "render-worker"|"worker", count: int}`):
```python
import subprocess, shutil
compose_file = os.environ.get("COMPOSE_FILE", "/docker-compose.yml")
result = subprocess.run(
["docker", "compose", "-f", compose_file,
"up", "--scale", f"{service}={count}", "--no-deps", "-d"],
capture_output=True, text=True, timeout=60
)
```
- Erfordert Docker-Socket-Mount (docker-compose.yml Änderung, Task 10)
- Validierung: count zwischen 0 und 10, service in erlaubte Liste
- **Akzeptanzkriterium**: `GET /api/worker/celery-workers` gibt Worker-Liste zurück (leer wenn keine aktiv)
- **Abhängigkeiten**: keine
### Task 10: docker-compose.yml — Docker-Socket + Compose-File-Mount
- **Datei**: `docker-compose.yml`
- **Was**: Im `backend`-Service:
```yaml
volumes:
- ./backend:/app
- uploads:/app/uploads
- /var/run/docker.sock:/var/run/docker.sock
- ./docker-compose.yml:/docker-compose.yml
environment:
- COMPOSE_FILE=/docker-compose.yml
```
Außerdem `docker-cli` im Backend-Dockerfile installieren:
```dockerfile
RUN apt-get update && apt-get install -y --no-install-recommends \
... docker.io \
&& rm -rf /var/lib/apt/lists/*
```
- **Akzeptanzkriterium**: `docker compose exec backend docker compose version` funktioniert
- **Abhängigkeiten**: Task 9
### Task 11: Frontend — WorkerManagement.tsx
- **Datei**: `frontend/src/pages/WorkerManagement.tsx` (NEU)
- **Was**: Seite mit 3 Bereichen:
**Section 1 — Worker-Status** (useQuery `['celery-workers']`, refetchInterval 15s):
- Tabelle: Worker-Name, Hostname, Aktive Tasks, Status-Dot (grün=online, grau=keine Tasks)
- Leerer Zustand: "No active workers"
**Section 2 — Queue-Tiefe** (aus `GET /api/worker/activity`, bestehend):
- Karten: `step_processing` + `thumbnail_rendering` Queue-Tiefe
- Nutzt vorhandene WorkerActivity-Daten
**Section 3 — Scale-Worker** (require admin):
- Zwei Slider/Spinner: "step-worker (worker)" 1-8, "render-worker" 1-4
- Button "Scale" → `POST /api/worker/scale`
- Warnung: "Scaling down kills active renders"
- Toast bei Erfolg/Fehler
- **Akzeptanzkriterium**: Seite lädt, Worker-Liste zeigt laufende Worker, Scale-Button sendet Request
- **Abhängigkeiten**: Task 9, Task 12
### Task 12: Frontend — worker.ts API-Client
- **Datei**: `frontend/src/api/worker.ts` (NEU oder ergänzen)
### Task 5: Asset Library API-Client (Frontend)
- **Datei**: `frontend/src/api/asset_libraries.ts` (NEU)
- **Was**:
```typescript
export interface CeleryWorkerInfo {
worker_name: string
hostname: string
active_tasks: number
status: 'online' | 'idle'
}
export async function getCeleryWorkers(): Promise<CeleryWorkerInfo[]>
export async function scaleWorker(service: string, count: number): Promise<void>
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
- **Abhängigkeiten**: Task 9
- **Akzeptanzkriterium**: TypeScript kompiliert fehlerfrei
### Task 13: Frontend — Route + Sidebar-Link für WorkerManagement
### 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`
- **Was**:
- App.tsx: Route `/workers` → `<WorkerManagement />`
- Layout.tsx: Sidebar-Link "Workers" mit `Server`-Icon (admin only)
- **Akzeptanzkriterium**: `/workers` erreichbar, Link erscheint für Admins
- **Abhängigkeiten**: Task 11
- App.tsx: Route `/asset-libraries` → `<AssetLibraryPage />` (AdminRoute)
- Layout.tsx: Sidebar-Link "Asset Libraries" mit `Library`-Icon (admin/PM)
- **Abhängigkeiten**: Task 6
### Task 14: Dockerfile — pytest installieren
- **Datei**: `backend/Dockerfile`
- **Was**: `pip install --no-cache-dir -e .` → `pip install --no-cache-dir -e ".[dev]"`
- **Akzeptanzkriterium**: `docker compose exec backend pytest --version` gibt Versionsnummer aus (nach Rebuild)
- **Abhängigkeiten**: keine
### 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 15: Backend-Tests — test_rendering_service.py
- **Datei**: `backend/tests/domains/test_rendering_service.py` (NEU)
- **Was**: ≥5 Tests:
1. `test_dispatch_workflow_unknown_type_raises` — ValueError bei unbekanntem Typ
2. `test_dispatch_workflow_still_builds_chain` — `_build_still` gibt Celery-Chain zurück (ohne apply_async)
3. `test_dispatch_workflow_still_with_exports_builds_chain` — group in chain
4. `test_publish_asset_creates_media_asset(db, admin_user)` — async, erstellt MediaAsset
5. `test_publish_asset_nonexistent_order_line_returns_none` — graceful None
6. (Bonus) `test_legacy_dispatch_queues_task(monkeypatch)` — mock_celery, prüft Task wurde eingereicht
- **Akzeptanzkriterium**: `pytest tests/domains/test_rendering_service.py` → alles grün
- **Abhängigkeiten**: Task 14
### Task 16: Backend-Tests — test_orders_service.py
- **Datei**: `backend/tests/domains/test_orders_service.py` (NEU)
- **Was**: ≥5 Tests gegen `GET/POST /api/orders` und Orders-Service-Funktionen:
1. `test_create_order_returns_201(client, auth_headers)` — POST /api/orders
2. `test_list_orders_empty(client, auth_headers)` — leere Liste zurück
3. `test_get_order_404_for_unknown_id(client, auth_headers)` — 404 bei unbekannter ID
4. `test_order_submit_status_change(client, auth_headers)` — Submit ändert Status
5. `test_order_requires_auth(client)` — 401 ohne Token
- **Akzeptanzkriterium**: `pytest tests/domains/test_orders_service.py` → alles grün
- **Abhängigkeiten**: Task 14
### Task 17: Frontend-Tests — WorkerActivity.test.tsx + WorkerManagement.test.tsx
- **Datei**: `frontend/src/__tests__/pages/WorkerActivity.test.tsx` (NEU), `WorkerManagement.test.tsx` (NEU)
### Task 9: Excel Sanity-Check Backend (Phase H)
- **Datei**: `backend/app/domains/imports/sanity_check.py` (NEU), `backend/app/domains/imports/router.py`
- **Was**:
- WorkerActivity: Test render + "No recent activity" leerer Zustand, Mock-API-Response
- WorkerManagement: Test render Header "Worker Management", Scale-Button vorhanden
- Nutzen MSW handlers aus `mocks/`
- **Akzeptanzkriterium**: `npm run test` → 0 Failures (≥5 Tests total neu)
- **Abhängigkeiten**: Task 11
- 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 3060°)
- `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:
```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
### 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
---
## 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 ✅
| Migration | Beschreibung | Notwendig? |
|-----------|-------------|------------|
| 047 | `media_assets.cad_file_id FK` | **Prüfen**: `grep cad_file_id backend/app/domains/media/models.py` — wenn fehlt → ja |
Vor Implementierung prüfen: `cat backend/app/domains/media/models.py | grep cad_file_id`
---
## Reihenfolge-Empfehlung
```
Parallel-Gruppe 1 (keine gegenseitigen Abhängigkeiten):
Task 1 (neue Celery-Tasks)
Task 4 (Migration 047 prüfen + ggf. erstellen)
Task 5 (ThreeDViewer Props)
Task 9 (Worker-Endpoints Backend)
Task 14 (Dockerfile pytest)
Nach Gruppe 1:
Task 2 (workflow_builder reparieren) — braucht Task 1
Task 3 (generate-gltf-geometry Endpoint) — braucht Task 1 + 4
Task 10 (docker-compose Mount) — braucht Task 9
Task 12 (worker.ts API) — braucht Task 9
Nach Gruppe 2:
Task 6 (CadPreview anpassen) — braucht Task 3, 5
Task 7 (media router cad_file_id param) — braucht Task 4
Task 8 (frontend API) — braucht Task 7
Task 11 (WorkerManagement.tsx) — braucht Task 9, 12
Nach Gruppe 3:
Task 13 (Route + Sidebar) — braucht Task 11
Task 15 (test_rendering_service.py) — braucht Task 14
Task 16 (test_orders_service.py) — braucht Task 14
Task 17 (frontend tests) — braucht Task 11
```
---
## Risiken / Offene Fragen
1. **media_assets.cad_file_id**: Muss vor Implementierung geprüft werden. Wenn schon vorhanden → Migration 047 entfällt.
2. **trimesh auf render-worker**: `trimesh` ist in `pyproject.toml` als optionale `cad`-Dependency gelistet (`trimesh>=4.2.0`). Der worker-Container muss sie installiert haben. Im render-worker Dockerfile prüfen: `pip install trimesh`.
3. **docker compose in Backend-Container**: Das scale-Feature setzt voraus, dass `docker.io` + compose-Plugin im Backend-Image installiert sind. Build-Zeit steigt ~30MB. Alternativ: Nur die Celery-Worker-Ansicht implementieren, Scale als Hinweis-Text mit dem CLI-Befehl.
4. **render_order_line_still_task vs. legacy render_order_line_task**: Beide tun ähnliches. Langfristig sollte `step_tasks.render_order_line_task` durch den neuen Task ersetzt werden. Für jetzt: Neuer Task läuft parallel, Legacy bleibt erhalten (backward-compat).
5. **Celery inspect Timeout**: `celery_app.control.inspect(timeout=2)` kann hängen wenn kein Worker läuft. Timeout setzen + leere Liste zurückgeben.
Keine neue Migration nötig.