# 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](#1-ziel-zusammenfassung)
2. [Architektur-Analyse: Ist vs. Soll](#2-architektur-analyse-ist-vs-soll)
3. [Architektur-Entscheidungen (ADRs)](#3-architektur-entscheidungen-adrs)
4. [Was wird entfernt / ersetzt (mit Risiken)](#4-was-wird-entfernt--ersetzt-mit-risiken)
5. [Was bleibt und wird erweitert](#5-was-bleibt-und-wird-erweitert)
6. [Neue Komponenten](#6-neue-komponenten)
7. [Phasenplan mit Tasks](#7-phasenplan-mit-tasks)
8. [Datenbankmigrationen-Übersicht](#8-datenbankmigrationen-übersicht)
9. [QC-Gates und Test-Checkliste](#9-qc-gates-und-test-checkliste)
10. [Offene Entscheidungen](#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)

```sql
-- 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;
```

```python
# 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)

```yaml
# 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"]
```

```python
# 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.

```python
# 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):
```json
{
  "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):
```python
# 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):
```python
# 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:**
```dockerfile
# 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
```

```python
# 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:

```python
# 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:**
```sql
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

```python
# 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`:
```python
# 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

```python
# 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)

```python
# 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

```python
# 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:**
```python
# 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:**
```python
# 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:**
```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`:**
```python
# 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":**
```python
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.py` → `render-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

```bash
# 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)

```bash
# 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

```bash
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)

```bash
# 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

```bash
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

```bash
# 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

```bash
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)

```bash
# 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:**

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

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

**Planung:**

- [x] Plan insgesamt freigegeben
- [x] Offene Entscheidungen aus Abschnitt 10 geklärt
- [x] Startphase A bestätigt
- [x] Git-Tag `v1-stable` auf main erstellt
- [x] 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.py` ↔ `domains/rendering/service.py` → `resolve_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:
```bash
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.
