381f44bc8b
- GET /api/worker/health/render: checks render-worker (thumbnail_rendering queue), Blender availability via active_queues inspect, queue depth, last render recency — returns ok/degraded/down status - scripts/test_render_pipeline.py: integration test for full pipeline (--health, --sample, --full modes) - PLAN.md: appended Render Pipeline Fixes section with all B-Fixes - LEARNINGS.md: documented 5 new learnings (queue mismatch, circular import, 307 redirect, worker capability detection) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1456 lines
58 KiB
Markdown
1456 lines
58 KiB
Markdown
# 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.
|