# 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`/`.webm` → `turntable`, alles andere → `still`. `is_animation` Flag ist für OutputType-Konfiguration, nicht für Asset-Klassifizierung.
**Für künftige Projekte**: MIME-Typ/Extension immer als primäre Typ-Quelle, niemals Meta-Flags des Auftrags.

### 2026-03-07 | OCC | Bounding-Box aus STEP mit `Bnd_Box` + `brepbndlib.Add()`
**Problem**: Keine Real-World-Dimensions in der DB — weder Breite/Höhe/Tiefe noch Bauteil-Mittelpunkt. OCC-Extraktion lieferte nur Kanten-Topologie.
**Lösung**: `from OCC.Core.Bnd import Bnd_Box; from OCC.Core.BRepBndLib import brepbndlib; bbox = Bnd_Box(); brepbndlib.Add(shape, bbox); xmin,ymin,zmin,xmax,ymax,zmax = bbox.Get()` → `dimensions_mm = {x, y, z}` in `mesh_attributes` JSONB. Kein neues DB-Feld nötig — JSONB-Erweiterung reicht.
**Für künftige Projekte**: OCC `Bnd_Box` gibt Werte in mm (STEP-Einheit). In Blender nach Scale-Apply (0.001) sind die Werte dann in m.

### 2026-03-07 | Storage | `storage_key` absolute Pfade brachen Volume-Moves
**Problem**: `step_tasks.py` und `admin.py` schrieben `storage_key=str(output_path)` mit absoluten Pfaden (`/shared/data/uploads/...`). Nach Volume-Umzug in v2 waren 398 Assets nicht mehr erreichbar.
**Lösung**: `_normalize_key()` Helper: strippt `UPLOAD_DIR`-Prefix. In `download_asset` Legacy-Remapping für alte Pfade als Fallback behalten. Neue Assets immer relativ speichern.
**Für künftige Projekte**: `storage_key` immer relativ zu `UPLOAD_DIR` → `candidate = Path(settings.upload_dir) / key`. Absolute Pfade nie in die DB schreiben.

### 2026-03-07 | Workflow | Turntable-Workflow brauchte step_path zur Laufzeit
**Problem**: `WorkflowDefinition.config` ist statisch (JSON) — enthält keine produktspezifischen Pfade. `_build_turntable()` erwartet `step_path` + `output_dir` in params → `ValueError` bei Workflow-Dispatch.
**Lösung**: `dispatch_render_with_workflow()` löst `step_path` + `output_dir` aus dem `OrderLine → Product → CadFile` Graph auf und injiziert sie in params vor `dispatch_workflow()`.
**Für künftige Projekte**: Workflow-Configs müssen zwischen statischen Parametern (engine, samples) und laufzeit-abhängigen (Dateipfade, IDs) unterscheiden. Letztere immer im Dispatch-Service auflösen.

### 2026-03-06 | Docker | `COPY --from=docker-cli cli-plugins` schlägt fehl wenn Pfad nicht existiert
**Problem**: `docker:cli` Image hat `/usr/local/bin/docker` aber KEIN `/usr/local/lib/docker/cli-plugins` Verzeichnis — `COPY --from` bricht ab.
**Lösung**: Nur `/usr/local/bin/docker` kopieren. Compose-Plugin wird über `docker compose` (space, nicht `-`) aufgerufen — das Binary enthält compose bereits bei neueren docker:cli Images.

### 2026-03-06 | OCC | Dihedralwinkel für sharp-edge Extraktion aus STEP
**Problem**: STEP-Dateien enthalten B-Rep Topologie, STL verliert Kantendaten. Blender braucht Winkelinformation für mark_sharp / UV-Seams.
**Lösung**: `topexp.MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, edge_face_map)` liefert alle Face-Paare pro Edge. `BRepAdaptor_Surface.DN()` berechnet Flächennormalen. Medianwinkel der Hartkanten → `suggested_smooth_angle`. Midpunkte in `sharp_edge_midpoints` für KD-Tree-Matching in Blender.
**Wichtig**: Nur im render-worker Container verfügbar (OCC + cadquery). Backend-Container gibt gracefully `{}` zurück.

### 2026-03-06 | Blender | UV-Seams aus sharp edges ableiten
**Problem**: Ohne korrekte UV-Seams wird Texturmapping auf Lagerteilen fehlerhaft.
**Lösung**: Nach `edges_select_sharp(sharpness=radians(angle))` → `mark_sharp()` → `mark_seam(clear=False)`. Optional: bmesh KD-Tree für OCC-Midpoints (Toleranz 0.5mm, vor scale(0.001)). Aufruf nach jedem _apply_smooth() in Mode A + B.

### 2026-03-06 | Workflow-Dispatch | dispatch_render_with_workflow als Drop-in für dispatch_order_line_render
**Problem**: Legacy-Code rief `dispatch_order_line_render.delay()` direkt auf. Neue Workflow-Canvas-Engine wurde nie aktiviert.
**Lösung**: `dispatch_render_with_workflow(order_line_id)` ist synchron (Celery-safe), lädt OutputType.workflow_definition_id, nutzt Canvas wenn gesetzt, fällt sonst auf Legacy zurück. In `dispatch_renders()` als Drop-in-Replacement mit try/except-Fallback.

### 2026-03-06 | Celery | `@shared_task` verbindet sich mit localhost statt Redis-Container
**Problem**: Neuer Celery-Task in `app/domains/materials/tasks.py` mit `@shared_task` (aus `celery`) statt `@celery_app.task` — beim Aufruf via FastAPI-Endpoint kam `kombu.exceptions.OperationalError: [Errno 111] Connection refused` weil `@shared_task` keinen expliziten App-Kontext hat und daher den Default-Broker `localhost:6379` nutzt, nicht `redis://redis:6379/0`.
**Lösung**: Immer `from app.tasks.celery_app import celery_app` importieren und `@celery_app.task(...)` nutzen. `@shared_task` nur verwenden wenn der Modul garantiert nach `celery_app.py` geladen wird (was in Domain-Modulen nicht der Fall ist).

### 2026-03-06 | SQLAlchemy | Relationship-Auflösung schlägt fehl wenn Models nicht alle importiert sind
**Problem**: Celery-Task importierte nur `AssetLibrary`, aber `Material.creator` hat eine String-Relationship zu `"User"`. SQLAlchemy kann den String-Verweis nur auflösen wenn `User` bereits im Mapper registriert ist → `InvalidRequestError: 'User' failed to locate a name`.
**Lösung**: `import app.models  # noqa: F401` vor dem ersten DB-Zugriff in Celery-Tasks einfügen. Das `__init__.py` importiert alle 14 Modelle und registriert sie alle im SQLAlchemy-Mapper.

### 2026-03-06 | MinIO / Storage | `storage.upload()` erwartet `Path`, nicht `str`
**Problem**: `store_stl_cache(step_hash, quality, stl_path: str)` übergab einen `str` an `storage.upload(local_path, key)`. Die Implementierung nutzt intern `local_path.name` — `str` hat kein `.name`-Attribut → `AttributeError: 'str' object has no attribute 'name'`. STL-Dateien wurden lokal gespeichert, aber nie in MinIO gecacht.
**Lösung**: `storage.upload(Path(stl_path), key)` — immer `Path`-Objekt übergeben. Generell: alle `storage.upload()`-Aufrufe mit explizitem `Path()`-Cast absichern.

### 2026-03-06 | Blender / Scripts | `catalog_assets.py` Pfad in Docker falsch
**Problem**: Script-Pfad via `Path(__file__).parent...` aufgelöst — in Docker zeigt `__file__` auf den Python-Pfad im `backend`-Container, nicht im `render-worker`. Der render-worker kopiert Scripts nach `/render-scripts/` (via `COPY render-worker/scripts/ /render-scripts/`).
**Lösung**: `RENDER_SCRIPTS_DIR` Env-Var nutzen: `Path(os.environ.get("RENDER_SCRIPTS_DIR", "/render-scripts")) / "catalog_assets.py"` — identisch zur Konvention in `domains/rendering/tasks.py`.

### 2026-01-15 | Architektur | Backend-Port-Konflikt
**Problem:** FastAPI standardmäßig auf Port 8000 — war auf dem Entwicklungsrechner belegt
**Lösung:** Port 8888 in `docker-compose.yml` und Vite-Proxy konfiguriert
**Für künftige Projekte:** Port früh festlegen und in CLAUDE.md dokumentieren

---

### 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:**
```python
# app/models/user.py (Compat-Shim)
from app.domains.auth.models import User
__all__ = ["User"]
```
**Für künftige Projekte:** Immer Compat-Shims anlegen vor dem Verschieben; erst nach vollständiger Migration aller Imports die Shims entfernen

---

### 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_render` → `dispatch_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:0` → `overlay=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()`.

---

### 2026-03-07 | Frontend Auth | Bearer-Token bei direktem Link-Download fehlt
**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_DIR` → `settings.upload_dir` korrigiert.
**Quality Gate:** `docker compose exec backend python -c "from app.config import settings; print(settings.upload_dir)"` als Smoke-Test für Config-Zugriff.


---

### 2026-03-07 | Media ZIP | MIME-Type-basierte Extension → ".bin" statt ".png"
**Problem:** `zip_download` ermittelte Datei-Extension via `(a.mime_type or "").split("/")[-1] or "bin"`. Für Assets mit `mime_type=None` (importierte Flamenco-Renders) → Extension `"bin"` → Dateien im ZIP als `.bin` statt `.png`/`.jpg` — ZIP öffnet, aber keine Bilder erkennbar.
**Lösung:** Extension primär aus `Path(storage_key).suffix` lesen — der storage_key enthält immer die echte Datei-Extension. MIME-Type nur als Fallback. Zusätzlich: Original-Dateiname aus `storage_key` statt generischem `{type}_{uuid}.{ext}` verwenden. Duplikat-Filenames (mehrere Assets mit gleichem Dateinamen) werden mit `_1`, `_2` Suffix dedupliziert.
**Regel:** Datei-Erweiterung IMMER aus dem tatsächlichen Dateinamen (storage_key) lesen, nie nur aus MIME-Type. MIME-Types können null sein oder nicht dem tatsächlichen Format entsprechen.

---

### 2026-03-07 | 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 als `src` nutzen. Cleanup via `URL.revokeObjectURL` + `cancelled`-Flag gegen Race Conditions.
**Für künftige Projekte:** Jeder auth-geschützte Media-Endpoint der in `<img>`/`<video>` eingebettet wird, braucht einen Blob-URL-Wrapper. Alternativ: kurzzeitige signed URLs (S3-Presigned) auf Backend-Seite.

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