- workflow_builder.py: fix broken stubs, add render_order_line_still_task
(resolves step_path from DB instead of passing order_line_id as step_path)
- domains/rendering/tasks.py: add render_order_line_still_task,
export_gltf_for_order_line_task, export_blend_for_order_line_task,
generate_gltf_geometry_task (trimesh STL→GLB, no Blender needed)
- tasks/step_tasks.py: add generate_gltf_geometry_task for CadFile GLB export
- cad router: POST /{id}/generate-gltf-geometry endpoint (admin/PM)
- worker router: GET /celery-workers + POST /scale (docker compose subprocess)
- Dockerfile: pip install -e "[dev]" to enable pytest
- docker-compose.yml: docker socket + compose file mount on backend
- ThreeDViewer.tsx: mode toggle (geometry/production), wireframe, env presets,
download buttons (GLB + .blend)
- CadPreview.tsx: load gltf_geometry/gltf_production/blend_production assets
from MediaAsset table and pass URLs to ThreeDViewer
- ProductDetail.tsx: "View 3D" button → /cad/:id, "Generate GLB" button
- media router/service: cad_file_id filter on GET /api/media
- WorkerManagement.tsx: new page with worker status, queue depth, scale controls
- App.tsx + Layout.tsx: /workers route + sidebar link (admin/PM)
- tests: test_rendering_service.py, test_orders_service.py (backend)
- tests: WorkerActivity.test.tsx, WorkerManagement.test.tsx (frontend)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
18 KiB
Plan: Phase N — Workflow-Pipeline, 3D-Viewer Production-Modus, Worker-Management, QC-Tests
Kontext
Vier offene Bereiche aus dem PLAN.md müssen abgeschlossen werden:
-
Workflow-Pipeline verdrahten:
workflow_builder.pyenthält nur defekte Stubs._build_stillübergibtorder_line_idalsstep_pathanrender_still_task→ würde crashen. Der neuestill_with_exports-Workflow (still + gltf_export + blend_export) ist nicht implementiert. Die Celery-Tasks für export_gltf/export_blend fehlen indomains/rendering/tasks.py. -
K6: 3D-Viewer Production-Modus:
ThreeDViewer.tsxhat keinen Mode-Toggle, Wireframe, Env-Preset oder Download-Buttons. Für Testdaten wirdPOST /api/cad/{id}/generate-gltf-geometrybenötigt (trimesh STL→GLB, kein Blender nötig). -
L3: Worker-Management UI:
WorkerManagement.tsxfehlt. Backend braucht/celery-workers(Celery inspect) und/scale(docker compose subprocess). Backend-Container bekommt Docker-Socket-Mount. -
M: QC-Tests:
pytestist im Backend-Container nicht installiert. Dockerfile:pip install -e ".[dev]". Neue Service-Tests für rendering und orders domains. 2 neue Vitest-Dateien.
Betroffene Dateien
| 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 |
Tasks (in Reihenfolge)
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):
render_order_line_still_task(order_line_id, **params)— Queuethumbnail_rendering:- Lädt OrderLine + CadFile via sync SQLAlchemy (wie
publish_asset) - Setzt
render_status = 'processing' - Ruft
render_still()ausapp.services.render_blenderauf - Setzt
render_status = 'completed', speichertrender_log - Bei Fehler:
render_status = 'failed' - Returns dict mit
output_path
export_gltf_for_order_line_task(order_line_id)— Queuethumbnail_rendering:- Lädt OrderLine + CadFile sync
- Sucht STL-Cache (
{step_stem}_low.stl) - Ruft Blender subprocess mit
export_gltf.pyauf: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)— Queuethumbnail_rendering:- Analog zu export_gltf, aber mit
export_blend.py - MediaAsset type:
blend_production
- Lädt OrderLine + CadFile via sync SQLAlchemy (wie
-
Akzeptanzkriterium: Tasks in
domains/rendering/tasks.pyvorhanden, keine Import-Fehler -
Abhängigkeiten: keine
Task 2: Backend — workflow_builder.py reparieren + still_with_exports
-
Datei:
backend/app/domains/rendering/workflow_builder.py -
Was:
_build_still: Nutztrender_order_line_still_taskstattrender_still_task_build_turntable: Bleibt vorerst mitrender_turntable_task(file-path-basiert, funktioniert via legacy path)_build_multi_angle: Nutztrender_order_line_still_taskmitcamera_angleparam- NEU
_build_still_with_exports(order_line_id, params):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"zubuildershinzufügen
-
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_taskindomains/rendering/tasks.py(Queuethumbnail_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=...)→MediaAssetbrauchtcad_file_idFK — prüfen ob vorhanden
Wichtig: Prüfen ob
media_assets.cad_file_idexistiert. Falls nicht: Migration 047 notwendig. - Prüft ob CadFile existiert + STL-Cache vorhanden (
-
Akzeptanzkriterium:
POST /api/cad/{id}/generate-gltf-geometrygibt 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 NULLaufmedia_assets - Prüfen:
grep -n "cad_file_id" backend/app/domains/media/models.py— falls schon vorhanden: Task überspringen - Akzeptanzkriterium:
alembic upgrade headerfolgreich - 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:
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
productionGltfUrlgesetzt): Button-Gruppe "Geometry | Production" - Wireframe-Toggle: Button
- Env-Preset-Dropdown:
<select>mit city/studio/sunset - Download-Buttons (wenn
downloadUrlsgesetzt): Download-Icon + "GLB" + optional "BLEND"
Canvas-Änderungen:
Environment preset={envPreset}(jetzt konfigurierbar, bisher hardcoded "city")WireframeToggle-Komponente: setztmaterial.wireframe = wireframeauf allen Mesh-Children- Model-URL:
mode === 'production' && productionGltfUrl ? productionGltfUrl : modelUrl
GltfErrorBoundary: Reset bei mode-Wechsel (key prop ändern)
-
Akzeptanzkriterium: Mode-Toggle erscheint wenn
productionGltfUrlvorhanden, 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-geometryauf + 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 zulist_assetshinzufügen (optional, nullable)list_media_assets(db, cad_file_id=...)in service.py erweitern
- Akzeptanzkriterium:
GET /api/media-assets?cad_file_id=abcgibt 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
MediaAssetumcad_file_id?: stringergä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: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, queuesResponse:
list[CeleryWorkerInfo]mit Feldern:worker_name, hostname, active_tasks, statusPOST /api/worker/scale(Body:{service: "render-worker"|"worker", count: int}):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-workersgibt 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:Außerdemvolumes: - ./backend:/app - uploads:/app/uploads - /var/run/docker.sock:/var/run/docker.sock - ./docker-compose.yml:/docker-compose.yml environment: - COMPOSE_FILE=/docker-compose.ymldocker-cliim Backend-Dockerfile installieren: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 versionfunktioniert - 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_renderingQueue-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) - Was:
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> - Akzeptanzkriterium: TypeScript kompiliert
- Abhängigkeiten: Task 9
Task 13: Frontend — Route + Sidebar-Link für WorkerManagement
- 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)
- App.tsx: Route
- Akzeptanzkriterium:
/workerserreichbar, Link erscheint für Admins - Abhängigkeiten: Task 11
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 --versiongibt Versionsnummer aus (nach Rebuild) - Abhängigkeiten: keine
Task 15: Backend-Tests — test_rendering_service.py
- Datei:
backend/tests/domains/test_rendering_service.py(NEU) - Was: ≥5 Tests:
test_dispatch_workflow_unknown_type_raises— ValueError bei unbekanntem Typtest_dispatch_workflow_still_builds_chain—_build_stillgibt Celery-Chain zurück (ohne apply_async)test_dispatch_workflow_still_with_exports_builds_chain— group in chaintest_publish_asset_creates_media_asset(db, admin_user)— async, erstellt MediaAssettest_publish_asset_nonexistent_order_line_returns_none— graceful None- (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/ordersund Orders-Service-Funktionen:test_create_order_returns_201(client, auth_headers)— POST /api/orderstest_list_orders_empty(client, auth_headers)— leere Liste zurücktest_get_order_404_for_unknown_id(client, auth_headers)— 404 bei unbekannter IDtest_order_submit_status_change(client, auth_headers)— Submit ändert Statustest_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) - 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
Migrations-Check
| 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
-
media_assets.cad_file_id: Muss vor Implementierung geprüft werden. Wenn schon vorhanden → Migration 047 entfällt.
-
trimesh auf render-worker:
trimeshist inpyproject.tomlals optionalecad-Dependency gelistet (trimesh>=4.2.0). Der worker-Container muss sie installiert haben. Im render-worker Dockerfile prüfen:pip install trimesh. -
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. -
render_order_line_still_task vs. legacy render_order_line_task: Beide tun ähnliches. Langfristig sollte
step_tasks.render_order_line_taskdurch den neuen Task ersetzt werden. Für jetzt: Neuer Task läuft parallel, Legacy bleibt erhalten (backward-compat). -
Celery inspect Timeout:
celery_app.control.inspect(timeout=2)kann hängen wenn kein Worker läuft. Timeout setzen + leere Liste zurückgeben.