Files
HartOMat/LEARNINGS.md
T
Hartmut bfd58e3419 fix: media thumbnails, product dimensions, inline 3D viewer, GLB export
Bug A: Media Library thumbnails were gray because <img src> cannot send
JWT auth headers. Added useAuthBlob() hook (fetch + createObjectURL) in
MediaBrowser.tsx. Also fixed publish_asset Celery task to populate
product_id + cad_file_id on MediaAsset for thumbnail fallback resolution.

Bug B: Product dimensions now shown in Product Details card with Ruler
icon and "from CAD" label when cad_mesh_attributes.dimensions_mm exists.

Bug C: Replaced 128×128 CAD thumbnail with InlineCadViewer component.
Queries gltf_geometry MediaAssets, fetches GLB via auth fetch → blob URL
→ Three.js Canvas with OrbitControls. Falls back to thumbnail + "Load 3D
Model" button. Polling when GLB generation is in progress.

Bug D: trimesh was in [cad] optional extra but Dockerfile only installed
[dev]. Changed to pip install -e ".[dev,cad]" — trimesh now available in
backend container, GLB + Colors export works.

Also added bbox extraction (STL-first numpy parsing) in render_step_thumbnail
and admin "Re-extract CAD Metadata" bulk endpoint.

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

472 lines
47 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Projekt-Learnings — Schaeffler Automat
## Format
**Datum | Kategorie | Problem → Lösung**
---
## Learnings
### 2026-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.