Files
HartOMat/PLAN.md
T
Hartmut bf0c55c970 docs: Phasen B-E abgeschlossen — PLAN.md + LEARNINGS.md aktualisiert
PLAN.md: Phasen A-E als  ABGESCHLOSSEN markiert, Status auf Phase F.
LEARNINGS.md: 4 neue Learnings:
- Bash CWD-Problem durch Hook-Pfad-Auflösung (Symlink-Fix)
- PostgreSQL RLS current_setting Null-Safety + Admin-Bypass-Pattern
- Domain-Migration mit Compat-Shims (Big-Bang vermeiden)
- Celery Canvas vs. Custom Workflow-Engine

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 17:14:43 +01:00

1408 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Refactor-Plan: Schaeffler Automat v2
**Erstellt:** 2026-03-05
**Aktualisiert:** 2026-03-06 — Phasen A, B, C, D, E abgeschlossen
**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