# Refactor-Plan: Schaeffler Automat v2 **Erstellt:** 2026-03-05 **Aktualisiert:** 2026-03-06 — Phasen A, B, C, D, E abgeschlossen **Status:** IN UMSETZUNG — Phase F als nächstes **Branch:** `refactor/render-pipeline` → Ziel: neuer Branch `refactor/v2` --- ## Inhaltsverzeichnis 1. [Ziel-Zusammenfassung](#1-ziel-zusammenfassung) 2. [Architektur-Analyse: Ist vs. Soll](#2-architektur-analyse-ist-vs-soll) 3. [Architektur-Entscheidungen (ADRs)](#3-architektur-entscheidungen-adrs) 4. [Was wird entfernt / ersetzt (mit Risiken)](#4-was-wird-entfernt--ersetzt-mit-risiken) 5. [Was bleibt und wird erweitert](#5-was-bleibt-und-wird-erweitert) 6. [Neue Komponenten](#6-neue-komponenten) 7. [Phasenplan mit Tasks](#7-phasenplan-mit-tasks) 8. [Datenbankmigrationen-Übersicht](#8-datenbankmigrationen-übersicht) 9. [QC-Gates und Test-Checkliste](#9-qc-gates-und-test-checkliste) 10. [Offene Entscheidungen](#10-offene-entscheidungen) --- ## 1. Ziel-Zusammenfassung Das System wird von einem Einzelkunden-Render-Tool zu einer **produktionstauglichen Multi-Tenant Render-Plattform** ausgebaut: | Ziel | Umsetzung | |---|---| | Produktionspipeline maintainbar | Flamenco entfernen, vereinfachte Docker-Architektur (8 statt 11 Services) | | Multi-Customer | Tenant-Modell mit PostgreSQL Row-Level Security | | Externe Worker | Celery render-worker auf beliebigen Maschinen via Redis + MinIO | | Modulare Render-Konfiguration | Celery Canvas Workflows, deklarative WorkflowDefinition JSON-Config | | Template-basierte Outputs | RenderTemplate mit Workflow-Integration, React Flow Visualisierung | | Media-Verwaltung | MediaAsset-Katalog, Filter/Sort/Zip-Download, Audit-Log | | Modernes Design | Responsive, Widget-Dashboard, WebSocket für Live-Updates | | Skalierbar | Celery horizontal skalierbar, Hash-basiertes Conversion-Caching | | Produktdatenbank | Excel-Import mit Sanity-Check und Material-Validierung | | Node-basierter Workflow | React Flow Editor (Visualisierung), Celery Canvas (Execution) | | Keine doppelten Konvertierungen | SHA256-Hash-basierter zentraler Conversion-Cache | | Dynamische Worker-Skalierung | Docker API Scaling + Worker-Registrierung via Redis | | Cycles + EEVEE | Konfigurierbar pro OutputType | | Nutzerverwaltung | Admin / ProjectManager / Client (Tenant-gebunden, RLS-isoliert) | | Preise + Abrechnung | PricingTier, Invoice-Modul, WeasyPrint PDF-Export | | Modulare Dashboards | Widget-basiert, rollenabhängig, WebSocket-Live-Updates | | Reporting | Invoice-Report, Produktions-Report, Excel/PDF-Export | | Blender Asset Library | Native Blender Asset Library für Materialien UND Geometry-Node-Modifier, modular pro OutputType | | Interaktive 3D-Vorschau | Three.js Browser-Viewer mit Production-glTF (Materialien angewendet), OrbitControls | | Production-Exports | glTF/GLB + .blend mit eingebetteten Produktionsmaterialien downloadbar | | Frontend-Logs | SSE-Stream für Render-Task-Logs (1 Stream pro Task) | | Real-Time Dashboard | WebSocket für Queue-Status, Worker-Status, Render-Events | | Notifications | Konfigurierbar per Event-Typ und User | | Schaeffler-Workflow | Sanity-Check, Material-Validierung, Order-Readiness | | OCC Mesh-Attribute | Sharp Edges, UV-Seams aus STEP-Topologie | | Blender-Version | >= 5.0.1 Pflicht, Upgrade-Pfad auf 5.1 vorbereitet | --- ## 2. Architektur-Analyse: Ist vs. Soll ### IST-Architektur (11 Services) ``` Internet ↓ frontend:5173 (React/Vite) ↓ HTTP backend:8888 (FastAPI) ↓ SQL ↓ Celery tasks ↓ HTTP postgres:5432 redis:6379 blender-renderer:8100 ↓ ↑ (nur 1 concurrent) worker (concurrency=8) threejs-renderer:8101 worker-thumbnail (c=1) ↑ beat flamenco-manager:8080 ↓ flamenco-worker (GPU) ``` **Probleme IST:** - `blender-renderer` ist Flask-HTTP-Service → max. 1 concurrent Request, kein echtes Scaling - `threejs-renderer` redundant zu Blender für Thumbnails (eigener Container, eigene Playwright-Instanz) - `flamenco` ist komplexes externes System (Job-Types in JS) — Mehraufwand ohne Mehrwert über verteilte Celery-Worker - `worker-thumbnail` mit concurrency=1 ist Workaround für blender-renderer-Limitation - STEP-Konvertierung passiert mehrfach (blender-renderer + threejs-renderer unabhängig voneinander) - Kein Tenant-Konzept — alle Kunden teilen dieselbe DB-Namespace - Keine echte Pipeline-Konfiguration — Logik ist hartcodiert in step_tasks.py - Kein Shared Storage → externe Worker können keine STEP-Dateien lesen ### SOLL-Architektur (8 Core-Services + n render-worker) ``` Internet ↓ frontend:5173 (React/Vite + React Flow + WebSocket) ↓ HTTP / WebSocket / SSE backend:8888 (FastAPI, Domain-driven, RLS-enabled) ↓ SQL (RLS) ↓ Celery Canvas ↓ S3 API postgres:5432 redis:6379 minio:9000 (+ RLS) ↓ ↑ (shared object storage) step-worker render-worker ← lokal (Maschine A) beat render-worker ← Netzwerk (Maschine B) render-worker ← GPU (Maschine C) ``` **Vorteile SOLL:** - Blender läuft **direkt im Celery-Worker** als subprocess → kein HTTP-Overhead, kein Timeout-Problem - Worker auf **beliebigen Maschinen**: brauchen nur `REDIS_URL` + `MINIO_URL` + Blender installiert - **MinIO** als S3-kompatibler Object-Store ersetzt NFS — kein Mount nötig, funktioniert überall - **PostgreSQL RLS** sichert Tenant-Isolation automatisch — kein manueller WHERE-Filter nötig - **Celery Canvas** für Workflow-Execution — keine custom Workflow-Engine - **React Flow** nur als Visualisierungsschicht — deutlich reduzierter Scope - Kein Flamenco, kein threejs-renderer → 3 Services weniger --- ## 3. Architektur-Entscheidungen (ADRs) ### ADR-01: PostgreSQL Row-Level Security statt manuellem tenant_id-Filter **Problem:** Jeder neue Router-Query müsste manuell `WHERE tenant_id = :x` haben. Ein vergessenes Filter = Datenleck zwischen Kunden. **Entscheidung:** PostgreSQL Row-Level Security (RLS) ```sql -- Einmalig pro Tabelle (in Migration 035) ALTER TABLE products ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON products USING (tenant_id = current_setting('app.current_tenant_id')::uuid); -- Admin-Bypass via BYPASSRLS-Rolle ALTER ROLE schaeffler_admin BYPASSRLS; ``` ```python # FastAPI Dependency: einmal pro Request setzen async def get_db_for_tenant( db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user) ) -> AsyncSession: await db.execute( text("SET LOCAL app.current_tenant_id = :tid"), {"tid": str(user.tenant_id)} ) yield db ``` **Vorteile:** - Unmöglich Cross-Tenant-Leaks durch vergessene Filter - Gilt automatisch für alle zukünftigen Queries — auch neue Endpoints - Testbar: RLS-Policies sind SQL, unabhängig von Anwendungscode **Nachteile / Risiken:** - Migration muss RLS für alle betroffenen Tabellen aktivieren - `BYPASSRLS` für Admin-User muss in DB-Migrationen gesetzt werden - Alembic-Autogenerate erkennt keine RLS-Policies → Policies müssen manuell in Migration geschrieben werden --- ### ADR-02: MinIO statt NFS für Shared Storage **Problem:** Externe Worker müssen STEP-Dateien und Render-Outputs lesen/schreiben. NFS ist operationell komplex, plattformabhängig, und ein Single-Point-of-Failure. **Entscheidung:** MinIO (S3-kompatibel, Docker-nativ, self-hosted) ```yaml # docker-compose.yml minio: image: minio/minio:latest command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: ${MINIO_USER:-minioadmin} MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD:-minioadmin} ports: - "9000:9000" # S3 API - "9001:9001" # Web Console volumes: - minio-data:/data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] ``` ```python # backend/core/storage.py import boto3 from pathlib import Path class MinIOStorage: def __init__(self): self.client = boto3.client( 's3', endpoint_url=settings.MINIO_URL, aws_access_key_id=settings.MINIO_USER, aws_secret_access_key=settings.MINIO_PASSWORD, ) self.bucket = 'uploads' def upload(self, local_path: Path, object_key: str) -> str: self.client.upload_file(str(local_path), self.bucket, object_key) return object_key def download(self, object_key: str, local_path: Path) -> Path: self.client.download_file(self.bucket, object_key, str(local_path)) return local_path def exists(self, object_key: str) -> bool: try: self.client.head_object(Bucket=self.bucket, Key=object_key) return True except: return False ``` **Render-Worker:** Lädt STEP-File vor Render aus MinIO in lokales tmpdir, lädt Output zurück nach MinIO. **Externe Worker brauchen nur:** - `REDIS_URL=redis://server:6379/0` - `MINIO_URL=http://server:9000` - `MINIO_USER` + `MINIO_PASSWORD` Kein Mount, kein NFS, funktioniert auf Windows/Linux/Mac gleich. --- ### ADR-03: Celery Canvas für Workflow-Execution, React Flow nur Visualisierung **Problem:** Eine custom Workflow-Engine (Graph-Traversal, Dependency-Resolution, Retry-Logic) ist ~2-3 Wochen Eigenentwicklung — Celery hat das bereits eingebaut. **Entscheidung:** Celery Canvas als Execution-Engine, deklarative JSON-Config als Definition, React Flow als Visualisierung. ```python # domains/rendering/workflow_builder.py from celery import chain, group WORKFLOW_BUILDERS = { "still": lambda order_line_id: chain( convert_step.si(order_line_id), extract_mesh_attributes.si(order_line_id), render_still.si(order_line_id), generate_thumbnail.si(order_line_id), publish_asset.si(order_line_id), ), "turntable": lambda order_line_id: chain( convert_step.si(order_line_id), render_turntable_frames.si(order_line_id), composite_ffmpeg.si(order_line_id), publish_asset.si(order_line_id), ), "multi_angle": lambda order_line_id: chain( convert_step.si(order_line_id), group( # parallele Renders render_still.si(order_line_id, angle=0), render_still.si(order_line_id, angle=45), render_still.si(order_line_id, angle=90), ), publish_asset.si(order_line_id), ), } def dispatch_workflow(workflow_type: str, order_line_id: str): canvas = WORKFLOW_BUILDERS[workflow_type](order_line_id) return canvas.apply_async() ``` **WorkflowDefinition** speichert die **deklarative Config** (welcher workflow_type, welche Parameter): ```json { "type": "still", "params": { "render_engine": "cycles", "samples": 256, "resolution": [2048, 2048], "material_library_id": "uuid-..." } } ``` **React Flow Editor** zeigt den Workflow visuell an und bearbeitet diese JSON-Config. Er erzeugt **keine** eigene Execution-Logic — er ist reine Visualisierung des Canvas-Workflows. **Vorteile:** - Celery übernimmt Retry, Error-Handling, Status-Tracking, Parallelisierung - `workflow_node_results` wird aus Celery-Task-Results befüllt (nicht custom Engine) - Scope von Phase C reduziert sich um ~50% --- ### ADR-04: Domain-Driven Projektstruktur **Problem:** Flache `routers/` + `services/` + `models/` Struktur mit 15+ Domains wird unübersichtlich. Agenten können keine isolierten Domains parallel bearbeiten. **Entscheidung:** Domain-Driven Structure ``` backend/app/ ├── core/ # Shared: auth, config, database, storage, websocket │ ├── auth.py │ ├── config.py │ ├── database.py │ ├── storage.py # MinIO StorageBackend │ └── websocket.py # WebSocket broadcast ├── domains/ │ ├── tenants/ # Tenant CRUD, RLS setup │ │ ├── models.py │ │ ├── schemas.py │ │ ├── router.py │ │ └── service.py │ ├── products/ # Product, CadFile, STEP processing │ │ ├── models.py │ │ ├── schemas.py │ │ ├── router.py │ │ ├── service.py │ │ └── tasks.py # extract_cad_metadata, convert_step_to_stl │ ├── rendering/ # OutputType, RenderTemplate, Workflow, render tasks │ │ ├── models.py │ │ ├── schemas.py │ │ ├── router.py │ │ ├── service.py │ │ ├── workflow_builder.py # Celery Canvas workflows │ │ └── tasks.py # render_still, render_turntable │ ├── orders/ # Order, OrderItem, OrderLine │ │ ├── models.py │ │ ├── schemas.py │ │ ├── router.py │ │ └── service.py │ ├── media/ # MediaAsset, download, zip │ │ ├── models.py │ │ ├── schemas.py │ │ ├── router.py │ │ └── service.py │ ├── materials/ # Material, MaterialAlias, MaterialLibrary │ │ ├── models.py │ │ ├── schemas.py │ │ ├── router.py │ │ └── service.py │ ├── billing/ # Invoice, PricingTier │ │ ├── models.py │ │ ├── schemas.py │ │ ├── router.py │ │ └── service.py │ ├── notifications/ # AuditLog, NotificationConfig │ │ ├── models.py │ │ ├── schemas.py │ │ ├── router.py │ │ └── service.py │ └── imports/ # Excel-Parser, Sanity-Check │ ├── schemas.py │ ├── router.py │ ├── excel_parser.py │ └── tasks.py # validate_excel_import └── main.py # Nur Router-Registrierung ``` **Vorteile:** - Neue Domain = neues Verzeichnis, kein bestehender Code angefasst - Jede Domain isoliert testbar - Agenten können Domains parallel implementieren ohne Konflikte - Imports sind selbstdokumentierend: `from app.domains.billing.service import create_invoice` **Migration:** Bestehender Code wird schrittweise pro Phase in neue Struktur verschoben (nicht alles auf einmal). --- ### ADR-05: WebSocket für Dashboard-Events, SSE nur für Task-Logs **Problem:** SSE ist auf max. 6 gleichzeitige Verbindungen pro Browser (HTTP/1.1) limitiert. Für ein Live-Dashboard mit mehreren Datenquellen (Queue-Status, Worker-Status, Render-Events) ist das zu wenig. **Entscheidung:** Zwei separate Real-Time-Kanäle: **WebSocket** — für Dashboard-Events (1 Verbindung, multiplexed): ```python # core/websocket.py @router.websocket("/ws") async def websocket_endpoint(ws: WebSocket, user=Depends(get_ws_user)): await ws.accept() await subscribe_to_tenant_events(ws, user.tenant_id) # Events: queue_update, render_complete, render_failed, # worker_online, worker_offline, order_status_change ``` **SSE** — für Render-Task-Logs (1 Stream pro Task, kurzlebig): ```python # domains/rendering/router.py @router.get("/tasks/{task_id}/logs") async def stream_task_logs(task_id: str, user=Depends(get_current_user)): async def event_generator(): while True: logs = await redis.lrange(f"task_logs:{task_id}", -50, -1) for line in logs: yield f"data: {line}\n\n" if await task_is_done(task_id): break await asyncio.sleep(0.5) return EventSourceResponse(event_generator()) ``` **Einsatz:** - Dashboard, Worker-Status, Queue-Längen → **WebSocket** - Blender-Stdout während Render → **SSE** --- ### ADR-06: Blender Version Policy — >= 5.0.1, Upgrade-Pfad auf 5.1 **Entscheidung:** Blender < 5.0.1 wird nicht unterstützt. Während der Entwicklung erscheint Blender 5.1 — der Wechsel erfolgt dann ausschließlich auf >= 5.1. **Umsetzung:** ```dockerfile # render-worker/Dockerfile ARG BLENDER_VERSION=5.0.1 ARG BLENDER_MIN_VERSION=5.0.1 RUN BLENDER_URL="https://download.blender.org/release/Blender${BLENDER_VERSION}/blender-${BLENDER_VERSION}-linux-x64.tar.xz" \ && curl -L $BLENDER_URL | tar xJ -C /opt/blender --strip-components=1 ``` ```python # render-worker/scripts/check_version.py — wird beim Container-Start geprüft import bpy, sys major, minor, patch = bpy.app.version if (major, minor) < (5, 0): print(f"ERROR: Blender {major}.{minor}.{patch} nicht unterstützt. Minimum: 5.0.1") sys.exit(1) ``` **Upgrade-Strategie 5.0.1 → 5.1:** - `BLENDER_VERSION` Build-Arg in `.env` ändern, neu bauen - Render-Scripts auf API-Änderungen prüfen (Blender Changelog 5.1) - Bestehende `.blend`-Templates: in 5.1 öffnen + resaven (automatisch migriert) - QC-Gate: Test-Render mit Sample-STEP-File nach Upgrade **Hinweis:** Blender 5.x verwendet den neuen Asset Library Standard (ab 3.0 eingeführt, in 5.x vollständig stabil) — dieser wird für ADR-07 vorausgesetzt. --- ### ADR-07: Blender Asset Library für Materialien UND Modifier **Problem:** Das bisherige `material_libraries`-Konzept erlaubt nur Material-Linking. Modifier (insbesondere Geometry Nodes) können damit nicht verwaltet werden. Blenders natives Asset-Library-System ist breiter und deckt beides ab. **Entscheidung:** Blender Asset Library als primäres System für Assets: ```python # render-worker/scripts/asset_library.py def apply_asset_library(blend_path: str, material_map: dict, modifier_map: dict): """ Lädt Assets aus einer Asset-Library .blend-Datei: 1. Materialien: linked/appended per Namen aus material_map 2. Geometry-Node-Modifier: appended per Namen aus modifier_map, auf Mesh angewendet """ with bpy.data.libraries.load(blend_path, link=False, assets_only=True) as (data_from, data_to): # Materialien laden data_to.materials = [ name for name in data_from.materials if name in material_map.values() ] # Geometry-Node-Gruppen laden (für Modifier) data_to.node_groups = [ name for name in data_from.node_groups if name in modifier_map.values() ] # Materialien auf Parts anwenden for obj in bpy.data.objects: if obj.type == 'MESH': for slot in obj.material_slots: resolved = material_map.get(slot.material.name if slot.material else '') if resolved and resolved in bpy.data.materials: slot.material = bpy.data.materials[resolved] # Geometry-Node-Modifier anwenden for obj in bpy.data.objects: if obj.type == 'MESH': for part_name, modifier_name in modifier_map.items(): if part_name in obj.name and modifier_name in bpy.data.node_groups: mod = obj.modifiers.new(name=modifier_name, type='NODES') mod.node_group = bpy.data.node_groups[modifier_name] ``` **Datenmodell:** `material_libraries` → umbenannt in `asset_libraries`: ``` asset_libraries ( id UUID PK, tenant_id FK, name VARCHAR(200), blend_file_key TEXT, -- MinIO key zur .blend-Datei catalog JSONB, -- Asset-Katalog: {materials: [...], node_groups: [...]} description TEXT, is_active BOOL DEFAULT TRUE, created_at TIMESTAMP ) ``` **Workflow-Integration:** Zwei neue Node-Typen: - `apply_asset_library_materials` — Material-Substitution via Asset Library - `apply_asset_library_modifiers` — Geometry-Node-Modifier via Asset Library **Katalog-Refresh:** Nach Upload einer neuen `.blend`-Datei analysiert ein Celery-Task via Blender `--background --python` die Assets und schreibt den Katalog in `asset_libraries.catalog JSONB` — damit weiß die UI welche Assets verfügbar sind ohne die .blend zu öffnen. **Vorteile gegenüber bisherigem Ansatz:** - Ein `.blend` kann Materialien *und* Modifier enthalten → weniger Dateien zu verwalten - Natives Blender-System → zukunftssicher (Blender entwickelt Asset Library weiter) - Modifier als Assets: z.B. "Bevel Sharp Edges", "Add Chamfer", "Clean Geometry" als wiederverwendbare Node-Groups - Asset-Katalog im Browser durchsuchbar ohne Blender zu starten --- ## 4. Was wird entfernt / ersetzt (mit Risiken) ### 4.1 Flamenco (Manager + Worker + Job-Scripts) **Entfernt:** - `flamenco/` Verzeichnis komplett - `flamenco-manager`, `flamenco-worker` Services aus docker-compose - `flamenco_client.py`, `flamenco_tasks.py` - Celery-Beat-Task `poll_flamenco_jobs` - `flamenco_job_id`, `render_backend_used` Spalten (Migration 032: nullable, später entfernen) - `render_backend` System-Setting **Ersetzt durch:** Distributed Celery render-worker + MinIO shared storage **Risiken:** - Laufende Flamenco-Jobs → Migration setzt Status auf `cancelled` - render_dispatcher.py muss vereinfacht werden (nur Celery-Pfad) **Migration:** ```sql UPDATE order_lines SET render_status = 'cancelled', flamenco_job_id = NULL WHERE render_status = 'processing' AND flamenco_job_id IS NOT NULL; ``` --- ### 4.2 blender-renderer (Flask HTTP-Service) **Entfernt:** - `blender-renderer/app.py` (Flask-Wrapper) - Service aus docker-compose - HTTP-Aufrufe zu `:8100` aus step_processor.py **Ersetzt durch:** - Blender als **subprocess im render-worker Celery-Container** - `blender_render.py` wandert nach `render-worker/scripts/` - Render-Logik: `domains/rendering/tasks.py` **Risiken:** - render-worker Container benötigt Blender + cadquery → größeres Image (~3GB) - Build-Zeit steigt → Base-Image vorab bauen und in lokale Registry pushen --- ### 4.3 threejs-renderer (Playwright HTTP-Service) **Entfernt:** Kompletter Service + alle server-seitigen Three.js-Render-Pfade **Three.js bleibt als:** Frontend-3D-Viewer (ThreeDViewer.tsx, läuft im Browser mit glTF) **Risiken:** - Alle Three.js-generierten Thumbnails müssen mit Blender neu gerendert werden - Admin-Batch-Regenerierung wird beim Deploy ausgeführt --- ### 4.4 system_settings Key-Value-Store **Entfernt:** `system_settings` Tabelle + `_save_setting()` direktes SQL-Hack **Ersetzt durch:** `app_config` Modell mit JSONB-Spalten pro Kategorie (Render, Storage, Notifications, Worker, Billing) — vollständig ORM-native --- ### 4.5 Flache Projektstruktur (routers/ services/ models/) **Ersetzt durch:** Domain-Driven Structure (ADR-04) **Migration:** Schrittweise pro Phase, nicht alles auf einmal. --- ## 5. Was bleibt und wird erweitert ### 5.1 FastAPI Backend - Strukturell erhalten, in Domain-Driven Structure migriert - RLS-fähige DB-Dependency ersetzt einfaches `get_db` - Neue Domains: `rendering`, `media`, `billing`, `tenants`, `imports` ### 5.2 SQLAlchemy 2 + Alembic - Alle bestehenden Models bleiben (umstrukturiert in Domains) - RLS-Policies als raw SQL in Migrationen - Migration 032+ für neue Tabellen ### 5.3 Celery + Redis — erweiterte Queue-Struktur | Queue | Worker | Concurrency | Tasks | |---|---|---|---| | `step_processing` | step-worker | 8 | `extract_cad_metadata`, `validate_excel_import` | | `convert` | step-worker | 4 | `convert_step_to_stl`, `extract_mesh_attributes` | | `render_default` | render-worker | **1 pro Container** | `render_still`, `render_turntable_frames` | | `notify` | step-worker | 4 | `send_notification` | **Scaling-Modell:** Jeder render-worker hat concurrency=1 (1 Blender-Prozess). Mehr Worker-Container = mehr parallele Renders. `docker compose scale render-worker=4` → 4 parallele Renders. ### 5.4 Material-Alias-System - Lookup-Reihenfolge (Aliases zuerst) bleibt - Erweitert: `material_library_id` FK auf `material_aliases` - Erweitert: Unbekannte-Materialien-Report beim Excel-Import ### 5.5 RenderTemplate + Pricing + Notification (bleibt, in Domains integriert) - `lighting_only`, `shadow_catcher` bleiben - PricingTier → um Invoice-Modul erweitert - Notification → um `notification_configs` erweitert --- ## 6. Neue Komponenten ### 6.1 MinIO Object Storage (ADR-02) Service in `docker-compose.yml`. Alle Datei-Operationen über `StorageBackend` Abstraction in `core/storage.py`. Externe Worker benötigen nur URL + Credentials — kein Mount. Buckets: - `uploads` — STEP-Dateien, Thumbnails, Render-Outputs - `blend-templates` — .blend RenderTemplate-Dateien - `asset-libraries` — .blend Asset-Library-Dateien (Materialien + Modifier) - `production-exports` — glTF/GLB + .blend Production-Downloads (kurzlebig, TTL 7d) - `exports` — Zip-Downloads, PDF-Invoices (kurzlebig, TTL 24h) --- ### 6.2 Tenant-Modell + PostgreSQL RLS (ADR-01) ``` tenants (id UUID PK, name VARCHAR, slug VARCHAR UNIQUE, is_active BOOL, created_at) ``` FK `tenant_id` auf: `users`, `orders`, `products`, `cad_files`, `media_assets`, `invoices`, `material_libraries`, `render_templates` RLS-Policies in Migration 035 — danach ist Datenisolation automatisch. --- ### 6.3 Workflow-System: Celery Canvas + React Flow (ADR-03) **Datenmodell:** ``` workflow_definitions (id, name, output_type_id FK, config JSONB, is_active) config = { "type": "still"|"turntable"|"multi_angle", "params": {...} } workflow_runs (id, workflow_def_id FK, order_line_id FK, celery_task_id, status, started_at, completed_at) workflow_node_results (id, run_id FK, node_name, status, output JSONB, log TEXT, duration_s FLOAT) ``` **Execution:** `workflow_builder.py` baut Celery Canvas aus `config.type` + `config.params`. Jeder Node-Task schreibt sein Ergebnis in `workflow_node_results`. **Node-Typen (Celery Tasks):** - `convert_step` → STEP→STL via cadquery, prüft SHA256-Cache - `extract_mesh_attributes` → OCC Topologie → sharp_edges JSON - `apply_asset_library_materials` → Lädt Materialien aus Asset-Library .blend, wendet auf Mesh-Parts an - `apply_asset_library_modifiers` → Lädt Geometry-Node-Gruppen aus Asset-Library, wendet als Modifier an - `render_still` → Blender subprocess → PNG nach MinIO - `render_turntable_frames` → Blender subprocess → Frame-Ordner nach MinIO - `composite_ffmpeg` → Frames + bg_color → MP4 nach MinIO - `export_gltf` → Blender exportiert GLB mit angewendeten Produktionsmaterialien → MinIO - `export_blend` → Blender speichert .blend mit `pack_all()` → MinIO (alle Texturen eingebettet) - `generate_thumbnail` → Pillow resize → Thumb nach MinIO - `publish_asset` → MediaAsset-Record in DB erstellen **React Flow Frontend:** `WorkflowEditor.tsx` — visualisiert den Canvas-Workflow, bearbeitet `config JSONB`. Kein eigener Execution-Code. --- ### 6.4 MediaAsset-Katalog ``` media_assets ( id UUID PK, tenant_id FK, product_id FK, order_line_id FK, workflow_run_id FK, asset_type ENUM(thumbnail, still, turntable, stl_low, stl_high, gltf_geometry, -- glTF ohne Materialien (aus STEP-Konvertierung) gltf_production, -- GLB mit Produktionsmaterialien (aus export_gltf Node) blend_production), -- .blend mit eingebetteten Produktionsmaterialien storage_key TEXT, -- MinIO object key file_size_bytes BIGINT, mime_type VARCHAR(100), width INT, height INT, duration_s FLOAT, render_config JSONB, created_at TIMESTAMP, is_archived BOOL DEFAULT FALSE ) ``` **API:** Filter, Single-Download, Zip-Download (StreamingResponse), Soft-Delete --- ### 6.5 OCC Mesh-Attribute Extraktion ```python # domains/products/tasks.py def extract_mesh_attributes(step_path: str) -> dict: """ Via pythonOCC BRep-Topologie: - sharp_edges: Kanten-Indices mit Dihedral-Winkel > Threshold (default 30°) - seam_candidates: Kanten zwischen verschiedenen Face-Typen - face_groups: Flächen nach Typ (planar, cylindrical, toroidal, ...) """ ``` Output in `cad_files.mesh_attributes JSONB` → wird beim Render als Parameter übergeben. Blender-Integration in `render_still`: ```python # render-worker/scripts/blender_render.py if mesh_attributes and mesh_attributes.get("sharp_edges"): for edge_idx in mesh_attributes["sharp_edges"]: mesh.edges[edge_idx].use_edge_sharp = True bpy.ops.mesh.mark_seam(clear=False) bpy.ops.uv.smart_project() ``` --- ### 6.6 Hash-basiertes Conversion-Caching ```python # domains/products/tasks.py def get_stl_cache_key(step_object_key: str, quality: str) -> str: content = storage.download_bytes(step_object_key) sha256 = hashlib.sha256(content).hexdigest() return f"conversion-cache/{sha256[:2]}/{sha256}/{quality}.stl" ``` Zentraler Cache in MinIO `uploads/conversion-cache/`. Gleiches STEP-File → 1x konvertiert, egal wie oft hochgeladen oder unter welchem Namen. --- ### 6.7 Billing / Invoice-Modul ``` invoices (id, tenant_id FK, period_start, period_end, status ENUM, total_amount, created_at) invoice_lines (id, invoice_id FK, order_line_id FK, product_name, asset_type, quantity, unit_price, total) ``` PDF-Export via WeasyPrint (HTML-Template → PDF). Excel-Export via openpyxl. --- ### 6.8 Excel Sanity-Check **Task `validate_excel_import`:** 1. Parse Excel 2. Für jede Row prüfen: STEP vorhanden + completed? Materialien in Aliases? Produkt in DB? 3. Fuzzy-Match-Vorschläge für unbekannte Materialien (via `difflib.get_close_matches`) 4. Report in `import_validations` DB + WebSocket-Event an Client **Frontend:** Sanity-Check-Dialog nach Upload, Ampel-Anzeige, Material-Lücken direkt schließbar. --- ### 6.9 WebSocket Live-Events (ADR-05) ```python # core/websocket.py EVENT_TYPES = [ "queue_update", # Queue-Länge geändert "render_complete", # Render erfolgreich "render_failed", # Render gescheitert "worker_online", # Neuer Worker registriert "worker_offline", # Worker nicht mehr erreichbar "order_status_change", # Order-Status geändert "import_validated", # Excel-Sanity-Check abgeschlossen ] ``` Dashboard, WorkerManagement, OrderDetail — alle abonnieren denselben WebSocket und filtern Events nach Typ. --- ### 6.10 Worker-Registrierung ```python # render-worker entrypoint redis.hset('registered_workers', f'{hostname}:{pid}', json.dumps({ 'hostname': hostname, 'queues': ['render_default'], 'blender_version': get_blender_version(), 'gpu': detect_gpu(), # nvidia-smi oder None 'started_at': utcnow().isoformat(), 'last_heartbeat': utcnow().isoformat(), })) # Heartbeat alle 30s; Beat-Task entfernt stale Workers nach 90s ``` `GET /api/workers` liest Redis-Hash, berechnet Queue-Stats via Celery Inspect. --- ### 6.11 Blender Asset Library Management (ADR-07) **Datenmodell:** `asset_libraries` (ersetzt `material_libraries`) ``` asset_libraries ( id UUID PK, tenant_id FK, name VARCHAR(200), blend_file_key TEXT, -- MinIO key: "asset-libraries/{id}.blend" catalog JSONB, -- {materials: ["SCHAEFFLER_010101_Steel-Bare", ...], -- node_groups: ["Bevel_Sharp_Edges", "Clean_Geometry", ...]} description TEXT, is_active BOOL DEFAULT TRUE, created_at TIMESTAMP ) ``` **Katalog-Refresh-Task:** ```python # domains/materials/tasks.py def refresh_asset_library_catalog(asset_library_id: str): """ Öffnet .blend via Blender --background, liest alle markierten Assets, schreibt Katalog nach asset_libraries.catalog JSONB. Läuft automatisch nach jedem .blend-Upload. """ script = "render-worker/scripts/catalog_assets.py" result = subprocess.run(['blender', '--background', '--python', script, '--', blend_path, '--output', 'json'], ...) catalog = json.loads(result.stdout) db.execute(update(AssetLibrary).values(catalog=catalog)) ``` **API:** - `POST /api/asset-libraries` — Upload .blend, Katalog wird automatisch gelesen - `GET /api/asset-libraries/{id}/catalog` — Verfügbare Assets durchsuchen - `PUT /api/asset-libraries/{id}` — Metadaten aktualisieren - `DELETE /api/asset-libraries/{id}` — Löschen (nur wenn nicht in Verwendung) **Frontend:** Asset-Library-Manager in Admin — Upload, Katalog-Anzeige (Materialien + Node-Groups als Badges), Zuweisung zu OutputTypes. --- ### 6.12 Interaktive 3D Browser-Vorschau mit Production-Materialien **Konzept:** Der vorhandene `ThreeDViewer.tsx` (Three.js, OrbitControls) wird um Production-glTF-Support erweitert. Zwei Ansichtsmodi: | Modus | glTF-Quelle | Materialien | |---|---|---| | Geometrie-Preview | `gltf_geometry` — aus STEP-Konvertierung | Farbige Part-Gruppen (OCC-Extraktion) | | Production-Preview | `gltf_production` — aus `export_gltf` Workflow-Node | Echte Produktionsmaterialien (PBR) | **Blender → GLB Pipeline:** ```python # render-worker/scripts/export_gltf.py def export_gltf(stl_path, blend_key, material_map, modifier_map, output_key): # 1. STL importieren bpy.ops.import_mesh.stl(filepath=stl_path) # 2. Asset Library laden (Materialien + Modifier) apply_asset_library(blend_path, material_map, modifier_map) # 3. Als GLB exportieren bpy.ops.export_scene.gltf( filepath=output_path, export_format='GLB', export_materials='EXPORT', # Materialien einbetten export_apply=True, # Modifier vor Export anwenden export_draco_mesh_compression_enable=True, # Komprimierung export_texture_dir='', ) ``` **Hinweis Materialtreue:** Blenders glTF-Exporter konvertiert `Principled BSDF` → PBR (metallic/roughness). Komplexe Shader-Nodes (z.B. Procedural Textures) werden nicht vollständig übertragen — für diese Fälle: Texture Baking vor Export (optionaler Workflow-Node `bake_textures`). **Frontend-Erweiterungen ThreeDViewer.tsx:** ```tsx // Neue Props/Features: interface ThreeDViewerProps { geometryGltfUrl?: string // Geometrie-Preview (sofort verfügbar) productionGltfUrl?: string // Production-Preview (nach Workflow-Abschluss) showMaterialToggle?: boolean // Umschalten zwischen Modi showWireframe?: boolean // Wireframe-Overlay environmentPreset?: 'studio' | 'outdoor' | 'dark' } ``` Progressive Loading: Geometrie-Preview sofort zeigen → Production-Preview nachladen wenn verfügbar. **Download-Buttons direkt im Viewer:** - "GLB herunterladen" → `GET /api/media/{gltf_production_id}/download` - ".blend herunterladen" → `GET /api/media/{blend_production_id}/download` --- ### 6.13 Production Export: glTF + .blend Download **Workflow-Node `export_gltf`:** - Input: STL-Pfad, Asset-Library-ID, Material-Map, Modifier-Map - Output: GLB-Datei in MinIO `production-exports/{cad_file_id}/{run_id}.glb` - MediaAsset-Record: `asset_type = gltf_production` **Workflow-Node `export_blend`:** ```python # render-worker/scripts/export_blend.py def export_blend(stl_path, blend_key, material_map, modifier_map, output_key): # 1. STL + Asset Library laden (wie export_gltf) # ... # 2. Alle externen Daten einbetten bpy.ops.file.pack_all() # 3. Als .blend speichern (komprimiert) bpy.ops.wm.save_as_mainfile( filepath=output_path, compress=True, copy=True # Original-Session unangetastet ) ``` **Größen-Warnung:** .blend mit eingebetteten Texturen kann 50-500MB werden. Daher: - `production-exports` Bucket TTL: 7 Tage (konfigurierbar in `app_config`) - Maximale Dateigröße: 1GB (konfigurierbar) - Frontend-Warnung bei Dateien > 100MB vor Download **Standard-Workflow "Still mit Production-Exports":** ```python chain( convert_step.si(order_line_id), extract_mesh_attributes.si(order_line_id), apply_asset_library_materials.si(order_line_id), apply_asset_library_modifiers.si(order_line_id), group( render_still.si(order_line_id), # PNG für Produktion export_gltf.si(order_line_id), # GLB für 3D-Viewer + Download export_blend.si(order_line_id), # .blend für Archiv/Post-Processing ), generate_thumbnail.si(order_line_id), publish_asset.si(order_line_id), ) ``` --- ## 7. Phasenplan mit Tasks ### Phase A: Infrastruktur-Cleanup + MinIO ✅ ABGESCHLOSSEN (2026-03-06) **A1: Flamenco entfernen** ✅ - `docker-compose.yml` → flamenco-manager, flamenco-worker entfernen - `flamenco_client.py`, `flamenco_tasks.py` löschen - `render_dispatcher.py` → vereinfachen (nur Celery-Pfad) - Migration 032: laufende Flamenco-Jobs auf `cancelled` setzen - Akzeptanzkriterium: `docker compose up` startet ohne flamenco, alle bestehenden Renders laufen via Celery **A2: blender-renderer → render-worker Celery-Container (ADR-06 umsetzen)** ✅ - `render-worker/Dockerfile` (neu): Ubuntu + Blender (>= 5.0.1, via `BLENDER_VERSION` Build-Arg) + cadquery + Python-Deps - `check_version.py` läuft beim Container-Start: prüft Blender >= 5.0.1, Exit 1 wenn nicht erfüllt - `blender-renderer/blender_render.py` → `render-worker/scripts/blender_render.py` - `domains/rendering/tasks.py` (neu): `render_still_task`, `render_turntable_task` - Blender via `subprocess.run`, stdout in Redis für SSE - `docker-compose.yml`: `blender-renderer` entfernen, `render-worker` hinzufügen - `.env.example`: `BLENDER_VERSION=5.0.1` dokumentieren - Akzeptanzkriterium: Thumbnail via Celery-Task, kein HTTP-Call zu :8100, Version-Check besteht **A3: threejs-renderer entfernen** ✅ - Service entfernen, threejs-Pfad in step_processor.py entfernen - Batch-Regenerierung aller threejs-Thumbnails (Admin-Funktion) - ThreeDViewer.tsx (Frontend) bleibt - Akzeptanzkriterium: Alle Thumbnails Blender-gerendert **A4: MinIO hinzufügen + Storage-Abstraction** ✅ - MinIO Service in `docker-compose.yml` - `core/storage.py`: `MinIOStorage` + `LocalStorage` (für Dev-Fallback) - Bestehende Upload-Endpoints: Dateien nach MinIO statt in lokales `/uploads` - Migration bestehender Dateien: Skript das `/uploads` nach MinIO hochlädt - `.env.example`: `MINIO_URL`, `MINIO_USER`, `MINIO_PASSWORD` - `docker-compose.worker.yml` (neu): render-worker für externe Maschinen - Akzeptanzkriterium: File-Upload → MinIO, Worker-Container läuft auf Maschine B und rendert Jobs **A5: system_settings → app_config** ✅ - Migration 033: `app_config` Tabelle (JSONB-Spalten: render, storage, notifications, worker, billing) - `core/config_service.py` (neu), `system_settings` Tabelle deprecated - Migrate bestehende Settings - Akzeptanzkriterium: Alle Settings ORM-native persistierbar, kein direktes SQL --- ### Phase B: Domain-Driven Umstrukturierung + Tenant-Modell ✅ ABGESCHLOSSEN (2026-03-06) **B1: Domain-Driven Struktur anlegen** ✅ - `backend/app/domains/` Verzeichnis erstellen - Bestehende Models/Services/Routers schrittweise in Domains verschieben (products, orders, materials, rendering, notifications zuerst) - `main.py` registriert nur noch Domain-Router - Akzeptanzkriterium: Alle bestehenden Tests grün, Imports funktionieren, API-Endpoints unverändert **B2: Tenant-Datenmodell + RLS** ✅ - Migration 035: `tenants` Tabelle + 'Schaeffler' Default-Seed - Migration 036: `tenant_id` FK auf alle Tabellen + RLS-Policies (tenant_isolation + admin_bypass) + Backfill - `domains/tenants/` mit CRUD-Router, Service, Modellen - `core/database.py`: `get_db_for_tenant` + `set_tenant_context()` Dependency - Admin-Bypass via `current_setting('app.current_tenant_id', true) = 'bypass'` - BYPASSRLS-Versuch mit graceful fallback **B3: Tenant-Management UI** ✅ - `frontend/src/pages/Tenants.tsx`: CRUD-Tabelle + Tenant-Selektor Dropdown - `frontend/src/api/tenants.ts`: vollständiger API-Client - X-Tenant-ID Header-Interceptor in `api/client.ts` - Route `/tenants` + Sidebar-Link (admin-only) --- ### Phase C: Workflow-System ✅ ABGESCHLOSSEN (2026-03-06) **C1: WorkflowDefinition Datenmodell** ✅ - Migration 036: `workflow_definitions`, `workflow_runs`, `workflow_node_results` - `domains/rendering/models.py` erweitern - `domains/rendering/workflow_builder.py` (neu): Celery-Canvas-Builder für "still", "turntable", "multi_angle" - `output_types.workflow_definition_id` FK (Migration 037) - Akzeptanzkriterium: Render via `dispatch_workflow("still", order_line_id)` erfolgreich **C2: Standard-Workflows seeden + render_dispatcher migrieren** ✅ - 3 Standard-Workflows direkt in Migration 037 geseedet (Still, Turntable, Multi-Angle) - `workflow_builder.py`: `dispatch_workflow()` mit Celery Canvas (chain/group) - `dispatch_service.py`: prüft `output_type.workflow_definition_id` → neu vs. Legacy-Pfad - Backward-Compat: ohne `workflow_definition_id` → alter direkter Task-Call **C3: React Flow Workflow-Editor (Frontend)** ✅ - `@xyflow/react` zu `package.json` hinzugefügt (npm install nötig) - `frontend/src/pages/WorkflowEditor.tsx`: 6 Custom-Node-Typen, ConfigSidepanel, Node-Palette mit Drag-Drop - `frontend/src/api/workflows.ts`: vollständiger CRUD-Client - Route `/workflows` + Sidebar-Link (admin + project_manager) --- ### Phase D: OCC Mesh-Attribute ✅ ABGESCHLOSSEN (2026-03-06) **D1: Attribut-Extraktion** ✅ - `domains/products/tasks.py`: `extract_mesh_attributes` Celery-Task - Migration 038: `cad_files.mesh_attributes JSONB` - Läuft nach `extract_cad_metadata` in Workflow-Chain - Akzeptanzkriterium: STEP-Upload → mesh_attributes JSON in DB mit sharp_edges **D2: Blender-Integration** ✅ - `render-worker/scripts/still_render.py` + `turntable_render.py`: `_apply_mesh_attributes()` setzt Auto-Smooth basierend auf `curved_ratio` und `sharp_angle_threshold_deg` - `render_blender.py`: übergibt `--mesh-attributes JSON` an Blender-Subprocess - `render_still_task`: lädt `mesh_attributes` aus DB und reicht sie weiter --- ### Phase E: MediaAsset-Katalog ✅ ABGESCHLOSSEN (2026-03-06) **E1: Datenmodell + API** ✅ - Migration 040: `media_assets` Tabelle mit RLS-Policies - `domains/media/`: MediaAsset-Model, Schemas, Service, Router - `publish_asset` Celery-Task in `rendering/tasks.py` - `core/storage.py`: `download_bytes()` für MinIO + Local **E2: Frontend** ✅ - `frontend/src/pages/MediaBrowser.tsx`: Grid/List-Toggle, Multi-Select, Floating Action Bar (ZIP + Archiv) - `frontend/src/api/media.ts`: vollständiger API-Client mit `zipDownloadAssets()` - Route `/media` + Sidebar-Link (admin + project_manager) --- ### Phase F: Hash-basiertes Conversion-Caching (Woche 5) **F1: Cache-Service** - `domains/products/tasks.py`: SHA256-Check vor jeder STL-Konvertierung - Migration 040: `cad_files.step_file_hash VARCHAR(64)` - Cache in MinIO `uploads/conversion-cache/` - Akzeptanzkriterium: Gleiches STEP-File → Log zeigt "cache hit" beim 2. Upload --- ### Phase G: Billing & Reporting (Woche 6) **G1: Invoice Datenmodell + API** - Migration 041: `invoices`, `invoice_lines` - `domains/billing/` (neu) - `POST /api/billing/invoices`, `GET /api/billing/invoices/{id}/pdf` (WeasyPrint) - Akzeptanzkriterium: PDF-Invoice mit korrekten Positionen downloadbar **G2: Billing Dashboard (Frontend)** - `frontend/src/pages/Billing.tsx` (neu) - Kosten-Übersicht per Tenant/Zeitraum, Invoice-Liste + Download - Akzeptanzkriterium: Invoice generierbar und downloadbar --- ### Phase H: Excel Sanity-Check (Woche 7) **H1: Sanity-Check Task + Fuzzy-Match** - `domains/imports/tasks.py`: `validate_excel_import` - Migration 042: `import_validations` Tabelle - `difflib.get_close_matches` für Materialvorschläge - WebSocket-Event nach Abschluss **H2: Sanity-Check UI** - Ampel-Dialog nach Excel-Upload - Material-Lücken direkt im Dialog schließbar (neuer Alias) - Akzeptanzkriterium: Klar welche Produkte produzierbar sind, Material-Aliases ergänzbar --- ### Phase I: Konfigurierbare Notifications (Woche 7) **I1: Notification-Config** - Migration 043: `notification_configs` Tabelle - `domains/notifications/service.py`: prüft Config vor Emit - Standard-Seeding: alle Events für Admin aktiviert **I2: Settings UI** - `frontend/src/pages/NotificationSettings.tsx` (neu) - Toggle-Matrix: Event × Kanal (In-App, E-Mail optional) - Akzeptanzkriterium: Events abschaltbar, Einstellungen wirksam --- ### Phase J: WebSocket + SSE Log-Streaming (Woche 8) **J1: WebSocket Backend** - `core/websocket.py`: Connection-Manager, Tenant-basiertes Broadcasting - Alle relevanten Tasks/Services broadcasten WebSocket-Events - `GET /ws` Endpoint **J2: SSE Task-Logs** - `GET /api/tasks/{task_id}/logs` — SSE, Worker schreibt in Redis-Liste - `LiveRenderLog.tsx` erweitern: `EventSource` API, Auto-scroll **J3: Frontend WebSocket-Integration** - Dashboard, WorkerManagement, OrderDetail abonnieren `/ws` - Ersetzt polling-basierte `useQuery`-Intervalle wo sinnvoll - Akzeptanzkriterium: Render-Start → Dashboard zeigt Status-Update ohne Reload --- ### Phase K: Blender Asset Library + Production Exports (Woche 8-9) **K1: Asset Library Datenmodell + Upload** - Migration 044: `asset_libraries` Tabelle (id, name, blend_file_key, catalog JSONB, tenant_id) - `render_templates.asset_library_id` FK, `output_types.asset_library_id` Default-Library - Upload via MinIO `asset-libraries/` Bucket - Nach Upload: Celery-Task `refresh_asset_library_catalog` → öffnet .blend via Blender --background, liest Asset-Namen, schreibt in `catalog` JSONB - Akzeptanzkriterium: .blend hochladen → Katalog mit Materialien + Node-Groups in DB sichtbar **K2: Asset Library Management UI** - `domains/materials/` → Asset-Library-Manager (Upload, Katalog-Anzeige als Badge-Grid) - Materialien + Node-Groups aus Katalog anzeigen - Zuweisung per OutputType + RenderTemplate wählbar - Akzeptanzkriterium: 2 Libraries für verschiedene OutputTypes konfigurierbar **K3: Workflow-Nodes apply_asset_library_materials + apply_asset_library_modifiers** - `render-worker/scripts/asset_library.py`: Materialien und Node-Groups aus .blend linken/appenden - Workflow-Builder: Nodes in Standard-Workflow "Still mit Production-Exports" integrieren - Akzeptanzkriterium: Render mit Asset-Library zeigt korrekte Produktionsmaterialien im PNG **K4: export_gltf Workflow-Node** - `render-worker/scripts/export_gltf.py`: Blender exportiert GLB mit angewendeten Materialien - Modifier vor Export anwenden (`export_apply=True`), Draco-Komprimierung aktiviert - MediaAsset-Eintrag: `asset_type = gltf_production` - Akzeptanzkriterium: GLB-Download aus Browser ladbar, Materialien sichtbar in Three.js-Viewer **K5: export_blend Workflow-Node** - `render-worker/scripts/export_blend.py`: `pack_all()` + `save_as_mainfile(compress=True)` - Größenwarnung-Config in `app_config` (Default: Warnung ab 100MB, Limit 1GB) - MediaAsset-Eintrag: `asset_type = blend_production` - TTL in MinIO `production-exports/`: 7 Tage (konfigurierbar) - Akzeptanzkriterium: .blend-Download enthält alle Texturen, öffnet in Blender 5.x ohne fehlende Links **K6: 3D-Viewer Production-Modus (Frontend)** - `ThreeDViewer.tsx` erweitern: Modus-Toggle Geometrie ↔ Production-glTF - Wireframe-Toggle, Environment-Preset-Auswahl (studio/outdoor/dark) - Download-Buttons im Viewer für GLB + .blend - Progressive Loading: Geometrie-Preview sofort, Production-glTF nachladen - Akzeptanzkriterium: Interaktiver Viewer zeigt Produktionsmaterialien; Download funktioniert --- ### Phase L: Dashboard & UX (Woche 9-10) **L1: Modular Widget-Dashboard** - `Widget.tsx` generischer Container, Widget-Config per User in DB - Widget-Typen: ProductionStats, QueueStatus, RecentRenders, CostOverview, WorkerStatus - WebSocket-Feed für Live-Updates **L2: Responsive Design** - Tailwind CSS-Variablen auf RGB-Channel-Format (behebt Learning 2026-02-18) - 768px Minimum (iPad-Breite) **L3: Worker-Management UI** - `WorkerManagement.tsx` (neu): Worker-Liste aus Redis, Queue-Stats, Scale-Button --- ### Phase M: QC-Tests (Woche 10-11) **M1: Pytest Backend** - `tests/domains/` — pro Domain: API-Tests + Service-Tests - Fixtures: Test-DB mit RLS-Setup, Mock-MinIO (moto), Mock-Celery - Akzeptanzkriterium: > 80% Coverage auf Service-Layer, alle Domains **M2: Frontend Vitest** - `frontend/src/__tests__/` — Komponenten-Tests mit Testing Library - Akzeptanzkriterium: `npm run test` → 0 Failures **M3: Integration-Tests** - End-to-End: STEP Upload → MinIO → Celery → Render (Mock-Blender) → MediaAsset → Download - Tenant-Isolation-Test: Client A sieht keine Client-B-Daten - Akzeptanzkriterium: Pipeline durchlaufbar in CI ohne echtes Blender --- ## 8. Datenbankmigrationen-Übersicht | Migration | Beschreibung | Phase | |---|---|---| | 032 | Flamenco-Felder bereinigen, Jobs auf cancelled | A | | 033 | app_config (strukturiertes Config-Modell, ersetzt system_settings) | A | | 034 | tenants Tabelle | B | | 035 | tenant_id FKs + **PostgreSQL RLS-Policies** + Backfill | B | | 036 | workflow_definitions, workflow_runs, workflow_node_results | C | | 037 | output_types.workflow_definition_id FK | C | | 038 | cad_files.mesh_attributes JSONB | D | | 039 | media_assets Tabelle | E | | 040 | cad_files.step_file_hash VARCHAR(64) | F | | 041 | invoices, invoice_lines | G | | 042 | import_validations | H | | 043 | notification_configs | I | | 044 | **asset_libraries** (ersetzt material_libraries), FKs auf render_templates/output_types | K | | 045 | media_assets.asset_type: ENUM um gltf_production, blend_production erweitern | K | --- ## 9. QC-Gates und Test-Checkliste Diese Checkliste ist für Agenten konzipiert — jeder Task muss diese Gates passieren bevor Commit. ### 9.1 Backend QC-Gates ```bash # Syntax-Check docker compose exec backend python -m py_compile app/domains/[domain]/[changed_file].py # Alembic docker compose exec backend alembic current # → head # Pytest docker compose exec backend pytest tests/ -x --tb=short # → 0 Failures # Import + Schema docker compose exec backend python -c "from app.main import app; print('OK')" ``` ### 9.2 RLS QC-Gate (neu, nach Phase B) ```bash # Tenant-Isolation Test docker compose exec backend pytest tests/domains/tenants/test_rls.py -v # → Client A kann keine Client-B-Daten lesen/schreiben # → Admin mit BYPASSRLS sieht alle Daten ``` ### 9.3 Celery QC-Gates ```bash docker compose exec step-worker celery -A app.celery_app inspect registered # → extract_cad_metadata, convert_step_to_stl, extract_mesh_attributes, validate_excel_import docker compose exec render-worker celery -A app.celery_app inspect registered # → render_still, render_turntable_frames, composite_ffmpeg, generate_thumbnail, publish_asset docker compose exec step-worker celery -A app.celery_app inspect active_queues # → step_processing, convert, notify ``` ### 9.4 MinIO QC-Gate (neu, nach Phase A4) ```bash # MinIO erreichbar curl http://localhost:9000/minio/health/live # → 200 # Upload + Download funktioniert docker compose exec backend python -c " from app.core.storage import storage storage.upload('/tmp/test.txt', 'test/test.txt') assert storage.exists('test/test.txt') print('MinIO OK') " # Externer Worker kann MinIO erreichen # (auf Maschine B ausführen) docker compose -f docker-compose.worker.yml exec render-worker python -c " from app.core.storage import storage assert storage.exists('test/test.txt') print('External worker MinIO access OK') " ``` ### 9.5 Frontend QC-Gates ```bash cd frontend npm run type-check # → 0 Errors npm run lint # → 0 Errors npm run test # → 0 Failures npm run build # → Erfolg ``` ### 9.6 Datenbank QC-Gates ```bash # Migration prüfen (manuell lesen!) — besonders RLS-Policies in 035 cat backend/alembic/versions/035_*.py # Up + Down testen docker compose exec backend alembic upgrade head docker compose exec backend alembic downgrade -1 docker compose exec backend alembic upgrade head ``` ### 9.7 Docker QC-Gates ```bash docker compose up -d docker compose ps # → alle "healthy" nach 90s (MinIO braucht ~30s) curl http://localhost:8888/health # → 200 curl http://localhost:9000/minio/health/live # → 200 ``` ### 9.8 Render-Pipeline QC-Gate (End-to-End) ```bash # Upload STEP → Workflow → Thumbnail curl -X POST http://localhost:8888/api/cad/upload -F "file=@step-sample-file/81113-l_cut.stp" # → cad_file_id sleep 30 curl http://localhost:8888/api/cad/{id} | jq .processing_status # → "completed" curl -I http://localhost:8888/api/cad/{id}/thumbnail # → 200, image/png # MediaAsset angelegt? curl http://localhost:8888/api/media?product_id={product_id} | jq length # → > 0 ``` ### 9.9 Security QC-Gates - [ ] Kein Endpoint ohne Auth (außer `/health`, `/ws`, `/api/cad/{id}/thumbnail`) - [ ] Alle File-Uploads: MIME-Type + Größe validiert - [ ] Zip-Download: `assert asset.tenant_id == current_tenant.id` vor Hinzufügen - [ ] MinIO: Buckets nicht public; Presigned URLs mit TTL für Downloads - [ ] RLS aktiv: `SELECT relrowsecurity FROM pg_class WHERE relname = 'products'` → `t` - [ ] JWT-Secret in `.env`, nicht im Code ### 9.10 Performance QC-Gates - [ ] Kein N+1-Query (`selectinload` / `joinedload` in List-Endpoints) - [ ] List-Endpoints paginiert (max. 100 Items/Page) - [ ] Zip-Download streamt (StreamingResponse) - [ ] WebSocket: kein Broadcasting an alle Tenants (nur eigener Tenant) - [ ] Thumbnails: `Cache-Control: max-age=3600` Header --- ## 10. Offene Entscheidungen | # | Frage | Optionen | Empfehlung / Status | |---|---|---|---| | 1 | **Blender-Version** | ~~4.x / 5.0.1~~ | **Entschieden: >= 5.0.1 Pflicht, Upgrade auf 5.1 sobald verfügbar** | | 2 | React Flow Lizenz | MIT / Pro | MIT reicht für internes System | | 3 | PDF-Generator | WeasyPrint / ReportLab | WeasyPrint (HTML→PDF) | | 4 | Mobile-Support Scope | iPad (768px) / Vollmobil (375px) | 768px Minimum | | 5 | OrderItem-Refactor | Jetzt / v3 | v3 (zu viel abhängiger Code) | | 6 | Blender GPU-Config | Pro-Worker via `deploy.resources` | Bleibt, NVIDIA-Support via ENV | | 7 | E-Mail-Notifications | SMTP jetzt / später | Später — nur In-App in v2 | | 8 | Three.js-Thumbnails Batch-Regenerierung | Obligatorisch / On-Demand | Obligatorisch beim Refactor-Deploy | | 9 | MinIO Backup-Strategie | MinIO Replication / S3 Sync | Außerhalb Scope v2 — in `.env` dokumentieren | | 10 | CI/CD Pipeline | GitHub Actions / lokal | GitHub Actions für Lint + Tests | | 11 | **glTF Materialtreue** | PBR-Export / Texture-Baking | **✅ 11A: PBR-Export only** — Principled BSDF → GLB, kein Baking | | 12 | **Asset Library: link vs. append** | `link=True` (Referenz bleibt) / `link=False` (Kopie) | **✅ 12B: link=True** — Library als Referenz; .blend-Exports nutzen pack_all() für self-contained Files | | 13 | **blend_production TTL** | 7 Tage / 30 Tage / permanent | **✅ 13C: Permanent** — .blend-Dateien bleiben dauerhaft in MinIO; Größen-Warnungen via app_config | | 14 | **ThreeDViewer Environment** | Nur Studio / mehrere Presets | Studio-Preset im v2-Scope; weitere Presets v3 | --- ## Freigabe **Architektur-Entscheidungen bestätigen:** - [x] ADR-01: PostgreSQL RLS für Tenant-Isolation - [x] ADR-02: MinIO als Shared Object Storage (ersetzt NFS) - [x] ADR-03: Celery Canvas als Workflow-Engine, React Flow nur Visualisierung - [x] ADR-04: Domain-Driven Projektstruktur - [x] ADR-05: WebSocket für Dashboard-Events, SSE nur für Task-Logs - [x] ADR-06: Blender >= 5.0.1 Pflicht, BLENDER_VERSION als Build-Arg, Upgrade auf 5.1 - [x] ADR-07: Blender Asset Library (Materialien + Modifier), `asset_libraries` Modell **Bestätigte Entscheidungen (Abschnitt 10):** - [x] 11A: glTF PBR-Export only (kein Texture-Baking) - [x] 12B: Asset Library link=True + pack_all() für .blend-Exports - [x] 13C: blend_production permanent in MinIO - [x] Bestehende API-Endpoints bleiben während Refactor erhalten (17) - [x] Phasenweise Implementierung mit Quality Gates (18) **Planung:** - [x] Plan insgesamt freigegeben - [x] Offene Entscheidungen aus Abschnitt 10 geklärt - [x] Startphase A bestätigt - [x] Git-Tag `v1-stable` auf main erstellt - [x] Git-Branch `refactor/v2` erstellt