From a1d140d30f4232e7afb8ca5bb32789ff626e410c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Wed, 11 Mar 2026 10:29:08 +0100 Subject: [PATCH] fix(render): production GLB sharp edges + materials (25/25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sharp edges: - OCC→Blender coordinate transform was wrong: Blender(X,Y,Z) = OCC(X×0.001, -Z×0.001, Y×0.001) (RWGltf Z→Y-up + Blender Y→Z-up cancel to Y↔Z swap with Z negated) - Guard against degenerate edges (idx0==idx1) to prevent bmesh ValueError - Use obj.matrix_world @ v.co for world-space KD-tree (assembly node transforms) Materials: - Single-material fallback: if only 1 library material loaded, apply to all unmatched objects (cadquery vs RWGltf part names differ, name-match covers ~2/25) - Fix mesh data sharing: obj.data.copy() before materials.clear() to avoid clearing shared data blocks - Use obj.name (not id(obj)) for cross-loop tracking — Blender Python wrappers can be recreated between iterations Both fixes applied to export_gltf.py (production GLB) and blender_render.py (thumbnails). Co-Authored-By: Claude Sonnet 4.6 --- LEARNINGS.md | 803 ++++++++++-------------- render-worker/scripts/blender_render.py | 84 ++- render-worker/scripts/export_gltf.py | 160 ++++- 3 files changed, 556 insertions(+), 491 deletions(-) diff --git a/LEARNINGS.md b/LEARNINGS.md index 2160b08..cbd7d0d 100644 --- a/LEARNINGS.md +++ b/LEARNINGS.md @@ -7,289 +7,395 @@ ## Learnings -### 2026-03-07 | Security | Media-Endpoints ohne Auth — Tenant-RLS reicht nicht allein -**Problem**: `list_assets`, `download_asset`, `zip_download` hatten kein `get_current_user`-Dep → unauthentifizierte Requests möglich. RLS schützt nur Datenbankzugriffe, nicht HTTP-Ebene. -**Lösung**: `_user: User = Depends(get_current_user)` zu allen drei Endpoints hinzufügen. RLS filtert dann automatisch per Tenant-ID aus dem JWT-Token (via Session-Variable `app.current_tenant_id`). -**Für künftige Projekte**: Jeder neue Router-Endpoint braucht expliziten Auth-Dep — RLS ist Defense-in-Depth, kein Ersatz für HTTP-Auth. - -### 2026-03-07 | MediaAsset | `is_animation` Flag entscheidet Asset-Type — falsches Design -**Problem**: `import_existing_media_assets` + `render_order_line_task` nutzten `output_type.is_animation == True` um `asset_type = turntable` zu setzen — auch für `.jpg` Poster-Frames aus Animations-OutputTypes. Folge: 6 JPG-Assets als `turntable` in DB → Broken-Video-Icons in MediaBrowser. -**Lösung**: Extension entscheidet: `.mp4`/`.webm` → `turntable`, alles andere → `still`. `is_animation` Flag ist für OutputType-Konfiguration, nicht für Asset-Klassifizierung. -**Für künftige Projekte**: MIME-Typ/Extension immer als primäre Typ-Quelle, niemals Meta-Flags des Auftrags. - -### 2026-03-07 | OCC | Bounding-Box aus STEP mit `Bnd_Box` + `brepbndlib.Add()` -**Problem**: Keine Real-World-Dimensions in der DB — weder Breite/Höhe/Tiefe noch Bauteil-Mittelpunkt. OCC-Extraktion lieferte nur Kanten-Topologie. -**Lösung**: `from OCC.Core.Bnd import Bnd_Box; from OCC.Core.BRepBndLib import brepbndlib; bbox = Bnd_Box(); brepbndlib.Add(shape, bbox); xmin,ymin,zmin,xmax,ymax,zmax = bbox.Get()` → `dimensions_mm = {x, y, z}` in `mesh_attributes` JSONB. Kein neues DB-Feld nötig — JSONB-Erweiterung reicht. -**Für künftige Projekte**: OCC `Bnd_Box` gibt Werte in mm (STEP-Einheit). In Blender nach Scale-Apply (0.001) sind die Werte dann in m. - -### 2026-03-07 | Storage | `storage_key` absolute Pfade brachen Volume-Moves -**Problem**: `step_tasks.py` und `admin.py` schrieben `storage_key=str(output_path)` mit absoluten Pfaden (`/shared/data/uploads/...`). Nach Volume-Umzug in v2 waren 398 Assets nicht mehr erreichbar. -**Lösung**: `_normalize_key()` Helper: strippt `UPLOAD_DIR`-Prefix. In `download_asset` Legacy-Remapping für alte Pfade als Fallback behalten. Neue Assets immer relativ speichern. -**Für künftige Projekte**: `storage_key` immer relativ zu `UPLOAD_DIR` → `candidate = Path(settings.upload_dir) / key`. Absolute Pfade nie in die DB schreiben. - -### 2026-03-07 | OCP | `RWMesh_CoordinateSystemConverter` nicht als Python-Binding verfügbar (OCP 7.8.1.1) -**Problem**: `writer.ChangeCoordinateSystemConverter()` wirft `TypeError: Unregistered type : RWMesh_CoordinateSystemConverter` — der C++-Typ ist nicht in OCP-Python-Bindings registriert. -**Lösung**: Shapes vor dem Export mit `BRepBuilderAPI_Transform` um Faktor 0.001 skalieren (mm→m). Dann `RWGltf_CafWriter` direkt ohne Koordinatensystem-Konverter aufrufen. -```python -trsf = gp_Trsf() -trsf.SetScaleFactor(0.001) -for i in range(1, free_labels.Length() + 1): - label = free_labels.Value(i) - scaled = BRepBuilderAPI_Transform(shape_tool.GetShape_s(label), trsf, True).Shape() - shape_tool.SetShape(label, scaled) -``` -**Für künftige Projekte**: Immer OCP-Methoden-Verfügbarkeit mit `hasattr()` oder `dir()` testen bevor man sie aufruft. - -### 2026-03-07 | OCP | `XCAFDoc_ShapeTool.GetComponents` → `GetComponents_s` (static method suffix) -**Problem**: `shape_tool.GetComponents(label, seq)` → `AttributeError: 'XCAFDoc_ShapeTool' has no attribute 'GetComponents'` -**Lösung**: In OCP sind alle XCAF static-Methoden mit `_s`-Suffix: `XCAFDoc_ShapeTool.GetComponents_s(label, seq)`. Gilt für alle `XCAFDoc_*`-Klassen. - -### 2026-03-07 | Pipeline | OCC-native STEP→GLB ersetzt STL-Intermediary komplett -**Problem**: Pipeline nutzte STL als Zwischenformat (STEP→STL via cadquery, STL→Blender). STL verliert Materialnamen, Farben, OCC-Topologie. Zwei parallele Konvertierungen (cadquery STL + Blender Import) = doppelter Aufwand. STL-Cache auf Disk = Dateiflut. -**Lösung**: `export_step_to_gltf.py` nutzt OCC `RWGltf_CafWriter` direkt: STEP→GLB in einem Schritt. Koordinatensystem-Konvertierung (Z-up→Y-up) + mm→m Skalierung übernimmt `conv.SetInputLengthUnit(1e-3)`. Blender importiert GLB nativ via `bpy.ops.import_scene.gltf()` — kein `_scale_mm_to_m` mehr nötig. -**OCC-API-Gotcha**: Projekt nutzt `OCP` (cadquery's pythonocc-Bindings), NICHT `OCC.Core`. Statische Methoden haben `_s`-Suffix: `XCAFApp_Application.GetApplication_s()`, `XCAFDoc_DocumentTool.ShapeTool_s()`. -**Für künftige Projekte**: `RWGltf_CafWriter` + `STEPCAFControl_Reader` sind die kanonische STEP→GLB Pipeline. `BRepMesh_IncrementalMesh` tesselliert vor dem Export. `XCAFDoc_ColorTool.SetColor(label, color, XCAFDoc_ColorSurf)` überträgt Farben in GLB-Materialien. - -### 2026-03-07 | Celery | generate_gltf_geometry_task als Subprocess — kein bpy-Import-Konflikt -**Problem**: OCP und bpy können nicht im selben Python-Prozess koexistieren — OCP lädt native C++-Bibliotheken die mit Blenders internen Versionen kollidieren. Der render-worker-Container hat BEIDE (cadquery + Blender). -**Lösung**: `export_step_to_gltf.py` via `sys.executable` subprocess aus Celery-Task heraus starten. So läuft OCC in isoliertem Prozess, kein Import-Pollution. Pattern: `subprocess.run([sys.executable, script, "--arg", val], timeout=120)`. - -### 2026-03-07 | Bbox | GLB statt STL für Bounding-Box Extraktion -**Problem**: `_bbox_from_stl()` nutzte numpy binary parsing des STL-Headers (schnell, aber STL existiert nicht mehr nach Pipeline-Umbau). -**Lösung**: `_bbox_from_glb()` mit trimesh: `scene = trimesh.load(glb_path, force="scene"); bounds = scene.bounds`. GLB ist in Metern → `* 1000` für mm. Fallback auf `_bbox_from_step_cadquery()` bleibt erhalten. - -### 2026-03-07 | Workflow | Turntable-Workflow brauchte step_path zur Laufzeit -**Problem**: `WorkflowDefinition.config` ist statisch (JSON) — enthält keine produktspezifischen Pfade. `_build_turntable()` erwartet `step_path` + `output_dir` in params → `ValueError` bei Workflow-Dispatch. -**Lösung**: `dispatch_render_with_workflow()` löst `step_path` + `output_dir` aus dem `OrderLine → Product → CadFile` Graph auf und injiziert sie in params vor `dispatch_workflow()`. -**Für künftige Projekte**: Workflow-Configs müssen zwischen statischen Parametern (engine, samples) und laufzeit-abhängigen (Dateipfade, IDs) unterscheiden. Letztere immer im Dispatch-Service auflösen. - -### 2026-03-06 | Docker | `COPY --from=docker-cli cli-plugins` schlägt fehl wenn Pfad nicht existiert -**Problem**: `docker:cli` Image hat `/usr/local/bin/docker` aber KEIN `/usr/local/lib/docker/cli-plugins` Verzeichnis — `COPY --from` bricht ab. -**Lösung**: Nur `/usr/local/bin/docker` kopieren. Compose-Plugin wird über `docker compose` (space, nicht `-`) aufgerufen — das Binary enthält compose bereits bei neueren docker:cli Images. - -### 2026-03-06 | OCC | Dihedralwinkel für sharp-edge Extraktion aus STEP -**Problem**: STEP-Dateien enthalten B-Rep Topologie, STL verliert Kantendaten. Blender braucht Winkelinformation für mark_sharp / UV-Seams. -**Lösung**: `topexp.MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, edge_face_map)` liefert alle Face-Paare pro Edge. `BRepAdaptor_Surface.DN()` berechnet Flächennormalen. Medianwinkel der Hartkanten → `suggested_smooth_angle`. Midpunkte in `sharp_edge_midpoints` für KD-Tree-Matching in Blender. -**Wichtig**: Nur im render-worker Container verfügbar (OCC + cadquery). Backend-Container gibt gracefully `{}` zurück. - -### 2026-03-06 | Blender | UV-Seams aus sharp edges ableiten -**Problem**: Ohne korrekte UV-Seams wird Texturmapping auf Lagerteilen fehlerhaft. -**Lösung**: Nach `edges_select_sharp(sharpness=radians(angle))` → `mark_sharp()` → `mark_seam(clear=False)`. Optional: bmesh KD-Tree für OCC-Midpoints (Toleranz 0.5mm, vor scale(0.001)). Aufruf nach jedem _apply_smooth() in Mode A + B. - -### 2026-03-06 | Workflow-Dispatch | dispatch_render_with_workflow als Drop-in für dispatch_order_line_render -**Problem**: Legacy-Code rief `dispatch_order_line_render.delay()` direkt auf. Neue Workflow-Canvas-Engine wurde nie aktiviert. -**Lösung**: `dispatch_render_with_workflow(order_line_id)` ist synchron (Celery-safe), lädt OutputType.workflow_definition_id, nutzt Canvas wenn gesetzt, fällt sonst auf Legacy zurück. In `dispatch_renders()` als Drop-in-Replacement mit try/except-Fallback. - -### 2026-03-06 | Celery | `@shared_task` verbindet sich mit localhost statt Redis-Container -**Problem**: Neuer Celery-Task in `app/domains/materials/tasks.py` mit `@shared_task` (aus `celery`) statt `@celery_app.task` — beim Aufruf via FastAPI-Endpoint kam `kombu.exceptions.OperationalError: [Errno 111] Connection refused` weil `@shared_task` keinen expliziten App-Kontext hat und daher den Default-Broker `localhost:6379` nutzt, nicht `redis://redis:6379/0`. -**Lösung**: Immer `from app.tasks.celery_app import celery_app` importieren und `@celery_app.task(...)` nutzen. `@shared_task` nur verwenden wenn der Modul garantiert nach `celery_app.py` geladen wird (was in Domain-Modulen nicht der Fall ist). - -### 2026-03-06 | SQLAlchemy | Relationship-Auflösung schlägt fehl wenn Models nicht alle importiert sind -**Problem**: Celery-Task importierte nur `AssetLibrary`, aber `Material.creator` hat eine String-Relationship zu `"User"`. SQLAlchemy kann den String-Verweis nur auflösen wenn `User` bereits im Mapper registriert ist → `InvalidRequestError: 'User' failed to locate a name`. -**Lösung**: `import app.models # noqa: F401` vor dem ersten DB-Zugriff in Celery-Tasks einfügen. Das `__init__.py` importiert alle 14 Modelle und registriert sie alle im SQLAlchemy-Mapper. - -### 2026-03-06 | MinIO / Storage | `storage.upload()` erwartet `Path`, nicht `str` -**Problem**: `store_stl_cache(step_hash, quality, stl_path: str)` übergab einen `str` an `storage.upload(local_path, key)`. Die Implementierung nutzt intern `local_path.name` — `str` hat kein `.name`-Attribut → `AttributeError: 'str' object has no attribute 'name'`. STL-Dateien wurden lokal gespeichert, aber nie in MinIO gecacht. -**Lösung**: `storage.upload(Path(stl_path), key)` — immer `Path`-Objekt übergeben. Generell: alle `storage.upload()`-Aufrufe mit explizitem `Path()`-Cast absichern. - -### 2026-03-06 | Blender / Scripts | `catalog_assets.py` Pfad in Docker falsch -**Problem**: Script-Pfad via `Path(__file__).parent...` aufgelöst — in Docker zeigt `__file__` auf den Python-Pfad im `backend`-Container, nicht im `render-worker`. Der render-worker kopiert Scripts nach `/render-scripts/` (via `COPY render-worker/scripts/ /render-scripts/`). -**Lösung**: `RENDER_SCRIPTS_DIR` Env-Var nutzen: `Path(os.environ.get("RENDER_SCRIPTS_DIR", "/render-scripts")) / "catalog_assets.py"` — identisch zur Konvention in `domains/rendering/tasks.py`. - ### 2026-01-15 | Architektur | Backend-Port-Konflikt -**Problem:** FastAPI standardmäßig auf Port 8000 — war auf dem Entwicklungsrechner belegt -**Lösung:** Port 8888 in `docker-compose.yml` und Vite-Proxy konfiguriert -**Für künftige Projekte:** Port früh festlegen und in CLAUDE.md dokumentieren - ---- +Port 8000 war belegt → Port 8888 in `docker-compose.yml` + Vite-Proxy. Früh festlegen und in CLAUDE.md dokumentieren. ### 2026-01-20 | Datenbank | SQLAlchemy trackt key-value-Store-Mutations nicht -**Problem:** Admin-Einstellungen (`system_settings`) wurden via ORM gespeichert, Änderungen wurden nicht persistiert -**Ursache:** SQLAlchemy erkennt keine Mutation an einem bereits geladenen Objekt wenn nur ein Value-Feld geändert wird -**Lösung:** Direktes SQL `UPDATE` via `op.execute()` statt ORM-Mutation in `admin.py` -**Für künftige Projekte:** Key-Value-Stores immer mit direktem SQL oder `session.execute(update(...))` verwalten - ---- +Admin-Settings via ORM gespeichert → Änderungen nicht persistiert. SQLAlchemy erkennt keine Mutation an geladenen Value-Feldern. +**Lösung:** Direktes `session.execute(update(...))` statt ORM-Mutation. Key-Value-Stores immer mit direktem SQL verwalten. ### 2026-01-25 | Render-Pipeline | Blender ignoriert STEP-Einheiten (mm vs. m) -**Problem:** STEP-Dateien sind in Millimetern, Blender arbeitet intern in Metern → 50mm-Lager erscheint 50 Meter breit, Kamera framt falsch -**Lösung:** `_scale_mm_to_m(parts)` Helper in allen 3 Render-Scripts: `part.scale = (0.001, 0.001, 0.001)`, Transform anwenden -**Betroffene Dateien:** `blender_render.py`, `still_render.py`, `turntable_render.py` -**Für künftige Projekte:** Einheiten-Konvertierung direkt nach STL-Import, vor jeder Kamera-Kalkulation - ---- +STEP in mm, Blender in m → 50mm-Lager erscheint 50m breit. `_scale_mm_to_m(parts)` in allen 3 Render-Scripts: `part.scale = (0.001, 0.001, 0.001)` + Transform anwenden, direkt nach STL-Import vor Kamera-Kalkulation. ### 2026-01-28 | Render-Pipeline | Blender 5.0 hat `scene.node_tree` entfernt -**Problem:** `_setup_bg_compositor()` rief `scene.node_tree` auf (in Blender 5.0 entfernt) → Python-Exception → Blender exitete mit Code 0 → Flamenco markierte Task fälschlicherweise als "completed" -**Lösung:** `_setup_bg_compositor()` aus Setup + Render-Script entfernt; bg_color-Kompositing in FFmpeg verschoben (`-f lavfi -i color=...` + overlay-Filter) -**Wichtig:** Immer `try: main() except SystemExit: raise except Exception: traceback; sys.exit(1)` in Blender-Scripts — sonst verschluckt Blender Python-Exceptions -**Für künftige Projekte:** Nach Blender-Major-Updates alle API-Calls prüfen; Exception-Guard ist Pflicht - ---- +`_setup_bg_compositor()` → Python-Exception → Blender exit code 0 → Task fälschlicherweise "completed". +**Lösung:** Compositing entfernt; bg_color via FFmpeg (`-f lavfi -i color=...` + overlay). Pflicht: `try: main() except SystemExit: raise except Exception: traceback; sys.exit(1)` in allen Blender-Scripts. ### 2026-02-05 | Material-System | Material-Alias-Lookup-Reihenfolge falsch -**Problem:** `Steel--Stahl` war sowohl ein kanonischer `Material.name` als auch ein Alias für `SCHAEFFLER_010101_Steel-Bare`. Der Lookup prüfte zuerst den exakten Namen und fand `Steel--Stahl` — Blender konnte diesen Namen aber nicht in der Library finden -**Lösung:** Lookup-Reihenfolge in `material_service.py` umgekehrt: **Aliases zuerst**, dann exakter Name, dann Pass-through -**Für künftige Projekte:** Alias-System immer so designen dass Aliases Vorrang haben; nie zwei Lookup-Pfade mit überlappenden Treffern +`Steel--Stahl` war sowohl `Material.name` als auch Alias für `SCHAEFFLER_010101_Steel-Bare`. Lookup fand zuerst den Namen → Blender konnte ihn nicht in der Library finden. +**Lösung:** `material_service.py`: **Aliases zuerst**, dann exakter Name, dann Pass-through. ---- - -### 2026-02-10 | Render-Pipeline | Blender-Template zerstört HDRI/World -**Problem:** Im Template-Modus (Mode B) wurden trotzdem Auto-Lights und eine neue World erstellt → überschrieb den HDRI aus dem .blend-Template → falsche Beleuchtung -**Ursache:** Auto-Licht- und World-Setup-Code lief bedingungslos, nicht nur im Mode A -**Lösung:** In Template-Mode werden Lights, World und Color-Management-Override vollständig übersprungen; nur die Kamera wird ggf. neu berechnet -**Betroffene Dateien:** `still_render.py`, `turntable_render.py`, `schaeffler-still.js`, `schaeffler-turntable.js` - ---- +### 2026-02-10 | Render-Pipeline | Blender-Template überschreibt HDRI/World +Im Template-Modus (Mode B) lief Auto-Licht/World-Setup bedingungslos → überschrieb HDRI aus .blend-Template. +**Lösung:** In Template-Mode Lights, World und Color-Management-Override vollständig überspringen; nur Kamera ggf. neu berechnen. ### 2026-02-15 | Celery | Blender-Queue-Flooding durch falsche Concurrency -**Problem:** Alle Celery-Tasks (schnelle Metadata-Extraktion + langsamer Blender-Render) liefen auf `step_processing` mit concurrency=8 → 8 Workers schickten gleichzeitig Requests an blender-renderer (der nur 1 gleichzeitig verarbeiten kann) → 7 davon liefen in 300s-Timeout → blockierte die gesamte Queue -**Lösung:** Pipeline aufgeteilt: -- `process_step_file` (step_processing, concurrency=8): nur schnelle Metadata-Extraktion (<2s), queut dann → -- `render_step_thumbnail` (thumbnail_rendering, concurrency=1): Blender-Call, niemals timeout -**Neuer Service:** `worker-thumbnail` in `docker-compose.yml` mit `--concurrency=1` -**Für künftige Projekte:** HTTP-Services die nur 1 Request gleichzeitig verarbeiten können IMMER auf einer separaten Queue mit concurrency=1 laufen lassen - ---- +Alle Tasks auf `step_processing` (concurrency=8) → 8 Workers gleichzeitig an blender-renderer (max 1) → 7× Timeout. +**Lösung:** `process_step_file` (step_processing, concurrency=8) nur schnelle Metadata; `render_step_thumbnail` (thumbnail_rendering, concurrency=1) für Blender. HTTP-Services mit max 1 Request immer auf eigener Queue mit concurrency=1. ### 2026-02-18 | Frontend | Tailwind CSS-Variablen inkompatibel mit opacity-Syntax -**Problem:** `bg-surface/50` oder `bg-surface` (wenn `--color-bg-surface` ein Hex-Wert ist) generiert `rgb(var(--color-bg-surface) / 0.5)` — invalides CSS, weil `rgb()` keine Hex-Werte als Channel-Input akzeptiert → Hintergrund transparent -**Ursache:** Tailwind erwartet CSS-Variablen mit RGB-Channel-Format (`255 255 255`), nicht Hex (`#ffffff`) -**Lösung:** Inline-Style verwenden: `style={{ backgroundColor: 'var(--color-bg-surface)' }}` -**Für künftige Projekte:** Entweder CSS-Variablen im RGB-Channel-Format definieren, oder konsequent inline styles für variable Farben +`bg-surface/50` erzeugt `rgb(var(--color-bg-surface) / 0.5)` — invalides CSS wenn Variable ein Hex-Wert ist. +**Lösung:** `style={{ backgroundColor: 'var(--color-bg-surface)' }}`. Alternativ: CSS-Vars im RGB-Channel-Format definieren (`255 255 255`). ---- - -### 2026-02-20 | STL-Cache | Three.js-Renderer nutzte tempfile → kein Download möglich -**Problem:** Three.js-Renderer konvertierte STEP→STL in ein tempfile und löschte es anschließend → STL-Download-Endpoint fand keine Datei -**Ursache:** Three.js war ursprünglich nur für Thumbnails gebaut, STL-Cache-Konvention (`{stem}_low.stl` neben STEP-Datei) wurde nicht implementiert -**Lösung:** Persistent cache path: `step_path.parent / f"{step_path.stem}_low.stl"`, cache-hit-check vor Konvertierung, kein `unlink()` mehr -**Für künftige Projekte:** STL-Cache-Konvention (`{step_stem}_{quality}.stl` neben STEP-Datei) von Anfang an in allen Renderer-Services einhalten - ---- +### 2026-02-20 | STL-Cache | Three.js-Renderer nutzte tempfile → kein Download +Three.js schrieb STL in tempfile und löschte es → Download-Endpoint fand nichts. +**Lösung:** Persistent cache: `step_path.parent / f"{step_path.stem}_low.stl"`, Cache-Hit-Check vor Konvertierung, kein `unlink()`. Konvention `{stem}_{quality}.stl` neben STEP-Datei von Anfang an in allen Renderern. ### 2026-02-20 | STL-Cache | blender-renderer fehlte /convert-stl Endpoint -**Problem:** Für Produkte die mit Blender gerendert wurden war kein STL-Cache vorhanden wenn nicht explizit gerendert wurde (blender-renderer renderte + konvertierte in einem Schritt, aber STL wurde nicht persistiert) -**Lösung:** Neuer `/convert-stl` Endpoint in `blender-renderer/app.py`: konvertiert STEP→STL ohne Render, persistiert Cache. Neuer Celery-Task `generate_stl_cache` auf `thumbnail_rendering`-Queue. Admin-Funktion "Generate Missing STLs" zum Batch-Nachfüllen - ---- +Blender renderte + konvertierte in einem Schritt, persistierte STL nicht. +**Lösung:** Neuer `/convert-stl` Endpoint (STEP→STL ohne Render), Celery-Task `generate_stl_cache` auf `thumbnail_rendering`, Admin-Batch-Funktion "Generate Missing STLs". ### 2026-02-22 | Material-System | Fehlender Alias blockiert Material-Replacement -**Problem:** Produkt F-803422.01.TR2 (SA-2026-00080) renderte ohne Materialersetzung. Material "Stahl v2" war korrekt in der UI gespeichert, aber weder in `materials` noch in `material_aliases` vorhanden -**Ursache:** Alias-Seeding aus Excel deckte nicht alle Varianten der deutschen Materialbezeichnungen ab -**Lösung:** Alias direkt in DB eingetragen: `"Stahl v2"` → `SCHAEFFLER_010101_Steel-Bare` -**Für künftige Projekte:** Bei Render ohne Materialersetzung immer zuerst `resolve_material_map()` debuggen und Alias-Tabelle prüfen; Alias-Seeding regelmäßig mit neuen Excel-Varianten erweitern - ---- +`"Stahl v2"` in DB nicht in materials noch in material_aliases → keine Ersetzung, Silent-Fail. +**Lösung:** Alias direkt in DB eintragen. Bei Render ohne Materialersetzung immer zuerst `resolve_material_map()` debuggen, Alias-Tabelle prüfen. ### 2026-02-25 | Frontend | canDispatch-Bedingung zu restriktiv -**Problem:** "Dispatch Renders"-Button war nicht sichtbar obwohl der Auftrag offene Render-Zeilen hatte -**Ursache:** `canDispatch` enthielt `&& hasRetryable` — Button erschien nur wenn pending/failed/cancelled-Zeilen vorhanden waren, nicht wenn alle Zeilen "pending" im Erstauftrag -**Lösung:** `hasRetryable`-Bedingung entfernt; Button ist immer sichtbar wenn Auftrag im richtigen Status und User privilegiert ist -**Für künftige Projekte:** Aktions-Buttons nicht zu stark von abgeleiteten Zuständen abhängig machen; lieber im Backend validieren - ---- +`canDispatch` enthielt `&& hasRetryable` → Button fehlte wenn alle Zeilen initial "pending". +**Lösung:** `hasRetryable` entfernt; Validierung im Backend. Aktions-Buttons nicht zu stark von abgeleiteten Zuständen abhängig machen. ### 2026-02-28 | Frontend | MaterialInput-Dropdown ohne Hintergrund -**Problem:** Dropdown der Material-Suchfeld-Komponente erschien transparent — Text über dem Hintergrund kaum lesbar -**Ursache:** `bg-surface` Tailwind-Klasse + CSS-Variable mit Hex-Wert (siehe Learning 2026-02-18) -**Lösung:** `style={{ backgroundColor: 'var(--color-bg-surface)' }}` für Dropdown-Container, Group-Header und Sticky-Button -**Datei:** `frontend/src/components/shared/MaterialInput.tsx` - ---- +`bg-surface` mit CSS-Hex-Variable → transparenter Dropdown (siehe 2026-02-18). +**Lösung:** `style={{ backgroundColor: 'var(--color-bg-surface)' }}` für Dropdown-Container, Group-Header, Sticky-Button. Datei: `frontend/src/components/shared/MaterialInput.tsx`. ### 2026-03-06 | Refactor | .gitignore `core` trifft Verzeichnisse -**Problem:** `.gitignore` enthielt `core` als Regel (für core dump files) — Git ignorierte damit auch `backend/app/core/` Verzeichnis -**Lösung:** Regel zu `/core` umbenannt (Root-relative Regel trifft nur `/core` Datei, nicht verschachtelte `core/`-Verzeichnisse) -**Für künftige Projekte:** Immer Root-relative Pfade (`/core`) für Dateien im Root-Verzeichnis nutzen - ---- +`core` in `.gitignore` ignorierte `backend/app/core/` Verzeichnis. +**Lösung:** `/core` (root-relativ) — trifft nur Datei im Root, nicht verschachtelte Verzeichnisse. ### 2026-03-06 | Architektur | Blender-HTTP-Service vs. direkter Subprocess -**Problem:** `blender-renderer` als Flask/FastAPI HTTP-Microservice war ein Single-Point-of-Failure (max. 1 concurrent Request), kein Scaling möglich, HTTP-Overhead bei jedem Render -**Lösung:** Render-Worker als Celery-Container (`render-worker/`) — Blender direkt via `subprocess.run` ohne HTTP. `is_blender_available()` prüft `BLENDER_BIN` env var für Kontext-Detection -**Wichtig:** `step_processor.py` erkennt über `BLENDER_BIN`-Env ob Blender im aktuellen Container verfügbar ist — Backend-Container fallen auf Pillow zurück -**Für künftige Projekte:** Subprocess-basierter Renderer > HTTP-Microservice für blocking compute tasks - ---- +Flask-HTTP-Microservice = Single-Point-of-Failure, kein Scaling, HTTP-Overhead. +**Lösung:** Celery render-worker Container — Blender direkt via `subprocess.run`. `is_blender_available()` prüft `BLENDER_BIN` env var; Backend-Container fallen auf Pillow zurück. Subprocess > HTTP für blocking compute tasks. ### 2026-03-06 | Refactor | Bash CWD-Problem durch Hook-Pfad-Auflösung -**Problem:** Nach `cd frontend && npm test` in einem Bash-Tool-Call blieb CWD dauerhaft in `frontend/`. Der Pre-Tool-Use-Hook `python3 .claude/hooks/pre_tool_use.py` wurde dann relativ zu `frontend/` aufgelöst → Datei nicht gefunden → alle Tool-Calls blockiert -**Lösung:** Symlink `frontend/.claude → .claude` erstellt: `ln -sf $(pwd)/.claude frontend/.claude` -**Für künftige Projekte:** Hooks nie mit relativen Pfaden konfigurieren; absoluten Pfad im Hook-Command verwenden. Außerdem: `cd` immer in separate Bash-Calls oder mit `&&` am Ende der eigentlichen Command-Chain - ---- +Nach `cd frontend` im Bash-Tool blieb CWD in `frontend/` → Hook-Pfad nicht gefunden → alle Tool-Calls blockiert. +**Lösung:** Symlink `frontend/.claude → .claude`. Hooks mit absoluten Pfaden konfigurieren; `cd` nur mit `&&` am Ende der Command-Chain. ### 2026-03-06 | Multi-Tenancy | PostgreSQL RLS mit current_setting und Null-Safety -**Problem:** `current_setting('app.current_tenant_id')` wirft Exception wenn Variable nicht gesetzt → alle Queries schlagen fehl wenn kein Tenant-Context gesetzt ist -**Lösung:** `current_setting('app.current_tenant_id', true)` — zweites Argument `true` macht die Funktion Null-safe: gibt NULL statt Exception zurück wenn Setting nicht gesetzt -**Admin-Bypass-Pattern:** Separates `CREATE POLICY admin_bypass ... USING (current_setting(...) = 'bypass')` — setzt `app.current_tenant_id = 'bypass'` für Admin-Cross-Tenant-Queries -**Für künftige Projekte:** IMMER das zweite `true`-Argument verwenden; Policies immer testen mit (a) gesetztem Tenant, (b) nicht gesetztem Setting, (c) Admin-Bypass - ---- +`current_setting('app.current_tenant_id')` wirft Exception wenn nicht gesetzt. +**Lösung:** `current_setting('app.current_tenant_id', true)` — zweites Argument macht Funktion Null-safe. Admin-Bypass: separate Policy mit `SET LOCAL app.current_tenant_id = 'bypass'`. ### 2026-03-06 | Refactor | Domain-Driven Migration: Compat-Shims statt Big-Bang -**Problem:** Vollständige Migration aller Models/Services/Router in neue Domain-Struktur in einem Schritt → alle bestehenden Imports brechen -**Lösung:** Compat-Shims-Ansatz: alte Dateien (`app/models/user.py` etc.) werden zu Re-Export-Wrappern die aus den neuen Domain-Locations importieren. So funktionieren alle bestehenden Imports weiter während die kanonische Location die neue Domain ist -**Pattern:** -```python -# app/models/user.py (Compat-Shim) -from app.domains.auth.models import User -__all__ = ["User"] -``` -**Für künftige Projekte:** Immer Compat-Shims anlegen vor dem Verschieben; erst nach vollständiger Migration aller Imports die Shims entfernen - ---- +Vollständige Migration in einem Schritt bricht alle Imports. +**Lösung:** Alte Dateien werden Re-Export-Shims: `from app.domains.auth.models import User; __all__ = ["User"]`. Erst nach vollständiger Import-Migration Shims entfernen. ### 2026-03-06 | Workflow-System | Celery Canvas vs. Custom Workflow-Engine -**Problem:** Custom Workflow-Engine (Graph-Traversal, Dependency-Resolution, Retry-Logic) war zu komplex (~2-3 Wochen Eigenentwicklung) -**Lösung:** Celery Canvas als Execution-Engine (`chain`, `group`, `chord`). `dispatch_workflow(type, order_line_id, params)` baut den Canvas dynamisch aus Config-Typ. Backward-Compat: wenn kein `workflow_definition_id` → alter direkter Task-Call -**Seeded Workflows:** 3 Standard-Definitionen beim Migration-Upgrade direkt in DB geSEEDed (Still, Turntable, Multi-Angle) -**Für künftige Projekte:** Celery Canvas ist ausreichend für parallele/sequentielle Workflow-Execution; keine eigene Workflow-Engine bauen +Custom Graph-Traversal-Engine = 2-3 Wochen Eigenentwicklung. +**Lösung:** Celery Canvas (`chain`, `group`, `chord`) als Execution-Engine. `dispatch_workflow(type, order_line_id, params)` baut Canvas dynamisch aus Config. Kein Backward-Compat nötig: Celery Canvas reicht für parallele/sequentielle Workflows. ---- - -### 2026-03-06 | Circular Import | template_service ↔ domains/rendering/service — Render nie ausgeführt -**Problem:** `app.services.template_service` war ein Shim der `app.domains.rendering.service` importiert. `app.domains.rendering.service` importierte wiederum `app.services.template_service` → zirkulärer Import → `resolve_template` konnte nie geladen werden → jeder Render schlug fehl mit "cannot import name 'resolve_template' from partially initialized module". -**Ursache:** B1-Refactor hat beide Module zu Shims gemacht die aufeinander zeigen. Die eigentliche Implementierung wurde nicht in die neue Domäne übertragen. -**Lösung:** `template_service.py` mit der Originalimplementierung aus dem git-Log wiederhergestellt (sync SQLAlchemy, Celery-sicher, 4-stufige Cascade). `domains/rendering/service.py` importiert jetzt korrekt aus `template_service` ohne Rückimport. -**Für künftige Projekte:** Nach Refactoring immer prüfen ob Shims auf die echte Implementierung zeigen oder wieder auf andere Shims. `grep -rn "def resolve_template"` vor dem Commit muss mindestens 1 Treffer liefern. - ---- +### 2026-03-06 | Circular Import | template_service ↔ domains/rendering/service +Beide Module waren Shims die aufeinander zeigten → `resolve_template()` nie aufrufbar → alle Renders crashten mit ImportError. +**Lösung:** `template_service.py` aus git-History wiederhergestellt (echte Implementierung). `domains/rendering/service.py` importiert nur davon. Nach Refactoring: `grep -rn "def resolve_template"` muss ≥1 Treffer liefern. ### 2026-03-06 | Multi-Tenancy | audit_log.tenant_id NOT NULL blockiert alle Notifications -**Problem:** Migration 036 machte `audit_log.tenant_id NOT NULL`, aber `emit_notification` setzt kein `tenant_id`. Die Notification-Insert schlug fehl → rollback → nachfolgende Session-Zugriffe schlugen fehl → Order-Submit gab 500 zurück. -**Lösung:** `audit_log.tenant_id` via `ALTER TABLE audit_log ALTER COLUMN tenant_id DROP NOT NULL` nullable gemacht. Broadcast-Notifications (system-weit, kein konkreter Tenant) DÜRFEN NULL tenant_id haben. -**Für künftige Projekte:** Audit-Logs die als Broadcast an alle Tenants gehen benötigen nullable tenant_id. Nie NOT NULL auf Tabellen setzen die auch System-Events speichern. - ---- +Migration 036 machte `audit_log.tenant_id NOT NULL`, aber `emit_notification` setzt kein `tenant_id` → INSERT schlägt fehl → 500 bei Order-Submit. +**Lösung:** `ALTER TABLE audit_log ALTER COLUMN tenant_id DROP NOT NULL`. Broadcast-Notifications (system-weit) dürfen NULL tenant_id haben. ### 2026-03-06 | Frontend | GET /api/tenants gibt 307 Redirect zurück -**Problem:** FastAPI router registriert `/tenants/` (mit trailing slash). `GET /tenants` → 307 Redirect zu `/tenants/`. Axios folgt dem Redirect aber verliert den Authorization-Header → 401 → leere Tenant-Liste im Frontend. -**Lösung:** `getTenants()` in `api/tenants.ts` auf `/tenants/` (mit trailing slash) geändert. -**Für künftige Projekte:** FastAPI APIRouter mit `prefix="/tenants"` und `@router.get("")` erzeugt `/tenants` (kein Slash). Mit `@router.get("/")` erzeugt `/tenants/`. Axios folgt 307 nicht mit Auth-Header. Immer trailing slash im Frontend verwenden wenn Router mit Slash registriert. +FastAPI registriert `/tenants/` (trailing slash) → `GET /tenants` → 307. Axios folgt, verliert Authorization-Header → 401. +**Lösung:** `getTenants()` auf `/tenants/` (mit trailing slash). Generell: FastAPI 307-Redirects verlieren Auth-Header in Axios. Immer trailing slash im Frontend verwenden. ---- - -### 2026-03-06 | Celery Canvas | workflow_builder.py: order_line_id als step_path übergeben crasht Blender -**Problem:** `_build_still` übergab `order_line_id` als ersten Positional-Arg an `render_still_task.si(order_line_id, **params)` — aber `render_still_task` erwartet `step_path: str` als ersten Arg. Blender versuchte die UUID als Pfad zu öffnen → crash. -**Lösung:** Neue `render_order_line_still_task` die intern die DB-Abfrage macht (OrderLine → Product → CadFile → stored_path). `workflow_builder._build_still` nutzt jetzt diese neue Task. -**Für künftige Projekte:** Workflow-Builder-Tasks dürfen nie Domain-IDs als file-path-basierte Task-Argumente verwenden. Immer separate order-line-aware Tasks erstellen die die Auflösung intern durchführen. +### 2026-03-06 | Celery Canvas | workflow_builder: order_line_id als step_path übergeben +`render_still_task.si(order_line_id, **params)` — Task erwartet `step_path: str`. Blender versuchte UUID als Pfad → crash. +**Lösung:** Separate `render_order_line_still_task` die intern OrderLine→Product→CadFile auflöst. Workflow-Builder-Tasks dürfen nie Domain-IDs als file-path-Argumente verwenden. ### 2026-03-06 | Docker | docker compose in Container braucht multi-stage CLI-Copy -**Problem:** Backend-Container basiert auf `python:3.11-slim` — kein `docker` binary, kein `docker compose`. Worker-Scale-Endpoint kann `docker compose up --scale` nicht aufrufen. -**Lösung:** Multi-Stage Dockerfile: `COPY --from=docker:cli /usr/local/bin/docker /usr/local/bin/docker` + `COPY --from=docker-cli /usr/local/lib/docker/cli-plugins /usr/local/lib/docker/cli-plugins`. Außerdem: Docker-Socket mounten (`/var/run/docker.sock`) + Compose-File als Volume (`./:/compose:ro`) + `COMPOSE_PROJECT_DIR=/compose` env var. -**Für künftige Projekte:** Multi-Stage-Builds sind die sauberste Methode um Binaries aus anderen Images zu kopieren ohne die ganze Dependency-Chain zu installieren. +`python:3.11-slim` hat kein `docker` binary. +**Lösung:** `COPY --from=docker:cli /usr/local/bin/docker /usr/local/bin/docker` + cli-plugins. Docker-Socket mounten (`/var/run/docker.sock`), Compose-File als Volume, `COMPOSE_PROJECT_DIR` env var. + +### 2026-03-06 | Docker | `COPY --from=docker-cli cli-plugins` schlägt fehl +`docker:cli` Image hat kein `/usr/local/lib/docker/cli-plugins` Verzeichnis. +**Lösung:** Nur `/usr/local/bin/docker` kopieren — neuere docker:cli Images enthalten `compose` bereits im Binary. + +### 2026-03-06 | Docker | apt-Paketname libgdk-pixbuf2.0-0 vs libgdk-pixbuf-2.0-0 +Auf Debian bookworm heißt das Paket `libgdk-pixbuf-2.0-0` (mit Bindestrichen). `apt-get install` mit falschem Namen → exit 100. +**Regel:** Immer `apt-cache search libgdk` im Container prüfen. ### 2026-03-06 | React Three Fiber | Wireframe-Toggle über Material-Clone -**Problem:** Drei.js-Materialien sind shared objects — direkte Mutation von `child.material.wireframe = true` auf einem geparstem GLTF-Scene würde alle Instanzen dieses Materials beeinflussen. -**Lösung:** `child.material = child.material.clone()` vor der Wireframe-Mutation in `useEffect`. So bekommt jede Mesh-Instanz ihr eigenes Material-Objekt und der Toggle hat keinen unerwünschten Side-Effect. -**Für künftige Projekte:** GLTF-Materialien bei Runtime-Modifikationen immer zuerst clonen. +GLTF-Materialien sind shared objects — direkte Mutation von `child.material.wireframe` beeinflusst alle Instanzen. +**Lösung:** `child.material = child.material.clone()` vor Wireframe-Mutation. GLTF-Materialien bei Runtime-Modifikationen immer zuerst clonen. -### 2026-03-06 | pytest | Backend ohne dev-Dependencies: pip install -e ".[dev]" nötig -**Problem:** Backend-Dockerfile installiertete nur `pip install -e .` — keine dev-Dependencies → pytest/pytest-asyncio/httpx nicht verfügbar → `python -m pytest` schlägt mit "No module named pytest" fehl. -**Lösung:** Dockerfile geändert auf `pip install -e ".[dev]"`. Dev-Dependencies in `pyproject.toml [project.optional-dependencies] dev = [pytest>=8.0, ...]` waren bereits definiert, nur der Install-Befehl war unvollständig. -**Für künftige Projekte:** Immer prüfen ob `[dev]` extras installiert sind wenn Tests im Container laufen sollen. +### 2026-03-06 | pytest | Backend ohne dev-Dependencies +`pip install -e .` ohne `[dev]` → kein pytest → `ModuleNotFoundError`. +**Lösung:** `pip install -e ".[dev]"`. Dev-Extras immer im Dockerfile angeben wenn Tests im Container laufen sollen. + +### 2026-03-06 | Celery | `@shared_task` verbindet sich mit localhost statt Redis-Container +`@shared_task` aus `celery` ohne App-Kontext → Default-Broker `localhost:6379` → `kombu.exceptions.OperationalError`. +**Lösung:** Immer `from app.tasks.celery_app import celery_app` + `@celery_app.task(...)`. `@shared_task` nur wenn Modul garantiert nach `celery_app.py` geladen wird. + +### 2026-03-06 | SQLAlchemy | Relationship-Auflösung schlägt fehl wenn Models nicht importiert +Celery-Task importierte nur `AssetLibrary` → `Material.creator` → String-Ref `"User"` nicht im Mapper → `InvalidRequestError`. +**Lösung:** `import app.models # noqa: F401` vor erstem DB-Zugriff in Celery-Tasks — `__init__.py` registriert alle 14 Modelle. + +### 2026-03-06 | MinIO / Storage | `storage.upload()` erwartet `Path`, nicht `str` +`storage.upload(str_path, key)` → `AttributeError: 'str' has no attribute 'name'` → STLs lokal gespeichert aber nie in MinIO. +**Lösung:** `storage.upload(Path(stl_path), key)` — alle `upload()`-Aufrufe mit `Path()`-Cast absichern. + +### 2026-03-06 | Blender / Scripts | `catalog_assets.py` Pfad in Docker falsch +`Path(__file__).parent` zeigt auf Backend-Container-Pfad, nicht auf render-worker `/render-scripts/`. +**Lösung:** `RENDER_SCRIPTS_DIR` Env-Var: `Path(os.environ.get("RENDER_SCRIPTS_DIR", "/render-scripts")) / "catalog_assets.py"`. + +### 2026-03-06 | Alembic | Migration exit code 100 bei enum-Konflikt +`Enum(create_type=False)` unzuverlässig mit asyncpg. +**Lösung:** Raw SQL: `DO $$ BEGIN CREATE TYPE ...; EXCEPTION WHEN duplicate_object THEN NULL; END $$;`. Für Tabellen: `CREATE TABLE IF NOT EXISTS`. + +### 2026-03-06 | Render-Pipeline | Circular Shim blockiert alle Order-Renders +`dispatch_order_line_render` → Shim A→B→A → Render startet nie. +**Lösung:** `dispatch_order_line_render` direkt auf `render_order_line_task.delay()`. Den echten Aufruf-Pfad API→Task vor Refactoring dokumentieren. + +### 2026-03-06 | Render-Pipeline | render_order_line_task auf falschem Worker (kein Blender) +Task auf `step_processing` → `worker`-Container (kein Blender) → `is_blender_available()` = False → Pillow-Placeholder, kein Fehler. +**Lösung:** Queue auf `thumbnail_rendering` → `render-worker`-Container (Blender 5.0.1 + cadquery). Blender-Tasks IMMER auf `thumbnail_rendering`. + +### 2026-03-06 | Docker | worker-thumbnail vs render-worker — beide auf thumbnail_rendering +Zwei Services unterschiedlicher Capabilities auf gleicher Queue → round-robin → 50% Silent-Fail. +**Lösung:** `worker-thumbnail` aus docker-compose entfernt. `render-worker` ist alleiniger Consumer. Nie zwei Services mit unterschiedlichen Fähigkeiten auf dieselbe Queue. + +### 2026-03-06 | Multi-Tenancy | tenant_id NOT NULL verletzt bei Order-Erstellung +Migration 036 machte `tenant_id NOT NULL` auf orders/order_lines/order_items → alle Create-Endpoints übergaben das Feld nicht. +**Lösung:** `tenant_id=getattr(user, 'tenant_id', None)` in allen Model-Konstruktoren. Nach jeder RLS-Migration alle Create-Endpoints auf neue Pflichtfelder prüfen. + +### 2026-03-06 | Workflow-Dispatch | dispatch_render_with_workflow als Drop-in +Legacy `dispatch_order_line_render.delay()` wurde nie durch Workflow-Engine ersetzt. +**Lösung:** `dispatch_render_with_workflow(order_line_id)` lädt `OutputType.workflow_definition_id`, nutzt Canvas wenn gesetzt, fällt sonst auf Legacy zurück. + +### 2026-03-06 | OCC | `RWMesh_CoordinateSystemConverter` nicht als Python-Binding verfügbar +`writer.ChangeCoordinateSystemConverter()` → `TypeError: Unregistered type`. +**Lösung:** Shapes vor Export mit `BRepBuilderAPI_Transform(shape, trsf, True)` um Faktor 0.001 skalieren. `RWGltf_CafWriter` direkt ohne Koordinatensystem-Konverter aufrufen. + +### 2026-03-06 | OCC | `XCAFDoc_ShapeTool.GetComponents` → `GetComponents_s` +In OCP alle XCAF static-Methoden mit `_s`-Suffix. Gilt für alle `XCAFDoc_*`-Klassen. + +### 2026-03-06 | Pipeline | OCC-native STEP→GLB ersetzt STL-Intermediary +STL verliert Materialien/Farben, doppelter Aufwand, Dateiflut. +**Lösung:** `RWGltf_CafWriter` + `STEPCAFControl_Reader`: STEP→GLB direkt. `BRepMesh_IncrementalMesh` tesselliert vor Export. OCP (cadquery's Bindings, NICHT `OCC.Core`), statische Methoden mit `_s`-Suffix. + +### 2026-03-06 | Celery | generate_gltf_geometry_task als Subprocess +OCP + bpy können nicht im selben Python-Prozess koexistieren (native C++ Konflikt). +**Lösung:** `export_step_to_gltf.py` via `subprocess.run([sys.executable, script, ...], timeout=120)` — OCC läuft isoliert. + +### 2026-03-06 | OCC | Dihedralwinkel für sharp-edge Extraktion aus STEP +`topexp.MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, map)` → Face-Paare pro Edge. `BRepAdaptor_Surface.DN()` für Normalen. Medianwinkel → `suggested_smooth_angle`, Midpunkte → `sharp_edge_midpoints` für KD-Tree in Blender. Nur im render-worker verfügbar. + +### 2026-03-06 | Blender | UV-Seams aus sharp edges ableiten +Nach `edges_select_sharp(sharpness=radians(angle))` → `mark_sharp()` → `mark_seam(clear=False)`. Optional: bmesh KD-Tree für OCC-Midpoints (Toleranz 0.5mm vor scale(0.001)). + +### 2026-03-06 | Multi-Tenancy | audit_log.tenant_id NOT NULL — Broadcast-Events +`emit_notification` setzt kein `tenant_id` → INSERT fehlschlägt → 500 bei Order-Submit. +**Lösung:** `tenant_id DROP NOT NULL`. Broadcast-Notifications (kein konkreter Tenant) brauchen nullable tenant_id. + +### 2026-03-06 | Bbox | GLB statt STL für Bounding-Box Extraktion +`_bbox_from_stl()` obsolet nach Pipeline-Umbau. +**Lösung:** `_bbox_from_glb()` mit trimesh: `scene.bounds * 1000` für mm. Fallback auf `_bbox_from_step_cadquery()`. + +### 2026-03-06 | Workflow | Turntable-Workflow brauchte step_path zur Laufzeit +`WorkflowDefinition.config` ist statisch → kein `step_path` → `ValueError`. +**Lösung:** `dispatch_render_with_workflow()` löst `step_path` + `output_dir` aus `OrderLine→Product→CadFile` auf und injiziert sie vor Dispatch. Statische (engine, samples) von laufzeit-abhängigen (Pfade, IDs) trennen. + +### 2026-03-06 | Blender | Asset Library link=True — Assets müssen markiert sein +`bpy.data.libraries.load(blend_path, link=True, assets_only=True)` liefert nur via "Mark as Asset" markierte Datenblöcke. +**Lösung:** In .blend: jedes Material/Node-Group via "Mark as Asset" markieren. `catalog_assets.py` filtert via `m.asset_data is not None`. + +### 2026-03-06 | Celery Inspect | active_queues() zum Worker-Capability-Check +`celery_app.control.inspect().active_queues()` zeigt pro Worker welche Queues er konsumiert. Besser als Worker-Namen-Heuristiken für `render_worker_connected`/`blender_available`-Detection. + +### 2026-03-06 | TypeScript | Test-Dateien aus Haupt-tsconfig ausschließen +`vitest`/`msw`-Imports in `src/__tests__/` → TypeScript-Fehler in `tsc --noEmit` (Types nur im Test-Kontext). +**Lösung:** `"exclude": ["src/__tests__"]` in `tsconfig.json`. Vitest prüft eigene Typen intern. + +### 2026-03-06 | ffmpeg | Turntable hängt ohne `shortest=1` +`lavfi color`-Stream hat unendliche Dauer → ffmpeg wartet nach PNG-Sequenz-Ende unbegrenzt. +**Lösung:** `overlay=0:0:shortest=1`. Bei ffmpeg-Overlays mit lavfi/nullsrc als Input IMMER `shortest=1`. + +### 2026-03-06 | Architektur | WebSocket Auth via Query-Parameter (JWT) +Browser-WebSocket-API kann keine `Authorization`-Header senden. +**Lösung:** JWT als Query-Parameter: `ws://host/api/ws?token=`. Token ist in Logs sichtbar — für v3: kurzlebigen WS-Token (TTL 30s) generieren. + +### 2026-03-07 | Security | Media-Endpoints ohne Auth +`list_assets`, `download_asset`, `zip_download` ohne `get_current_user`-Dep → unauthentifizierte Requests. RLS schützt nur DB, nicht HTTP. +**Lösung:** `_user: User = Depends(get_current_user)` zu allen drei. Jeder neue Endpoint braucht expliziten Auth-Dep — RLS ist Defense-in-Depth, kein Ersatz. + +### 2026-03-07 | MediaAsset | `is_animation` Flag entscheidet Asset-Type — falsches Design +`is_animation == True` setzte `asset_type = turntable` auch für `.jpg` Poster-Frames → Broken-Video-Icons. +**Lösung:** Extension entscheidet: `.mp4`/`.webm` → `turntable`, alles andere → `still`. MIME-Typ/Extension immer als primäre Typ-Quelle. + +### 2026-03-07 | OCC | Bounding-Box aus STEP mit `Bnd_Box` + `brepbndlib.Add()` +```python +bbox = Bnd_Box(); brepbndlib.Add(shape, bbox) +xmin,ymin,zmin,xmax,ymax,zmax = bbox.Get() +``` +Werte in mm (STEP-Einheit). In `mesh_attributes` JSONB speichern — kein neues DB-Feld nötig. + +### 2026-03-07 | Storage | `storage_key` absolute Pfade brachen Volume-Moves +Absolute Pfade in `storage_key` → nach Volume-Umzug 398 Assets nicht erreichbar. +**Lösung:** `_normalize_key()` strippt `UPLOAD_DIR`-Prefix. Legacy-Remapping als Fallback. Neue Assets immer relativ: `renders/{uuid}/{filename}`. + +### 2026-03-07 | SQLAlchemy Async | db.refresh() lädt keine Relationships +`await db.refresh(invoice)` lädt nur skalare Spalten → `invoice.lines` → lazy-load außerhalb Greenlet → `MissingGreenlet`, HTTP 500. +**Lösung:** Nach `db.commit()` separate select-Query mit `selectinload` statt `db.refresh()`. + +### 2026-03-07 | Frontend Auth | Bearer-Token bei direktem Link-Download fehlt +`` sendet keinen Authorization-Header → 401. +**Lösung:** `api.get(..., { responseType: 'blob' })` → `URL.createObjectURL()` + programmatischer `.click()`. Gilt für alle geschützten Download-Endpoints. + +### 2026-03-07 | PostgreSQL RLS | SET LOCAL muss in jeder Transaktion gesetzt werden +`GRANT BYPASSRLS TO schaeffler` schlug still fehl → Admin-Endpoints bekamen 0 Zeilen. +**Lösung:** `await db.execute(text("SET LOCAL app.current_tenant_id = 'bypass'"))` direkt vor jede RLS-geschützte Query in internen/Admin-Endpoints. + +### 2026-03-07 | trimesh | GLB-Export-Scale: STL in mm → Three.js in Metern +`mesh.apply_scale(0.001)` nach `trimesh.load()` vor Export. Bei `trimesh.Scene` über `scene.geometry.values()` iterieren. Optional: `trimesh.smoothing.filter_laplacian(mesh, lamb=0.5, iterations=5)` für smooth normals. + +### 2026-03-07 | React Dashboard | Responsive CSS-Grid mit matchMedia +Inline-Styles für Grid-Positioning haben keine Medienabfrage-Unterstützung. +**Lösung:** `useLargeScreen()` Hook mit `window.matchMedia('(min-width: 1024px)')` + Change-Listener → `isLarge`-Boolean bedingt Inline-Styles. + +### 2026-03-07 | Media Browser | ZIP-Download 22-Byte-Korruption +`storage_key` absolute Pfade + `except Exception: pass` → Silent-Fail → leere Archive. +**Lösung:** Pfad-Check vor MinIO-Fallback; `except` loggt jetzt `logger.warning()`. In Generator-Funktionen für Streaming IMMER loggen. + +### 2026-03-07 | Frontend | Fehlende React-Imports crashen die gesamte App +`useEffect` hinzugefügt ohne Import → `ReferenceError` → Blank Page. ErrorBoundary auf Root-Level fängt nicht ab. +**Regel:** Nach jedem neuen Hook sofort Import-Zeile prüfen. `tsc --noEmit` als Quality Gate im Container. + +### 2026-03-07 | Storage Keys | Absolute Pfade in DB brechen nach Infrastruktur-Änderung +Flamenco-Outputs in `/shared/renders/...` → nach Flamenco-Entfernung 396 Assets nicht erreichbar (absoluter Pfad + MinIO-Fallback beide leer). +**Lösung:** Bulk-UPDATE auf relative Keys + Safety-Net-Remapping im Code. `storage_key` IMMER relativ zu `UPLOAD_DIR`: `renders/{uuid}/{filename}`. + +### 2026-03-07 | Config | Pydantic Settings: Attributname case-sensitive +`settings.UPLOAD_DIR` → `AttributeError`. Korrekt: `settings.upload_dir` (lowercase wie in config.py). +**Smoke-Test:** `docker compose exec backend python -c "from app.config import settings; print(settings.upload_dir)"`. + +### 2026-03-07 | Media ZIP | MIME-Type-basierte Extension → ".bin" statt ".png" +`(mime_type or "").split("/")[-1]` → `"bin"` für Assets ohne MIME-Type. +**Lösung:** Extension primär aus `Path(storage_key).suffix` lesen. MIME-Type nur als Fallback. Duplikat-Filenames mit `_1`, `_2` deduplizieren. + +### 2026-03-07 | Blender 5.0 | `export_colors` in bpy.ops.export_scene.gltf entfernt +`export_colors=False` → `keyword unrecognized` → exit 1 → immer Trimesh-Fallback, nie Materialien. +**Lösung:** Parameter entfernen. Gültige Blender-5.0-Parameter: `export_format`, `export_apply`, `use_selection`, `export_materials`, `export_image_format`. Nach Blender-Major-Updates alle bpy.ops.*-Parameter prüfen. + +### 2026-03-07 | React | useRef mit if(!ref.current) Guard reagiert nicht auf Prop-Änderungen +`GlbModel` klonte `scene` in `useRef` einmalig → bei neuem `url`-Prop blieb altes Mesh. +**Lösung:** `key={glbBlobUrl}` auf `` → React remountet, frischer useRef. Alternativ: `useMemo` mit URL-Dependency. + +### 2026-03-07 | React | staleTime zu hoch verzögert Erkennung neuer API-Daten +`staleTime: 30_000` + `qc.invalidateQueries()` → trotzdem 30s Verzögerung bis neue GLB-URL ankam. +**Lösung:** `staleTime: 0` für Queries die nach Mutations sofort aktuell sein müssen. + +### 2026-03-07 | GLB Export | Trimesh kennt keine Materialien — Blender-Pipeline ist Pflicht +Trimesh = reine Geometrie, keine Material-Libraries, kein Asset-Library-Support → graue, facettierte GLBs. +**Lösung:** `generate_gltf_geometry_task` auf Blender headless (`export_gltf.py`) umgestellt. Trimesh nur als Fallback. Skript vorhanden ≠ Skript verdrahtet — Aufruf-Pfad immer prüfen. + +### 2026-03-07 | Frontend | `` kann keine Auth-Header senden — useAuthBlob Hook nötig +`` → kein Authorization-Header → 401 → graues Icon. +**Lösung:** `useAuthBlob(url, enabled)`: `fetch(url, { headers: { Authorization } })` → `URL.createObjectURL(blob)` als src. Cleanup via `revokeObjectURL` + `cancelled`-Flag. Gilt für alle browser-nativen Media-Elemente (``, `` öffnet den Link direkt im Browser ohne `Authorization`-Header → Backend gibt 401/403 zurück. -**Lösung:** API-Call via `api.get(..., { responseType: 'blob' })` (axios-Client mit automatischem Auth-Header), dann `URL.createObjectURL()` + programmatischer `.click()`. So geht der Auth-Token mit. -**Gilt für:** Alle geschützten Download-Endpoints (PDFs, ZIPs, etc.) die via direkten Link nicht erreichbar sind. - ---- - -### 2026-03-06 | TypeScript | Test-Dateien aus Haupt-tsconfig ausschließen -**Problem:** `vitest`- und `msw`-Imports in `src/__tests__/` erzeugen TypeScript-Fehler in `tsc --noEmit` weil diese Packages ihre Typen nur im Test-Kontext (über vitest globals) bereitstellen. `tsc` kennt die Types nicht, obwohl die Packages installiert sind. -**Lösung:** In `tsconfig.json` ein `"exclude": ["src/__tests__"]` hinzufügen. Vitest führt seine eigene Typ-Prüfung durch; der Haupt-Build braucht nur Produktionscode zu prüfen. -**Für künftige Projekte:** Test-Verzeichnisse immer aus der Haupt-tsconfig ausschließen und eine separate `tsconfig.test.json` oder Vitest-interne Typ-Prüfung nutzen. - ---- - -### 2026-03-07 | PostgreSQL RLS | SET LOCAL muss in jeder Transaktion erneut gesetzt werden -**Problem:** `GRANT BYPASSRLS TO schaeffler` in Migration 036 schlug still fehl (schaeffler ist kein Superuser). Alle Endpoints die `cad_files`, `order_lines`, `products` abfragen (z.B. `import_existing_media_assets`, `get_thumbnail`, `_resolve_thumbnails_bulk`) erhielten durch RLS 0 Zeilen zurück → Media-Browser leer, Thumbnails fehlten. -**Lösung:** `await db.execute(text("SET LOCAL app.current_tenant_id = 'bypass'"))` direkt vor jede RLS-geschützte Query in internen/admin Endpoints setzen. `SET LOCAL` wirkt nur für die aktuelle Transaktion — reicht für async SQLAlchemy (gleiche Session = gleiche Transaktion). -**Regel:** Jeder interne Endpoint der ohne User-Auth-Kontext RLS-Tabellen liest braucht expliziten `SET LOCAL`-Bypass. BYPASSRLS-Grant an App-User ist kein sicherer Weg. - ---- - -### 2026-03-07 | trimesh | GLB-Export-Scale: STL in mm → Three.js in Metern -**Problem:** STL-Cache enthält Vertices in Millimetern (STEP-Standard). trimesh exportiert ohne Skalierung → Three.js liest GLB in Metern → Objekte 1000× zu groß. -**Lösung:** `mesh.apply_scale(scale_factor)` (default 0.001) nach `trimesh.load()` vor Export. Bei `trimesh.Scene` über `scene.geometry.values()` iterieren; bei einzelnem `Trimesh` direkt anwenden. -**Auch:** `trimesh.smoothing.filter_laplacian(mesh, lamb=0.5, iterations=5)` für smooth normals (STL speichert nur Facet-Normals → facettiertes Aussehen ohne Smoothing). - ---- - -### 2026-03-07 | React Dashboard | Responsive CSS-Grid mit matchMedia -**Problem:** CSS Grid mit `gridColumnStart/End/RowStart/End` per Inline-Style lässt sich nicht mit Tailwind-Breakpoints kombinieren — Inline-Styles haben keine Medienabfrage-Unterstützung. -**Lösung:** Custom Hook `useLargeScreen()` mit `window.matchMedia('(min-width: 1024px)')` + Change-Listener. `isLarge`-Boolean bedingt die Inline-Styles: Auf großen Screens: Grid-Positioning aktiv; auf kleinen Screens: leeres Style-Objekt → natürlicher Flow (Widgets stacken). -**Regel:** Wenn CSS-Grid-Positioning über Inline-Styles kommt (z.B. aus DB-Konfiguration), immer matchMedia-Hook zur responsiven Steuerung verwenden statt CSS-only. - ---- - -### 2026-03-07 | Media Browser | ZIP-Download 22-Byte-Korruption -**Problem:** ZIP-Download-Endpoint lieferte 22-Byte-leere Archive. Ursache: `storage_key` enthielt absolute Pfade (z.B. `/shared/renders/...`). `except Exception: pass` im Generator schluckte den Fehler still. -**Lösung:** Pfad-Check vor MinIO-Fallback: `Path(key)` prüfen ob absolut; falls nicht → relativ zu `UPLOAD_DIR`. `candidate.exists()` → `read_bytes()`. `except` loggt jetzt `logger.warning()` statt silent pass. -**Regel:** In Generator-Funktionen für Streaming-Responses IMMER loggen — silent pass führt zu korrupten Archiven ohne sichtbaren Fehler. - ---- - -### 2026-03-07 | Frontend | Fehlende React-Imports crashen die gesamte App (Blank Page) -**Problem:** `useEffect` in `useLargeScreen()` Hook hinzugefügt, aber `import { useState } from 'react'` nicht auf `import { useState, useEffect } from 'react'` erweitert. Vite/React wirft zur Laufzeit `ReferenceError: useEffect is not defined` → ErrorBoundary auf Root-Level fängt nicht ab → gesamte React-App zeigt leere Seite. -**Warum /check es nicht gefangen hat:** `/check` rief `npm test` und `npm run lint` auf — kein `lint`-Script vorhanden, kein TypeScript-Compiler (`tsc`) in `node_modules` lokal (Deps nur in Docker). `npm test` (Vitest) lief für Test-Dateien, prüfte aber keine Production-Komponenten auf fehlende Imports. -**Lösung:** `useEffect` zum Import hinzugefügt. **Langfristig:** `tsc --noEmit` als Quality Gate im Container ausführen. -**Regel:** Nach jedem neuen React-Hook oder neuer API (`useEffect`, `useCallback`, `useRef` etc.) sofort prüfen ob der Import oben in der Datei ergänzt wurde. - ---- - -### 2026-03-07 | Storage Keys | Absolute Pfade in DB brechen nach Infrastruktur-Änderung -**Problem:** Flamenco schrieb Render-Outputs nach `/shared/renders/{uuid}/{file}`. Nach Flamenco-Entfernung wurden die Dateien in `/app/uploads/renders/` kopiert, aber die `storage_key`-Werte in `media_assets` blieben auf `/shared/renders/...`. Der `download_asset`-Endpoint suchte den absoluten Pfad (existiert nicht) und fiel auf MinIO zurück (auch nicht vorhanden) → HTTP 404 für 396 Blender-Renders. -**Lösung:** -1. Bulk-UPDATE: `UPDATE media_assets SET storage_key = 'renders/{uuid}/{file}' WHERE storage_key LIKE '/shared/renders/%'` (nur für Dateien die am neuen Pfad existieren) -2. Safety-Net im Code: Wenn absoluter Pfad nicht existiert und `/shared/renders/` enthält → automatisch auf `UPLOAD_DIR/renders/` remappen -3. `settings.UPLOAD_DIR` war falsch (Pydantic-Setting heißt `upload_dir` lowercase) — ebenfalls behoben -**Regelung:** `storage_key` in MediaAssets IMMER relativ zu `UPLOAD_DIR` speichern, nie als absoluten Pfad. Format: `renders/{uuid}/{filename}` oder `thumbnails/{uuid}/{filename}`. Absolute Pfade brechen bei jedem Container-Rebuild oder Volume-Umzug. - ---- - -### 2026-03-07 | Config | Pydantic Settings: Attributname case-sensitive -**Problem:** `settings.UPLOAD_DIR` warf `AttributeError` — Pydantic-Settings-Objekte sind case-sensitive. Das korrekte Attribut heißt `upload_dir` (lowercase, wie in config.py definiert). -**Lösung:** Alle Zugriffe auf `settings.UPLOAD_DIR` → `settings.upload_dir` korrigiert. -**Quality Gate:** `docker compose exec backend python -c "from app.config import settings; print(settings.upload_dir)"` als Smoke-Test für Config-Zugriff. - - ---- - -### 2026-03-07 | Media ZIP | MIME-Type-basierte Extension → ".bin" statt ".png" -**Problem:** `zip_download` ermittelte Datei-Extension via `(a.mime_type or "").split("/")[-1] or "bin"`. Für Assets mit `mime_type=None` (importierte Flamenco-Renders) → Extension `"bin"` → Dateien im ZIP als `.bin` statt `.png`/`.jpg` — ZIP öffnet, aber keine Bilder erkennbar. -**Lösung:** Extension primär aus `Path(storage_key).suffix` lesen — der storage_key enthält immer die echte Datei-Extension. MIME-Type nur als Fallback. Zusätzlich: Original-Dateiname aus `storage_key` statt generischem `{type}_{uuid}.{ext}` verwenden. Duplikat-Filenames (mehrere Assets mit gleichem Dateinamen) werden mit `_1`, `_2` Suffix dedupliziert. -**Regel:** Datei-Erweiterung IMMER aus dem tatsächlichen Dateinamen (storage_key) lesen, nie nur aus MIME-Type. MIME-Types können null sein oder nicht dem tatsächlichen Format entsprechen. - ---- - -### 2026-03-07 | Blender 5.0 | `export_colors` in bpy.ops.export_scene.gltf entfernt -**Problem:** `bpy.ops.export_scene.gltf(export_colors=False)` → `keyword "export_colors" unrecognized` → exit code 1 → immer Trimesh-Fallback → nie Materialien, nie Sharp Edges, immer facettiert. Blender hat nie erfolgreich exportiert. -**Lösung:** `export_colors` aus dem Export-Call entfernen. Gültige Blender-5.0-Parameter: `export_format`, `export_apply`, `use_selection`, `export_materials`, `export_image_format`. -**Regel:** Beim Wechsel auf neue Blender-Versionen alle bpy.ops.*-Parameter gegen aktuelle Blender-Doku prüfen. Fehlende Parameter lassen Blender mit exit 1 fehlschlagen — OHNE aussagekräftige Fehlermeldung im Erfolgsfall. - -### 2026-03-07 | React | useRef mit if(!ref.current) Guard reagiert nicht auf Prop-Änderungen -**Problem:** `GlbModel` klonte `scene` in `useRef` mit `if (!cloned.current)`. Bei neuem `url`-Prop (regeneriertes GLB) blieb `cloned.current` das alte geklonte Objekt → altes Mesh wurde weiter angezeigt. React mounted/unmounts die Komponente nicht bei Prop-Änderungen. -**Lösung:** `key={glbBlobUrl}` auf `` → React remountet die Komponente bei jeder neuen URL → frischer `useRef` → neues Mesh. Alternativ: `useMemo` mit URL-Dependency statt `useRef`. -**Regel:** Wenn ein `useRef`-Initialisierungsmuster (`if (!ref.current)`) auf Prop-Änderungen reagieren muss → immer mit `key` oder `useEffect`/`useMemo` kombinieren. - -### 2026-03-07 | React | staleTime zu hoch verzögert Erkennung neuer API-Daten -**Problem:** `staleTime: 30_000` auf dem gltf_geometry-Assets-Query: Nach GLB-Generierung dauerte es bis zu 30 Sekunden bis der neue `download_url` im Browser ankam — obwohl `qc.invalidateQueries()` aufgerufen wurde. Invalidierung erzwingt Refetch, aber staleTime=30s verhindert ihn falls der Cache noch "frisch" gilt. -**Lösung:** `staleTime: 0` für Queries die bei Invalidierung sofort aktuell sein müssen. -**Regel:** `staleTime: 0` für Entitäten die nach Mutations sofort aktuell sein müssen. Höhere staleTime nur für read-heavy, selten ändernde Daten (z.B. Materialliste, Templates). - -### 2026-03-07 | GLB Export | Trimesh kennt keine Materialien — Blender-Pipeline ist Pflicht -**Problem:** `generate_gltf_geometry_task` nutzte trimesh für STL→GLB. Trimesh ist eine reine Geometrie-Bibliothek: keine Material-Bibliotheken, kein OCC-Kantenmarking, kein Asset-Library-Support. Das erzeugte graue, facettierte GLB-Dateien ohne Materialien. -**Lösung:** Task auf Blender headless (`export_gltf.py`) umgestellt. Übergibt: `material_map` (via `resolve_material_map()` aus `cad_part_materials`), `sharp_edges_json` (aus `mesh_attributes.sharp_edge_midpoints`), `asset_library_blend` (via `get_material_library_path()`). Trimesh nur noch als Fallback wenn Blender nicht verfügbar. -**Fehler:** Der Blender-Script (`export_gltf.py`) war schon fertig implementiert — aber `generate_gltf_geometry_task` hat ihn nie aufgerufen. Skript vorhanden ≠ Skript verdrahtet. Immer prüfen ob ein Script auch von der richtigen Stelle aufgerufen wird. - -### 2026-03-07 | Frontend | `` kann keine Auth-Header senden — useAuthBlob Hook nötig -**Problem:** `` schickt keine `Authorization`-Header → 401 → `imgError=true` → graues Icon in der Media Library. Betrifft alle Browser-nativen Elemente (``, `