Files
HartOMat/LEARNINGS.md
T

428 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Projekt-Learnings — Schaeffler Automat
## Format
**Datum | Kategorie | Problem → Lösung**
---
## Learnings
### 2026-01-15 | Architektur | Backend-Port-Konflikt
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
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)
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
`_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
`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 ü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
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
`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
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
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
`"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
`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
`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
`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
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
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
`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
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
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
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
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
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: 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
`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
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 .` 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=<jwt>`. 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
`<a href="/api/billing/invoices/{id}/pdf">` sendet keinen Authorization-Header → 401.
**Lösung:** `api.get(..., { responseType: 'blob' })``URL.createObjectURL()` + programmatischer `<a>.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 `<GlbModel>` → 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 | `<img src>` kann keine Auth-Header senden — useAuthBlob Hook nötig
`<img src="/api/media/{id}/download">` → 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 (`<img>`, `<video>`, `<audio>`).
### 2026-03-07 | Backend | publish_asset fehlte product_id + cad_file_id
`MediaAsset` ohne `product_id`/`cad_file_id``get_thumbnail_url()` konnte keinen Thumbnail-Fallback berechnen → graue Icons.
**Lösung:** In `publish_asset`: `Product` laden, `product_id=line.product_id` + `cad_file_id=product.cad_file_id` setzen. MediaAssets immer mit allen verfügbaren Referenz-FKs erstellen.
### 2026-03-07 | Frontend | Inline 3D Viewer — GLB mit Auth via Blob URL laden
`useGLTF(url)` aus `@react-three/drei` kann keine Auth-Header senden.
**Lösung:** `fetch(url, { headers: { Authorization } })``.blob()``URL.createObjectURL(blob)` → String-URL an `useGLTF(blobUrl)`. Revoke in useEffect-Cleanup. Three.js / drei kennen kein Auth-Konzept.
### 2026-03-07 | Backend | trimesh in optionalem [cad]-Extra — nicht im Docker-Build installiert
`pip install -e ".[dev]"` installiert kein `trimesh``ModuleNotFoundError` beim ersten Aufruf.
**Lösung:** `pip install -e ".[dev,cad]"`. Beim Hinzufügen optionaler Extras immer prüfen ob alle relevanten Container-Images das Extra installieren.
### 2026-03-07 | Frontend | URL.revokeObjectURL sofort nach click() → Race Condition
`revokeObjectURL(url)` synchron nach `a.click()` → Download manchmal leer (click() ist in manchen Browsern asynchron).
**Lösung:** `setTimeout(() => URL.revokeObjectURL(url), 100)` für alle programmatischen Blob-Downloads.
### 2026-03-07 | Media Import | Falsche asset_type-Klassifizierung durch Dateinamen-Matching
Dateiname enthielt "Turntable" → `asset_type=turntable` auch für `.jpg` Poster-Frames → Browser versuchte JPGs als `<video>` zu rendern.
**Lösung:** Daten-Fix: `UPDATE media_assets SET asset_type='still' WHERE asset_type='turntable' AND storage_key LIKE '%.jpg'`. Code-Fix: `isVideoAsset()` nutzt `mime_type` zusätzlich. Asset-Typ IMMER aus mime_type + Dateiendung, nie nur aus Dateiname.
### 2026-03-08 | Render | OptiX BVH cache ephemeral → first render slow after rebuild
Nach `docker compose up --build render-worker`: erstes Render 130150s statt 22s. `/root/.nv/ComputeCache/` liegt im ephemeren Container-Dateisystem → wird bei jedem Rebuild geleert.
**Lösung:** Named volume: `optix-cache:/root/.nv` in docker-compose.yml. Detection: Render 1 >> Render 2+3 nach Rebuild → OptiX cold-start.
### 2026-03-10 | R3F | Invisible meshes: check `e.object.visible` in handlers causes regression
`mesh.visible = false` + Guard `if (!e.object.visible) return` in Event-Handlern blockiert alle Folge-Events — R3F's Event-Tracking feuert events mit `e.object` auf Meshes die nach dem Tracking unsichtbar wurden.
**Lösung:** `mesh.raycast = () => {}` wenn `mesh.visible = false`; restore mit `THREE.Mesh.prototype.raycast` beim Einblenden. Meshes vom Raycasting ausschließen an der Quelle, nicht im Handler.
**Regel:** Nie `e.object.visible` in R3F pointer/click-Handlern prüfen.
### 2026-03-10 | Three.js/GLTF | `mesh.name` wird durch sanitizeNodeName verändert — `mesh.userData.name` nutzen
Three.js `GLTFLoader.createUniqueName()` ruft `PropertyBinding.sanitizeNodeName()` auf, welches reservierte Zeichen `[]:./` **entfernt**. Blender-Dedup-Suffixe `.001`, `.012` etc. werden zu `001`, `012` — ohne Punkt.
Unser `normalizeMeshName` strippte `\.\d{3}$` (mit Punkt), aber der Punkt ist bereits weg → `GE360-HF-0051-EIN_1.012` → (Three.js sanitize) → `GE360-HF-0051-EIN_1012` → kein Match in partMaterials → fälschlich "unassigned".
**Fix:** `mesh.userData.name` verwenden statt `mesh.name`. Three.js speichert den Original-GLTF-Node-Namen (unsanitized, mit Punkten) in `node.userData.name = nodeDef.name` **bevor** `node.name = sanitize(...)` gesetzt wird. Pattern: `(mesh.userData?.name as string) || mesh.name`.
**Gilt für:** handleClick, handlePointerOver/Out, visibility/glow/color effects, onReady callback, glbMeshNames-Traversal in InlineCadViewer + ThreeDViewer.
### 2026-03-10 | OCC/GLTF | Assembly containers have no geometry → no mesh in GLB
`cad_part_materials` enthält Keys für Assembly-Nodes (`_ASM_1`, `_BASIS_ASM_1`), die in cadquery geparst werden, aber keine Geometrie haben → nicht im GLB. Debug-Log "matched: 4 of 9 stored keys" = 5 Keys sind Assembly-Container, kein Bug.
**Regel:** Für assigned/total-Zählung immer `glbMeshNames` (Set aus scene traversal) nutzen, nie `Object.keys(partMaterials).length`.
### 2026-03-10 | Debug-Workflow | GLB-Geometrie-Analyse direkt per Python — ohne Browser-DevTools
Bei Mesh-Name-Mismatch-Bugs: GLB-Datei direkt parsen statt im Browser debuggen. Spart viele Iterationen.
**Schritt 1 — GLB-Download-URL holen:**
```python
import urllib.request, json, struct
# Login
data = json.dumps({'email':'admin@schaeffler.com','password':'Admin1234!'}).encode()
req = urllib.request.Request('http://localhost:8888/api/auth/login', data=data, headers={'Content-Type':'application/json'})
token = json.load(urllib.request.urlopen(req))['access_token']
# Media-Assets für CAD-File
assets = json.load(urllib.request.urlopen(urllib.request.Request(
f'http://localhost:8888/api/media/?cad_file_id={CAD_FILE_ID}&asset_types=gltf_production&asset_types=gltf_geometry',
headers={'Authorization': f'Bearer {token}'})))
```
**Schritt 2 — GLB parsen und Mesh-/Node-Namen ausgeben:**
```python
req = urllib.request.Request(DOWNLOAD_URL, headers={'Authorization': f'Bearer {token}'})
glb = urllib.request.urlopen(req).read()
chunk_len, _ = struct.unpack('<II', glb[12:20])
gltf = json.loads(glb[20:20+chunk_len])
nodes, meshes = gltf['nodes'], gltf['meshes']
for n in nodes:
if n.get('mesh') is not None:
print(f"node={repr(n['name'])} mesh_def={repr(meshes[n['mesh']]['name'])}")
```
**Schritt 3 — normalizeMeshName + resolvePartMaterial in Python nachbauen und alle Namen durchlaufen:**
So lässt sich der vollständige Matching-Status (ASSIGNED/UNASSIGNED) offline prüfen, ohne den Browser zu öffnen. Ggf. gespeicherte `cad_part_materials` via `GET /api/cad/{id}/part-materials` dazuladen.
**Produkt-ID → CAD-File-ID:** `GET /api/products/{product_id}` → Feld `cad_file_id`.
**CAD-File-ID für Media-Assets:** `GET /api/media/?cad_file_id={id}&asset_types=gltf_production` (nutzt `listMediaAssets`, nicht `getMediaAssets`).
### 2026-03-11 | Render-Pipeline | Production GLB: Koordinaten OCC→Blender falsch
OCC STEP-Koordinaten (Z-up, mm) werden von `export_step_to_gltf.py` auf Y-up (glTF) rotiert, dann vom Blender-Importer wieder auf Z-up. Netto: `Blender(X, Y, Z) = OCC(X×0.001, -Z×0.001, Y×0.001)` — Y und Z tauschen plus Z-Negierung. Einfach mm→m skalieren (X=X, Y=Y, Z=Z) ist FALSCH.
**Lösung:** In allen `_apply_sharp_edges_from_occ()`-Funktionen: `v = Vector((p[0]*0.001, -p[2]*0.001, p[1]*0.001))`.
**Diagnose:** `DEBUG first_vert_local=[0.0, -0.1175, 0.26]` vs `occ_pair=[0.0, 0.26, 0.1175]` zeigte eindeutig Y↔Z-Swap + Z-Negierung.
### 2026-03-11 | Render-Pipeline | Production GLB: Mesh-Data-Sharing bei Material-Zuweisung
Nach GLB-Import teilen sich mehrere Blender-Objekte denselben Mesh-Datablock (z.B. Instanzen). `obj.data.materials.clear()` auf geteilten Daten löscht Materialien für ALLE Objekte, die diesen Mesh teilen.
**Lösung:** Vor `obj.data.materials.clear()` immer: `if obj.data.users > 1: obj.data = obj.data.copy()` (single-user machen).
### 2026-03-11 | Render-Pipeline | Production GLB: Name-Mismatch cadquery vs. RWGltf
cadquery-Parser und RWGltf_CafWriter lesen dieselbe STEP-Datei, erzeugen aber unterschiedliche Part-Namen (z.B. cadquery: `GE360-HF-0011-EIN_HAELFTE_AF0_1_AF0` vs. RWGltf: `GE360-HF-0021-EIN`). Material-Mapping via Namen deckt nur 2/25 Objekte ab.
**Lösung 1:** Prefix-Match: `key.startswith(lower_base)` oder `lower_base.startswith(key)`.
**Lösung 2:** Single-Material-Fallback: wenn `len(appended) == 1` (nur 1 Bibliotheks-Material geladen), alle ungematchten Objekte ebenfalls mit diesem Material belegen.
**Generell:** Part-Namen aus zwei verschiedenen OCC-Lese-Pfaden NIE direkt vergleichen.
### 2026-03-11 | Python/Blender | `id(bpy_object)` über Iterationen hinweg unzuverlässig
Blender-Objekte sind Python-Wrapper über C++-Objekte. Nach einer Schleife können gleiche C++-Objekte neue Python-Wrapper bekommen → `id()` ändert sich. Tracking mit `id(obj)` in einem Set und Prüfung in einer zweiten Schleife funktioniert NICHT.
**Lösung:** `obj.name` als stabiler Identifier (Blender-Namen sind innerhalb der Szene eindeutig und bleiben stabil).
### 2026-03-11 | Python/Blender | `bmesh.edges.get((v0, v1))` schlägt fehl bei idx0==idx1
Wenn KD-Tree für beide Endpunkte denselben Vertex findet (z.B. degenerate/zero-length edge), wirft `edges.get((v, v))` `ValueError: found the same (BMVert) used multiple times`.
**Lösung:** Guard: `if idx0 == idx1: continue` direkt nach dem `kd.find()` für Endpunkt 2.
### 2026-03-11 | Workflow | System-Daten direkt lesen — kein docker exec nötig
Für den `/plan`-Agent: alle relevanten System-Daten sind OHNE docker-Befehle zugänglich:
- **Datenbankmodelle:** `backend/app/models/` und `backend/app/domains/*/models.py` direkt lesen
- **API-Endpoints:** `backend/app/api/routers/` und `backend/app/domains/*/routers.py` lesen — kein curl/docker exec nötig
- **Celery-Tasks:** `backend/app/tasks/step_tasks.py` und `backend/app/domains/pipeline/tasks/` lesen
- **Render-Scripts:** `render-worker/scripts/` lesen — `blender_render.py`, `export_gltf.py` etc.
- **Frontend-API:** `frontend/src/api/*.ts` zeigt alle API-Calls und Typen
- **Gespeicherte Daten:** `CadFile.mesh_attributes`, `Product.cad_part_materials`, `SystemSetting` in Modellen beschrieben
- **Docker-Logs nur für Debugging:** `docker compose logs --since 5m render-worker 2>&1 | grep -E "pattern"` — sparsam einsetzen, nicht für Codeanalyse
- **Kein `docker exec backend python3 -c`** für Code-Exploration — dauert lang und braucht laufende Container
- **MEMORY.md** enthält Container-Capabilities: OCP nur in `render-worker`, nicht in `worker` oder `backend`
### 2026-03-11 | Render-Pipeline | Production GLB: edit-mode mark_sharp+mark_seam für korrekte GLB Sharp Edges
In Blender 5.0: `shade_smooth_by_angle()` allein reicht nicht für scharfe Kanten im GLB-Export.
**Lösung:** Edit-mode-Operators verwenden: `edges_select_sharp(sharpness=angle)``mark_sharp()``mark_seam()`.
Der glTF-Exporter erstellt an den als sharp markierten Edges Vertex-Splits (duplizierte Vertices mit verschiedenen Normalen).
**Verifizierung:** 543 sharp+seam Edges markiert → production GLB hat 812 extra Vertices + 6027 Positionen mit mehreren Normalen = scharfe Kanten korrekt enkodiert.
**Wichtig:** `calc_normals_split()` wurde in Blender 5.0 entfernt (→ AttributeError). Nicht mehr nötig: `export_apply=True` triggert Vertex-Splitting automatisch.
### 2026-03-11 | Render-Pipeline | MediaAsset DELETE+INSERT erzeugt neue UUID → 404 auf gecachte URLs
`generate_gltf_production_task` und `generate_gltf_geometry_task` löschten alten MediaAsset-Record und erstellten neuen mit neuer UUID.
Gecachte Download-URLs (z.B. im Frontend-State) zeigten auf die alte, gelöschte UUID → 404.
**Lösung:** UPSERT: existierenden Record aktualisieren (`existing.storage_key = _key`) statt DELETE+INSERT. UUID bleibt stabil.
### 2026-03-11 | Render-Pipeline | Production GLB: OCC custom_normal überschreibt Blender-Normalen
`export_step_to_gltf.py` (RWGltf_CafWriter) embedded per-corner normals from OCC tessellation as a `custom_normal` attribute (CORNER, INT16_2D) in the geometry GLB. Blender's glTF importer preserves this as a custom attribute. The glTF exporter then re-exports these pre-baked normals **unchanged**, ignoring `shade_smooth_by_angle` processing and explicit `edge.smooth=False` sharp marks — sharp edges are invisible in the production GLB.
**Lösung:** In `export_gltf.py` und `blender_render.py`: nach GLB-Import das `custom_normal`-Attribut von allen Mesh-Objekten entfernen, BEVOR `shade_smooth_by_angle()` aufgerufen wird. Dann berechnet Blender die Normalen neu aus den sharp-edge-Marks.
**Diagnose:** File-Size-Test: WITH custom_normal=1218960 Bytes, WITHOUT=1137944 Bytes. Re-import zeigte `has_custom_normals=True`. GLB-JSON-Inspektion mit `struct.unpack` + `json.loads` direkt auf `.glb`-Datei.
```python
for obj in mesh_objects:
if "custom_normal" in obj.data.attributes:
obj.data.attributes.remove(obj.data.attributes["custom_normal"])
```
---
## Offene Fragen
- [ ] Azure AI Credentials für Phase 4 (Bildvalidierung) noch nicht konfiguriert
- [ ] pythonOCC verfügbar im render-worker (via cadquery dependency)? Deployment-Test ausstehend
- [ ] @xyflow/react noch nicht installiert — npm install nötig nach nächstem `docker compose up --build frontend`
- [ ] Material-Alias-Seeding deckt noch nicht alle deutschen Materialbezeichnungs-Varianten ab
- [ ] Turntable-Animation: bg_color via FFmpeg-Overlay — Qualität bei Transparenz-Edges prüfen