Files
HartOMat/LEARNINGS.md
T
HartmutandClaude Sonnet 4.6 3401b06b19 feat: BLENDER_CINEMATIC workflow graph node (M1)
Add StepName.BLENDER_CINEMATIC and full graph runtime support so cinematic
output types can be promoted from legacy_only to graph/shadow rollout mode.

- process_steps.py: add BLENDER_CINEMATIC = "blender_cinematic" enum value
- workflow_executor.py: map to render_cinematic_task in STEP_TASK_MAP
- workflow_node_registry.py: node definition with render/scene/camera fields
  (no animation params — cinematic is fixed at 250 frames @ 25fps)
- workflow_graph_runtime.py: _ORDER_LINE_RENDER_STEPS, _CINEMATIC_TASK_KEYS,
  shadow queue routing, predict_render_output_artifact (mp4),
  _build_task_kwargs, _artifact_kind_override_for_step
- tasks.py: _normalize_cinematic_params + render_cinematic_task Celery task
  (calls render_cinematic_to_file, publishes as turntable asset type since mp4)

No DB migration needed: admins can now manually set cinematic output types
to graph rollout mode via the admin panel and assign a workflow definition.

docs: learnings erfasst — BLENDER_CINEMATIC workflow graph node M1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-22 15:09:47 +02:00

61 KiB
Raw Blame History

Projekt-Learnings — HartOMat

Format

Datum | Kategorie | Problem → Lösung


Learnings

2026-07-22 | Architecture | BLENDER_CINEMATIC Workflow-Graph-Node implementiert

Der cinematic Render-Pfad hatte keinen eigenen Workflow-Graph-Node — Migration 071 hatte alle cinematic Output-Types auf legacy_only gezwungen als Sicherheitsnetz. M1 fügt jetzt StepName.BLENDER_CINEMATIC hinzu, zusammen mit: (1) Node-Definition im workflow_node_registry.py mit denselben Szene/Camera/Material-Feldern wie BLENDER_STILL, aber ohne Animations-Params (frame_count/fps sind im cinematic_render.py-Script hartkodiert auf 250 @ 25fps), (2) render_cinematic_task in tasks.py — folgt dem Pattern von render_order_line_still_task, gibt mp4 aus, nutzt _finalize_graph_turntable_output/_finalize_shadow_turntable_output da cinematic = mp4, published mit asset_type="turntable", (3) STEP_TASK_MAP + _ORDER_LINE_RENDER_STEPS + _build_task_kwargs + _predict_render_output_artifact + _artifact_kind_override_for_step in workflow_graph_runtime.py alle aktualisiert. Kein neues DB-Migration nötig: Admins können cinematic Output-Types jetzt manuell von legacy_only auf graph umstellen und ein Workflow-Definition mit BLENDER_CINEMATIC-Node zuweisen.

2026-07-22 | Architecture | Cinematic Output Type existierte bereits mit falschem workflow_rollout_mode

Der "Cinematic Highlight" Output-Type war seit März 2026 in der DB mit workflow_rollout_mode = shadow und einer workflow_definition_id gesetzt. Da es keinen BLENDER_CINEMATIC-Node im Workflow-Graph gibt, hätte jeder Cinematic-Render eine Shadow-Graph-Execution ausgelöst, die still scheitert. Fix via Migration 071: alle cinematic Output-Types auf legacy_only + workflow_definition_id = NULL patchen. Zusätzlich API-Guard in POST/PATCH output_types.py eingebaut: cinematic + workflow_definition_id → 400 Error. Defense-in-depth im dispatch_service.py: frühzeitiger Legacy-Exit wenn render_settings.cinematic = true.

2026-03-15 | Architecture | Per-order-line render overrides via JSONB

Render overrides (JSONB) on OrderLine allow overriding any output type render setting (format, resolution, samples, engine, etc.) at order time without duplicating output types. Applied AFTER output type render_settings AND after transparent_bg/cycles_device_val assignment, so they take final priority. Also affects dispatch queue routing (width/height overrides change light vs heavy queue routing).

2026-03-14 | Blender | OBJ-Rotation Turntable: Reparenting von USD-Parts verschiebt Geometrie

USD-importierte Parts haben existierende Eltern-Objekte (Xform-Nodes). part.parent = pivot; part.matrix_parent_inverse = pivot.matrix_world.inverted() verliert den Beitrag des alten Parents zur Welt-Position → Parts verschieben sich ~14m. Fix: World-Matrix vor Reparenting sichern, dann via part.matrix_local = pivot.inverted() @ saved_world wiederherstellen. Zusätzlich bpy.context.view_layer.update() vor dem Parenting aufrufen, damit pivot.matrix_world aktuell ist.

2026-03-13 | Blender/USD | USD-Import erzeugt leere Material-Stubs → schwarze Renders

Blender's bpy.ops.wm.usd_import() erstellt leere Material-Stubs (use_nodes=True, nodes=0) aus USD MaterialBinding-Referenzen. Wenn apply_material_library_direct dann die echten Materialien aus der .blend-Bibliothek laden will, findet es die Stubs bereits in bpy.data.materials und nutzt sie direkt — ohne die echten Shader-Node-Trees zu laden. Leere Materialien rendern in Cycles als pures Schwarz (RGB max=1). Fix: _find_material_with_nodes() Hilfsfunktion, die .NNN-Suffix-Kollisionen auflöst und nur Materialien mit echten Nodes zurückgibt.

2026-03-12 | Caching | Composite Cache Keys für Tessellierung

Hash-basiertes Caching in Celery Tasks muss alle relevanten Parameter einschließen, nicht nur den Datei-Hash. Bei generate_gltf_geometry_task und generate_usd_master_task wurde der Cache-Key auf {hash}:{linear}:{angular}:{engine} erweitert. Außerdem: immer Disk-Existenz des gecachten Assets prüfen (storage_key.exists()) bevor ein Cache-Hit zurückgegeben wird — der Asset-Record kann existieren, die Datei aber nicht.

2026-03-12 | React | Modal in braucht createPortal

Ein Modal-Dialog, der aus einer <tr>-Tabellenzeile gerendert wird, erzeugt invalides HTML (<div> in <tr> nicht erlaubt). Fix: createPortal(modal, document.body) — rendert das Modal am Body-Root, außerhalb der Tabellen-Hierarchie.

2026-03-12 | Prozess | ROADMAP war dem Code weit hinterher

Pre-flight code audit vor dem Schreiben eines Plans ist zwingend erforderlich. Beim Sprint vom 2026-03-12 zeigte sich: P1 (dead-code cleanup, blender_render.py split), P5 (USD render wiring), P8 (Celery tenant context) und P10 (UI polish) waren bereits größtenteils im Code implementiert, aber nicht im ROADMAP.md reflektiert. Lesson: Immer zuerst grep/ls-Gates und Line-Count-Checks laufen lassen, bevor Tasks geplant werden.

2026-03-12 | Material | SQLAlchemy Mapper-Fehler bei isoliertem Domain-Import

Wenn eine Celery-Task nur from app.domains.orders.models import Order importiert (ohne app.models), kann SQLAlchemy die String-Referenzen relationship("Template", ...) und relationship("User", ...) nicht auflösen — alle Mapper-Initialisierungen schlagen fehl. Fix: import app.models as _all_models am Modul-Level von beat_tasks.py eintragen. So werden alle Models geladen bevor irgendeine Task-Funktion die Mapper initialisiert. Gilt für alle Tasks die Domain-Models direkt importieren.

2026-03-12 | Material | OCC-Part-Namen vs. USD-Part-Keys beim Material-Matching

OCC XCAF-Namen enthalten Bindestriche und _AF\d+-Suffixe (z.B. GE360-HF-0011-EIN_HAELFTE_AF0_1_AF0). Das USD-Export-Tool slugifiziert den Namen: [^a-z0-9]+_ (d.h. ge360_hf_0011_ein_haelfte_af0_1_af0). Blender importiert das USD-Objekt als ge360_hf_0011_ein_haelfte (base-name aus XCAF). Das Mat-Map aus der DB ist lowercased mit Bindestrichen. Fix: In build_mat_map_lower() auch eine slug-normierte Variante jeden Keys hinzufügen (re.sub(r'[^a-z0-9]+', '_', kl).strip('_')). Der bestehende Prefix-Fallback (key.startswith(part_key)) fangt dann den Rest ab.

2026-03-12 | USD | Stale USD-Master-Assets invalidieren

USD-Assets die vor dem elif shape_has_loc-Fix (Commit de7f97b, 2026-03-12 16:43) generiert wurden, haben fehlerhafte Geometrie (double-transform). Fix: DELETE FROM media_assets WHERE asset_type='usd_master' AND created_at < '2026-03-12 16:43:33' + UPDATE cad_files SET step_file_hash = NULL WHERE id IN (...). Nächster Render generiert neue korrekte USD-Datei.

2026-03-12 | USD | Mesh-Prim-Benennung für Blender 5.0

Blender 5.0 importiert USD und kollabiert single-child Xform+Mesh-Hierarchien zu einem einzigen Objekt. Der Objektname entspricht dabei dem Leaf-Namen des Mesh-Prims (nicht dem Xform). Die Lösung: mesh_path = f"{part_path}/{part_key}" statt f"{part_path}/Mesh". Damit importiert Blender jedes Objekt direkt mit dem korrekten part_key als Namen — kein Post-Import-Rename nötig. Blender setzt obj["usd:path"] nicht, daher ist der pfadbasierte Rename-Ansatz nicht funktionsfähig.

2026-01-15 | Architektur | Backend-Port-Konflikt

Port 8000 war belegt → Port 8888 in docker-compose.yml + Vite-Proxy. Früh festlegen und in CLAUDE.md dokumentieren.

2026-01-20 | Datenbank | SQLAlchemy trackt key-value-Store-Mutations nicht

Admin-Settings via ORM gespeichert → Änderungen nicht persistiert. SQLAlchemy erkennt keine Mutation an geladenen Value-Feldern. Lösung: Direktes session.execute(update(...)) statt ORM-Mutation. Key-Value-Stores immer mit direktem SQL verwalten.

2026-01-25 | Render-Pipeline | Blender ignoriert STEP-Einheiten (mm vs. m)

STEP in mm, Blender in m → 50mm-Lager erscheint 50m breit. _scale_mm_to_m(parts) in allen 3 Render-Scripts: part.scale = (0.001, 0.001, 0.001) + Transform anwenden, direkt nach STL-Import vor Kamera-Kalkulation.

2026-01-28 | Render-Pipeline | Blender 5.0 hat scene.node_tree entfernt

_setup_bg_compositor() → Python-Exception → Blender exit code 0 → Task fälschlicherweise "completed". Lösung: Compositing entfernt; bg_color via FFmpeg (-f lavfi -i color=... + overlay). Pflicht: try: main() except SystemExit: raise except Exception: traceback; sys.exit(1) in allen Blender-Scripts.

2026-02-05 | Material-System | Material-Alias-Lookup-Reihenfolge falsch

Steel--Stahl war sowohl Material.name als auch Alias für HARTOMAT_010101_Steel-Bare. Lookup fand zuerst den Namen → Blender konnte ihn nicht in der Library finden. Lösung: material_service.py: Aliases zuerst, dann exakter Name, dann Pass-through.

2026-02-10 | Render-Pipeline | Blender-Template überschreibt HDRI/World

Im Template-Modus (Mode B) lief Auto-Licht/World-Setup bedingungslos → überschrieb HDRI aus .blend-Template. Lösung: In Template-Mode Lights, World und Color-Management-Override vollständig überspringen; nur Kamera ggf. neu berechnen.

2026-02-15 | Celery | Blender-Queue-Flooding durch falsche Concurrency

Alle Tasks auf step_processing (concurrency=8) → 8 Workers gleichzeitig an blender-renderer (max 1) → 7× Timeout. Lösung: process_step_file (step_processing, concurrency=8) nur schnelle Metadata; render_step_thumbnail (asset_pipeline, concurrency=1) für Blender. HTTP-Services mit max 1 Request immer auf eigener Queue mit concurrency=1.

2026-02-18 | Frontend | Tailwind CSS-Variablen inkompatibel mit opacity-Syntax

bg-surface/50 erzeugt rgb(var(--color-bg-surface) / 0.5) — invalides CSS wenn Variable ein Hex-Wert ist. Lösung: style={{ backgroundColor: 'var(--color-bg-surface)' }}. Alternativ: CSS-Vars im RGB-Channel-Format definieren (255 255 255).

2026-02-20 | STL-Cache | Three.js-Renderer nutzte tempfile → kein Download

Three.js schrieb STL in tempfile und löschte es → Download-Endpoint fand nichts. Lösung: Persistent cache: step_path.parent / f"{step_path.stem}_low.stl", Cache-Hit-Check vor Konvertierung, kein unlink(). Konvention {stem}_{quality}.stl neben STEP-Datei von Anfang an in allen Renderern.

2026-02-20 | STL-Cache | blender-renderer fehlte /convert-stl Endpoint

Blender renderte + konvertierte in einem Schritt, persistierte STL nicht. Lösung: Neuer /convert-stl Endpoint (STEP→STL ohne Render), Celery-Task generate_stl_cache auf asset_pipeline, Admin-Batch-Funktion "Generate Missing STLs".

2026-02-22 | Material-System | Fehlender Alias blockiert Material-Replacement

"Stahl v2" in DB nicht in materials noch in material_aliases → keine Ersetzung, Silent-Fail. Lösung: Alias direkt in DB eintragen. Bei Render ohne Materialersetzung immer zuerst resolve_material_map() debuggen, Alias-Tabelle prüfen.

2026-02-25 | Frontend | canDispatch-Bedingung zu restriktiv

canDispatch enthielt && hasRetryable → Button fehlte wenn alle Zeilen initial "pending". Lösung: hasRetryable entfernt; Validierung im Backend. Aktions-Buttons nicht zu stark von abgeleiteten Zuständen abhängig machen.

2026-02-28 | Frontend | MaterialInput-Dropdown ohne Hintergrund

bg-surface mit CSS-Hex-Variable → transparenter Dropdown (siehe 2026-02-18). Lösung: style={{ backgroundColor: 'var(--color-bg-surface)' }} für Dropdown-Container, Group-Header, Sticky-Button. Datei: frontend/src/components/shared/MaterialInput.tsx.

2026-03-06 | Refactor | .gitignore core trifft Verzeichnisse

core in .gitignore ignorierte backend/app/core/ Verzeichnis. Lösung: /core (root-relativ) — trifft nur Datei im Root, nicht verschachtelte Verzeichnisse.

2026-03-06 | Architektur | Blender-HTTP-Service vs. direkter Subprocess

Flask-HTTP-Microservice = Single-Point-of-Failure, kein Scaling, HTTP-Overhead. Lösung: Celery render-worker Container — Blender direkt via subprocess.run. is_blender_available() prüft BLENDER_BIN env var; Backend-Container fallen auf Pillow zurück. Subprocess > HTTP für blocking compute tasks.

2026-03-06 | Refactor | Bash CWD-Problem durch Hook-Pfad-Auflösung

Nach cd frontend im Bash-Tool blieb CWD in frontend/ → Hook-Pfad nicht gefunden → alle Tool-Calls blockiert. Lösung: Symlink frontend/.claude → .claude. Hooks mit absoluten Pfaden konfigurieren; cd nur mit && am Ende der Command-Chain.

2026-03-06 | Multi-Tenancy | PostgreSQL RLS mit current_setting und Null-Safety

current_setting('app.current_tenant_id') wirft Exception wenn nicht gesetzt. Lösung: current_setting('app.current_tenant_id', true) — zweites Argument macht Funktion Null-safe. Admin-Bypass: separate Policy mit SET LOCAL app.current_tenant_id = 'bypass'.

2026-03-11 | Tessellation | GMSH CharacteristicLength ≠ OCC linear_deflection

OCC linear_deflection ist ein Oberflächenabweichungs-Toleranzwert (max. Abstand Mesh→echte Fläche). GMSH CharacteristicLengthMax ist eine Kantenlängenvorgabe. Gleicher Wert (0.1) erzeugt bei GMSH 50× mehr Dreiecke → 231MB statt 3MB. Lösung: CharacteristicLengthMax = linear_deflection * 15.0 (15× Faktor). MinimumCirclePoints = min(20, ceil(2π/angular_deflection)) — ohne Cap liefert angular_deflection=0.1rad → 63 Punkte/Kreis (10× zu dicht). Mit Cap 20: ~20MB statt 231MB, OCC-ähnliche Dichte bei 0 Fan-Dreiecken.

2026-03-11 | Tessellation | BRep_Builder.UpdateFace — richtige Signatur

OCP Python API: BRep_Builder.UpdateFace(face, triangulation) — 2-Argumente-Form, NICHT (face, tri, loc, tolerance) wie in C++-Doku. Falsche Signatur führt zu Silent-Exception, alle Faces fallen auf BRepMesh zurück.

2026-03-11 | Tessellation | GMSH OOM bei Assembly-Compound

GMSH verarbeitet ganzen Compound auf einmal → 25-teilige Lager-Baugruppe: 2.3GB RAM → OOM-Kill (exit -9). Lösung: Per-Solid-Iteration via TopExp_Explorer(root_shape, TopAbs_SOLID). BRep_Builder.UpdateFace aktualisiert Face-Objekte in-place; Parent-Compound sieht Updates automatisch.

2026-03-11 | Celery | Timeout in Worker-Code ≠ Running Worker liest neue Version

export_glb.py mit 600s-Timeout in Container-Datei — aber Celery-Worker hatte Code beim Start geladen. Fehler zeigt timeout=120 obwohl Datei 600 enthält. Lösung: docker compose restart render-worker nach Datei-Update. Celery lädt Module beim Start, nicht bei Task-Ausführung.

2026-03-06 | Refactor | Domain-Driven Migration: Compat-Shims statt Big-Bang

Vollständige Migration in einem Schritt bricht alle Imports. Lösung: Alte Dateien werden Re-Export-Shims: from app.domains.auth.models import User; __all__ = ["User"]. Erst nach vollständiger Import-Migration Shims entfernen.

2026-03-06 | Workflow-System | Celery Canvas vs. Custom Workflow-Engine

Custom Graph-Traversal-Engine = 2-3 Wochen Eigenentwicklung. Lösung: Celery Canvas (chain, group, chord) als Execution-Engine. dispatch_workflow(type, order_line_id, params) baut Canvas dynamisch aus Config. Kein Backward-Compat nötig: Celery Canvas reicht für parallele/sequentielle Workflows.

2026-03-06 | Circular Import | template_service ↔ domains/rendering/service

Beide Module waren Shims die aufeinander zeigten → resolve_template() nie aufrufbar → alle Renders crashten mit ImportError. Lösung: template_service.py aus git-History wiederhergestellt (echte Implementierung). domains/rendering/service.py importiert nur davon. Nach Refactoring: grep -rn "def resolve_template" muss ≥1 Treffer liefern.

2026-03-06 | Multi-Tenancy | audit_log.tenant_id NOT NULL blockiert alle Notifications

Migration 036 machte audit_log.tenant_id NOT NULL, aber emit_notification setzt kein tenant_id → INSERT schlägt fehl → 500 bei Order-Submit. Lösung: ALTER TABLE audit_log ALTER COLUMN tenant_id DROP NOT NULL. Broadcast-Notifications (system-weit) dürfen NULL tenant_id haben.

2026-03-06 | Frontend | GET /api/tenants gibt 307 Redirect zurück

FastAPI registriert /tenants/ (trailing slash) → GET /tenants → 307. Axios folgt, verliert Authorization-Header → 401. Lösung: getTenants() auf /tenants/ (mit trailing slash). Generell: FastAPI 307-Redirects verlieren Auth-Header in Axios. Immer trailing slash im Frontend verwenden.

2026-03-06 | Celery Canvas | workflow_builder: order_line_id als step_path übergeben

render_still_task.si(order_line_id, **params) — Task erwartet step_path: str. Blender versuchte UUID als Pfad → crash. Lösung: Separate render_order_line_still_task die intern OrderLine→Product→CadFile auflöst. Workflow-Builder-Tasks dürfen nie Domain-IDs als file-path-Argumente verwenden.

2026-03-06 | Docker | docker compose in Container braucht multi-stage CLI-Copy

python:3.11-slim hat kein docker binary. Lösung: COPY --from=docker:cli /usr/local/bin/docker /usr/local/bin/docker + cli-plugins. Docker-Socket mounten (/var/run/docker.sock), Compose-File als Volume, COMPOSE_PROJECT_DIR env var.

2026-03-06 | Docker | COPY --from=docker-cli cli-plugins schlägt fehl

docker:cli Image hat kein /usr/local/lib/docker/cli-plugins Verzeichnis. Lösung: Nur /usr/local/bin/docker kopieren — neuere docker:cli Images enthalten compose bereits im Binary.

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

Auf Debian bookworm heißt das Paket libgdk-pixbuf-2.0-0 (mit Bindestrichen). apt-get install mit falschem Namen → exit 100. Regel: Immer apt-cache search libgdk im Container prüfen.

2026-03-06 | React Three Fiber | Wireframe-Toggle über Material-Clone

GLTF-Materialien sind shared objects — direkte Mutation von child.material.wireframe beeinflusst alle Instanzen. Lösung: child.material = child.material.clone() vor Wireframe-Mutation. GLTF-Materialien bei Runtime-Modifikationen immer zuerst clonen.

2026-03-06 | pytest | Backend ohne dev-Dependencies

pip install -e . ohne [dev] → kein pytest → ModuleNotFoundError. Lösung: pip install -e ".[dev]". Dev-Extras immer im Dockerfile angeben wenn Tests im Container laufen sollen.

2026-03-06 | Celery | @shared_task verbindet sich mit localhost statt Redis-Container

@shared_task aus celery ohne App-Kontext → Default-Broker localhost:6379kombu.exceptions.OperationalError. Lösung: Immer from app.tasks.celery_app import celery_app + @celery_app.task(...). @shared_task nur wenn Modul garantiert nach celery_app.py geladen wird.

2026-03-06 | SQLAlchemy | Relationship-Auflösung schlägt fehl wenn Models nicht importiert

Celery-Task importierte nur AssetLibraryMaterial.creator → String-Ref "User" nicht im Mapper → InvalidRequestError. Lösung: import app.models # noqa: F401 vor erstem DB-Zugriff in Celery-Tasks — __init__.py registriert alle 14 Modelle.

2026-03-06 | MinIO / Storage | storage.upload() erwartet Path, nicht str

storage.upload(str_path, key)AttributeError: 'str' has no attribute 'name' → STLs lokal gespeichert aber nie in MinIO. Lösung: storage.upload(Path(stl_path), key) — alle upload()-Aufrufe mit Path()-Cast absichern.

2026-03-06 | Blender / Scripts | catalog_assets.py Pfad in Docker falsch

Path(__file__).parent zeigt auf Backend-Container-Pfad, nicht auf render-worker /render-scripts/. Lösung: RENDER_SCRIPTS_DIR Env-Var: Path(os.environ.get("RENDER_SCRIPTS_DIR", "/render-scripts")) / "catalog_assets.py".

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

Enum(create_type=False) unzuverlässig mit asyncpg. Lösung: Raw SQL: DO $$ BEGIN CREATE TYPE ...; EXCEPTION WHEN duplicate_object THEN NULL; END $$;. Für Tabellen: CREATE TABLE IF NOT EXISTS.

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

dispatch_order_line_render → Shim A→B→A → Render startet nie. Lösung: dispatch_order_line_render direkt auf render_order_line_task.delay(). Den echten Aufruf-Pfad API→Task vor Refactoring dokumentieren.

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

Task auf step_processingworker-Container (kein Blender) → is_blender_available() = False → Pillow-Placeholder, kein Fehler. Lösung: Queue auf asset_pipelinerender-worker-Container (Blender 5.0.1 + cadquery). Blender-Tasks IMMER auf asset_pipeline.

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

Zwei Services unterschiedlicher Capabilities auf gleicher Queue → round-robin → 50% Silent-Fail. Lösung: worker-thumbnail aus docker-compose entfernt. render-worker ist alleiniger Consumer. Nie zwei Services mit unterschiedlichen Fähigkeiten auf dieselbe Queue.

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

Migration 036 machte tenant_id NOT NULL auf orders/order_lines/order_items → alle Create-Endpoints übergaben das Feld nicht. Lösung: tenant_id=getattr(user, 'tenant_id', None) in allen Model-Konstruktoren. Nach jeder RLS-Migration alle Create-Endpoints auf neue Pflichtfelder prüfen.

2026-03-06 | Workflow-Dispatch | dispatch_render_with_workflow als Drop-in

Legacy dispatch_order_line_render.delay() wurde nie durch Workflow-Engine ersetzt. Lösung: dispatch_render_with_workflow(order_line_id) lädt OutputType.workflow_definition_id, nutzt Canvas wenn gesetzt, fällt sonst auf Legacy zurück.

2026-03-06 | OCC | RWMesh_CoordinateSystemConverter nicht als Python-Binding verfügbar

writer.ChangeCoordinateSystemConverter()TypeError: Unregistered type. Lösung: Shapes vor Export mit BRepBuilderAPI_Transform(shape, trsf, True) um Faktor 0.001 skalieren. RWGltf_CafWriter direkt ohne Koordinatensystem-Konverter aufrufen.

2026-03-06 | OCC | XCAFDoc_ShapeTool.GetComponentsGetComponents_s

In OCP alle XCAF static-Methoden mit _s-Suffix. Gilt für alle XCAFDoc_*-Klassen.

2026-03-06 | Pipeline | OCC-native STEP→GLB ersetzt STL-Intermediary

STL verliert Materialien/Farben, doppelter Aufwand, Dateiflut. Lösung: RWGltf_CafWriter + STEPCAFControl_Reader: STEP→GLB direkt. BRepMesh_IncrementalMesh tesselliert vor Export. OCP (cadquery's Bindings, NICHT OCC.Core), statische Methoden mit _s-Suffix.

2026-03-06 | Celery | generate_gltf_geometry_task als Subprocess

OCP + bpy können nicht im selben Python-Prozess koexistieren (native C++ Konflikt). Lösung: export_step_to_gltf.py via subprocess.run([sys.executable, script, ...], timeout=120) — OCC läuft isoliert.

2026-03-06 | OCC | Dihedralwinkel für sharp-edge Extraktion aus STEP

topexp.MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, map) → Face-Paare pro Edge. BRepAdaptor_Surface.DN() für Normalen. Medianwinkel → suggested_smooth_angle, Midpunkte → sharp_edge_midpoints für KD-Tree in Blender. Nur im render-worker verfügbar.

2026-03-06 | Blender | UV-Seams aus sharp edges ableiten

Nach edges_select_sharp(sharpness=radians(angle))mark_sharp()mark_seam(clear=False). Optional: bmesh KD-Tree für OCC-Midpoints (Toleranz 0.5mm vor scale(0.001)).

2026-03-06 | Multi-Tenancy | audit_log.tenant_id NOT NULL — Broadcast-Events

emit_notification setzt kein tenant_id → INSERT fehlschlägt → 500 bei Order-Submit. Lösung: tenant_id DROP NOT NULL. Broadcast-Notifications (kein konkreter Tenant) brauchen nullable tenant_id.

2026-03-06 | Bbox | GLB statt STL für Bounding-Box Extraktion

_bbox_from_stl() obsolet nach Pipeline-Umbau. Lösung: _bbox_from_glb() mit trimesh: scene.bounds * 1000 für mm. Fallback auf _bbox_from_step_cadquery().

2026-03-06 | Workflow | Turntable-Workflow brauchte step_path zur Laufzeit

WorkflowDefinition.config ist statisch → kein step_pathValueError. Lösung: dispatch_render_with_workflow() löst step_path + output_dir aus OrderLine→Product→CadFile auf und injiziert sie vor Dispatch. Statische (engine, samples) von laufzeit-abhängigen (Pfade, IDs) trennen.

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

bpy.data.libraries.load(blend_path, link=True, assets_only=True) liefert nur via "Mark as Asset" markierte Datenblöcke. Lösung: In .blend: jedes Material/Node-Group via "Mark as Asset" markieren. catalog_assets.py filtert via m.asset_data is not None.

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

celery_app.control.inspect().active_queues() zeigt pro Worker welche Queues er konsumiert. Besser als Worker-Namen-Heuristiken für render_worker_connected/blender_available-Detection.

2026-03-06 | TypeScript | Test-Dateien aus Haupt-tsconfig ausschließen

vitest/msw-Imports in src/__tests__/ → TypeScript-Fehler in tsc --noEmit (Types nur im Test-Kontext). Lösung: "exclude": ["src/__tests__"] in tsconfig.json. Vitest prüft eigene Typen intern.

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

lavfi color-Stream hat unendliche Dauer → ffmpeg wartet nach PNG-Sequenz-Ende unbegrenzt. Lösung: overlay=0:0:shortest=1. Bei ffmpeg-Overlays mit lavfi/nullsrc als Input IMMER shortest=1.

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

Browser-WebSocket-API kann keine Authorization-Header senden. Lösung: JWT als Query-Parameter: ws://host/api/ws?token=<jwt>. Token ist in Logs sichtbar — für v3: kurzlebigen WS-Token (TTL 30s) generieren.

2026-03-07 | Security | Media-Endpoints ohne Auth

list_assets, download_asset, zip_download ohne get_current_user-Dep → unauthentifizierte Requests. RLS schützt nur DB, nicht HTTP. Lösung: _user: User = Depends(get_current_user) zu allen drei. Jeder neue Endpoint braucht expliziten Auth-Dep — RLS ist Defense-in-Depth, kein Ersatz.

2026-03-07 | MediaAsset | is_animation Flag entscheidet Asset-Type — falsches Design

is_animation == True setzte asset_type = turntable auch für .jpg Poster-Frames → Broken-Video-Icons. Lösung: Extension entscheidet: .mp4/.webmturntable, alles andere → still. MIME-Typ/Extension immer als primäre Typ-Quelle.

2026-03-07 | OCC | Bounding-Box aus STEP mit Bnd_Box + brepbndlib.Add()

bbox = Bnd_Box(); brepbndlib.Add(shape, bbox)
xmin,ymin,zmin,xmax,ymax,zmax = bbox.Get()

Werte in mm (STEP-Einheit). In mesh_attributes JSONB speichern — kein neues DB-Feld nötig.

2026-03-07 | Storage | storage_key absolute Pfade brachen Volume-Moves

Absolute Pfade in storage_key → nach Volume-Umzug 398 Assets nicht erreichbar. Lösung: _normalize_key() strippt UPLOAD_DIR-Prefix. Legacy-Remapping als Fallback. Neue Assets immer relativ: renders/{uuid}/{filename}.

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

await db.refresh(invoice) lädt nur skalare Spalten → invoice.lines → lazy-load außerhalb Greenlet → MissingGreenlet, HTTP 500. Lösung: Nach db.commit() separate select-Query mit selectinload statt db.refresh().

<a href="/api/billing/invoices/{id}/pdf"> sendet keinen Authorization-Header → 401. Lösung: api.get(..., { responseType: 'blob' })URL.createObjectURL() + programmatischer <a>.click(). Gilt für alle geschützten Download-Endpoints.

2026-03-07 | PostgreSQL RLS | SET LOCAL muss in jeder Transaktion gesetzt werden

GRANT BYPASSRLS TO hartomat schlug still fehl → Admin-Endpoints bekamen 0 Zeilen. Lösung: await db.execute(text("SET LOCAL app.current_tenant_id = 'bypass'")) direkt vor jede RLS-geschützte Query in internen/Admin-Endpoints.

2026-03-07 | trimesh | GLB-Export-Scale: STL in mm → Three.js in Metern

mesh.apply_scale(0.001) nach trimesh.load() vor Export. Bei trimesh.Scene über scene.geometry.values() iterieren. Optional: trimesh.smoothing.filter_laplacian(mesh, lamb=0.5, iterations=5) für smooth normals.

2026-03-07 | React Dashboard | Responsive CSS-Grid mit matchMedia

Inline-Styles für Grid-Positioning haben keine Medienabfrage-Unterstützung. Lösung: useLargeScreen() Hook mit window.matchMedia('(min-width: 1024px)') + Change-Listener → isLarge-Boolean bedingt Inline-Styles.

2026-03-07 | Media Browser | ZIP-Download 22-Byte-Korruption

storage_key absolute Pfade + except Exception: pass → Silent-Fail → leere Archive. Lösung: Pfad-Check vor MinIO-Fallback; except loggt jetzt logger.warning(). In Generator-Funktionen für Streaming IMMER loggen.

2026-03-07 | Frontend | Fehlende React-Imports crashen die gesamte App

useEffect hinzugefügt ohne Import → ReferenceError → Blank Page. ErrorBoundary auf Root-Level fängt nicht ab. Regel: Nach jedem neuen Hook sofort Import-Zeile prüfen. tsc --noEmit als Quality Gate im Container.

2026-03-07 | Storage Keys | Absolute Pfade in DB brechen nach Infrastruktur-Änderung

Flamenco-Outputs in /shared/renders/... → nach Flamenco-Entfernung 396 Assets nicht erreichbar (absoluter Pfad + MinIO-Fallback beide leer). Lösung: Bulk-UPDATE auf relative Keys + Safety-Net-Remapping im Code. storage_key IMMER relativ zu UPLOAD_DIR: renders/{uuid}/{filename}.

2026-03-07 | Config | Pydantic Settings: Attributname case-sensitive

settings.UPLOAD_DIRAttributeError. Korrekt: settings.upload_dir (lowercase wie in config.py). Smoke-Test: docker compose exec backend python -c "from app.config import settings; print(settings.upload_dir)".

2026-03-07 | Media ZIP | MIME-Type-basierte Extension → ".bin" statt ".png"

(mime_type or "").split("/")[-1]"bin" für Assets ohne MIME-Type. Lösung: Extension primär aus Path(storage_key).suffix lesen. MIME-Type nur als Fallback. Duplikat-Filenames mit _1, _2 deduplizieren.

2026-03-07 | Blender 5.0 | export_colors in bpy.ops.export_scene.gltf entfernt

export_colors=Falsekeyword unrecognized → exit 1 → immer Trimesh-Fallback, nie Materialien. Lösung: Parameter entfernen. Gültige Blender-5.0-Parameter: export_format, export_apply, use_selection, export_materials, export_image_format. Nach Blender-Major-Updates alle bpy.ops.*-Parameter prüfen.

2026-03-07 | React | useRef mit if(!ref.current) Guard reagiert nicht auf Prop-Änderungen

GlbModel klonte scene in useRef einmalig → bei neuem url-Prop blieb altes Mesh. Lösung: key={glbBlobUrl} auf <GlbModel> → React remountet, frischer useRef. Alternativ: useMemo mit URL-Dependency.

2026-03-07 | React | staleTime zu hoch verzögert Erkennung neuer API-Daten

staleTime: 30_000 + qc.invalidateQueries() → trotzdem 30s Verzögerung bis neue GLB-URL ankam. Lösung: staleTime: 0 für Queries die nach Mutations sofort aktuell sein müssen.

2026-03-07 | GLB Export | Trimesh kennt keine Materialien — Blender-Pipeline ist Pflicht

Trimesh = reine Geometrie, keine Material-Libraries, kein Asset-Library-Support → graue, facettierte GLBs. Lösung: generate_gltf_geometry_task auf Blender headless (export_gltf.py) umgestellt. Trimesh nur als Fallback. Skript vorhanden ≠ Skript verdrahtet — Aufruf-Pfad immer prüfen.

2026-03-07 | Frontend | <img src> kann keine Auth-Header senden — useAuthBlob Hook nötig

<img src="/api/media/{id}/download"> → kein Authorization-Header → 401 → graues Icon. Lösung: useAuthBlob(url, enabled): fetch(url, { headers: { Authorization } })URL.createObjectURL(blob) als src. Cleanup via revokeObjectURL + cancelled-Flag. Gilt für alle browser-nativen Media-Elemente (<img>, <video>, <audio>).

2026-03-07 | Backend | publish_asset fehlte product_id + cad_file_id

MediaAsset ohne product_id/cad_file_idget_thumbnail_url() konnte keinen Thumbnail-Fallback berechnen → graue Icons. Lösung: In publish_asset: Product laden, product_id=line.product_id + cad_file_id=product.cad_file_id setzen. MediaAssets immer mit allen verfügbaren Referenz-FKs erstellen.

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

useGLTF(url) aus @react-three/drei kann keine Auth-Header senden. Lösung: fetch(url, { headers: { Authorization } }).blob()URL.createObjectURL(blob) → String-URL an useGLTF(blobUrl). Revoke in useEffect-Cleanup. Three.js / drei kennen kein Auth-Konzept.

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

pip install -e ".[dev]" installiert kein trimeshModuleNotFoundError beim ersten Aufruf. Lösung: pip install -e ".[dev,cad]". Beim Hinzufügen optionaler Extras immer prüfen ob alle relevanten Container-Images das Extra installieren.

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

revokeObjectURL(url) synchron nach a.click() → Download manchmal leer (click() ist in manchen Browsern asynchron). Lösung: setTimeout(() => URL.revokeObjectURL(url), 100) für alle programmatischen Blob-Downloads.

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

Dateiname enthielt "Turntable" → asset_type=turntable auch für .jpg Poster-Frames → Browser versuchte JPGs als <video> zu rendern. Lösung: Daten-Fix: UPDATE media_assets SET asset_type='still' WHERE asset_type='turntable' AND storage_key LIKE '%.jpg'. Code-Fix: isVideoAsset() nutzt mime_type zusätzlich. Asset-Typ IMMER aus mime_type + Dateiendung, nie nur aus Dateiname.

2026-03-08 | Render | OptiX BVH cache ephemeral → first render slow after rebuild

Nach docker compose up --build render-worker: erstes Render 130150s statt 22s. /root/.nv/ComputeCache/ liegt im ephemeren Container-Dateisystem → wird bei jedem Rebuild geleert. Lösung: Named volume: optix-cache:/root/.nv in docker-compose.yml. Detection: Render 1 >> Render 2+3 nach Rebuild → OptiX cold-start.

2026-03-10 | R3F | Invisible meshes: check e.object.visible in handlers causes regression

mesh.visible = false + Guard if (!e.object.visible) return in Event-Handlern blockiert alle Folge-Events — R3F's Event-Tracking feuert events mit e.object auf Meshes die nach dem Tracking unsichtbar wurden. Lösung: mesh.raycast = () => {} wenn mesh.visible = false; restore mit THREE.Mesh.prototype.raycast beim Einblenden. Meshes vom Raycasting ausschließen an der Quelle, nicht im Handler. Regel: Nie e.object.visible in R3F pointer/click-Handlern prüfen.

2026-03-10 | Three.js/GLTF | mesh.name wird durch sanitizeNodeName verändert — mesh.userData.name nutzen

Three.js GLTFLoader.createUniqueName() ruft PropertyBinding.sanitizeNodeName() auf, welches reservierte Zeichen []:./ entfernt. Blender-Dedup-Suffixe .001, .012 etc. werden zu 001, 012 — ohne Punkt. Unser normalizeMeshName strippte \.\d{3}$ (mit Punkt), aber der Punkt ist bereits weg → GE360-HF-0051-EIN_1.012 → (Three.js sanitize) → GE360-HF-0051-EIN_1012 → kein Match in partMaterials → fälschlich "unassigned". Fix: mesh.userData.name verwenden statt mesh.name. Three.js speichert den Original-GLTF-Node-Namen (unsanitized, mit Punkten) in node.userData.name = nodeDef.name bevor node.name = sanitize(...) gesetzt wird. Pattern: (mesh.userData?.name as string) || mesh.name. Gilt für: handleClick, handlePointerOver/Out, visibility/glow/color effects, onReady callback, glbMeshNames-Traversal in InlineCadViewer + ThreeDViewer.

2026-03-10 | OCC/GLTF | Assembly containers have no geometry → no mesh in GLB

cad_part_materials enthält Keys für Assembly-Nodes (_ASM_1, _BASIS_ASM_1), die in cadquery geparst werden, aber keine Geometrie haben → nicht im GLB. Debug-Log "matched: 4 of 9 stored keys" = 5 Keys sind Assembly-Container, kein Bug. Regel: Für assigned/total-Zählung immer glbMeshNames (Set aus scene traversal) nutzen, nie Object.keys(partMaterials).length.

2026-03-10 | Debug-Workflow | GLB-Geometrie-Analyse direkt per Python — ohne Browser-DevTools

Bei Mesh-Name-Mismatch-Bugs: GLB-Datei direkt parsen statt im Browser debuggen. Spart viele Iterationen.

Schritt 1 — GLB-Download-URL holen:

import urllib.request, json, struct
# Login
data = json.dumps({'email':'admin@hartomat.com','password':'Admin1234!'}).encode()
req = urllib.request.Request('http://localhost:8888/api/auth/login', data=data, headers={'Content-Type':'application/json'})
token = json.load(urllib.request.urlopen(req))['access_token']
# Media-Assets für CAD-File
assets = json.load(urllib.request.urlopen(urllib.request.Request(
    f'http://localhost:8888/api/media/?cad_file_id={CAD_FILE_ID}&asset_types=gltf_production&asset_types=gltf_geometry',
    headers={'Authorization': f'Bearer {token}'})))

Schritt 2 — GLB parsen und Mesh-/Node-Namen ausgeben:

req = urllib.request.Request(DOWNLOAD_URL, headers={'Authorization': f'Bearer {token}'})
glb = urllib.request.urlopen(req).read()
chunk_len, _ = struct.unpack('<II', glb[12:20])
gltf = json.loads(glb[20:20+chunk_len])
nodes, meshes = gltf['nodes'], gltf['meshes']
for n in nodes:
    if n.get('mesh') is not None:
        print(f"node={repr(n['name'])}  mesh_def={repr(meshes[n['mesh']]['name'])}")

Schritt 3 — normalizeMeshName + resolvePartMaterial in Python nachbauen und alle Namen durchlaufen: So lässt sich der vollständige Matching-Status (ASSIGNED/UNASSIGNED) offline prüfen, ohne den Browser zu öffnen. Ggf. gespeicherte cad_part_materials via GET /api/cad/{id}/part-materials dazuladen.

Produkt-ID → CAD-File-ID: GET /api/products/{product_id} → Feld cad_file_id. CAD-File-ID für Media-Assets: GET /api/media/?cad_file_id={id}&asset_types=gltf_production (nutzt listMediaAssets, nicht getMediaAssets).

2026-03-11 | Render-Pipeline | Production GLB: Koordinaten OCC→Blender falsch

OCC STEP-Koordinaten (Z-up, mm) werden von export_step_to_gltf.py auf Y-up (glTF) rotiert, dann vom Blender-Importer wieder auf Z-up. Netto: Blender(X, Y, Z) = OCC(X×0.001, -Z×0.001, Y×0.001) — Y und Z tauschen plus Z-Negierung. Einfach mm→m skalieren (X=X, Y=Y, Z=Z) ist FALSCH. Lösung: In allen _apply_sharp_edges_from_occ()-Funktionen: v = Vector((p[0]*0.001, -p[2]*0.001, p[1]*0.001)). Diagnose: DEBUG first_vert_local=[0.0, -0.1175, 0.26] vs occ_pair=[0.0, 0.26, 0.1175] zeigte eindeutig Y↔Z-Swap + Z-Negierung.

2026-03-11 | Render-Pipeline | Production GLB: Mesh-Data-Sharing bei Material-Zuweisung

Nach GLB-Import teilen sich mehrere Blender-Objekte denselben Mesh-Datablock (z.B. Instanzen). obj.data.materials.clear() auf geteilten Daten löscht Materialien für ALLE Objekte, die diesen Mesh teilen. Lösung: Vor obj.data.materials.clear() immer: if obj.data.users > 1: obj.data = obj.data.copy() (single-user machen).

2026-03-11 | Render-Pipeline | Production GLB: Name-Mismatch cadquery vs. RWGltf

cadquery-Parser und RWGltf_CafWriter lesen dieselbe STEP-Datei, erzeugen aber unterschiedliche Part-Namen (z.B. cadquery: GE360-HF-0011-EIN_HAELFTE_AF0_1_AF0 vs. RWGltf: GE360-HF-0021-EIN). Material-Mapping via Namen deckt nur 2/25 Objekte ab. Lösung 1: Prefix-Match: key.startswith(lower_base) oder lower_base.startswith(key). Lösung 2: Single-Material-Fallback: wenn len(appended) == 1 (nur 1 Bibliotheks-Material geladen), alle ungematchten Objekte ebenfalls mit diesem Material belegen. Generell: Part-Namen aus zwei verschiedenen OCC-Lese-Pfaden NIE direkt vergleichen.

2026-03-11 | Python/Blender | id(bpy_object) über Iterationen hinweg unzuverlässig

Blender-Objekte sind Python-Wrapper über C++-Objekte. Nach einer Schleife können gleiche C++-Objekte neue Python-Wrapper bekommen → id() ändert sich. Tracking mit id(obj) in einem Set und Prüfung in einer zweiten Schleife funktioniert NICHT. Lösung: obj.name als stabiler Identifier (Blender-Namen sind innerhalb der Szene eindeutig und bleiben stabil).

2026-03-11 | Python/Blender | bmesh.edges.get((v0, v1)) schlägt fehl bei idx0==idx1

Wenn KD-Tree für beide Endpunkte denselben Vertex findet (z.B. degenerate/zero-length edge), wirft edges.get((v, v)) ValueError: found the same (BMVert) used multiple times. Lösung: Guard: if idx0 == idx1: continue direkt nach dem kd.find() für Endpunkt 2.

2026-03-11 | Workflow | System-Daten direkt lesen — kein docker exec nötig

Für den /plan-Agent: alle relevanten System-Daten sind OHNE docker-Befehle zugänglich:

  • Datenbankmodelle: backend/app/models/ und backend/app/domains/*/models.py direkt lesen
  • API-Endpoints: backend/app/api/routers/ und backend/app/domains/*/routers.py lesen — kein curl/docker exec nötig
  • Celery-Tasks: backend/app/tasks/step_tasks.py und backend/app/domains/pipeline/tasks/ lesen
  • Render-Scripts: render-worker/scripts/ lesen — blender_render.py, export_gltf.py etc.
  • Frontend-API: frontend/src/api/*.ts zeigt alle API-Calls und Typen
  • Gespeicherte Daten: CadFile.mesh_attributes, Product.cad_part_materials, SystemSetting in Modellen beschrieben
  • Docker-Logs nur für Debugging: docker compose logs --since 5m render-worker 2>&1 | grep -E "pattern" — sparsam einsetzen, nicht für Codeanalyse
  • Kein docker exec backend python3 -c für Code-Exploration — dauert lang und braucht laufende Container
  • MEMORY.md enthält Container-Capabilities: OCP nur in render-worker, nicht in worker oder backend

2026-03-11 | Render-Pipeline | Production GLB: edit-mode mark_sharp+mark_seam für korrekte GLB Sharp Edges

In Blender 5.0: shade_smooth_by_angle() allein reicht nicht für scharfe Kanten im GLB-Export. Lösung: Edit-mode-Operators verwenden: edges_select_sharp(sharpness=angle)mark_sharp()mark_seam(). Der glTF-Exporter erstellt an den als sharp markierten Edges Vertex-Splits (duplizierte Vertices mit verschiedenen Normalen). Verifizierung: 543 sharp+seam Edges markiert → production GLB hat 812 extra Vertices + 6027 Positionen mit mehreren Normalen = scharfe Kanten korrekt enkodiert. Wichtig: calc_normals_split() wurde in Blender 5.0 entfernt (→ AttributeError). Nicht mehr nötig: export_apply=True triggert Vertex-Splitting automatisch.

2026-03-11 | Render-Pipeline | MediaAsset DELETE+INSERT erzeugt neue UUID → 404 auf gecachte URLs

generate_gltf_production_task und generate_gltf_geometry_task löschten alten MediaAsset-Record und erstellten neuen mit neuer UUID. Gecachte Download-URLs (z.B. im Frontend-State) zeigten auf die alte, gelöschte UUID → 404. Lösung: UPSERT: existierenden Record aktualisieren (existing.storage_key = _key) statt DELETE+INSERT. UUID bleibt stabil.

2026-03-11 | Render-Pipeline | Production GLB: OCC custom_normal überschreibt Blender-Normalen

export_step_to_gltf.py (RWGltf_CafWriter) embedded per-corner normals from OCC tessellation as a custom_normal attribute (CORNER, INT16_2D) in the geometry GLB. Blender's glTF importer preserves this as a custom attribute. The glTF exporter then re-exports these pre-baked normals unchanged, ignoring shade_smooth_by_angle processing and explicit edge.smooth=False sharp marks — sharp edges are invisible in the production GLB. Lösung: In export_gltf.py und blender_render.py: nach GLB-Import das custom_normal-Attribut von allen Mesh-Objekten entfernen, BEVOR shade_smooth_by_angle() aufgerufen wird. Dann berechnet Blender die Normalen neu aus den sharp-edge-Marks. Diagnose: File-Size-Test: WITH custom_normal=1218960 Bytes, WITHOUT=1137944 Bytes. Re-import zeigte has_custom_normals=True. GLB-JSON-Inspektion mit struct.unpack + json.loads direkt auf .glb-Datei.

for obj in mesh_objects:
    if "custom_normal" in obj.data.attributes:
        obj.data.attributes.remove(obj.data.attributes["custom_normal"])

2026-03-11 | Render-Pipeline | OCC B-rep sharp edges: BRep_Tool.Polygon3D_s() gibt None zurück

BRep_Tool.Polygon3D_s(edge, loc) und PolygonOnTriangulation_s() geben in XCAF-Compound-Kontext immer None zurück — Tessellation-Polygon-Daten liegen auf Component-Instanzen, nicht auf den Compound-Edges. Ergebnis: 612 scharfe Kanten erkannt, 0 Segment-Paare extrahiert. Lösung: GCPnts_UniformAbscissa(curve3d, step_mm=0.3, tol=1e-6) auf der analytischen B-rep-Kurve (BRepAdaptor_Curve) samplen. 0.3mm-Schritt garantiert dass konsekutive Sample-Paare die Tessellations-Kanten (~0.78-1.55mm) straddeln — die KD-Tree-Suche (TOL=0.5mm) findet dann die richtigen Blender-Mesh-Edges. Ergebnis: 17.129 Segment-Paare, 1.364 Kanten in Blender markiert. Imports: from OCP.GCPnts import GCPnts_UniformAbscissa; from OCP.BRepAdaptor import BRepAdaptor_Curve

2026-03-12 | OCC/USD | export_step_to_usd.py: face_loc == shape_loc → Doppel-Transform → falsche Mesh-Positionen

BRepMesh_IncrementalMesh auf dem Root-Compound tesselliert alle Instanzen. BRep_Tool.Triangulation_s(face, face_loc) liefert dabei einen face_loc, der exakt die Instance-Platzierung des shape kodiert (identisch zu shape.Location()). In _extract_mesh() wurden beide Transforms nacheinander angewendet: erst face_loc.Transformation(), dann shape_trsf — was eine Doppel-Rotation ergibt. Für einen Zylinder-Rollensatz (12 Rollen à 30° Abstand) führt das dazu, dass mehrere Rollen auf dieselbe falsche Position kollabieren (z.B. -75° × 2 = -150° = +105° × 2 mod 360° → Rollen 3 und 9 landen identisch). Lösung: if face_has_loc: ... elif shape_has_loc: ... statt if face_has_loc: ... if shape_has_loc: .... shape_loc ist nur ein Fallback für direkt tessellierte Shapes (nicht als Teil eines Compounds), bei denen face_loc identity ist. Beweis: Mit face_only-Extraktion (nur face_loc, kein shape_loc) erscheinen alle 10 Rollen bei genau 223,9mm Radius, gleichmäßig 30° verteilt.


Offene Fragen

(keine offenen Fragen)

2026-03-11 | OCP/Python | id(solid.TShape()) ist nicht stabil

In OCP (pybind11-basiert) gibt jeder Aufruf von solid.TShape() ein neues Python-Wrapper-Objekt zurück, das dieselbe C++ TShape-Instanz wrapet. id() gibt daher jedes Mal einen anderen Wert → Deduplizierung per id() schlägt immer fehl. Lösung: solid.IsSame(other_solid) verwenden (vergleicht TShape-Zeiger intern, liefert True für gleiche TShape mit unterschiedlicher Location/Orientation).

2026-03-11 | GMSH | TopExp_Explorer(SOLID) übersieht freie Faces und Shells

TopExp_Explorer(root, SOLID) findet nur TopoDS_Solid-Shapes, nicht freie TopoDS_Shell- oder TopoDS_Face-Shapes auf Compound-Ebene. OCC's BRepMesh_IncrementalMesh auf dem Root-Compound tesselliert alle Typen rekursiv. Lösung: BRepMesh-Baseline zuerst auf den vollen Root-Compound, dann GMSH als Override nur für SOLID-Shapes. So werden keine Shapes übersprungen.

2026-03-11 | GMSH | CharacteristicLengthMax vs. OCC linear_deflection

OCC linear_deflection=0.1mm auf einem 50mm-Zylinder → Kantenlänge ~5mm. GMSH CharacteristicLengthMax=0.1×15=1.5mm → 3× mehr Unterteilungen → 9× mehr Dreiecke → GLB 7× größer. Lösung: CharacteristicLengthMax = linear_deflection × 50 (≈5mm), MinimumCirclePoints = min(12, ...) statt min(20). Ergebnis: GMSH 91% von OCC-Größe (Ziel ≤120% ✓).

2026-03-12 | GMSH | Priority 3 vollständig — GMSH-Pipeline Status

GMSH 4.15.1 in render-worker installiert. tessellation_engine=gmsh ist der aktive DB-Default. _tessellate_with_gmsh() in export_step_to_gltf.py vollständig: CharacteristicLengthMax = linear_deflection × 50, MinimumCirclePoints = min(12, ...), REVERSED Solids bleiben erhalten (kein invertierter Jacobian). Produktion-GLB nutzt Cache-Reuse (kein Re-Tessellieren bei Materialwechsel). Sharp-Edge-Extraktion läuft nach Tessellierung unabhängig vom Engine-Typ — Injected N segment pairs into GLB extras gilt für beide Pfade.

2026-03-12 | OCC | BRepMesh auf Compound: Triangulation in Definition-Space, Face-loc = Instance-Placement

BRepMesh_IncrementalMesh(compound) tesselliert alle Faces in Definition-Space-Koordinaten. Für Instanzen mit Placement enthält face.Location() (= TopoDS_Shape-Location) die Instance-Transformation. BRep_Tool.Triangulation_s(face, loc) gibt die Triangulation-Knoten in Definition-Space zurück, loc enthält die Face-Location (= Instance-Placement). BRep_Builder.UpdateFace(face_def, tri) mit einer aus solid.Located(TopLoc_Location()) gewonnenen Face schreibt auf das geteilte TShape — ALLE Instanzen der gleichen Geometrie sehen die neue Triangulation, da sie IsPartner teilen.

2026-03-12 | OCC/XCAF | IsSame() vs IsPartner() — Deduplizierung bei Assembly-Instanzen

IsSame() prüft TShape-Pointer UND Location → für 16 Instanzen desselben Wälzkörpers sind alle 16 "unique" (unterschiedliche Location). IsPartner() prüft nur TShape-Pointer → gibt 9 tatsächlich unterschiedliche Geometrien. In export_step_to_gltf.py GMSH-Schleife: IsSame()-Deduplizierung tesselliert alle 16 Instanzen separat, aber da sie das gleiche TShape teilen, werden alle 16 mal auf dasselbe TShape geschrieben (idempotent, korrekt). RWGltf_CafWriter traversiert XCAF-Labelhierarchie und liest Triangulation von Definition-Labels (Identity-Location) — kein Double-Transform.

2026-03-12 | Debugging | Stale GLB-Cache maskiert Code-Fixes

Bug "Wälzkörper an falscher Position" war in Code durch commit 638b93b (IsSame-Fix) bereits behoben. Aber gecachtes Produktions-GLB (vor dem Fix generiert) zeigte weiterhin falsche Positionen im Viewer. Lösung: Geometry-GLB manuell neu generieren oder step_file_hash = NULL in DB um Cache-Invalidierung zu erzwingen. Nach Code-Fixes an Tessellierung/Export IMMER alle betroffenen GLB-Caches invalidieren.

2026-03-13 | OCC/GLB | BRepBuilderAPI_Transform zerstört Tessellierung + RWGltf_CafWriter MergeFaces

Problem: GLB-Dateien hatten nur ~164 Vertices pro Bauteil statt ~7000. Zwei Ursachen:

  1. BRepBuilderAPI_Transform(shape, trsf, copy=True) zerstört alle Poly_Triangulation-Daten. Die mm→m-Skalierung vor dem Export löschte die BRepMesh-Tessellierung.
  2. RWGltf_CafWriter mit MergeFaces=False (Default) findet keine per-Face-Tessellierung aus der XCAF-Komponentenhierarchie und produziert degenerierte Meshes. Lösung:
  • BRepBuilderAPI_Transform komplett entfernt — RWGltf_CafWriter konvertiert intern mm→m und Z-up→Y-up.
  • writer.SetMergeFaces(True) hinzugefügt — composited Face-Triangulationen zu korrekten Shape-Buffern (1.212 → 46.573 Vertices). Merke: OCC TopLoc_Location kann keine Skalierung (wirft Standard_DomainError). Für Skalierung entweder den Writer intern konvertieren lassen oder GLB post-process.

2026-07-21 | Debugging | NameError in cinematic render error path maskiert echten Blender-Fehler

In render_cinematic_to_file (render_blender.py) las der Fehler-Handler nach proc.returncode != 0 die Variablen stdout und stderr — beide nie definiert (tatsächlich heißen sie log_lines und stderr_lines). Jeder Blender-Absturz produzierte {"error": "name 'stdout' is not defined"} in der DB statt dem echten Fehlertext. Lösung: stdout/stderrlog_lines/stderr_lines-Referenz korrigiert.

2026-07-21 | Architektur | Cancel-Race im Render-Task — completed überschreibt cancelled

Wenn ein Render-Job gecancelt wird, läuft Blender weiter. Wenn er fertig ist, schreibt persist_order_line_output render_status=completed ohne zu prüfen ob die Line mittlerweile cancelled wurde. Lösung: Guard am Anfang von persist_order_line_output: wenn line.render_status == "cancelled", Early-Return ohne Statusmutation.

2026-07-21 | Architektur | Cinematic-Render ist ausschließlich Legacy-Pfad — kein Graph-Node

Es gibt keinen BLENDER_CINEMATIC-Node im Workflow-Graph-System. Cinematic-Renders laufen ausschließlich über den Legacy-Pfad (render_order_line_task), ausgelöst durch output_type.render_settings.cinematic=true. Für einen Cinematic-Output-Type darf deshalb kein workflow_definition_id gesetzt sein — sonst wird der Job als Graph-Workflow geroutet, der keinen Cinematic-Node hat und leer bleibt.

2026-07-21 | Render-Pipeline | is_cinematic immer False wegen Invocation-Override-Filter

workflow_runtime_services.py las is_cinematic aus dem bereits gefilterten render_settings-Dict. resolve_output_type_invocation_overrides entfernt aber alle Keys die nicht in _STATIC_RENDER_OVERRIDE_KEYS oder _ANIMATION_OVERRIDE_KEYS stehen — cinematic ist in keiner dieser Listen. Ergebnis: is_cinematic=False immer, Cinematic-Jobs liefen auf den Turntable-Pfad. Lösung: is_cinematic direkt aus output_type.render_settings.get("cinematic") lesen statt aus dem gefilterten Dict.

2026-07-21 | Render-Pipeline | usd_path.exists() auf str in turntable und cinematic render functions

render_turntable_to_file und render_cinematic_to_file in render_blender.py riefen .exists() auf usd_path auf, ohne zu prüfen ob es ein str oder Path ist. Das Invocation-Dataclass speichert usd_path als str | None. render_still_to_file hatte den isinstance(usd_path, str)Path-Cast bereits (Zeilen 300301), die anderen beiden Funktionen nicht. Lösung: Denselben Cast (if isinstance(usd_path, str) and usd_path.strip(): usd_path = Path(usd_path)) vor dem usd_path.exists()-Check in turntable und cinematic ergänzt.

2026-07-22 | Frontend | Editierbares Branding — public settings endpoint vor Auth nötig

Der Admin-Settings-Endpunkt (GET /api/admin/settings) erfordert global_admin-Auth. Die Login-Seite benötigt den App-Namen aber vor dem Login. Lösung: eigener öffentlicher GET /api/admin/branding-Endpunkt ohne Auth-Dependency, der nur app_name + app_subtitle aus system_settings liest. Frontend: useBranding()-Hook via React Query mit 5-min-Stale-Time — Query-Key ['branding'] wird nach dem Speichern aus dem Admin-Panel invalidiert, damit Sidebar und Login-Seite sofort aktualisieren.

2026-07-22 | Workflow-Editor | Blueprint-Workflows waren nach jedem Speichern read-only

canonicalize_workflow_config in workflow_config_utils.py hat bei jedem Aufruf Configs mit ui.blueprint (z.B. still_graph_reference, order_rendering) und ui.preset == "still_graph" auf das kanonische Template zurückgebaut — alle Node-Parameter-Änderungen und Strukturänderungen des Users wurden lautlos verworfen. Das betraf Update, Preflight und Execution-Dispatch gleichermaßen. Lösung: Parameter preserve_user_graph: bool = False — bei True werden die Rebuild-Blöcke übersprungen; nur Basic-Normalisierung läuft. update_workflow, _workflow_to_out, Preflight und Execution-Dispatch rufen jetzt mit preserve_user_graph=True auf. create_workflow bleibt bei False (Blueprints werden beim Erstellen korrekt expandiert).

2026-07-22 | Workflow-Editor | _legacy_dispatch umging cancelled/rejected Pre-Check

_legacy_dispatch in dispatch_service.py rief render_order_line_task.delay() direkt auf und umging damit den Pre-Check in dispatch_order_line_render (der cancelled/rejected Lines überspringt). Alle Legacy-Dispatch-Pfade (auch im Graph-Fallback) liefen so durch, auch für bereits gecancelte Jobs. Lösung: _legacy_dispatch ruft jetzt dispatch_order_line_render.delay() auf statt render_order_line_task.delay() direkt.

2026-07-22 | Maintenance | E-Mail-Kanal in NotificationSettings war dauerhaft deaktiviert

NotificationSettings.tsx hatte comingSoon: true für den E-Mail-Kanal — das Backend-SMTP-System existierte bereits vollständig. Toggle war nur im Frontend deaktiviert. Lösung: comingSoon entfernt.

2026-07-22 | Maintenance | Bare-except-Blöcke in step_processor.py schlugen Fehler still

8 except Exception: pass/continue Blöcke in step_processor.py (Kanten-Winkelberechnung, Bounding-Box, Volumen/Flächen-Properties, Dreieckszählung, STEP-Raw-Parse) unterdrückten Fehler vollständig. Debugging von kaputten STEP-Dateien war so nahezu unmöglich. Lösung: Alle Blöcke auf except Exception as _exc: logger.debug(...) umgestellt; STEP-Raw-Parse-Fehler auf logger.warning da relevanter.

2026-07-22 | Architecture | CORS-Origins und Order-Prefix nicht konfigurierbar

main.py hatte CORS-Origins hardcoded als Python-Liste. orders/service.py hatte den Ordernummer-Prefix SA- hardcoded. Beide Werte lassen sich jetzt per Umgebungsvariable überschreiben: CORS_ORIGINS='["https://myapp.com"]' und ORDER_NUMBER_PREFIX=HM. Default-Werte bleiben unverändert, keine Migration nötig.

2026-07-22 | Refactoring | placeholder.mp4 als Pfad-Trick in tasks.py

tasks.py:934 nutzte build_order_line_step_render_path(..., "placeholder.mp4", ensure_exists=True).parent um das Render-Verzeichnis zu erzeugen — ein fake Dateiname nur um .parent aufzurufen. Lösung: neue Funktion build_order_line_step_render_dir(step_path, order_line_id, *, ensure_exists=False) in render_paths.py hinzugefügt. tasks.py nutzt die direkte Funktion ohne Workaround.

2026-07-22 | Correctness | Unbekannte Workflow-Nodes wurden lautlos übersprungen

workflow_graph_runtime.py:511 setzte Status "skipped" für Steps ohne Executor — der Workflow-Run galt als erfolgreich obwohl Nodes nicht ausgeführt wurden. Lösung: Status zu "failed" geändert und logger.error ergänzt. Der Run-Status-Check (if any(...status == "failed")) markiert den gesamten Run als failed.

2026-07-22 | Correctness | Invoice-Beschreibung war nackte UUID

billing/service.py:259 setzte description=f"Render: {ol.id}" — im PDF erschien eine UUID als Rechnungsposition. Lösung: OrderLine wird jetzt mit selectinload(product) und selectinload(output_type) geladen; Beschreibung ist f"{product_name} — {ot_name}".

2026-07-22 | Security | JWT secret "changeme" ohne Startup-Guard

jwt_secret_key in config.py hatte den Standardwert "changeme". Deployments, die vergessen JWT_SECRET_KEY zu setzen, ließen gültige JWTs fälschen. Lösung: @model_validator(mode="after") in Settings — wirft ValueError wenn key "changeme" ist UND _is_running_in_container() True ist. Lokale Entwicklung außerhalb Docker ist nicht betroffen.

2026-07-22 | Security | Race condition bei Ordernummer-Generierung

generate_order_number in orders/service.py verwendete SELECT MAX(order_number) ohne Advisory Lock. Bei gleichzeitigen Requests konnten zwei Sessions denselben MAX-Wert lesen und identische Nummern generieren, was zu einem IntegrityError beim Flush führte. Lösung: pg_advisory_xact_lock(hashtext(:key)) vor dem SELECT — gleicher Pattern wie generate_invoice_number in billing/service.py. Lock wird beim Commit/Rollback automatisch freigegeben.

2026-07-22 | Security | localhost:8888 hardcoded in chat_service.py

chat_service.py verwendete httpx.AsyncClient(base_url="http://localhost:8888", ...) an 5 Stellen. In Docker löst localhost nicht zur Backend-Adresse auf, wenn der Service in einem anderen Container läuft. Lösung: internal_api_base_url: str = "http://localhost:8888" zu config.py Settings hinzugefügt (env-konfigurierbar via INTERNAL_API_BASE_URL). Alle 5 Stellen nutzen jetzt settings.internal_api_base_url.

2026-07-22 | Security | Upload-Größenlimit konfiguriert aber nie geprüft

settings.max_upload_size_mb (Default 500 MB) existierte in config.py, wurde aber in beiden Upload-Handlern (uploads.py) nie gegen die tatsächliche Dateigröße geprüft. file.read() las beliebig große Dateien vollständig in den RAM. Lösung: POST-Read-Guard nach content = await file.read() in beiden Handlern — len(content) > max_bytes → HTTP 413.

2026-07-22 | Security | Billing-Rechnungen ohne Tenant-Isolation

list_invoices in billing/router.py übergab tenant_id nicht an get_invoices. get_invoices im Service ignorierte den Parameter — kein WHERE-Filter vorhanden. Jeder admin_or_pm-User konnte alle Rechnungen aller Tenants sehen. Lösung: Router extrahiert tenant_id aus current_user (nur wenn nicht global_admin), Service filtert mit .where(Invoice.tenant_id == tenant_id). get_invoice_endpoint prüft zusätzlich inv.tenant_id == current_user.tenant_id für Nicht-Admins.

2026-07-22 | Security | Invoice-Status nicht validiert — beliebige Strings schreibbar

InvoiceStatusUpdate.status war str ohne Einschränkung. VALID_STATUSES in billing/service.py war definiert aber nie referenziert. Lösung: InvoiceStatusUpdate.status auf Literal["draft","sent","paid","cancelled"] geändert (Pydantic-Validierung auf Schemaebene). Zusätzlich guard in update_invoice_status als defence-in-depth.

2026-07-22 | Security | SMTP-Passwort im Klartext in GET /api/admin/settings

SettingsOut.smtp_password gab den gespeicherten Wert an jeden global_admin zurück. Lösung: _settings_to_out maskiert den Wert — "***" wenn gesetzt, "" wenn leer. update_settings überspringt das Schreiben wenn body.smtp_password == "***" (Sentinel für "unverändert lassen").

2026-07-22 | Refactoring | Turntable-Branch aus render_order_line_task extrahiert

render_order_line_task war ein 427-Zeilen-Monolith. Der Turntable-Branch (~55 Zeilen) wurde in _render_turntable(*, render_invocation, step_path, output_path, template, order_line_id, emit, pl) ausgelagert — resolved objects als Parameter (Option B), damit Session und PipelineLogger im Main Task verbleiben und kein doppeltes DB-Lookup entsteht. Der 68-Zeilen Exception-Handler (Mark-as-failed bei max_retries) wurde in _handle_render_task_exhausted(order_line_id, exc, tenant_id) extrahiert; die Retry-Logik (self.retry) bleibt im Main Task, da sie den Celery self-Context benötigt. Beide Helper stehen in render_order_line.py vor den Task-Definitionen.