The queue handles far more than thumbnails: OCC tessellation, USD master generation, GLB production, order line renders, and workflow renders. asset_pipeline better reflects its role as the render-worker's primary queue. Updated all references in: task decorators, celery_app.py, beat_tasks.py, docker-compose.yml worker command, worker.py MONITORED_QUEUES, admin.py, CLAUDE.md, LEARNINGS.md, Dockerfile, helpTexts.ts, test files, and all .claude/commands/*.md skill files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
58 KiB
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
- Ziel-Zusammenfassung
- Architektur-Analyse: Ist vs. Soll
- Architektur-Entscheidungen (ADRs)
- Was wird entfernt / ersetzt (mit Risiken)
- Was bleibt und wird erweitert
- Neue Komponenten
- Phasenplan mit Tasks
- Datenbankmigrationen-Übersicht
- QC-Gates und Test-Checkliste
- 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-rendererist Flask-HTTP-Service → max. 1 concurrent Request, kein echtes Scalingthreejs-rendererredundant zu Blender für Thumbnails (eigener Container, eigene Playwright-Instanz)flamencoist komplexes externes System (Job-Types in JS) — Mehraufwand ohne Mehrwert über verteilte Celery-Workerworker-thumbnailmit concurrency=1 ist Workaround für blender-renderer-Limitation- STEP-Konvertierung passiert mehrfach (blender-renderer + threejs-renderer unabhängig voneinander)
- Kein Tenant-Konzept — alle Kunden teilen dieselbe DB-Namespace
- Keine echte Pipeline-Konfiguration — Logik ist hartcodiert in step_tasks.py
- Kein Shared Storage → externe Worker können keine STEP-Dateien lesen
SOLL-Architektur (8 Core-Services + n render-worker)
Internet
↓
frontend:5173 (React/Vite + React Flow + WebSocket)
↓ HTTP / WebSocket / SSE
backend:8888 (FastAPI, Domain-driven, RLS-enabled)
↓ SQL (RLS) ↓ Celery Canvas ↓ S3 API
postgres:5432 redis:6379 minio:9000
(+ RLS) ↓ ↑ (shared object storage)
step-worker render-worker ← lokal (Maschine A)
beat render-worker ← Netzwerk (Maschine B)
render-worker ← GPU (Maschine C)
Vorteile SOLL:
- Blender läuft direkt im Celery-Worker als subprocess → kein HTTP-Overhead, kein Timeout-Problem
- Worker auf beliebigen Maschinen: brauchen nur
REDIS_URL+MINIO_URL+ Blender installiert - MinIO als S3-kompatibler Object-Store ersetzt NFS — kein Mount nötig, funktioniert überall
- PostgreSQL RLS sichert Tenant-Isolation automatisch — kein manueller WHERE-Filter nötig
- Celery Canvas für Workflow-Execution — keine custom Workflow-Engine
- React Flow nur als Visualisierungsschicht — deutlich reduzierter Scope
- Kein Flamenco, kein threejs-renderer → 3 Services weniger
3. Architektur-Entscheidungen (ADRs)
ADR-01: PostgreSQL Row-Level Security statt manuellem tenant_id-Filter
Problem: Jeder neue Router-Query müsste manuell WHERE tenant_id = :x haben. Ein vergessenes Filter = Datenleck zwischen Kunden.
Entscheidung: PostgreSQL Row-Level Security (RLS)
-- Einmalig pro Tabelle (in Migration 035)
ALTER TABLE products ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON products
USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
-- Admin-Bypass via BYPASSRLS-Rolle
ALTER ROLE schaeffler_admin BYPASSRLS;
# FastAPI Dependency: einmal pro Request setzen
async def get_db_for_tenant(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user)
) -> AsyncSession:
await db.execute(
text("SET LOCAL app.current_tenant_id = :tid"),
{"tid": str(user.tenant_id)}
)
yield db
Vorteile:
- Unmöglich Cross-Tenant-Leaks durch vergessene Filter
- Gilt automatisch für alle zukünftigen Queries — auch neue Endpoints
- Testbar: RLS-Policies sind SQL, unabhängig von Anwendungscode
Nachteile / Risiken:
- Migration muss RLS für alle betroffenen Tabellen aktivieren
BYPASSRLSfür Admin-User muss in DB-Migrationen gesetzt werden- Alembic-Autogenerate erkennt keine RLS-Policies → Policies müssen manuell in Migration geschrieben werden
ADR-02: MinIO statt NFS für Shared Storage
Problem: Externe Worker müssen STEP-Dateien und Render-Outputs lesen/schreiben. NFS ist operationell komplex, plattformabhängig, und ein Single-Point-of-Failure.
Entscheidung: MinIO (S3-kompatibel, Docker-nativ, self-hosted)
# docker-compose.yml
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${MINIO_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD:-minioadmin}
ports:
- "9000:9000" # S3 API
- "9001:9001" # Web Console
volumes:
- minio-data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
# backend/core/storage.py
import boto3
from pathlib import Path
class MinIOStorage:
def __init__(self):
self.client = boto3.client(
's3',
endpoint_url=settings.MINIO_URL,
aws_access_key_id=settings.MINIO_USER,
aws_secret_access_key=settings.MINIO_PASSWORD,
)
self.bucket = 'uploads'
def upload(self, local_path: Path, object_key: str) -> str:
self.client.upload_file(str(local_path), self.bucket, object_key)
return object_key
def download(self, object_key: str, local_path: Path) -> Path:
self.client.download_file(self.bucket, object_key, str(local_path))
return local_path
def exists(self, object_key: str) -> bool:
try:
self.client.head_object(Bucket=self.bucket, Key=object_key)
return True
except: return False
Render-Worker: Lädt STEP-File vor Render aus MinIO in lokales tmpdir, lädt Output zurück nach MinIO.
Externe Worker brauchen nur:
REDIS_URL=redis://server:6379/0MINIO_URL=http://server:9000MINIO_USER+MINIO_PASSWORD
Kein Mount, kein NFS, funktioniert auf Windows/Linux/Mac gleich.
ADR-03: Celery Canvas für Workflow-Execution, React Flow nur Visualisierung
Problem: Eine custom Workflow-Engine (Graph-Traversal, Dependency-Resolution, Retry-Logic) ist ~2-3 Wochen Eigenentwicklung — Celery hat das bereits eingebaut.
Entscheidung: Celery Canvas als Execution-Engine, deklarative JSON-Config als Definition, React Flow als Visualisierung.
# domains/rendering/workflow_builder.py
from celery import chain, group
WORKFLOW_BUILDERS = {
"still": lambda order_line_id: chain(
convert_step.si(order_line_id),
extract_mesh_attributes.si(order_line_id),
render_still.si(order_line_id),
generate_thumbnail.si(order_line_id),
publish_asset.si(order_line_id),
),
"turntable": lambda order_line_id: chain(
convert_step.si(order_line_id),
render_turntable_frames.si(order_line_id),
composite_ffmpeg.si(order_line_id),
publish_asset.si(order_line_id),
),
"multi_angle": lambda order_line_id: chain(
convert_step.si(order_line_id),
group( # parallele Renders
render_still.si(order_line_id, angle=0),
render_still.si(order_line_id, angle=45),
render_still.si(order_line_id, angle=90),
),
publish_asset.si(order_line_id),
),
}
def dispatch_workflow(workflow_type: str, order_line_id: str):
canvas = WORKFLOW_BUILDERS[workflow_type](order_line_id)
return canvas.apply_async()
WorkflowDefinition speichert die deklarative Config (welcher workflow_type, welche Parameter):
{
"type": "still",
"params": {
"render_engine": "cycles",
"samples": 256,
"resolution": [2048, 2048],
"material_library_id": "uuid-..."
}
}
React Flow Editor zeigt den Workflow visuell an und bearbeitet diese JSON-Config. Er erzeugt keine eigene Execution-Logic — er ist reine Visualisierung des Canvas-Workflows.
Vorteile:
- Celery übernimmt Retry, Error-Handling, Status-Tracking, Parallelisierung
workflow_node_resultswird aus Celery-Task-Results befüllt (nicht custom Engine)- Scope von Phase C reduziert sich um ~50%
ADR-04: Domain-Driven Projektstruktur
Problem: Flache routers/ + services/ + models/ Struktur mit 15+ Domains wird unübersichtlich. Agenten können keine isolierten Domains parallel bearbeiten.
Entscheidung: Domain-Driven Structure
backend/app/
├── core/ # Shared: auth, config, database, storage, websocket
│ ├── auth.py
│ ├── config.py
│ ├── database.py
│ ├── storage.py # MinIO StorageBackend
│ └── websocket.py # WebSocket broadcast
├── domains/
│ ├── tenants/ # Tenant CRUD, RLS setup
│ │ ├── models.py
│ │ ├── schemas.py
│ │ ├── router.py
│ │ └── service.py
│ ├── products/ # Product, CadFile, STEP processing
│ │ ├── models.py
│ │ ├── schemas.py
│ │ ├── router.py
│ │ ├── service.py
│ │ └── tasks.py # extract_cad_metadata, convert_step_to_stl
│ ├── rendering/ # OutputType, RenderTemplate, Workflow, render tasks
│ │ ├── models.py
│ │ ├── schemas.py
│ │ ├── router.py
│ │ ├── service.py
│ │ ├── workflow_builder.py # Celery Canvas workflows
│ │ └── tasks.py # render_still, render_turntable
│ ├── orders/ # Order, OrderItem, OrderLine
│ │ ├── models.py
│ │ ├── schemas.py
│ │ ├── router.py
│ │ └── service.py
│ ├── media/ # MediaAsset, download, zip
│ │ ├── models.py
│ │ ├── schemas.py
│ │ ├── router.py
│ │ └── service.py
│ ├── materials/ # Material, MaterialAlias, MaterialLibrary
│ │ ├── models.py
│ │ ├── schemas.py
│ │ ├── router.py
│ │ └── service.py
│ ├── billing/ # Invoice, PricingTier
│ │ ├── models.py
│ │ ├── schemas.py
│ │ ├── router.py
│ │ └── service.py
│ ├── notifications/ # AuditLog, NotificationConfig
│ │ ├── models.py
│ │ ├── schemas.py
│ │ ├── router.py
│ │ └── service.py
│ └── imports/ # Excel-Parser, Sanity-Check
│ ├── schemas.py
│ ├── router.py
│ ├── excel_parser.py
│ └── tasks.py # validate_excel_import
└── main.py # Nur Router-Registrierung
Vorteile:
- Neue Domain = neues Verzeichnis, kein bestehender Code angefasst
- Jede Domain isoliert testbar
- Agenten können Domains parallel implementieren ohne Konflikte
- Imports sind selbstdokumentierend:
from app.domains.billing.service import create_invoice
Migration: Bestehender Code wird schrittweise pro Phase in neue Struktur verschoben (nicht alles auf einmal).
ADR-05: WebSocket für Dashboard-Events, SSE nur für Task-Logs
Problem: SSE ist auf max. 6 gleichzeitige Verbindungen pro Browser (HTTP/1.1) limitiert. Für ein Live-Dashboard mit mehreren Datenquellen (Queue-Status, Worker-Status, Render-Events) ist das zu wenig.
Entscheidung: Zwei separate Real-Time-Kanäle:
WebSocket — für Dashboard-Events (1 Verbindung, multiplexed):
# core/websocket.py
@router.websocket("/ws")
async def websocket_endpoint(ws: WebSocket, user=Depends(get_ws_user)):
await ws.accept()
await subscribe_to_tenant_events(ws, user.tenant_id)
# Events: queue_update, render_complete, render_failed,
# worker_online, worker_offline, order_status_change
SSE — für Render-Task-Logs (1 Stream pro Task, kurzlebig):
# domains/rendering/router.py
@router.get("/tasks/{task_id}/logs")
async def stream_task_logs(task_id: str, user=Depends(get_current_user)):
async def event_generator():
while True:
logs = await redis.lrange(f"task_logs:{task_id}", -50, -1)
for line in logs:
yield f"data: {line}\n\n"
if await task_is_done(task_id):
break
await asyncio.sleep(0.5)
return EventSourceResponse(event_generator())
Einsatz:
- Dashboard, Worker-Status, Queue-Längen → WebSocket
- Blender-Stdout während Render → SSE
ADR-06: Blender Version Policy — >= 5.0.1, Upgrade-Pfad auf 5.1
Entscheidung: Blender < 5.0.1 wird nicht unterstützt. Während der Entwicklung erscheint Blender 5.1 — der Wechsel erfolgt dann ausschließlich auf >= 5.1.
Umsetzung:
# render-worker/Dockerfile
ARG BLENDER_VERSION=5.0.1
ARG BLENDER_MIN_VERSION=5.0.1
RUN BLENDER_URL="https://download.blender.org/release/Blender${BLENDER_VERSION}/blender-${BLENDER_VERSION}-linux-x64.tar.xz" \
&& curl -L $BLENDER_URL | tar xJ -C /opt/blender --strip-components=1
# render-worker/scripts/check_version.py — wird beim Container-Start geprüft
import bpy, sys
major, minor, patch = bpy.app.version
if (major, minor) < (5, 0):
print(f"ERROR: Blender {major}.{minor}.{patch} nicht unterstützt. Minimum: 5.0.1")
sys.exit(1)
Upgrade-Strategie 5.0.1 → 5.1:
BLENDER_VERSIONBuild-Arg in.envändern, neu bauen- Render-Scripts auf API-Änderungen prüfen (Blender Changelog 5.1)
- Bestehende
.blend-Templates: in 5.1 öffnen + resaven (automatisch migriert) - QC-Gate: Test-Render mit Sample-STEP-File nach Upgrade
Hinweis: Blender 5.x verwendet den neuen Asset Library Standard (ab 3.0 eingeführt, in 5.x vollständig stabil) — dieser wird für ADR-07 vorausgesetzt.
ADR-07: Blender Asset Library für Materialien UND Modifier
Problem: Das bisherige material_libraries-Konzept erlaubt nur Material-Linking. Modifier (insbesondere Geometry Nodes) können damit nicht verwaltet werden. Blenders natives Asset-Library-System ist breiter und deckt beides ab.
Entscheidung: Blender Asset Library als primäres System für Assets:
# render-worker/scripts/asset_library.py
def apply_asset_library(blend_path: str, material_map: dict, modifier_map: dict):
"""
Lädt Assets aus einer Asset-Library .blend-Datei:
1. Materialien: linked/appended per Namen aus material_map
2. Geometry-Node-Modifier: appended per Namen aus modifier_map, auf Mesh angewendet
"""
with bpy.data.libraries.load(blend_path, link=False, assets_only=True) as (data_from, data_to):
# Materialien laden
data_to.materials = [
name for name in data_from.materials
if name in material_map.values()
]
# Geometry-Node-Gruppen laden (für Modifier)
data_to.node_groups = [
name for name in data_from.node_groups
if name in modifier_map.values()
]
# Materialien auf Parts anwenden
for obj in bpy.data.objects:
if obj.type == 'MESH':
for slot in obj.material_slots:
resolved = material_map.get(slot.material.name if slot.material else '')
if resolved and resolved in bpy.data.materials:
slot.material = bpy.data.materials[resolved]
# Geometry-Node-Modifier anwenden
for obj in bpy.data.objects:
if obj.type == 'MESH':
for part_name, modifier_name in modifier_map.items():
if part_name in obj.name and modifier_name in bpy.data.node_groups:
mod = obj.modifiers.new(name=modifier_name, type='NODES')
mod.node_group = bpy.data.node_groups[modifier_name]
Datenmodell: material_libraries → umbenannt in asset_libraries:
asset_libraries (
id UUID PK, tenant_id FK,
name VARCHAR(200),
blend_file_key TEXT, -- MinIO key zur .blend-Datei
catalog JSONB, -- Asset-Katalog: {materials: [...], node_groups: [...]}
description TEXT,
is_active BOOL DEFAULT TRUE,
created_at TIMESTAMP
)
Workflow-Integration: Zwei neue Node-Typen:
apply_asset_library_materials— Material-Substitution via Asset Libraryapply_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
.blendkann 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 komplettflamenco-manager,flamenco-workerServices aus docker-composeflamenco_client.py,flamenco_tasks.py- Celery-Beat-Task
poll_flamenco_jobs flamenco_job_id,render_backend_usedSpalten (Migration 032: nullable, später entfernen)render_backendSystem-Setting
Ersetzt durch: Distributed Celery render-worker + MinIO shared storage
Risiken:
- Laufende Flamenco-Jobs → Migration setzt Status auf
cancelled - render_dispatcher.py muss vereinfacht werden (nur Celery-Pfad)
Migration:
UPDATE order_lines SET render_status = 'cancelled', flamenco_job_id = NULL
WHERE render_status = 'processing' AND flamenco_job_id IS NOT NULL;
4.2 blender-renderer (Flask HTTP-Service)
Entfernt:
blender-renderer/app.py(Flask-Wrapper)- Service aus docker-compose
- HTTP-Aufrufe zu
:8100aus step_processor.py
Ersetzt durch:
- Blender als subprocess im render-worker Celery-Container
blender_render.pywandert nachrender-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_idFK aufmaterial_aliases - Erweitert: Unbekannte-Materialien-Report beim Excel-Import
5.5 RenderTemplate + Pricing + Notification (bleibt, in Domains integriert)
lighting_only,shadow_catcherbleiben- PricingTier → um Invoice-Modul erweitert
- Notification → um
notification_configserweitert
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-Outputsblend-templates— .blend RenderTemplate-Dateienasset-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-Cacheextract_mesh_attributes→ OCC Topologie → sharp_edges JSONapply_asset_library_materials→ Lädt Materialien aus Asset-Library .blend, wendet auf Mesh-Parts anapply_asset_library_modifiers→ Lädt Geometry-Node-Gruppen aus Asset-Library, wendet als Modifier anrender_still→ Blender subprocess → PNG nach MinIOrender_turntable_frames→ Blender subprocess → Frame-Ordner nach MinIOcomposite_ffmpeg→ Frames + bg_color → MP4 nach MinIOexport_gltf→ Blender exportiert GLB mit angewendeten Produktionsmaterialien → MinIOexport_blend→ Blender speichert .blend mitpack_all()→ MinIO (alle Texturen eingebettet)generate_thumbnail→ Pillow resize → Thumb nach MinIOpublish_asset→ MediaAsset-Record in DB erstellen
React Flow Frontend: WorkflowEditor.tsx — visualisiert den Canvas-Workflow, bearbeitet config JSONB. Kein eigener Execution-Code.
6.4 MediaAsset-Katalog
media_assets (
id UUID PK, tenant_id FK, product_id FK, order_line_id FK,
workflow_run_id FK,
asset_type ENUM(thumbnail, still, turntable, stl_low, stl_high,
gltf_geometry, -- glTF ohne Materialien (aus STEP-Konvertierung)
gltf_production, -- GLB mit Produktionsmaterialien (aus export_gltf Node)
blend_production), -- .blend mit eingebetteten Produktionsmaterialien
storage_key TEXT, -- MinIO object key
file_size_bytes BIGINT,
mime_type VARCHAR(100),
width INT, height INT, duration_s FLOAT,
render_config JSONB,
created_at TIMESTAMP,
is_archived BOOL DEFAULT FALSE
)
API: Filter, Single-Download, Zip-Download (StreamingResponse), Soft-Delete
6.5 OCC Mesh-Attribute Extraktion
# domains/products/tasks.py
def extract_mesh_attributes(step_path: str) -> dict:
"""
Via pythonOCC BRep-Topologie:
- sharp_edges: Kanten-Indices mit Dihedral-Winkel > Threshold (default 30°)
- seam_candidates: Kanten zwischen verschiedenen Face-Typen
- face_groups: Flächen nach Typ (planar, cylindrical, toroidal, ...)
"""
Output in cad_files.mesh_attributes JSONB → wird beim Render als Parameter übergeben.
Blender-Integration in render_still:
# render-worker/scripts/blender_render.py
if mesh_attributes and mesh_attributes.get("sharp_edges"):
for edge_idx in mesh_attributes["sharp_edges"]:
mesh.edges[edge_idx].use_edge_sharp = True
bpy.ops.mesh.mark_seam(clear=False)
bpy.ops.uv.smart_project()
6.6 Hash-basiertes Conversion-Caching
# domains/products/tasks.py
def get_stl_cache_key(step_object_key: str, quality: str) -> str:
content = storage.download_bytes(step_object_key)
sha256 = hashlib.sha256(content).hexdigest()
return f"conversion-cache/{sha256[:2]}/{sha256}/{quality}.stl"
Zentraler Cache in MinIO uploads/conversion-cache/. Gleiches STEP-File → 1x konvertiert, egal wie oft hochgeladen oder unter welchem Namen.
6.7 Billing / Invoice-Modul
invoices (id, tenant_id FK, period_start, period_end, status ENUM, total_amount, created_at)
invoice_lines (id, invoice_id FK, order_line_id FK, product_name, asset_type, quantity, unit_price, total)
PDF-Export via WeasyPrint (HTML-Template → PDF). Excel-Export via openpyxl.
6.8 Excel Sanity-Check
Task validate_excel_import:
- Parse Excel
- Für jede Row prüfen: STEP vorhanden + completed? Materialien in Aliases? Produkt in DB?
- Fuzzy-Match-Vorschläge für unbekannte Materialien (via
difflib.get_close_matches) - Report in
import_validationsDB + WebSocket-Event an Client
Frontend: Sanity-Check-Dialog nach Upload, Ampel-Anzeige, Material-Lücken direkt schließbar.
6.9 WebSocket Live-Events (ADR-05)
# core/websocket.py
EVENT_TYPES = [
"queue_update", # Queue-Länge geändert
"render_complete", # Render erfolgreich
"render_failed", # Render gescheitert
"worker_online", # Neuer Worker registriert
"worker_offline", # Worker nicht mehr erreichbar
"order_status_change", # Order-Status geändert
"import_validated", # Excel-Sanity-Check abgeschlossen
]
Dashboard, WorkerManagement, OrderDetail — alle abonnieren denselben WebSocket und filtern Events nach Typ.
6.10 Worker-Registrierung
# render-worker entrypoint
redis.hset('registered_workers', f'{hostname}:{pid}', json.dumps({
'hostname': hostname,
'queues': ['render_default'],
'blender_version': get_blender_version(),
'gpu': detect_gpu(), # nvidia-smi oder None
'started_at': utcnow().isoformat(),
'last_heartbeat': utcnow().isoformat(),
}))
# Heartbeat alle 30s; Beat-Task entfernt stale Workers nach 90s
GET /api/workers liest Redis-Hash, berechnet Queue-Stats via Celery Inspect.
6.11 Blender Asset Library Management (ADR-07)
Datenmodell: asset_libraries (ersetzt material_libraries)
asset_libraries (
id UUID PK, tenant_id FK,
name VARCHAR(200),
blend_file_key TEXT, -- MinIO key: "asset-libraries/{id}.blend"
catalog JSONB, -- {materials: ["SCHAEFFLER_010101_Steel-Bare", ...],
-- node_groups: ["Bevel_Sharp_Edges", "Clean_Geometry", ...]}
description TEXT,
is_active BOOL DEFAULT TRUE,
created_at TIMESTAMP
)
Katalog-Refresh-Task:
# domains/materials/tasks.py
def refresh_asset_library_catalog(asset_library_id: str):
"""
Öffnet .blend via Blender --background, liest alle markierten Assets,
schreibt Katalog nach asset_libraries.catalog JSONB.
Läuft automatisch nach jedem .blend-Upload.
"""
script = "render-worker/scripts/catalog_assets.py"
result = subprocess.run(['blender', '--background', '--python', script,
'--', blend_path, '--output', 'json'], ...)
catalog = json.loads(result.stdout)
db.execute(update(AssetLibrary).values(catalog=catalog))
API:
POST /api/asset-libraries— Upload .blend, Katalog wird automatisch gelesenGET /api/asset-libraries/{id}/catalog— Verfügbare Assets durchsuchenPUT /api/asset-libraries/{id}— Metadaten aktualisierenDELETE /api/asset-libraries/{id}— Löschen (nur wenn nicht in Verwendung)
Frontend: Asset-Library-Manager in Admin — Upload, Katalog-Anzeige (Materialien + Node-Groups als Badges), Zuweisung zu OutputTypes.
6.12 Interaktive 3D Browser-Vorschau mit Production-Materialien
Konzept: Der vorhandene ThreeDViewer.tsx (Three.js, OrbitControls) wird um Production-glTF-Support erweitert. Zwei Ansichtsmodi:
| Modus | glTF-Quelle | Materialien |
|---|---|---|
| Geometrie-Preview | gltf_geometry — aus STEP-Konvertierung |
Farbige Part-Gruppen (OCC-Extraktion) |
| Production-Preview | gltf_production — aus export_gltf Workflow-Node |
Echte Produktionsmaterialien (PBR) |
Blender → GLB Pipeline:
# render-worker/scripts/export_gltf.py
def export_gltf(stl_path, blend_key, material_map, modifier_map, output_key):
# 1. STL importieren
bpy.ops.import_mesh.stl(filepath=stl_path)
# 2. Asset Library laden (Materialien + Modifier)
apply_asset_library(blend_path, material_map, modifier_map)
# 3. Als GLB exportieren
bpy.ops.export_scene.gltf(
filepath=output_path,
export_format='GLB',
export_materials='EXPORT', # Materialien einbetten
export_apply=True, # Modifier vor Export anwenden
export_draco_mesh_compression_enable=True, # Komprimierung
export_texture_dir='',
)
Hinweis Materialtreue: Blenders glTF-Exporter konvertiert Principled BSDF → PBR (metallic/roughness). Komplexe Shader-Nodes (z.B. Procedural Textures) werden nicht vollständig übertragen — für diese Fälle: Texture Baking vor Export (optionaler Workflow-Node bake_textures).
Frontend-Erweiterungen ThreeDViewer.tsx:
// Neue Props/Features:
interface ThreeDViewerProps {
geometryGltfUrl?: string // Geometrie-Preview (sofort verfügbar)
productionGltfUrl?: string // Production-Preview (nach Workflow-Abschluss)
showMaterialToggle?: boolean // Umschalten zwischen Modi
showWireframe?: boolean // Wireframe-Overlay
environmentPreset?: 'studio' | 'outdoor' | 'dark'
}
Progressive Loading: Geometrie-Preview sofort zeigen → Production-Preview nachladen wenn verfügbar.
Download-Buttons direkt im Viewer:
- "GLB herunterladen" →
GET /api/media/{gltf_production_id}/download - ".blend herunterladen" →
GET /api/media/{blend_production_id}/download
6.13 Production Export: glTF + .blend Download
Workflow-Node export_gltf:
- Input: STL-Pfad, Asset-Library-ID, Material-Map, Modifier-Map
- Output: GLB-Datei in MinIO
production-exports/{cad_file_id}/{run_id}.glb - MediaAsset-Record:
asset_type = gltf_production
Workflow-Node export_blend:
# render-worker/scripts/export_blend.py
def export_blend(stl_path, blend_key, material_map, modifier_map, output_key):
# 1. STL + Asset Library laden (wie export_gltf)
# ...
# 2. Alle externen Daten einbetten
bpy.ops.file.pack_all()
# 3. Als .blend speichern (komprimiert)
bpy.ops.wm.save_as_mainfile(
filepath=output_path,
compress=True,
copy=True # Original-Session unangetastet
)
Größen-Warnung: .blend mit eingebetteten Texturen kann 50-500MB werden. Daher:
production-exportsBucket TTL: 7 Tage (konfigurierbar inapp_config)- Maximale Dateigröße: 1GB (konfigurierbar)
- Frontend-Warnung bei Dateien > 100MB vor Download
Standard-Workflow "Still mit Production-Exports":
chain(
convert_step.si(order_line_id),
extract_mesh_attributes.si(order_line_id),
apply_asset_library_materials.si(order_line_id),
apply_asset_library_modifiers.si(order_line_id),
group(
render_still.si(order_line_id), # PNG für Produktion
export_gltf.si(order_line_id), # GLB für 3D-Viewer + Download
export_blend.si(order_line_id), # .blend für Archiv/Post-Processing
),
generate_thumbnail.si(order_line_id),
publish_asset.si(order_line_id),
)
7. Phasenplan mit Tasks
Phase A: Infrastruktur-Cleanup + MinIO ✅ ABGESCHLOSSEN (2026-03-06)
A1: Flamenco entfernen ✅
docker-compose.yml→ flamenco-manager, flamenco-worker entfernenflamenco_client.py,flamenco_tasks.pylöschenrender_dispatcher.py→ vereinfachen (nur Celery-Pfad)- Migration 032: laufende Flamenco-Jobs auf
cancelledsetzen - Akzeptanzkriterium:
docker compose upstartet 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, viaBLENDER_VERSIONBuild-Arg) + cadquery + Python-Depscheck_version.pyläuft beim Container-Start: prüft Blender >= 5.0.1, Exit 1 wenn nicht erfülltblender-renderer/blender_render.py→render-worker/scripts/blender_render.pydomains/rendering/tasks.py(neu):render_still_task,render_turntable_task- Blender via
subprocess.run, stdout in Redis für SSE docker-compose.yml:blender-rendererentfernen,render-workerhinzufügen.env.example:BLENDER_VERSION=5.0.1dokumentieren- 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
/uploadsnach MinIO hochlädt .env.example:MINIO_URL,MINIO_USER,MINIO_PASSWORDdocker-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_configTabelle (JSONB-Spalten: render, storage, notifications, worker, billing) core/config_service.py(neu),system_settingsTabelle 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.pyregistriert nur noch Domain-Router- Akzeptanzkriterium: Alle bestehenden Tests grün, Imports funktionieren, API-Endpoints unverändert
B2: Tenant-Datenmodell + RLS ✅
- Migration 035:
tenantsTabelle + 'Schaeffler' Default-Seed - Migration 036:
tenant_idFK auf alle Tabellen + RLS-Policies (tenant_isolation + admin_bypass) + Backfill domains/tenants/mit CRUD-Router, Service, Modellencore/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 Dropdownfrontend/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.pyerweiterndomains/rendering/workflow_builder.py(neu): Celery-Canvas-Builder für "still", "turntable", "multi_angle"output_types.workflow_definition_idFK (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üftoutput_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/reactzupackage.jsonhinzugefügt (npm install nötig)frontend/src/pages/WorkflowEditor.tsx: 6 Custom-Node-Typen, ConfigSidepanel, Node-Palette mit Drag-Dropfrontend/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_attributesCelery-Task- Migration 038:
cad_files.mesh_attributes JSONB - Läuft nach
extract_cad_metadatain 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 aufcurved_ratioundsharp_angle_threshold_degrender_blender.py: übergibt--mesh-attributes JSONan Blender-Subprocessrender_still_task: lädtmesh_attributesaus DB und reicht sie weiter
Phase E: MediaAsset-Katalog ✅ ABGESCHLOSSEN (2026-03-06)
E1: Datenmodell + API ✅
- Migration 040:
media_assetsTabelle mit RLS-Policies domains/media/: MediaAsset-Model, Schemas, Service, Routerpublish_assetCelery-Task inrendering/tasks.pycore/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 mitzipDownloadAssets()- 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_validationsTabelle difflib.get_close_matchesfü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_configsTabelle 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 /wsEndpoint
J2: SSE Task-Logs
GET /api/tasks/{task_id}/logs— SSE, Worker schreibt in Redis-ListeLiveRenderLog.tsxerweitern:EventSourceAPI, 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_librariesTabelle (id, name, blend_file_key, catalog JSONB, tenant_id) render_templates.asset_library_idFK,output_types.asset_library_idDefault-Library- Upload via MinIO
asset-libraries/Bucket - Nach Upload: Celery-Task
refresh_asset_library_catalog→ öffnet .blend via Blender --background, liest Asset-Namen, schreibt incatalogJSONB - 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.tsxerweitern: 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.tsxgenerischer Container, Widget-Config per User in DB- Widget-Typen: ProductionStats, QueueStatus, RecentRenders, CostOverview, WorkerStatus
- WebSocket-Feed für Live-Updates
L2: Responsive Design
- Tailwind CSS-Variablen auf RGB-Channel-Format (behebt Learning 2026-02-18)
- 768px Minimum (iPad-Breite)
L3: Worker-Management UI
WorkerManagement.tsx(neu): Worker-Liste aus Redis, Queue-Stats, Scale-Button
Phase M: QC-Tests (Woche 10-11)
M1: Pytest Backend
tests/domains/— pro Domain: API-Tests + Service-Tests- Fixtures: Test-DB mit RLS-Setup, Mock-MinIO (moto), Mock-Celery
- Akzeptanzkriterium: > 80% Coverage auf Service-Layer, alle Domains
M2: Frontend Vitest
frontend/src/__tests__/— Komponenten-Tests mit Testing Library- Akzeptanzkriterium:
npm run test→ 0 Failures
M3: Integration-Tests
- End-to-End: STEP Upload → MinIO → Celery → Render (Mock-Blender) → MediaAsset → Download
- Tenant-Isolation-Test: Client A sieht keine Client-B-Daten
- Akzeptanzkriterium: Pipeline durchlaufbar in CI ohne echtes Blender
8. Datenbankmigrationen-Übersicht
| Migration | Beschreibung | Phase |
|---|---|---|
| 032 | Flamenco-Felder bereinigen, Jobs auf cancelled | A |
| 033 | app_config (strukturiertes Config-Modell, ersetzt system_settings) | A |
| 034 | tenants Tabelle | B |
| 035 | tenant_id FKs + PostgreSQL RLS-Policies + Backfill | B |
| 036 | workflow_definitions, workflow_runs, workflow_node_results | C |
| 037 | output_types.workflow_definition_id FK | C |
| 038 | cad_files.mesh_attributes JSONB | D |
| 039 | media_assets Tabelle | E |
| 040 | cad_files.step_file_hash VARCHAR(64) | F |
| 041 | invoices, invoice_lines | G |
| 042 | import_validations | H |
| 043 | notification_configs | I |
| 044 | asset_libraries (ersetzt material_libraries), FKs auf render_templates/output_types | K |
| 045 | media_assets.asset_type: ENUM um gltf_production, blend_production erweitern | K |
9. QC-Gates und Test-Checkliste
Diese Checkliste ist für Agenten konzipiert — jeder Task muss diese Gates passieren bevor Commit.
9.1 Backend QC-Gates
# Syntax-Check
docker compose exec backend python -m py_compile app/domains/[domain]/[changed_file].py
# Alembic
docker compose exec backend alembic current # → head
# Pytest
docker compose exec backend pytest tests/ -x --tb=short # → 0 Failures
# Import + Schema
docker compose exec backend python -c "from app.main import app; print('OK')"
9.2 RLS QC-Gate (neu, nach Phase B)
# Tenant-Isolation Test
docker compose exec backend pytest tests/domains/tenants/test_rls.py -v
# → Client A kann keine Client-B-Daten lesen/schreiben
# → Admin mit BYPASSRLS sieht alle Daten
9.3 Celery QC-Gates
docker compose exec step-worker celery -A app.celery_app inspect registered
# → extract_cad_metadata, convert_step_to_stl, extract_mesh_attributes, validate_excel_import
docker compose exec render-worker celery -A app.celery_app inspect registered
# → render_still, render_turntable_frames, composite_ffmpeg, generate_thumbnail, publish_asset
docker compose exec step-worker celery -A app.celery_app inspect active_queues
# → step_processing, convert, notify
9.4 MinIO QC-Gate (neu, nach Phase A4)
# MinIO erreichbar
curl http://localhost:9000/minio/health/live # → 200
# Upload + Download funktioniert
docker compose exec backend python -c "
from app.core.storage import storage
storage.upload('/tmp/test.txt', 'test/test.txt')
assert storage.exists('test/test.txt')
print('MinIO OK')
"
# Externer Worker kann MinIO erreichen
# (auf Maschine B ausführen)
docker compose -f docker-compose.worker.yml exec render-worker python -c "
from app.core.storage import storage
assert storage.exists('test/test.txt')
print('External worker MinIO access OK')
"
9.5 Frontend QC-Gates
cd frontend
npm run type-check # → 0 Errors
npm run lint # → 0 Errors
npm run test # → 0 Failures
npm run build # → Erfolg
9.6 Datenbank QC-Gates
# Migration prüfen (manuell lesen!) — besonders RLS-Policies in 035
cat backend/alembic/versions/035_*.py
# Up + Down testen
docker compose exec backend alembic upgrade head
docker compose exec backend alembic downgrade -1
docker compose exec backend alembic upgrade head
9.7 Docker QC-Gates
docker compose up -d
docker compose ps # → alle "healthy" nach 90s (MinIO braucht ~30s)
curl http://localhost:8888/health # → 200
curl http://localhost:9000/minio/health/live # → 200
9.8 Render-Pipeline QC-Gate (End-to-End)
# Upload STEP → Workflow → Thumbnail
curl -X POST http://localhost:8888/api/cad/upload -F "file=@step-sample-file/81113-l_cut.stp"
# → cad_file_id
sleep 30
curl http://localhost:8888/api/cad/{id} | jq .processing_status # → "completed"
curl -I http://localhost:8888/api/cad/{id}/thumbnail # → 200, image/png
# MediaAsset angelegt?
curl http://localhost:8888/api/media?product_id={product_id} | jq length # → > 0
9.9 Security QC-Gates
- Kein Endpoint ohne Auth (außer
/health,/ws,/api/cad/{id}/thumbnail) - Alle File-Uploads: MIME-Type + Größe validiert
- Zip-Download:
assert asset.tenant_id == current_tenant.idvor 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/joinedloadin 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=3600Header
10. Offene Entscheidungen
| # | Frage | Optionen | Empfehlung / Status |
|---|---|---|---|
| 1 | Blender-Version | Entschieden: >= 5.0.1 Pflicht, Upgrade auf 5.1 sobald verfügbar | |
| 2 | React Flow Lizenz | MIT / Pro | MIT reicht für internes System |
| 3 | PDF-Generator | WeasyPrint / ReportLab | WeasyPrint (HTML→PDF) |
| 4 | Mobile-Support Scope | iPad (768px) / Vollmobil (375px) | 768px Minimum |
| 5 | OrderItem-Refactor | Jetzt / v3 | v3 (zu viel abhängiger Code) |
| 6 | Blender GPU-Config | Pro-Worker via deploy.resources |
Bleibt, NVIDIA-Support via ENV |
| 7 | E-Mail-Notifications | SMTP jetzt / später | Später — nur In-App in v2 |
| 8 | Three.js-Thumbnails Batch-Regenerierung | Obligatorisch / On-Demand | Obligatorisch beim Refactor-Deploy |
| 9 | MinIO Backup-Strategie | MinIO Replication / S3 Sync | Außerhalb Scope v2 — in .env dokumentieren |
| 10 | CI/CD Pipeline | GitHub Actions / lokal | GitHub Actions für Lint + Tests |
| 11 | glTF Materialtreue | PBR-Export / Texture-Baking | ✅ 11A: PBR-Export only — Principled BSDF → GLB, kein Baking |
| 12 | Asset Library: link vs. append | link=True (Referenz bleibt) / link=False (Kopie) |
✅ 12B: link=True — Library als Referenz; .blend-Exports nutzen pack_all() für self-contained Files |
| 13 | blend_production TTL | 7 Tage / 30 Tage / permanent | ✅ 13C: Permanent — .blend-Dateien bleiben dauerhaft in MinIO; Größen-Warnungen via app_config |
| 14 | ThreeDViewer Environment | Nur Studio / mehrere Presets | Studio-Preset im v2-Scope; weitere Presets v3 |
Freigabe
Architektur-Entscheidungen bestätigen:
- ADR-01: PostgreSQL RLS für Tenant-Isolation
- ADR-02: MinIO als Shared Object Storage (ersetzt NFS)
- ADR-03: Celery Canvas als Workflow-Engine, React Flow nur Visualisierung
- ADR-04: Domain-Driven Projektstruktur
- ADR-05: WebSocket für Dashboard-Events, SSE nur für Task-Logs
- ADR-06: Blender >= 5.0.1 Pflicht, BLENDER_VERSION als Build-Arg, Upgrade auf 5.1
- ADR-07: Blender Asset Library (Materialien + Modifier),
asset_librariesModell
Bestätigte Entscheidungen (Abschnitt 10):
- 11A: glTF PBR-Export only (kein Texture-Baking)
- 12B: Asset Library link=True + pack_all() für .blend-Exports
- 13C: blend_production permanent in MinIO
- Bestehende API-Endpoints bleiben während Refactor erhalten (17)
- Phasenweise Implementierung mit Quality Gates (18)
Planung:
- Plan insgesamt freigegeben
- Offene Entscheidungen aus Abschnitt 10 geklärt
- Startphase A bestätigt
- Git-Tag
v1-stableauf main erstellt - Git-Branch
refactor/v2erstellt
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 asset_pipeline → 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 asset_pipeline 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)
asset_pipelineQueue Tiefe < 10- Letzter Render < 30 min alt und erfolgreich
- Response:
{ status: "ok"|"degraded"|"down", render_worker_connected, blender_available, thumbnail_queue_depth, last_render_at, ... }
scripts/test_render_pipeline.py — Integration Test Script:
python scripts/test_render_pipeline.py --health # Health-Check only
python scripts/test_render_pipeline.py --sample # 1 STEP + 1 Output-Type (schnell)
python scripts/test_render_pipeline.py --full # Alle Output-Types (langsam)
Celery-Queue-Architektur (nach Fixes)
| Queue | Worker | Concurrency | Tasks |
|---|---|---|---|
step_processing |
worker |
8 | process_step_file, dispatch_order_line_render |
asset_pipeline |
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 → asset_pipeline Queue → nur render-worker → kein Timeout durch parallele Requests.