Files
HartOMat/PLAN.md
T
Hartmut 381f44bc8b feat: render health endpoint + test script + pipeline fixes
- GET /api/worker/health/render: checks render-worker (thumbnail_rendering
  queue), Blender availability via active_queues inspect, queue depth,
  last render recency — returns ok/degraded/down status
- scripts/test_render_pipeline.py: integration test for full pipeline
  (--health, --sample, --full modes)
- PLAN.md: appended Render Pipeline Fixes section with all B-Fixes
- LEARNINGS.md: documented 5 new learnings (queue mismatch, circular
  import, 307 redirect, worker capability detection)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 19:34:12 +01:00

58 KiB
Raw Blame History

Refactor-Plan: Schaeffler Automat v2

Erstellt: 2026-03-05 Aktualisiert: 2026-03-06 — Phasen A, B, C, D, E abgeschlossen + Render-Pipeline-Fixes Status: IN UMSETZUNG — Phase F als nächstes Branch: refactor/render-pipeline → Ziel: neuer Branch refactor/v2


Inhaltsverzeichnis

  1. Ziel-Zusammenfassung
  2. Architektur-Analyse: Ist vs. Soll
  3. Architektur-Entscheidungen (ADRs)
  4. Was wird entfernt / ersetzt (mit Risiken)
  5. Was bleibt und wird erweitert
  6. Neue Komponenten
  7. Phasenplan mit Tasks
  8. Datenbankmigrationen-Übersicht
  9. QC-Gates und Test-Checkliste
  10. Offene Entscheidungen

1. Ziel-Zusammenfassung

Das System wird von einem Einzelkunden-Render-Tool zu einer produktionstauglichen Multi-Tenant Render-Plattform ausgebaut:

Ziel Umsetzung
Produktionspipeline maintainbar Flamenco entfernen, vereinfachte Docker-Architektur (8 statt 11 Services)
Multi-Customer Tenant-Modell mit PostgreSQL Row-Level Security
Externe Worker Celery render-worker auf beliebigen Maschinen via Redis + MinIO
Modulare Render-Konfiguration Celery Canvas Workflows, deklarative WorkflowDefinition JSON-Config
Template-basierte Outputs RenderTemplate mit Workflow-Integration, React Flow Visualisierung
Media-Verwaltung MediaAsset-Katalog, Filter/Sort/Zip-Download, Audit-Log
Modernes Design Responsive, Widget-Dashboard, WebSocket für Live-Updates
Skalierbar Celery horizontal skalierbar, Hash-basiertes Conversion-Caching
Produktdatenbank Excel-Import mit Sanity-Check und Material-Validierung
Node-basierter Workflow React Flow Editor (Visualisierung), Celery Canvas (Execution)
Keine doppelten Konvertierungen SHA256-Hash-basierter zentraler Conversion-Cache
Dynamische Worker-Skalierung Docker API Scaling + Worker-Registrierung via Redis
Cycles + EEVEE Konfigurierbar pro OutputType
Nutzerverwaltung Admin / ProjectManager / Client (Tenant-gebunden, RLS-isoliert)
Preise + Abrechnung PricingTier, Invoice-Modul, WeasyPrint PDF-Export
Modulare Dashboards Widget-basiert, rollenabhängig, WebSocket-Live-Updates
Reporting Invoice-Report, Produktions-Report, Excel/PDF-Export
Blender Asset Library Native Blender Asset Library für Materialien UND Geometry-Node-Modifier, modular pro OutputType
Interaktive 3D-Vorschau Three.js Browser-Viewer mit Production-glTF (Materialien angewendet), OrbitControls
Production-Exports glTF/GLB + .blend mit eingebetteten Produktionsmaterialien downloadbar
Frontend-Logs SSE-Stream für Render-Task-Logs (1 Stream pro Task)
Real-Time Dashboard WebSocket für Queue-Status, Worker-Status, Render-Events
Notifications Konfigurierbar per Event-Typ und User
Schaeffler-Workflow Sanity-Check, Material-Validierung, Order-Readiness
OCC Mesh-Attribute Sharp Edges, UV-Seams aus STEP-Topologie
Blender-Version >= 5.0.1 Pflicht, Upgrade-Pfad auf 5.1 vorbereitet

2. Architektur-Analyse: Ist vs. Soll

IST-Architektur (11 Services)

Internet
    ↓
frontend:5173 (React/Vite)
    ↓ HTTP
backend:8888 (FastAPI)
    ↓ SQL          ↓ Celery tasks      ↓ HTTP
postgres:5432    redis:6379          blender-renderer:8100
                    ↓                    ↑ (nur 1 concurrent)
              worker (concurrency=8)   threejs-renderer:8101
              worker-thumbnail (c=1)      ↑
              beat                    flamenco-manager:8080
                                        ↓
                                    flamenco-worker (GPU)

Probleme IST:

  • blender-renderer ist Flask-HTTP-Service → max. 1 concurrent Request, kein echtes Scaling
  • threejs-renderer redundant zu Blender für Thumbnails (eigener Container, eigene Playwright-Instanz)
  • flamenco ist komplexes externes System (Job-Types in JS) — Mehraufwand ohne Mehrwert über verteilte Celery-Worker
  • worker-thumbnail mit concurrency=1 ist Workaround für blender-renderer-Limitation
  • STEP-Konvertierung passiert mehrfach (blender-renderer + threejs-renderer unabhängig voneinander)
  • Kein Tenant-Konzept — alle Kunden teilen dieselbe DB-Namespace
  • Keine echte Pipeline-Konfiguration — Logik ist hartcodiert in step_tasks.py
  • Kein Shared Storage → externe Worker können keine STEP-Dateien lesen

SOLL-Architektur (8 Core-Services + n render-worker)

Internet
    ↓
frontend:5173 (React/Vite + React Flow + WebSocket)
    ↓ HTTP / WebSocket / SSE
backend:8888 (FastAPI, Domain-driven, RLS-enabled)
    ↓ SQL (RLS)    ↓ Celery Canvas    ↓ S3 API
postgres:5432    redis:6379         minio:9000
   (+ RLS)            ↓            ↑ (shared object storage)
                step-worker    render-worker  ← lokal (Maschine A)
                beat           render-worker  ← Netzwerk (Maschine B)
                               render-worker  ← GPU (Maschine C)

Vorteile SOLL:

  • Blender läuft direkt im Celery-Worker als subprocess → kein HTTP-Overhead, kein Timeout-Problem
  • Worker auf beliebigen Maschinen: brauchen nur REDIS_URL + MINIO_URL + Blender installiert
  • MinIO als S3-kompatibler Object-Store ersetzt NFS — kein Mount nötig, funktioniert überall
  • PostgreSQL RLS sichert Tenant-Isolation automatisch — kein manueller WHERE-Filter nötig
  • Celery Canvas für Workflow-Execution — keine custom Workflow-Engine
  • React Flow nur als Visualisierungsschicht — deutlich reduzierter Scope
  • Kein Flamenco, kein threejs-renderer → 3 Services weniger

3. Architektur-Entscheidungen (ADRs)

ADR-01: PostgreSQL Row-Level Security statt manuellem tenant_id-Filter

Problem: Jeder neue Router-Query müsste manuell WHERE tenant_id = :x haben. Ein vergessenes Filter = Datenleck zwischen Kunden.

Entscheidung: PostgreSQL Row-Level Security (RLS)

-- Einmalig pro Tabelle (in Migration 035)
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON products
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

-- Admin-Bypass via BYPASSRLS-Rolle
ALTER ROLE schaeffler_admin BYPASSRLS;
# FastAPI Dependency: einmal pro Request setzen
async def get_db_for_tenant(
    db: AsyncSession = Depends(get_db),
    user: User = Depends(get_current_user)
) -> AsyncSession:
    await db.execute(
        text("SET LOCAL app.current_tenant_id = :tid"),
        {"tid": str(user.tenant_id)}
    )
    yield db

Vorteile:

  • Unmöglich Cross-Tenant-Leaks durch vergessene Filter
  • Gilt automatisch für alle zukünftigen Queries — auch neue Endpoints
  • Testbar: RLS-Policies sind SQL, unabhängig von Anwendungscode

Nachteile / Risiken:

  • Migration muss RLS für alle betroffenen Tabellen aktivieren
  • BYPASSRLS für Admin-User muss in DB-Migrationen gesetzt werden
  • Alembic-Autogenerate erkennt keine RLS-Policies → Policies müssen manuell in Migration geschrieben werden

ADR-02: MinIO statt NFS für Shared Storage

Problem: Externe Worker müssen STEP-Dateien und Render-Outputs lesen/schreiben. NFS ist operationell komplex, plattformabhängig, und ein Single-Point-of-Failure.

Entscheidung: MinIO (S3-kompatibel, Docker-nativ, self-hosted)

# docker-compose.yml
minio:
  image: minio/minio:latest
  command: server /data --console-address ":9001"
  environment:
    MINIO_ROOT_USER: ${MINIO_USER:-minioadmin}
    MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD:-minioadmin}
  ports:
    - "9000:9000"   # S3 API
    - "9001:9001"   # Web Console
  volumes:
    - minio-data:/data
  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
# backend/core/storage.py
import boto3
from pathlib import Path

class MinIOStorage:
    def __init__(self):
        self.client = boto3.client(
            's3',
            endpoint_url=settings.MINIO_URL,
            aws_access_key_id=settings.MINIO_USER,
            aws_secret_access_key=settings.MINIO_PASSWORD,
        )
        self.bucket = 'uploads'

    def upload(self, local_path: Path, object_key: str) -> str:
        self.client.upload_file(str(local_path), self.bucket, object_key)
        return object_key

    def download(self, object_key: str, local_path: Path) -> Path:
        self.client.download_file(self.bucket, object_key, str(local_path))
        return local_path

    def exists(self, object_key: str) -> bool:
        try:
            self.client.head_object(Bucket=self.bucket, Key=object_key)
            return True
        except: return False

Render-Worker: Lädt STEP-File vor Render aus MinIO in lokales tmpdir, lädt Output zurück nach MinIO.

Externe Worker brauchen nur:

  • REDIS_URL=redis://server:6379/0
  • MINIO_URL=http://server:9000
  • MINIO_USER + MINIO_PASSWORD

Kein Mount, kein NFS, funktioniert auf Windows/Linux/Mac gleich.


ADR-03: Celery Canvas für Workflow-Execution, React Flow nur Visualisierung

Problem: Eine custom Workflow-Engine (Graph-Traversal, Dependency-Resolution, Retry-Logic) ist ~2-3 Wochen Eigenentwicklung — Celery hat das bereits eingebaut.

Entscheidung: Celery Canvas als Execution-Engine, deklarative JSON-Config als Definition, React Flow als Visualisierung.

# domains/rendering/workflow_builder.py

from celery import chain, group

WORKFLOW_BUILDERS = {
    "still": lambda order_line_id: chain(
        convert_step.si(order_line_id),
        extract_mesh_attributes.si(order_line_id),
        render_still.si(order_line_id),
        generate_thumbnail.si(order_line_id),
        publish_asset.si(order_line_id),
    ),
    "turntable": lambda order_line_id: chain(
        convert_step.si(order_line_id),
        render_turntable_frames.si(order_line_id),
        composite_ffmpeg.si(order_line_id),
        publish_asset.si(order_line_id),
    ),
    "multi_angle": lambda order_line_id: chain(
        convert_step.si(order_line_id),
        group(  # parallele Renders
            render_still.si(order_line_id, angle=0),
            render_still.si(order_line_id, angle=45),
            render_still.si(order_line_id, angle=90),
        ),
        publish_asset.si(order_line_id),
    ),
}

def dispatch_workflow(workflow_type: str, order_line_id: str):
    canvas = WORKFLOW_BUILDERS[workflow_type](order_line_id)
    return canvas.apply_async()

WorkflowDefinition speichert die deklarative Config (welcher workflow_type, welche Parameter):

{
  "type": "still",
  "params": {
    "render_engine": "cycles",
    "samples": 256,
    "resolution": [2048, 2048],
    "material_library_id": "uuid-..."
  }
}

React Flow Editor zeigt den Workflow visuell an und bearbeitet diese JSON-Config. Er erzeugt keine eigene Execution-Logic — er ist reine Visualisierung des Canvas-Workflows.

Vorteile:

  • Celery übernimmt Retry, Error-Handling, Status-Tracking, Parallelisierung
  • workflow_node_results wird aus Celery-Task-Results befüllt (nicht custom Engine)
  • Scope von Phase C reduziert sich um ~50%

ADR-04: Domain-Driven Projektstruktur

Problem: Flache routers/ + services/ + models/ Struktur mit 15+ Domains wird unübersichtlich. Agenten können keine isolierten Domains parallel bearbeiten.

Entscheidung: Domain-Driven Structure

backend/app/
├── core/                        # Shared: auth, config, database, storage, websocket
│   ├── auth.py
│   ├── config.py
│   ├── database.py
│   ├── storage.py               # MinIO StorageBackend
│   └── websocket.py             # WebSocket broadcast
├── domains/
│   ├── tenants/                 # Tenant CRUD, RLS setup
│   │   ├── models.py
│   │   ├── schemas.py
│   │   ├── router.py
│   │   └── service.py
│   ├── products/                # Product, CadFile, STEP processing
│   │   ├── models.py
│   │   ├── schemas.py
│   │   ├── router.py
│   │   ├── service.py
│   │   └── tasks.py             # extract_cad_metadata, convert_step_to_stl
│   ├── rendering/               # OutputType, RenderTemplate, Workflow, render tasks
│   │   ├── models.py
│   │   ├── schemas.py
│   │   ├── router.py
│   │   ├── service.py
│   │   ├── workflow_builder.py  # Celery Canvas workflows
│   │   └── tasks.py             # render_still, render_turntable
│   ├── orders/                  # Order, OrderItem, OrderLine
│   │   ├── models.py
│   │   ├── schemas.py
│   │   ├── router.py
│   │   └── service.py
│   ├── media/                   # MediaAsset, download, zip
│   │   ├── models.py
│   │   ├── schemas.py
│   │   ├── router.py
│   │   └── service.py
│   ├── materials/               # Material, MaterialAlias, MaterialLibrary
│   │   ├── models.py
│   │   ├── schemas.py
│   │   ├── router.py
│   │   └── service.py
│   ├── billing/                 # Invoice, PricingTier
│   │   ├── models.py
│   │   ├── schemas.py
│   │   ├── router.py
│   │   └── service.py
│   ├── notifications/           # AuditLog, NotificationConfig
│   │   ├── models.py
│   │   ├── schemas.py
│   │   ├── router.py
│   │   └── service.py
│   └── imports/                 # Excel-Parser, Sanity-Check
│       ├── schemas.py
│       ├── router.py
│       ├── excel_parser.py
│       └── tasks.py             # validate_excel_import
└── main.py                      # Nur Router-Registrierung

Vorteile:

  • Neue Domain = neues Verzeichnis, kein bestehender Code angefasst
  • Jede Domain isoliert testbar
  • Agenten können Domains parallel implementieren ohne Konflikte
  • Imports sind selbstdokumentierend: from app.domains.billing.service import create_invoice

Migration: Bestehender Code wird schrittweise pro Phase in neue Struktur verschoben (nicht alles auf einmal).


ADR-05: WebSocket für Dashboard-Events, SSE nur für Task-Logs

Problem: SSE ist auf max. 6 gleichzeitige Verbindungen pro Browser (HTTP/1.1) limitiert. Für ein Live-Dashboard mit mehreren Datenquellen (Queue-Status, Worker-Status, Render-Events) ist das zu wenig.

Entscheidung: Zwei separate Real-Time-Kanäle:

WebSocket — für Dashboard-Events (1 Verbindung, multiplexed):

# core/websocket.py
@router.websocket("/ws")
async def websocket_endpoint(ws: WebSocket, user=Depends(get_ws_user)):
    await ws.accept()
    await subscribe_to_tenant_events(ws, user.tenant_id)
    # Events: queue_update, render_complete, render_failed,
    #         worker_online, worker_offline, order_status_change

SSE — für Render-Task-Logs (1 Stream pro Task, kurzlebig):

# domains/rendering/router.py
@router.get("/tasks/{task_id}/logs")
async def stream_task_logs(task_id: str, user=Depends(get_current_user)):
    async def event_generator():
        while True:
            logs = await redis.lrange(f"task_logs:{task_id}", -50, -1)
            for line in logs:
                yield f"data: {line}\n\n"
            if await task_is_done(task_id):
                break
            await asyncio.sleep(0.5)
    return EventSourceResponse(event_generator())

Einsatz:

  • Dashboard, Worker-Status, Queue-Längen → WebSocket
  • Blender-Stdout während Render → SSE

ADR-06: Blender Version Policy — >= 5.0.1, Upgrade-Pfad auf 5.1

Entscheidung: Blender < 5.0.1 wird nicht unterstützt. Während der Entwicklung erscheint Blender 5.1 — der Wechsel erfolgt dann ausschließlich auf >= 5.1.

Umsetzung:

# render-worker/Dockerfile
ARG BLENDER_VERSION=5.0.1
ARG BLENDER_MIN_VERSION=5.0.1

RUN BLENDER_URL="https://download.blender.org/release/Blender${BLENDER_VERSION}/blender-${BLENDER_VERSION}-linux-x64.tar.xz" \
    && curl -L $BLENDER_URL | tar xJ -C /opt/blender --strip-components=1
# render-worker/scripts/check_version.py — wird beim Container-Start geprüft
import bpy, sys
major, minor, patch = bpy.app.version
if (major, minor) < (5, 0):
    print(f"ERROR: Blender {major}.{minor}.{patch} nicht unterstützt. Minimum: 5.0.1")
    sys.exit(1)

Upgrade-Strategie 5.0.1 → 5.1:

  • BLENDER_VERSION Build-Arg in .env ändern, neu bauen
  • Render-Scripts auf API-Änderungen prüfen (Blender Changelog 5.1)
  • Bestehende .blend-Templates: in 5.1 öffnen + resaven (automatisch migriert)
  • QC-Gate: Test-Render mit Sample-STEP-File nach Upgrade

Hinweis: Blender 5.x verwendet den neuen Asset Library Standard (ab 3.0 eingeführt, in 5.x vollständig stabil) — dieser wird für ADR-07 vorausgesetzt.


ADR-07: Blender Asset Library für Materialien UND Modifier

Problem: Das bisherige material_libraries-Konzept erlaubt nur Material-Linking. Modifier (insbesondere Geometry Nodes) können damit nicht verwaltet werden. Blenders natives Asset-Library-System ist breiter und deckt beides ab.

Entscheidung: Blender Asset Library als primäres System für Assets:

# render-worker/scripts/asset_library.py

def apply_asset_library(blend_path: str, material_map: dict, modifier_map: dict):
    """
    Lädt Assets aus einer Asset-Library .blend-Datei:
    1. Materialien: linked/appended per Namen aus material_map
    2. Geometry-Node-Modifier: appended per Namen aus modifier_map, auf Mesh angewendet
    """
    with bpy.data.libraries.load(blend_path, link=False, assets_only=True) as (data_from, data_to):
        # Materialien laden
        data_to.materials = [
            name for name in data_from.materials
            if name in material_map.values()
        ]
        # Geometry-Node-Gruppen laden (für Modifier)
        data_to.node_groups = [
            name for name in data_from.node_groups
            if name in modifier_map.values()
        ]

    # Materialien auf Parts anwenden
    for obj in bpy.data.objects:
        if obj.type == 'MESH':
            for slot in obj.material_slots:
                resolved = material_map.get(slot.material.name if slot.material else '')
                if resolved and resolved in bpy.data.materials:
                    slot.material = bpy.data.materials[resolved]

    # Geometry-Node-Modifier anwenden
    for obj in bpy.data.objects:
        if obj.type == 'MESH':
            for part_name, modifier_name in modifier_map.items():
                if part_name in obj.name and modifier_name in bpy.data.node_groups:
                    mod = obj.modifiers.new(name=modifier_name, type='NODES')
                    mod.node_group = bpy.data.node_groups[modifier_name]

Datenmodell: material_libraries → umbenannt in asset_libraries:

asset_libraries (
  id UUID PK, tenant_id FK,
  name VARCHAR(200),
  blend_file_key TEXT,           -- MinIO key zur .blend-Datei
  catalog JSONB,                 -- Asset-Katalog: {materials: [...], node_groups: [...]}
  description TEXT,
  is_active BOOL DEFAULT TRUE,
  created_at TIMESTAMP
)

Workflow-Integration: Zwei neue Node-Typen:

  • apply_asset_library_materials — Material-Substitution via Asset Library
  • apply_asset_library_modifiers — Geometry-Node-Modifier via Asset Library

Katalog-Refresh: Nach Upload einer neuen .blend-Datei analysiert ein Celery-Task via Blender --background --python die Assets und schreibt den Katalog in asset_libraries.catalog JSONB — damit weiß die UI welche Assets verfügbar sind ohne die .blend zu öffnen.

Vorteile gegenüber bisherigem Ansatz:

  • Ein .blend kann Materialien und Modifier enthalten → weniger Dateien zu verwalten
  • Natives Blender-System → zukunftssicher (Blender entwickelt Asset Library weiter)
  • Modifier als Assets: z.B. "Bevel Sharp Edges", "Add Chamfer", "Clean Geometry" als wiederverwendbare Node-Groups
  • Asset-Katalog im Browser durchsuchbar ohne Blender zu starten

4. Was wird entfernt / ersetzt (mit Risiken)

4.1 Flamenco (Manager + Worker + Job-Scripts)

Entfernt:

  • flamenco/ Verzeichnis komplett
  • flamenco-manager, flamenco-worker Services aus docker-compose
  • flamenco_client.py, flamenco_tasks.py
  • Celery-Beat-Task poll_flamenco_jobs
  • flamenco_job_id, render_backend_used Spalten (Migration 032: nullable, später entfernen)
  • render_backend System-Setting

Ersetzt durch: Distributed Celery render-worker + MinIO shared storage

Risiken:

  • Laufende Flamenco-Jobs → Migration setzt Status auf cancelled
  • render_dispatcher.py muss vereinfacht werden (nur Celery-Pfad)

Migration:

UPDATE order_lines SET render_status = 'cancelled', flamenco_job_id = NULL
WHERE render_status = 'processing' AND flamenco_job_id IS NOT NULL;

4.2 blender-renderer (Flask HTTP-Service)

Entfernt:

  • blender-renderer/app.py (Flask-Wrapper)
  • Service aus docker-compose
  • HTTP-Aufrufe zu :8100 aus step_processor.py

Ersetzt durch:

  • Blender als subprocess im render-worker Celery-Container
  • blender_render.py wandert nach render-worker/scripts/
  • Render-Logik: domains/rendering/tasks.py

Risiken:

  • render-worker Container benötigt Blender + cadquery → größeres Image (~3GB)
  • Build-Zeit steigt → Base-Image vorab bauen und in lokale Registry pushen

4.3 threejs-renderer (Playwright HTTP-Service)

Entfernt: Kompletter Service + alle server-seitigen Three.js-Render-Pfade

Three.js bleibt als: Frontend-3D-Viewer (ThreeDViewer.tsx, läuft im Browser mit glTF)

Risiken:

  • Alle Three.js-generierten Thumbnails müssen mit Blender neu gerendert werden
  • Admin-Batch-Regenerierung wird beim Deploy ausgeführt

4.4 system_settings Key-Value-Store

Entfernt: system_settings Tabelle + _save_setting() direktes SQL-Hack

Ersetzt durch: app_config Modell mit JSONB-Spalten pro Kategorie (Render, Storage, Notifications, Worker, Billing) — vollständig ORM-native


4.5 Flache Projektstruktur (routers/ services/ models/)

Ersetzt durch: Domain-Driven Structure (ADR-04)

Migration: Schrittweise pro Phase, nicht alles auf einmal.


5. Was bleibt und wird erweitert

5.1 FastAPI Backend

  • Strukturell erhalten, in Domain-Driven Structure migriert
  • RLS-fähige DB-Dependency ersetzt einfaches get_db
  • Neue Domains: rendering, media, billing, tenants, imports

5.2 SQLAlchemy 2 + Alembic

  • Alle bestehenden Models bleiben (umstrukturiert in Domains)
  • RLS-Policies als raw SQL in Migrationen
  • Migration 032+ für neue Tabellen

5.3 Celery + Redis — erweiterte Queue-Struktur

Queue Worker Concurrency Tasks
step_processing step-worker 8 extract_cad_metadata, validate_excel_import
convert step-worker 4 convert_step_to_stl, extract_mesh_attributes
render_default render-worker 1 pro Container render_still, render_turntable_frames
notify step-worker 4 send_notification

Scaling-Modell: Jeder render-worker hat concurrency=1 (1 Blender-Prozess). Mehr Worker-Container = mehr parallele Renders. docker compose scale render-worker=4 → 4 parallele Renders.

5.4 Material-Alias-System

  • Lookup-Reihenfolge (Aliases zuerst) bleibt
  • Erweitert: material_library_id FK auf material_aliases
  • Erweitert: Unbekannte-Materialien-Report beim Excel-Import

5.5 RenderTemplate + Pricing + Notification (bleibt, in Domains integriert)

  • lighting_only, shadow_catcher bleiben
  • PricingTier → um Invoice-Modul erweitert
  • Notification → um notification_configs erweitert

6. Neue Komponenten

6.1 MinIO Object Storage (ADR-02)

Service in docker-compose.yml. Alle Datei-Operationen über StorageBackend Abstraction in core/storage.py. Externe Worker benötigen nur URL + Credentials — kein Mount.

Buckets:

  • uploads — STEP-Dateien, Thumbnails, Render-Outputs
  • blend-templates — .blend RenderTemplate-Dateien
  • asset-libraries — .blend Asset-Library-Dateien (Materialien + Modifier)
  • production-exports — glTF/GLB + .blend Production-Downloads (kurzlebig, TTL 7d)
  • exports — Zip-Downloads, PDF-Invoices (kurzlebig, TTL 24h)

6.2 Tenant-Modell + PostgreSQL RLS (ADR-01)

tenants (id UUID PK, name VARCHAR, slug VARCHAR UNIQUE, is_active BOOL, created_at)

FK tenant_id auf: users, orders, products, cad_files, media_assets, invoices, material_libraries, render_templates

RLS-Policies in Migration 035 — danach ist Datenisolation automatisch.


6.3 Workflow-System: Celery Canvas + React Flow (ADR-03)

Datenmodell:

workflow_definitions (id, name, output_type_id FK, config JSONB, is_active)
  config = { "type": "still"|"turntable"|"multi_angle", "params": {...} }

workflow_runs (id, workflow_def_id FK, order_line_id FK, celery_task_id, status, started_at, completed_at)

workflow_node_results (id, run_id FK, node_name, status, output JSONB, log TEXT, duration_s FLOAT)

Execution: workflow_builder.py baut Celery Canvas aus config.type + config.params. Jeder Node-Task schreibt sein Ergebnis in workflow_node_results.

Node-Typen (Celery Tasks):

  • convert_step → STEP→STL via cadquery, prüft SHA256-Cache
  • extract_mesh_attributes → OCC Topologie → sharp_edges JSON
  • apply_asset_library_materials → Lädt Materialien aus Asset-Library .blend, wendet auf Mesh-Parts an
  • apply_asset_library_modifiers → Lädt Geometry-Node-Gruppen aus Asset-Library, wendet als Modifier an
  • render_still → Blender subprocess → PNG nach MinIO
  • render_turntable_frames → Blender subprocess → Frame-Ordner nach MinIO
  • composite_ffmpeg → Frames + bg_color → MP4 nach MinIO
  • export_gltf → Blender exportiert GLB mit angewendeten Produktionsmaterialien → MinIO
  • export_blend → Blender speichert .blend mit pack_all() → MinIO (alle Texturen eingebettet)
  • generate_thumbnail → Pillow resize → Thumb nach MinIO
  • publish_asset → MediaAsset-Record in DB erstellen

React Flow Frontend: WorkflowEditor.tsx — visualisiert den Canvas-Workflow, bearbeitet config JSONB. Kein eigener Execution-Code.


6.4 MediaAsset-Katalog

media_assets (
  id UUID PK, tenant_id FK, product_id FK, order_line_id FK,
  workflow_run_id FK,
  asset_type ENUM(thumbnail, still, turntable, stl_low, stl_high,
                  gltf_geometry,       -- glTF ohne Materialien (aus STEP-Konvertierung)
                  gltf_production,     -- GLB mit Produktionsmaterialien (aus export_gltf Node)
                  blend_production),   -- .blend mit eingebetteten Produktionsmaterialien
  storage_key TEXT,         -- MinIO object key
  file_size_bytes BIGINT,
  mime_type VARCHAR(100),
  width INT, height INT, duration_s FLOAT,
  render_config JSONB,
  created_at TIMESTAMP,
  is_archived BOOL DEFAULT FALSE
)

API: Filter, Single-Download, Zip-Download (StreamingResponse), Soft-Delete


6.5 OCC Mesh-Attribute Extraktion

# domains/products/tasks.py
def extract_mesh_attributes(step_path: str) -> dict:
    """
    Via pythonOCC BRep-Topologie:
    - sharp_edges: Kanten-Indices mit Dihedral-Winkel > Threshold (default 30°)
    - seam_candidates: Kanten zwischen verschiedenen Face-Typen
    - face_groups: Flächen nach Typ (planar, cylindrical, toroidal, ...)
    """

Output in cad_files.mesh_attributes JSONB → wird beim Render als Parameter übergeben.

Blender-Integration in render_still:

# render-worker/scripts/blender_render.py
if mesh_attributes and mesh_attributes.get("sharp_edges"):
    for edge_idx in mesh_attributes["sharp_edges"]:
        mesh.edges[edge_idx].use_edge_sharp = True
    bpy.ops.mesh.mark_seam(clear=False)
    bpy.ops.uv.smart_project()

6.6 Hash-basiertes Conversion-Caching

# domains/products/tasks.py
def get_stl_cache_key(step_object_key: str, quality: str) -> str:
    content = storage.download_bytes(step_object_key)
    sha256 = hashlib.sha256(content).hexdigest()
    return f"conversion-cache/{sha256[:2]}/{sha256}/{quality}.stl"

Zentraler Cache in MinIO uploads/conversion-cache/. Gleiches STEP-File → 1x konvertiert, egal wie oft hochgeladen oder unter welchem Namen.


6.7 Billing / Invoice-Modul

invoices (id, tenant_id FK, period_start, period_end, status ENUM, total_amount, created_at)
invoice_lines (id, invoice_id FK, order_line_id FK, product_name, asset_type, quantity, unit_price, total)

PDF-Export via WeasyPrint (HTML-Template → PDF). Excel-Export via openpyxl.


6.8 Excel Sanity-Check

Task validate_excel_import:

  1. Parse Excel
  2. Für jede Row prüfen: STEP vorhanden + completed? Materialien in Aliases? Produkt in DB?
  3. Fuzzy-Match-Vorschläge für unbekannte Materialien (via difflib.get_close_matches)
  4. Report in import_validations DB + WebSocket-Event an Client

Frontend: Sanity-Check-Dialog nach Upload, Ampel-Anzeige, Material-Lücken direkt schließbar.


6.9 WebSocket Live-Events (ADR-05)

# core/websocket.py
EVENT_TYPES = [
    "queue_update",        # Queue-Länge geändert
    "render_complete",     # Render erfolgreich
    "render_failed",       # Render gescheitert
    "worker_online",       # Neuer Worker registriert
    "worker_offline",      # Worker nicht mehr erreichbar
    "order_status_change", # Order-Status geändert
    "import_validated",    # Excel-Sanity-Check abgeschlossen
]

Dashboard, WorkerManagement, OrderDetail — alle abonnieren denselben WebSocket und filtern Events nach Typ.


6.10 Worker-Registrierung

# render-worker entrypoint
redis.hset('registered_workers', f'{hostname}:{pid}', json.dumps({
    'hostname': hostname,
    'queues': ['render_default'],
    'blender_version': get_blender_version(),
    'gpu': detect_gpu(),              # nvidia-smi oder None
    'started_at': utcnow().isoformat(),
    'last_heartbeat': utcnow().isoformat(),
}))
# Heartbeat alle 30s; Beat-Task entfernt stale Workers nach 90s

GET /api/workers liest Redis-Hash, berechnet Queue-Stats via Celery Inspect.


6.11 Blender Asset Library Management (ADR-07)

Datenmodell: asset_libraries (ersetzt material_libraries)

asset_libraries (
  id UUID PK, tenant_id FK,
  name VARCHAR(200),
  blend_file_key TEXT,       -- MinIO key: "asset-libraries/{id}.blend"
  catalog JSONB,             -- {materials: ["SCHAEFFLER_010101_Steel-Bare", ...],
                             --  node_groups: ["Bevel_Sharp_Edges", "Clean_Geometry", ...]}
  description TEXT,
  is_active BOOL DEFAULT TRUE,
  created_at TIMESTAMP
)

Katalog-Refresh-Task:

# domains/materials/tasks.py
def refresh_asset_library_catalog(asset_library_id: str):
    """
    Öffnet .blend via Blender --background, liest alle markierten Assets,
    schreibt Katalog nach asset_libraries.catalog JSONB.
    Läuft automatisch nach jedem .blend-Upload.
    """
    script = "render-worker/scripts/catalog_assets.py"
    result = subprocess.run(['blender', '--background', '--python', script,
                             '--', blend_path, '--output', 'json'], ...)
    catalog = json.loads(result.stdout)
    db.execute(update(AssetLibrary).values(catalog=catalog))

API:

  • POST /api/asset-libraries — Upload .blend, Katalog wird automatisch gelesen
  • GET /api/asset-libraries/{id}/catalog — Verfügbare Assets durchsuchen
  • PUT /api/asset-libraries/{id} — Metadaten aktualisieren
  • DELETE /api/asset-libraries/{id} — Löschen (nur wenn nicht in Verwendung)

Frontend: Asset-Library-Manager in Admin — Upload, Katalog-Anzeige (Materialien + Node-Groups als Badges), Zuweisung zu OutputTypes.


6.12 Interaktive 3D Browser-Vorschau mit Production-Materialien

Konzept: Der vorhandene ThreeDViewer.tsx (Three.js, OrbitControls) wird um Production-glTF-Support erweitert. Zwei Ansichtsmodi:

Modus glTF-Quelle Materialien
Geometrie-Preview gltf_geometry — aus STEP-Konvertierung Farbige Part-Gruppen (OCC-Extraktion)
Production-Preview gltf_production — aus export_gltf Workflow-Node Echte Produktionsmaterialien (PBR)

Blender → GLB Pipeline:

# render-worker/scripts/export_gltf.py
def export_gltf(stl_path, blend_key, material_map, modifier_map, output_key):
    # 1. STL importieren
    bpy.ops.import_mesh.stl(filepath=stl_path)
    # 2. Asset Library laden (Materialien + Modifier)
    apply_asset_library(blend_path, material_map, modifier_map)
    # 3. Als GLB exportieren
    bpy.ops.export_scene.gltf(
        filepath=output_path,
        export_format='GLB',
        export_materials='EXPORT',           # Materialien einbetten
        export_apply=True,                   # Modifier vor Export anwenden
        export_draco_mesh_compression_enable=True,  # Komprimierung
        export_texture_dir='',
    )

Hinweis Materialtreue: Blenders glTF-Exporter konvertiert Principled BSDF → PBR (metallic/roughness). Komplexe Shader-Nodes (z.B. Procedural Textures) werden nicht vollständig übertragen — für diese Fälle: Texture Baking vor Export (optionaler Workflow-Node bake_textures).

Frontend-Erweiterungen ThreeDViewer.tsx:

// Neue Props/Features:
interface ThreeDViewerProps {
  geometryGltfUrl?: string        // Geometrie-Preview (sofort verfügbar)
  productionGltfUrl?: string      // Production-Preview (nach Workflow-Abschluss)
  showMaterialToggle?: boolean    // Umschalten zwischen Modi
  showWireframe?: boolean         // Wireframe-Overlay
  environmentPreset?: 'studio' | 'outdoor' | 'dark'
}

Progressive Loading: Geometrie-Preview sofort zeigen → Production-Preview nachladen wenn verfügbar.

Download-Buttons direkt im Viewer:

  • "GLB herunterladen" → GET /api/media/{gltf_production_id}/download
  • ".blend herunterladen" → GET /api/media/{blend_production_id}/download

6.13 Production Export: glTF + .blend Download

Workflow-Node export_gltf:

  • Input: STL-Pfad, Asset-Library-ID, Material-Map, Modifier-Map
  • Output: GLB-Datei in MinIO production-exports/{cad_file_id}/{run_id}.glb
  • MediaAsset-Record: asset_type = gltf_production

Workflow-Node export_blend:

# render-worker/scripts/export_blend.py
def export_blend(stl_path, blend_key, material_map, modifier_map, output_key):
    # 1. STL + Asset Library laden (wie export_gltf)
    # ...
    # 2. Alle externen Daten einbetten
    bpy.ops.file.pack_all()
    # 3. Als .blend speichern (komprimiert)
    bpy.ops.wm.save_as_mainfile(
        filepath=output_path,
        compress=True,
        copy=True           # Original-Session unangetastet
    )

Größen-Warnung: .blend mit eingebetteten Texturen kann 50-500MB werden. Daher:

  • production-exports Bucket TTL: 7 Tage (konfigurierbar in app_config)
  • Maximale Dateigröße: 1GB (konfigurierbar)
  • Frontend-Warnung bei Dateien > 100MB vor Download

Standard-Workflow "Still mit Production-Exports":

chain(
    convert_step.si(order_line_id),
    extract_mesh_attributes.si(order_line_id),
    apply_asset_library_materials.si(order_line_id),
    apply_asset_library_modifiers.si(order_line_id),
    group(
        render_still.si(order_line_id),         # PNG für Produktion
        export_gltf.si(order_line_id),          # GLB für 3D-Viewer + Download
        export_blend.si(order_line_id),         # .blend für Archiv/Post-Processing
    ),
    generate_thumbnail.si(order_line_id),
    publish_asset.si(order_line_id),
)

7. Phasenplan mit Tasks

Phase A: Infrastruktur-Cleanup + MinIO ABGESCHLOSSEN (2026-03-06)

A1: Flamenco entfernen

  • docker-compose.yml → flamenco-manager, flamenco-worker entfernen
  • flamenco_client.py, flamenco_tasks.py löschen
  • render_dispatcher.py → vereinfachen (nur Celery-Pfad)
  • Migration 032: laufende Flamenco-Jobs auf cancelled setzen
  • Akzeptanzkriterium: docker compose up startet ohne flamenco, alle bestehenden Renders laufen via Celery

A2: blender-renderer → render-worker Celery-Container (ADR-06 umsetzen)

  • render-worker/Dockerfile (neu): Ubuntu + Blender (>= 5.0.1, via BLENDER_VERSION Build-Arg) + cadquery + Python-Deps
  • check_version.py läuft beim Container-Start: prüft Blender >= 5.0.1, Exit 1 wenn nicht erfüllt
  • blender-renderer/blender_render.pyrender-worker/scripts/blender_render.py
  • domains/rendering/tasks.py (neu): render_still_task, render_turntable_task
  • Blender via subprocess.run, stdout in Redis für SSE
  • docker-compose.yml: blender-renderer entfernen, render-worker hinzufügen
  • .env.example: BLENDER_VERSION=5.0.1 dokumentieren
  • Akzeptanzkriterium: Thumbnail via Celery-Task, kein HTTP-Call zu :8100, Version-Check besteht

A3: threejs-renderer entfernen

  • Service entfernen, threejs-Pfad in step_processor.py entfernen
  • Batch-Regenerierung aller threejs-Thumbnails (Admin-Funktion)
  • ThreeDViewer.tsx (Frontend) bleibt
  • Akzeptanzkriterium: Alle Thumbnails Blender-gerendert

A4: MinIO hinzufügen + Storage-Abstraction

  • MinIO Service in docker-compose.yml
  • core/storage.py: MinIOStorage + LocalStorage (für Dev-Fallback)
  • Bestehende Upload-Endpoints: Dateien nach MinIO statt in lokales /uploads
  • Migration bestehender Dateien: Skript das /uploads nach MinIO hochlädt
  • .env.example: MINIO_URL, MINIO_USER, MINIO_PASSWORD
  • docker-compose.worker.yml (neu): render-worker für externe Maschinen
  • Akzeptanzkriterium: File-Upload → MinIO, Worker-Container läuft auf Maschine B und rendert Jobs

A5: system_settings → app_config

  • Migration 033: app_config Tabelle (JSONB-Spalten: render, storage, notifications, worker, billing)
  • core/config_service.py (neu), system_settings Tabelle deprecated
  • Migrate bestehende Settings
  • Akzeptanzkriterium: Alle Settings ORM-native persistierbar, kein direktes SQL

Phase B: Domain-Driven Umstrukturierung + Tenant-Modell ABGESCHLOSSEN (2026-03-06)

B1: Domain-Driven Struktur anlegen

  • backend/app/domains/ Verzeichnis erstellen
  • Bestehende Models/Services/Routers schrittweise in Domains verschieben (products, orders, materials, rendering, notifications zuerst)
  • main.py registriert nur noch Domain-Router
  • Akzeptanzkriterium: Alle bestehenden Tests grün, Imports funktionieren, API-Endpoints unverändert

B2: Tenant-Datenmodell + RLS

  • Migration 035: tenants Tabelle + 'Schaeffler' Default-Seed
  • Migration 036: tenant_id FK auf alle Tabellen + RLS-Policies (tenant_isolation + admin_bypass) + Backfill
  • domains/tenants/ mit CRUD-Router, Service, Modellen
  • core/database.py: get_db_for_tenant + set_tenant_context() Dependency
  • Admin-Bypass via current_setting('app.current_tenant_id', true) = 'bypass'
  • BYPASSRLS-Versuch mit graceful fallback

B3: Tenant-Management UI

  • frontend/src/pages/Tenants.tsx: CRUD-Tabelle + Tenant-Selektor Dropdown
  • frontend/src/api/tenants.ts: vollständiger API-Client
  • X-Tenant-ID Header-Interceptor in api/client.ts
  • Route /tenants + Sidebar-Link (admin-only)

Phase C: Workflow-System ABGESCHLOSSEN (2026-03-06)

C1: WorkflowDefinition Datenmodell

  • Migration 036: workflow_definitions, workflow_runs, workflow_node_results
  • domains/rendering/models.py erweitern
  • domains/rendering/workflow_builder.py (neu): Celery-Canvas-Builder für "still", "turntable", "multi_angle"
  • output_types.workflow_definition_id FK (Migration 037)
  • Akzeptanzkriterium: Render via dispatch_workflow("still", order_line_id) erfolgreich

C2: Standard-Workflows seeden + render_dispatcher migrieren

  • 3 Standard-Workflows direkt in Migration 037 geseedet (Still, Turntable, Multi-Angle)
  • workflow_builder.py: dispatch_workflow() mit Celery Canvas (chain/group)
  • dispatch_service.py: prüft output_type.workflow_definition_id → neu vs. Legacy-Pfad
  • Backward-Compat: ohne workflow_definition_id → alter direkter Task-Call

C3: React Flow Workflow-Editor (Frontend)

  • @xyflow/react zu package.json hinzugefügt (npm install nötig)
  • frontend/src/pages/WorkflowEditor.tsx: 6 Custom-Node-Typen, ConfigSidepanel, Node-Palette mit Drag-Drop
  • frontend/src/api/workflows.ts: vollständiger CRUD-Client
  • Route /workflows + Sidebar-Link (admin + project_manager)

Phase D: OCC Mesh-Attribute ABGESCHLOSSEN (2026-03-06)

D1: Attribut-Extraktion

  • domains/products/tasks.py: extract_mesh_attributes Celery-Task
  • Migration 038: cad_files.mesh_attributes JSONB
  • Läuft nach extract_cad_metadata in Workflow-Chain
  • Akzeptanzkriterium: STEP-Upload → mesh_attributes JSON in DB mit sharp_edges

D2: Blender-Integration

  • render-worker/scripts/still_render.py + turntable_render.py: _apply_mesh_attributes() setzt Auto-Smooth basierend auf curved_ratio und sharp_angle_threshold_deg
  • render_blender.py: übergibt --mesh-attributes JSON an Blender-Subprocess
  • render_still_task: lädt mesh_attributes aus DB und reicht sie weiter

Phase E: MediaAsset-Katalog ABGESCHLOSSEN (2026-03-06)

E1: Datenmodell + API

  • Migration 040: media_assets Tabelle mit RLS-Policies
  • domains/media/: MediaAsset-Model, Schemas, Service, Router
  • publish_asset Celery-Task in rendering/tasks.py
  • core/storage.py: download_bytes() für MinIO + Local

E2: Frontend

  • frontend/src/pages/MediaBrowser.tsx: Grid/List-Toggle, Multi-Select, Floating Action Bar (ZIP + Archiv)
  • frontend/src/api/media.ts: vollständiger API-Client mit zipDownloadAssets()
  • Route /media + Sidebar-Link (admin + project_manager)

Phase F: Hash-basiertes Conversion-Caching (Woche 5)

F1: Cache-Service

  • domains/products/tasks.py: SHA256-Check vor jeder STL-Konvertierung
  • Migration 040: cad_files.step_file_hash VARCHAR(64)
  • Cache in MinIO uploads/conversion-cache/
  • Akzeptanzkriterium: Gleiches STEP-File → Log zeigt "cache hit" beim 2. Upload

Phase G: Billing & Reporting (Woche 6)

G1: Invoice Datenmodell + API

  • Migration 041: invoices, invoice_lines
  • domains/billing/ (neu)
  • POST /api/billing/invoices, GET /api/billing/invoices/{id}/pdf (WeasyPrint)
  • Akzeptanzkriterium: PDF-Invoice mit korrekten Positionen downloadbar

G2: Billing Dashboard (Frontend)

  • frontend/src/pages/Billing.tsx (neu)
  • Kosten-Übersicht per Tenant/Zeitraum, Invoice-Liste + Download
  • Akzeptanzkriterium: Invoice generierbar und downloadbar

Phase H: Excel Sanity-Check (Woche 7)

H1: Sanity-Check Task + Fuzzy-Match

  • domains/imports/tasks.py: validate_excel_import
  • Migration 042: import_validations Tabelle
  • difflib.get_close_matches für Materialvorschläge
  • WebSocket-Event nach Abschluss

H2: Sanity-Check UI

  • Ampel-Dialog nach Excel-Upload
  • Material-Lücken direkt im Dialog schließbar (neuer Alias)
  • Akzeptanzkriterium: Klar welche Produkte produzierbar sind, Material-Aliases ergänzbar

Phase I: Konfigurierbare Notifications (Woche 7)

I1: Notification-Config

  • Migration 043: notification_configs Tabelle
  • domains/notifications/service.py: prüft Config vor Emit
  • Standard-Seeding: alle Events für Admin aktiviert

I2: Settings UI

  • frontend/src/pages/NotificationSettings.tsx (neu)
  • Toggle-Matrix: Event × Kanal (In-App, E-Mail optional)
  • Akzeptanzkriterium: Events abschaltbar, Einstellungen wirksam

Phase J: WebSocket + SSE Log-Streaming (Woche 8)

J1: WebSocket Backend

  • core/websocket.py: Connection-Manager, Tenant-basiertes Broadcasting
  • Alle relevanten Tasks/Services broadcasten WebSocket-Events
  • GET /ws Endpoint

J2: SSE Task-Logs

  • GET /api/tasks/{task_id}/logs — SSE, Worker schreibt in Redis-Liste
  • LiveRenderLog.tsx erweitern: EventSource API, Auto-scroll

J3: Frontend WebSocket-Integration

  • Dashboard, WorkerManagement, OrderDetail abonnieren /ws
  • Ersetzt polling-basierte useQuery-Intervalle wo sinnvoll
  • Akzeptanzkriterium: Render-Start → Dashboard zeigt Status-Update ohne Reload

Phase K: Blender Asset Library + Production Exports (Woche 8-9)

K1: Asset Library Datenmodell + Upload

  • Migration 044: asset_libraries Tabelle (id, name, blend_file_key, catalog JSONB, tenant_id)
  • render_templates.asset_library_id FK, output_types.asset_library_id Default-Library
  • Upload via MinIO asset-libraries/ Bucket
  • Nach Upload: Celery-Task refresh_asset_library_catalog → öffnet .blend via Blender --background, liest Asset-Namen, schreibt in catalog JSONB
  • Akzeptanzkriterium: .blend hochladen → Katalog mit Materialien + Node-Groups in DB sichtbar

K2: Asset Library Management UI

  • domains/materials/ → Asset-Library-Manager (Upload, Katalog-Anzeige als Badge-Grid)
  • Materialien + Node-Groups aus Katalog anzeigen
  • Zuweisung per OutputType + RenderTemplate wählbar
  • Akzeptanzkriterium: 2 Libraries für verschiedene OutputTypes konfigurierbar

K3: Workflow-Nodes apply_asset_library_materials + apply_asset_library_modifiers

  • render-worker/scripts/asset_library.py: Materialien und Node-Groups aus .blend linken/appenden
  • Workflow-Builder: Nodes in Standard-Workflow "Still mit Production-Exports" integrieren
  • Akzeptanzkriterium: Render mit Asset-Library zeigt korrekte Produktionsmaterialien im PNG

K4: export_gltf Workflow-Node

  • render-worker/scripts/export_gltf.py: Blender exportiert GLB mit angewendeten Materialien
  • Modifier vor Export anwenden (export_apply=True), Draco-Komprimierung aktiviert
  • MediaAsset-Eintrag: asset_type = gltf_production
  • Akzeptanzkriterium: GLB-Download aus Browser ladbar, Materialien sichtbar in Three.js-Viewer

K5: export_blend Workflow-Node

  • render-worker/scripts/export_blend.py: pack_all() + save_as_mainfile(compress=True)
  • Größenwarnung-Config in app_config (Default: Warnung ab 100MB, Limit 1GB)
  • MediaAsset-Eintrag: asset_type = blend_production
  • TTL in MinIO production-exports/: 7 Tage (konfigurierbar)
  • Akzeptanzkriterium: .blend-Download enthält alle Texturen, öffnet in Blender 5.x ohne fehlende Links

K6: 3D-Viewer Production-Modus (Frontend)

  • ThreeDViewer.tsx erweitern: Modus-Toggle Geometrie ↔ Production-glTF
  • Wireframe-Toggle, Environment-Preset-Auswahl (studio/outdoor/dark)
  • Download-Buttons im Viewer für GLB + .blend
  • Progressive Loading: Geometrie-Preview sofort, Production-glTF nachladen
  • Akzeptanzkriterium: Interaktiver Viewer zeigt Produktionsmaterialien; Download funktioniert

Phase L: Dashboard & UX (Woche 9-10)

L1: Modular Widget-Dashboard

  • Widget.tsx generischer Container, Widget-Config per User in DB
  • Widget-Typen: ProductionStats, QueueStatus, RecentRenders, CostOverview, WorkerStatus
  • WebSocket-Feed für Live-Updates

L2: Responsive Design

  • Tailwind CSS-Variablen auf RGB-Channel-Format (behebt Learning 2026-02-18)
  • 768px Minimum (iPad-Breite)

L3: Worker-Management UI

  • WorkerManagement.tsx (neu): Worker-Liste aus Redis, Queue-Stats, Scale-Button

Phase M: QC-Tests (Woche 10-11)

M1: Pytest Backend

  • tests/domains/ — pro Domain: API-Tests + Service-Tests
  • Fixtures: Test-DB mit RLS-Setup, Mock-MinIO (moto), Mock-Celery
  • Akzeptanzkriterium: > 80% Coverage auf Service-Layer, alle Domains

M2: Frontend Vitest

  • frontend/src/__tests__/ — Komponenten-Tests mit Testing Library
  • Akzeptanzkriterium: npm run test → 0 Failures

M3: Integration-Tests

  • End-to-End: STEP Upload → MinIO → Celery → Render (Mock-Blender) → MediaAsset → Download
  • Tenant-Isolation-Test: Client A sieht keine Client-B-Daten
  • Akzeptanzkriterium: Pipeline durchlaufbar in CI ohne echtes Blender

8. Datenbankmigrationen-Übersicht

Migration Beschreibung Phase
032 Flamenco-Felder bereinigen, Jobs auf cancelled A
033 app_config (strukturiertes Config-Modell, ersetzt system_settings) A
034 tenants Tabelle B
035 tenant_id FKs + PostgreSQL RLS-Policies + Backfill B
036 workflow_definitions, workflow_runs, workflow_node_results C
037 output_types.workflow_definition_id FK C
038 cad_files.mesh_attributes JSONB D
039 media_assets Tabelle E
040 cad_files.step_file_hash VARCHAR(64) F
041 invoices, invoice_lines G
042 import_validations H
043 notification_configs I
044 asset_libraries (ersetzt material_libraries), FKs auf render_templates/output_types K
045 media_assets.asset_type: ENUM um gltf_production, blend_production erweitern K

9. QC-Gates und Test-Checkliste

Diese Checkliste ist für Agenten konzipiert — jeder Task muss diese Gates passieren bevor Commit.

9.1 Backend QC-Gates

# Syntax-Check
docker compose exec backend python -m py_compile app/domains/[domain]/[changed_file].py

# Alembic
docker compose exec backend alembic current  # → head

# Pytest
docker compose exec backend pytest tests/ -x --tb=short  # → 0 Failures

# Import + Schema
docker compose exec backend python -c "from app.main import app; print('OK')"

9.2 RLS QC-Gate (neu, nach Phase B)

# Tenant-Isolation Test
docker compose exec backend pytest tests/domains/tenants/test_rls.py -v
# → Client A kann keine Client-B-Daten lesen/schreiben
# → Admin mit BYPASSRLS sieht alle Daten

9.3 Celery QC-Gates

docker compose exec step-worker celery -A app.celery_app inspect registered
# → extract_cad_metadata, convert_step_to_stl, extract_mesh_attributes, validate_excel_import

docker compose exec render-worker celery -A app.celery_app inspect registered
# → render_still, render_turntable_frames, composite_ffmpeg, generate_thumbnail, publish_asset

docker compose exec step-worker celery -A app.celery_app inspect active_queues
# → step_processing, convert, notify

9.4 MinIO QC-Gate (neu, nach Phase A4)

# MinIO erreichbar
curl http://localhost:9000/minio/health/live  # → 200

# Upload + Download funktioniert
docker compose exec backend python -c "
from app.core.storage import storage
storage.upload('/tmp/test.txt', 'test/test.txt')
assert storage.exists('test/test.txt')
print('MinIO OK')
"

# Externer Worker kann MinIO erreichen
# (auf Maschine B ausführen)
docker compose -f docker-compose.worker.yml exec render-worker python -c "
from app.core.storage import storage
assert storage.exists('test/test.txt')
print('External worker MinIO access OK')
"

9.5 Frontend QC-Gates

cd frontend
npm run type-check  # → 0 Errors
npm run lint        # → 0 Errors
npm run test        # → 0 Failures
npm run build       # → Erfolg

9.6 Datenbank QC-Gates

# Migration prüfen (manuell lesen!) — besonders RLS-Policies in 035
cat backend/alembic/versions/035_*.py

# Up + Down testen
docker compose exec backend alembic upgrade head
docker compose exec backend alembic downgrade -1
docker compose exec backend alembic upgrade head

9.7 Docker QC-Gates

docker compose up -d
docker compose ps  # → alle "healthy" nach 90s (MinIO braucht ~30s)
curl http://localhost:8888/health    # → 200
curl http://localhost:9000/minio/health/live  # → 200

9.8 Render-Pipeline QC-Gate (End-to-End)

# Upload STEP → Workflow → Thumbnail
curl -X POST http://localhost:8888/api/cad/upload -F "file=@step-sample-file/81113-l_cut.stp"
# → cad_file_id

sleep 30
curl http://localhost:8888/api/cad/{id} | jq .processing_status  # → "completed"
curl -I http://localhost:8888/api/cad/{id}/thumbnail             # → 200, image/png

# MediaAsset angelegt?
curl http://localhost:8888/api/media?product_id={product_id} | jq length  # → > 0

9.9 Security QC-Gates

  • Kein Endpoint ohne Auth (außer /health, /ws, /api/cad/{id}/thumbnail)
  • Alle File-Uploads: MIME-Type + Größe validiert
  • Zip-Download: assert asset.tenant_id == current_tenant.id vor Hinzufügen
  • MinIO: Buckets nicht public; Presigned URLs mit TTL für Downloads
  • RLS aktiv: SELECT relrowsecurity FROM pg_class WHERE relname = 'products't
  • JWT-Secret in .env, nicht im Code

9.10 Performance QC-Gates

  • Kein N+1-Query (selectinload / joinedload in List-Endpoints)
  • List-Endpoints paginiert (max. 100 Items/Page)
  • Zip-Download streamt (StreamingResponse)
  • WebSocket: kein Broadcasting an alle Tenants (nur eigener Tenant)
  • Thumbnails: Cache-Control: max-age=3600 Header

10. Offene Entscheidungen

# Frage Optionen Empfehlung / Status
1 Blender-Version 4.x / 5.0.1 Entschieden: >= 5.0.1 Pflicht, Upgrade auf 5.1 sobald verfügbar
2 React Flow Lizenz MIT / Pro MIT reicht für internes System
3 PDF-Generator WeasyPrint / ReportLab WeasyPrint (HTML→PDF)
4 Mobile-Support Scope iPad (768px) / Vollmobil (375px) 768px Minimum
5 OrderItem-Refactor Jetzt / v3 v3 (zu viel abhängiger Code)
6 Blender GPU-Config Pro-Worker via deploy.resources Bleibt, NVIDIA-Support via ENV
7 E-Mail-Notifications SMTP jetzt / später Später — nur In-App in v2
8 Three.js-Thumbnails Batch-Regenerierung Obligatorisch / On-Demand Obligatorisch beim Refactor-Deploy
9 MinIO Backup-Strategie MinIO Replication / S3 Sync Außerhalb Scope v2 — in .env dokumentieren
10 CI/CD Pipeline GitHub Actions / lokal GitHub Actions für Lint + Tests
11 glTF Materialtreue PBR-Export / Texture-Baking 11A: PBR-Export only — Principled BSDF → GLB, kein Baking
12 Asset Library: link vs. append link=True (Referenz bleibt) / link=False (Kopie) 12B: link=True — Library als Referenz; .blend-Exports nutzen pack_all() für self-contained Files
13 blend_production TTL 7 Tage / 30 Tage / permanent 13C: Permanent — .blend-Dateien bleiben dauerhaft in MinIO; Größen-Warnungen via app_config
14 ThreeDViewer Environment Nur Studio / mehrere Presets Studio-Preset im v2-Scope; weitere Presets v3

Freigabe

Architektur-Entscheidungen bestätigen:

  • ADR-01: PostgreSQL RLS für Tenant-Isolation
  • ADR-02: MinIO als Shared Object Storage (ersetzt NFS)
  • ADR-03: Celery Canvas als Workflow-Engine, React Flow nur Visualisierung
  • ADR-04: Domain-Driven Projektstruktur
  • ADR-05: WebSocket für Dashboard-Events, SSE nur für Task-Logs
  • ADR-06: Blender >= 5.0.1 Pflicht, BLENDER_VERSION als Build-Arg, Upgrade auf 5.1
  • ADR-07: Blender Asset Library (Materialien + Modifier), asset_libraries Modell

Bestätigte Entscheidungen (Abschnitt 10):

  • 11A: glTF PBR-Export only (kein Texture-Baking)
  • 12B: Asset Library link=True + pack_all() für .blend-Exports
  • 13C: blend_production permanent in MinIO
  • Bestehende API-Endpoints bleiben während Refactor erhalten (17)
  • Phasenweise Implementierung mit Quality Gates (18)

Planung:

  • Plan insgesamt freigegeben
  • Offene Entscheidungen aus Abschnitt 10 geklärt
  • Startphase A bestätigt
  • Git-Tag v1-stable auf main erstellt
  • Git-Branch refactor/v2 erstellt

Render Pipeline Fixes (2026-03-06)

Kontext

Nach Aktivierung von Multi-Tenancy (Migration 035/036) hatten mehrere Bugs die gesamte Render-Pipeline blockiert. Alle wurden behoben.

Durchgeführte Fixes

Fix Problem Lösung Datei
B-Fix-1 worker-thumbnail ohne Blender konkurrierte auf thumbnail_rendering → 50% Silent-Fails worker-thumbnail aus docker-compose.yml entfernt docker-compose.yml
B-Fix-2 render_order_line_task auf step_processing Queue → worker ohne Blender → Pillow-Fallback Queue zu thumbnail_rendering geändert step_tasks.py:247
B-Fix-3 Circular Import template_service.pydomains/rendering/service.pyresolve_template() nie aufrufbar Volle sync SQLAlchemy Implementierung in template_service.py wiederhergestellt services/template_service.py
B-Fix-4 audit_log.tenant_id NOT NULL → Broadcast-Notifications scheiterten → Order Submit 500 ALTER TABLE audit_log ALTER COLUMN tenant_id DROP NOT NULL DB direkt
B-Fix-5 Shared System-Tabellen (output_types, materials, etc.) tenant_id NOT NULL → Create-Endpoints schlugen fehl tenant_id DROP NOT NULL für alle System-Tabellen DB direkt
B-Fix-6 STEP Upload + Excel Import setzten tenant_id=NULL user.tenant_id durch alle Create-Pfade durchgezogen uploads.py, excel_import.py, products/service.py
B-Fix-7 GET /api/tenants → 307 Redirect → axios verliert Authorization-Header → 401 → leere Tenant-Liste Trailing Slash in API-Call: /tenants/ frontend/src/api/tenants.ts
B-Fix-8 Admin-UI zeigte noch Flamenco + Three.js Optionen Flamenco-Section + Three.js-Picker entfernt Admin.tsx, OutputTypeTable.tsx
B-Fix-9 5 Output-Types noch auf render_backend='flamenco' UPDATE output_types SET render_backend='celery' DB direkt

Neue Testing-Infrastruktur (DONE)

GET /api/worker/health/render — Render Health Endpoint:

  • Render-Worker connected (Celery inspect)
  • Blender erreichbar (HTTP GET blender-renderer:8100/health)
  • thumbnail_rendering Queue Tiefe < 10
  • Letzter Render < 30 min alt und erfolgreich
  • Response: { status: "ok"|"degraded"|"down", render_worker_connected, blender_available, thumbnail_queue_depth, last_render_at, ... }

scripts/test_render_pipeline.py — Integration Test Script:

python scripts/test_render_pipeline.py --health    # Health-Check only
python scripts/test_render_pipeline.py --sample    # 1 STEP + 1 Output-Type (schnell)
python scripts/test_render_pipeline.py --full      # Alle Output-Types (langsam)

Celery-Queue-Architektur (nach Fixes)

Queue Worker Concurrency Tasks
step_processing worker 8 process_step_file, dispatch_order_line_render
thumbnail_rendering render-worker (Blender 5.0.1) 1 render_step_thumbnail, regenerate_thumbnail, render_order_line_task, generate_stl_cache
ai_validation worker 8 Azure AI Validierung

Schlüsselprinzip: Alles was Blender aufruft → thumbnail_rendering Queue → nur render-worker → kein Timeout durch parallele Requests.