Files
HartOMat/LEARNINGS.md
T

35 KiB
Raw Blame History

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:6379kombu.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 AssetLibraryMaterial.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_processingworker-Container (kein Blender) → is_blender_available() = False → Pillow-Placeholder, kein Fehler. Lösung: Queue auf thumbnail_renderingrender-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.GetComponentsGetComponents_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_pathValueError. 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/.webmturntable, 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()

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().

<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_DIRAttributeError. 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=Falsekeyword 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_idget_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 trimeshModuleNotFoundError 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:

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:

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.

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