Files
HartOMat/LEARNINGS.md
T
Hartmut 3eba7b2d37 fix(glb): remove invalid export_colors param + fix viewer stale mesh
4 root causes fixed:

1. export_colors=False was removed in Blender 4.x — caused every Blender
   export to fail (exit 1) and always fall back to trimesh. Remove it.
   Blender now runs the full pipeline: materials + sharp edges.

2. GlbModel cloned ref never reset on url change — key={glbBlobUrl} forces
   React to remount GlbModel on each new blob URL, resetting the ref so
   fresh geometry is always loaded.

3. glbBlobUrl not cleared before re-fetch — setGlbBlobUrl(null) added at
   start of downloadUrl effect so spinner shows instead of stale mesh.

4. staleTime: 30_000 delayed picking up new MediaAsset after generation.
   Changed to staleTime: 0 so invalidation always triggers immediate refetch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 15:46:42 +01:00

50 KiB
Raw Blame History

Projekt-Learnings — Schaeffler Automat

Format

Datum | Kategorie | Problem → Lösung


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/.webmturntable, 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_DIRcandidate = Path(settings.upload_dir) / key. Absolute Pfade nie in die DB schreiben.

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.namestr 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


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


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


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


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


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-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

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


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 | 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


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


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


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


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


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


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


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


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:

# 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


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


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


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.


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

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.

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.

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

2026-03-06 | Docker | apt-Paketname libgdk-pixbuf2.0-0 vs libgdk-pixbuf-2.0-0

WeasyPrint benötigt libgdk-pixbuf. Auf Debian bookworm (python:3.11-slim) heißt das Paket libgdk-pixbuf-2.0-0 (mit Bindestrichen), nicht libgdk-pixbuf2.0-0. apt-get install schlägt mit exit code 100 fehl wenn der Name falsch ist. → Immer apt-cache search libgdk im Container prüfen bevor man Paketnamen in Dockerfiles schreibt.

2026-03-06 | Celery | thumbnail_rendering Queue braucht eigenen worker-thumbnail Service

Blender-Renderer verarbeitet nur 1 Request gleichzeitig. Wenn worker (concurrency=8) Tasks auf thumbnail_rendering queued, laufen 7 davon in Timeout (300s). Lösung: separaten worker-thumbnail Service mit --concurrency=1 und -Q thumbnail_rendering in docker-compose.yml. step_processing bleibt bei concurrency=8.

2026-03-06 | Alembic | Migration exit code 100 bei enum-Konflikt

SQLAlchemy Enum(create_type=False) funktioniert nicht zuverlässig mit asyncpg. Bei bereits existierenden PostgreSQL-Enum-Typen: Raw SQL mit DO $$ BEGIN CREATE TYPE ...; EXCEPTION WHEN duplicate_object THEN NULL; END $$; verwenden. Für Tabellen: CREATE TABLE IF NOT EXISTS.

2026-03-06 | Render-Pipeline | Circular Shim blockiert alle Order-Renders

Problem: dispatch_order_line_renderdispatch_render (Shim A→B→A Circular Import) → Render startet nie. Die einzige funktionierende Render-Implementierung render_order_line_task war nie aus dem Dispatch-Chain erreichbar. Lösung: dispatch_order_line_render direkt auf render_order_line_task.delay() umleiten. render_dispatcher.py-Shim ebenfalls repariert. Dispatch-Service _legacy_dispatch ebenfalls auf render_order_line_task umgeleitet. Erkenntnisse: Bei Refactoring immer prüfen ob Shims zirkulär werden. Wenn zwei Module sich gegenseitig importieren (A→B und B→A), entsteht ein Circular Import — keine echte Implementierung wird aufgerufen. Den echten Aufruf-Pfad von der API zum Task vor Refactoring dokumentieren.


2026-03-06 | Render-Pipeline | render_order_line_task auf falschem Worker (kein Blender)

Problem: render_order_line_task war auf Queue step_processing → lief im worker-Container (Backend-Dockerfile, kein Blender). render_to_file() fiel still auf Pillow-Placeholder zurück. Renders scheinbar erfolgreich aber nur graue Platzhalterbilder. Ursache: is_blender_available() prüft BLENDER_BIN-Env-Var — im worker-Container nicht gesetzt. Fallback auf Pillow passiert lautlos ohne Exception. Lösung: render_order_line_task queue auf thumbnail_rendering geändert → läuft jetzt im render-worker-Container (hat Blender 5.0.1 + cadquery). worker-thumbnail-Service aus docker-compose.yml entfernt (hatte keinen Blender, blockierte aber die Queue). Für künftige Projekte: Blender-Tasks IMMER auf thumbnail_rendering Queue routen. worker-thumbnail = kein Blender, render-worker = hat Blender. Wenn is_blender_available() False zurückgibt ist der Task auf dem falschen Worker.


2026-03-06 | Docker | worker-thumbnail vs render-worker — beide auf thumbnail_rendering

Problem: Sowohl worker-thumbnail (kein Blender) als auch render-worker (hat Blender) lauschten auf thumbnail_rendering Queue. Tasks wurden round-robin verteilt → 50% der Blender-Tasks schlugen fehl (Pillow-Fallback, kein echter Fehler). Lösung: worker-thumbnail-Service aus docker-compose entfernt. render-worker ist der alleinige Consumer von thumbnail_rendering. Dieser hat Blender + cadquery + alle Render-Scripts. Für künftige Projekte: Nie zwei Services mit unterschiedlichen Capabilities auf die gleiche Queue hören lassen.


2026-03-06 | Multi-Tenancy | tenant_id NOT NULL verletzt bei Order-Erstellung

Problem: Migration 036 machte tenant_id NOT NULL auf orders, order_lines, order_items. Alle Create-Endpoints übergaben tenant_id nicht → PostgreSQL NOT NULL Constraint Violation. Lösung: Überall tenant_id=getattr(user, 'tenant_id', None) in Model-Konstruktoren: orders.py (create_order, split_order, add_line_to_order), uploads.py (finalize_excel). Für künftige Projekte: Nach jeder RLS-Migration alle Create-Endpoints prüfen ob das neue Pflichtfeld befüllt wird. getattr(user, 'tenant_id', None) als sicheres Default-Pattern verwenden.

2026-03-06 | Celery | render_order_line_task auf falscher Queue → Pillow-Fallback

Problem: render_order_line_task war auf step_processing Queue → wurde von worker-Container bearbeitet, der kein Blender hat. is_blender_available() → False → Pillow-Placeholder-Bild ohne Fehlermeldung. Lösung: Queue zu thumbnail_rendering geändert → nur render-worker (mit Blender 5.0.1) verarbeitet diese Tasks. Für künftige Projekte: Nach jeder Architektur-Änderung (Container-Entfernung, Queue-Umbenennung) alle Celery-Task-Dekoratoren prüfen ob sie noch auf dem richtigen Worker laufen.

2026-03-06 | Celery | Zwei Worker auf derselben Queue mit unterschiedlichen Fähigkeiten

Problem: worker-thumbnail und render-worker konkurrierten auf thumbnail_rendering. worker-thumbnail hatte kein Blender → 50% aller Render-Tasks liefen auf dem falschen Worker → Silent-Fail. Lösung: worker-thumbnail aus docker-compose.yml entfernt. render-worker ist einziger Consumer von thumbnail_rendering. Regel: Jede Queue sollte nur von Workers mit identischen Fähigkeiten konsumiert werden. Nie zwei Worker unterschiedlicher Ausstattung auf dieselbe Queue setzen.

2026-03-06 | Python | Circular Import via doppelte Shim-Schicht

Problem: template_service.py importierte aus domains/rendering/service.py, das wiederum aus template_service.py importierte. Beide waren leere Shims. resolve_template() war nie aufrufbar → Render-Tasks crashing mit ImportError. Lösung: Volle Implementierung in template_service.py wiederhergestellt (aus git history). domains/rendering/service.py importiert nur davon — kein Rückimport. Für künftige Projekte: Shim-Layer immer auf circular imports prüfen. domains/X/service.py sollte entweder die echte Implementierung enthalten ODER aus einer anderen Domain importieren, aber nicht im Kreis.

2026-03-06 | FastAPI | 307-Redirect verliert Authorization-Header

Problem: GET /api/tenants → 307 Temporary Redirect zu /api/tenants/ (trailing slash). axios folgt dem Redirect, verliert dabei den Authorization-Header → 401 → leere Tenant-Liste im Frontend. Lösung: Frontend-API-Call auf /tenants/ mit trailing slash geändert. Für künftige Projekte: FastAPI-Router immer mit trailing slash aufrufen oder redirect_slashes=False am Router setzen.

2026-03-06 | Render-Pipeline | ffmpeg Turntable hängt ohne shortest=1

Problem: Turntable-Render (Order f0436188) mit bg_color schlug mit Timeout (300s) fehl. ffmpeg-Overlay-Befehl war [1:v][0:v]overlay=0:0 — der lavfi color-Quell-Stream hat unendliche Dauer. ffmpeg wartete nach Ende der PNG-Sequenz weiter auf weitere Farb-Stream-Frames → hing unbegrenzt. Lösung: overlay=0:0overlay=0:0:shortest=1. shortest=1 beendet den Output-Stream sobald der kürzeste Input-Stream endet (die PNG-Sequenz). Datei: backend/app/services/render_blender.py:507 Für künftige Projekte: Bei ffmpeg-Overlays mit lavfi/color/nullsrc als ein Input IMMER shortest=1 setzen. Sonst hängt ffmpeg nach Ende des finite Streams.


2026-03-06 | Architektur | WebSocket Auth via Query-Parameter (JWT)

Problem: WebSocket-Verbindungen können keinen Authorization-Header senden (Browser-WebSocket-API hat keine Header-Unterstützung). JWT muss anders übertragen werden. Lösung: JWT als Query-Parameter: ws://host/api/ws?token=<jwt>. Backend verifiziert via jwt.decode() im WebSocket-Endpoint. Sicherheitshinweis: Token ist in Server-Logs sichtbar. Für v2 akzeptabel. In v3: kurzlebigen WS-Token (TTL 30s) aus JWT generieren. Für künftige Projekte: Immer Query-Param oder Cookie (bei HTTPS) für WebSocket-Auth verwenden; nie erwarten dass der Browser Headers setzen kann.


2026-03-06 | Blender | Asset Library link=True — Assets müssen in .blend als Asset markiert sein

Problem: bpy.data.libraries.load(blend_path, link=True, assets_only=True) liefert nur Materialien/Node-Groups die explizit via Blender's Asset-System markiert wurden (asset_data is not None). Nicht markierte Datenblöcke werden ignoriert. Lösung: In der .blend-Datei: jedes Material/Node-Group das gelinkt werden soll muss via "Mark as Asset" (F3 → "Mark as Asset") markiert sein. catalog_assets.py filtert via m.asset_data is not None — dieser Filter muss konsistent in catalog_assets.py und asset_library.py verwendet werden. Für künftige Projekte: Immer "Mark as Asset" dokumentieren wenn .blend-Libraries an User weitergegeben werden.


2026-03-06 | Celery Inspect | active_queues() zum Worker-Capability-Check

Erkenntnis: celery_app.control.inspect().active_queues() gibt pro Worker zurück welche Queues er konsumiert. Damit kann man gezielt prüfen ob ein Worker mit bestimmten Fähigkeiten (z.B. thumbnail_rendering) connected ist — besser als Worker-Namen-Heuristiken. Anwendung: GET /api/worker/health/render nutzt active_queues() um render_worker_connected und blender_available korrekt zu bestimmen.


2026-03-07 | SQLAlchemy Async | db.refresh() lädt keine Relationships

Problem: create_invoice rief await db.refresh(invoice) — lädt nur skalare Spalten, nicht invoice.lines (Relationship). FastAPI serialisiert danach lines → SQLAlchemy versucht lazy-load außerhalb eines Greenlets → MissingGreenlet-Exception, HTTP 500. Lösung: Statt db.refresh() die bestehende get_invoice(db, invoice.id) Funktion aufrufen, die selectinload(Invoice.lines) verwendet und alle Relationships korrekt vorlädt. Regel: Nach db.commit() in Diensten die Relationships brauchen, immer eine separate select-Query mit selectinload machen anstatt db.refresh().


Problem: <a href="/api/billing/invoices/{id}/pdf" target="_blank"> ö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 <a>.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_DIRsettings.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 <GlbModel> → 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 | <img src> kann keine Auth-Header senden — useAuthBlob Hook nötig

Problem: <img src="/api/media/{id}/download"> schickt keine Authorization-Header → 401 → imgError=true → graues Icon in der Media Library. Betrifft alle Browser-nativen Elemente (<img>, <video>, <audio>). Lösung: useAuthBlob(url, enabled) Hook: fetch(url, { headers: { Authorization: \Bearer ${token}` } })URL.createObjectURL(blob)→ Blob-URL alssrcnutzen. Cleanup viaURL.revokeObjectURL+cancelled-Flag gegen Race Conditions. **Für künftige Projekte:** Jeder auth-geschützte Media-Endpoint der in /

2026-03-07 | Backend | publish_asset fehlte product_id + cad_file_id → kein Thumbnail-Fallback

Problem: publish_asset Celery-Task erstellte MediaAsset-Records ohne product_id/cad_file_id zu setzen. get_thumbnail_url() und _resolve_thumbnails_bulk() konnten keinen Thumbnail-Fallback für still-Assets berechnen → graue Icons für alle neu gerenderten Stills. Lösung: In publish_asset nach dem Laden der OrderLine auch Product laden und product_id=line.product_id + cad_file_id=product.cad_file_id auf das neue MediaAsset setzen. Regel: MediaAssets immer mit allen verfügbaren Referenz-FKs erstellen — diese werden für Thumbnail-Resolution und Tenant-Isolation benötigt.

2026-03-07 | Frontend | Inline 3D Viewer — GLB mit Auth via Blob URL laden

Problem: useGLTF(url) aus @react-three/drei kann keine Auth-Header setzen → direkte Asset-Download-URLs nicht nutzbar. <Canvas> braucht einen echten URL-String (keine Promise). Lösung: GLB per fetch(url, { headers: { Authorization } }).blob()URL.createObjectURL(blob) → String-URL an useGLTF(blobUrl) übergeben. Revoke in useEffect-Cleanup. Polling (4s) während GLB-Generierung läuft. Für künftige Projekte: Three.js / drei kennen kein Auth-Konzept. Alle auth-geschützten 3D-Assets immer als Blob-URL laden.

2026-03-07 | Backend | trimesh in optionalem [cad]-Extra — nicht im Docker-Build installiert

Problem: trimesh ist in pyproject.toml unter [project.optional-dependencies] cad = [...] definiert. Dockerfile installierte nur pip install -e ".[dev]"trimesh fehlte → export_gltf_colored warf ModuleNotFoundError beim ersten Aufruf. Lösung: Dockerfile auf pip install -e ".[dev,cad]" umgestellt + Backend-Container neu gebaut. Regel: Beim Hinzufügen optionaler Extras zu pyproject.toml immer prüfen ob alle relevanten Container-Images das Extra auch installieren. Im Zweifel alle Runtime-Deps in [project.dependencies] (nicht optional) packen.

2026-03-07 | Frontend | URL.revokeObjectURL sofort nach click() → Race Condition

Problem: URL.revokeObjectURL(url) wurde synchron nach a.click() aufgerufen. click() für Downloads ist in manchen Browsern asynchron — die Object-URL wird freigegeben bevor der Browser-Download starten kann → leere/korrupte Datei. Lösung: setTimeout(() => URL.revokeObjectURL(url), 100) — gibt dem Browser 100ms Zeit den Download zu registrieren, bevor die In-Memory-URL freigegeben wird. Gilt für: Alle programmatischen Blob-Downloads via createObjectURL + a.click().


2026-03-07 | Media Import | Falsche asset_type-Klassifizierung durch Dateinamen-Matching

Problem: import_existing_media_assets klassifizierte Dateien als turntable weil der Dateiname "Turntable" enthielt — unabhängig von der tatsächlichen Dateiendung. Poster-Frame-Bilder (F-802007_Turntable_Video_White.jpg) wurden als asset_type=turntable gespeichert. In der Media Browser UI wurde versucht, diese .jpg-Dateien als <video> zu rendern → kaputtes Video-Element. ZIP-Download lieferte .jpg statt .mp4. Lösung:

  1. Daten-Fix: UPDATE media_assets SET asset_type='still' WHERE asset_type='turntable' AND (storage_key LIKE '%.jpg' OR mime_type LIKE 'image/%') — 6 Assets reklassifiziert.
  2. Code-Fix: isVideoAsset() und isImageAsset() nutzen jetzt zusätzlich mime_type zur Entscheidung. Turntable + image/jpeg MIME → als Bild rendern, nicht als Video. Regel: Asset-Typ-Klassifizierung IMMER aus mime_type + Dateiendung ableiten, nie nur aus Dateiname. MIME-Type ist die verlässlichste Quelle.