Files
HartOMat/plan.md
T
Hartmut a70cb55d01 feat(N): workflow pipeline, 3D viewer, worker management, QC tests
- 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>
2026-03-06 22:56:53 +01:00

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:

  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.


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) — 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

    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

  • Was:

    • _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):
      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
  • 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:

    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:

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

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

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 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)
  • 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

  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.