Commit Graph

65 Commits

Author SHA1 Message Date
Hartmut fa7093307a chore: snapshot before HartOMat rebrand 2026-04-06 12:41:44 +02:00
Hartmut caffe7809c feat: live frame progress streaming for cinematic renders
Replaced communicate() (blocking) with selectors-based line-by-line
stdout streaming — same pattern as still render. Each frame now
streams live to the frontend:

  [cinematic_render] Frame 42/480 -- 55.3s elapsed (0.76 fps)

Pipeline: Blender stdout → log_callback → emit() → Redis →
LiveRenderLog poll (2s) → frontend display

Also added log_callback parameter to cinematic render task call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:39:41 +01:00
Hartmut f22b963be9 feat: cinematic highlight render — 20s procedural camera animation
New render type: 4-segment cinematic camera animation (480 frames @ 24fps)
for professional product highlight videos.

Camera sequence:
1. Establishing (5s): slow 45° orbit + push-in, 50mm lens
2. Detail sweep (5s): low-angle close arc, 85mm telephoto, shallow DOF
3. Crane up (5s): rising 30°→60°, 35mm wide reveal, pull-back
4. Hero close (5s): push-in to beauty angle, 65mm, smooth ease-out

Technical:
- cinematic_render.py: procedural camera from bounding sphere, cubic easing,
  per-frame keyframes (location, rotation, focal length, DOF)
- render_cinematic_to_file(): service function (same pattern as turntable)
- Pipeline routing: render_settings.cinematic flag → cinematic path
- Depth of field enabled (f-stop scales with product size)
- use_persistent_data for BVH caching between frames
- Same material/template/USD pipeline as turntable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:25:56 +01:00
Hartmut 531994cccd fix: OCP import for BRepGProp + route metadata task to asset_pipeline
- Fixed: from OCP.BRepGProp import BRepGProp as brepgprop (was lowercase)
- Routed reextract_rich_metadata_task to asset_pipeline queue (render-worker
  has OCC/OCP installed, worker container does not)
- Backfill verified: 45/45 products updated with volume, surface area,
  part count, complexity metrics

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 19:50:12 +01:00
Hartmut cfccdd5397 feat: rich product metadata extraction from STEP files
Extract volume, surface area, part count, assembly hierarchy, and
complexity from STEP files via OCC B-rep analysis.

Backend:
- extract_rich_metadata() in step_processor.py: computes per-part volume
  (BRepGProp), surface area, triangle/vertex count, assembly depth,
  instance count, complexity score, largest part identification
- cad_metadata JSONB column on Product model (DB migration)
- Auto-populated during STEP processing (non-fatal, 10s timeout)
- Also stored in cad_files.mesh_attributes["rich_metadata"]
- Batch re-extract endpoint: POST /admin/settings/reextract-rich-metadata

AI Agent:
- search_products returns part_count, volume_cm3, complexity, largest_part
- query_database tool description documents cad_metadata schema

Frontend:
- ProductDetail page: CAD Metadata section with stat cards
  (parts, volume, surface area, complexity, triangles, assembly depth)
- Admin System Tools: "Re-extract Rich Metadata" button for backfill

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 18:49:50 +01:00
Hartmut 502e2d0387 fix: use max_completion_tokens instead of max_tokens for GPT-4o
Azure OpenAI GPT-4o and newer models require 'max_completion_tokens'
instead of 'max_tokens'. Fixed in all 3 services:
- chat_service.py (2 call sites)
- azure_ai.py (validation service)
- tenants/router.py (test connection endpoint)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 14:20:40 +01:00
Hartmut daad2c64f3 fix: revert dual queue to single GPU — light worker caused 2x regression
Root cause: render-worker and render-worker-light shared the same GPU,
causing contention. Complex TRB renders went from 17s → 36s (2x slower).

Changes:
- Thumbnails back to asset_pipeline queue (not asset_pipeline_light)
- Dispatch routing always uses asset_pipeline (no queue splitting)
- render-worker-light gated behind "multi-gpu" profile — only starts with:
  docker compose --profile multi-gpu up -d
- For single-GPU setups: all rendering is sequential on one worker

The dual queue approach is correct for multi-GPU machines where each
worker gets its own GPU. On single-GPU, serial execution is faster
than concurrent GPU contention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:33:26 +01:00
Hartmut b892f72f7e feat: per-line render overrides — override any output type setting at order time
Instead of duplicating output types for every variation (WebP vs PNG,
different resolution), keep one canonical output type and override
specific fields per order line via render_overrides JSONB.

Backend:
- render_overrides JSONB column on OrderLine (DB migration)
- Render task merges overrides with output type settings (format, width,
  height, samples, engine, denoiser, transparent_bg, cycles_device)
- POST /orders/{id}/batch-render-overrides endpoint for bulk override
- PatchLineBody accepts render_overrides for per-line patching

Frontend:
- Batch render overrides section on OrderDetail: output format dropdown
  (PNG/JPG/WebP) + resolution dropdown (512-4096)
- Clear button to remove overrides

MCP:
- create_order tool: accepts product_ids, output_type, render_overrides,
  material_override — enables "render all products as WebP" via Claude
- set_render_overrides tool: batch override on existing orders

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:26:38 +01:00
Hartmut 5a148554c0 perf: dual queue, GLB caching, WebP output, persistent BVH
Task 4: Dual render queue
- render-worker: heavy (asset_pipeline, concurrency=1) — HQ 2048x2048, animations
- render-worker-light: light (asset_pipeline_light, concurrency=2) — thumbnails, <=1024
- Thumbnails routed to light queue automatically
- Order line renders routed by resolution at dispatch time

Task 5: GLB caching (skip re-tessellation)
- Before tessellating, check if gltf_geometry MediaAsset exists for the cad_file_id
- If found, copy to expected path — render_blender.py finds it and skips tessellation
- Saves 7-11s per re-render of the same product

Task 6: WebP output format
- New 'webp' option in output_format (OutputType admin)
- Blender renders PNG intermediate, Pillow converts to WebP (quality=90, method=4)
- 50-70% smaller files with no visible quality loss
- Correct MIME type (image/webp) in MediaAsset

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:07:12 +01:00
Hartmut ffe3eebfca perf: render pipeline optimizations — sample scaling, USD logging, persistent BVH
Task 1: Resolution-aware sample count
- Auto-scale samples for resolutions <= 1024: max(32, samples * max_dim / 2048)
- 512x512 thumbnails: 256 → 64 samples (75% GPU savings)
- Thumbnail tasks capped at 64 samples via context manager
- 2048x2048 HQ renders unchanged

Task 2: USD path preference audit + logging
- Verified USD master path is correctly preferred over GLB tessellation
- Added clear emit() messages: "Using USD master" vs "No USD master — GLB path"
- Dynamic render log label: "USD → Blender" vs "STEP → GLB → Blender"

Task 3: Persistent BVH for turntable animations
- Added scene.render.use_persistent_data = True before frame loop
- BVH acceleration structure cached between frames (not rebuilt per frame)
- Applies to both camera orbit and object rotation modes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 12:03:31 +01:00
Hartmut 9d6def84c1 feat: per-order-line material override — override materials for individual renders
- Add `material_override` nullable column on OrderLine (DB migration)
- Line override takes priority over OutputType override
- PATCH /orders/{id}/lines/{id} endpoint to update material_override
- Inline dropdown on each order line in the OrderDetail page
- Amber background when override is active
- Same output type, different material per line — no need to create a new output type

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:33:00 +01:00
Hartmut c054236d22 fix: material override pipeline — pass --material-override CLI arg to Blender scripts
The initial implementation only overrode the material_map dict in the task,
but the Blender USD primvar path bypassed it. Now:
- Added --material-override named CLI arg parsed in _blender_args.py
- Both Mode A (factory) and Mode B (template) in _blender_scene_setup.py
  override usd_material_lookup and material_map when set
- Passed through full chain: task → step_processor → render_blender → CLI → Blender
- Tested: 175-part bearing rendered with single Steel-Bare material (1/1 materials)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 14:19:21 +01:00
Hartmut 7c606953ec feat: global material override on OutputType for x-ray/clay render modes
- Add `material_override` nullable column on OutputType (DB migration)
- When set, ALL product parts get rendered with this single material
- Override applies after alias resolution in render_order_line task
- Admin UI: dropdown in OutputType table to select a library material
- Display: amber badge showing active override material name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 13:16:00 +01:00
Hartmut b6bac080bb feat: duplicate product detection — STEP conflict warnings on Excel import and CAD upload
- Excel preview detects when a product already has a different STEP file linked
- Excel preview detects intra-Excel conflicts (same product, different CAD model names)
- Product STEP upload warns when replacing an existing file and shows render count
- All warnings are non-blocking (amber badges, toast warnings)
- LEARNINGS.md: all open items resolved

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 13:05:40 +01:00
Hartmut b583b0d7a2 feat: per-position camera settings, material alias dialog, product delete, media browser links
- Per-render-position focal_length_mm/sensor_width_mm (DB → pipeline → Blender)
- FOV-based camera distance with min clamp fix for wide-angle lenses
- Unmapped materials blocking dialog on "Dispatch Renders" with batch alias creation
- Material check endpoint (GET /orders/{id}/check-materials)
- Batch alias endpoint (POST /materials/batch-aliases)
- Quick-map "No alias" badges on Materials page
- Full product hard-delete with storage cleanup (MinIO + disk files + orphaned CadFile)
- Delete button on ProductDetail page with confirmation
- Clickable product names in Media Browser (links to product page)
- Single-line render dispatch/retry (POST /orders/{id}/lines/{id}/dispatch-render)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 12:16:37 +01:00
Hartmut 0020376702 fix: GLB tessellation destroyed by BRepBuilderAPI_Transform + MergeFaces
Root cause 1: BRepBuilderAPI_Transform(shape, trsf, copy=True) destroys
all Poly_Triangulation data. The mm→m scaling was applied before export,
wiping the tessellation from BRepMesh_IncrementalMesh.

Fix: Remove BRepBuilderAPI_Transform entirely — RWGltf_CafWriter already
handles mm→m conversion and Z-up→Y-up rotation internally.

Root cause 2: RWGltf_CafWriter with MergeFaces=False (the default) fails
to find per-face tessellation from the XCAF component hierarchy, producing
degenerate meshes (~2 vertices per face instead of thousands).

Fix: SetMergeFaces(True) to compose face triangulations into proper
per-shape mesh buffers. Vertex count goes from 1,212 to 46,573.

Also bumps cache key version to v2 to invalidate broken cached GLBs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 16:23:41 +01:00
Hartmut 7054fa4b40 fix: skip render for cancelled order lines and rejected orders
Adds early-exit checks in dispatch_order_line_render and
render_order_line_task to prevent rendering when order lines are
cancelled or the parent order is rejected/completed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:28:39 +01:00
Hartmut 6c5873d51f feat: performance optimizations + part-materials validation
- @timed_step decorator with wall-clock + RSS tracking (pipeline_logger)
- Blender timing laps for sharp edges and material assignment
- MeshRegistry pattern: eliminate 13 scene.traverse() calls across viewers
- Lazy material cloning (clone-on-first-write in both viewers)
- _pipeline_session context manager: 7 create_engine() → 2 in render_thumbnail
- KD-tree spatial pre-filter for sharp edge marking (bbox-based pruning)
- Batch material library append: N bpy.ops.wm.append → single bpy.data.libraries.load
- GMSH single-session batching: compound all solids into one tessellation call
- Validate part-materials save endpoints against parsed_objects (prevents bogus keys)
- ROADMAP updated with completion status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 11:53:14 +01:00
Hartmut ec667dd56a refactor: remove dead export_gltf.py, cleanup rendering tasks, improve tessellation UI
- Remove export_gltf.py (Blender-based GLB export replaced by OCC direct)
- Remove unused export_gltf_for_order_line_task
- Add Ultra tessellation preset to Admin settings
- Improve tessellation preset descriptions and styling
- Minor cleanup across media, rendering, and workflow modules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:37:35 +01:00
Hartmut 577dd1ca7e refactor(P11+P12): codebase hygiene — CLAUDE.md rewrite, type safety, dead code removal
- Rewrite CLAUDE.md to match current 8-service architecture (was 11, 5 deleted)
- Remove all as-any casts in OrderDetail.tsx (9 casts → 0)
- Add cad_parsed_objects/cad_part_materials to OrderItem interface
- Rename require_admin → require_global_admin across 6 router files (22 calls)
- Remove EXPORT_GLB_PRODUCTION enum + generate_gltf_production_task (dead code)
- Remove worker-thumbnail from ALLOWED_SERVICES, replace Flamenco link
- Delete obsolete PLAN.md (1455 lines) and PLAN_REFACTOR.md (1174 lines)
- Fix digit-only USD prim names with p_ prefix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 07:22:04 +01:00
Hartmut b72d8498b9 fix(USD): correct coordinate transform (Z-up stage) and store canonical_material in DB
- export_step_to_usd.py: change stage from Y-up to Z-up, keep (X,-Z,Y)
  transform — matches GLB orientation exactly (verified: bounding box match)
- export_glb.py: include canonical_material in resolved_material_assignments
  DB field (was being dropped during manifest parsing)
- import_usd.py: use pxr customData read (not string primvars — Blender
  ignores those, confirmed by testing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:17:45 +01:00
Hartmut cc3071297b feat(M5-M7): embed canonical material names in USD via customData + pxr direct read
- export_step_to_usd.py: accept --material_map CLI arg, write
  schaeffler:canonicalMaterialName as customData on each Mesh prim,
  fix geometry transform (strip shape Location before face exploration,
  apply both face_loc and shape_loc sequentially)
- import_usd.py: after Blender USD import, use pxr to read customData
  directly from the USD file — builds {part_key: material_name} lookup
  (Blender ignores STRING primvars and customData, but pxr reads both)
- _blender_materials.py: add apply_material_library_direct() for exact
  dict-based material assignment without name-matching heuristics
- _blender_scene_setup.py: prefer direct USD lookup, fall back to
  name-matching for legacy USD files without material metadata
- export_glb.py (generate_usd_master_task): resolve material_map via
  material_service.resolve_material_map() and pass to subprocess;
  include material hash in cache key for invalidation
- ROADMAP.md: update P5 status, add M5-M7 milestones

Tested: 3/3 parts matched (ans_lfs120), 172/175 parts matched
(F-802007.TR4-D1-H122AG). Previous: 0/25 matched.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:04:26 +01:00
Hartmut 1321ef2bd4 refactor: rename thumbnail_rendering queue to asset_pipeline
The queue handles far more than thumbnails: OCC tessellation, USD master
generation, GLB production, order line renders, and workflow renders.
asset_pipeline better reflects its role as the render-worker's primary queue.

Updated all references in: task decorators, celery_app.py, beat_tasks.py,
docker-compose.yml worker command, worker.py MONITORED_QUEUES, admin.py,
CLAUDE.md, LEARNINGS.md, Dockerfile, helpTexts.ts, test files,
and all .claude/commands/*.md skill files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 22:28:38 +01:00
Hartmut 71e099305c fix: deduplicate GLB/USD generation with Redis locks + review fixes
- Add per-file Redis SET NX EX 1800 locks to generate_gltf_geometry_task
  and generate_usd_master_task — concurrent duplicates (e.g. double-click
  of bulk action buttons) now log a warning and return immediately instead
  of running two expensive OCC tessellation subprocesses on the same file
- Fix eng.dispose() called inside with Session() block in cache-hit path
  of both tasks — moved to after the with block exits (Tasks 3+4 from plan)
- Add cad.updated_at = datetime.utcnow() in save_manual_material_overrides
  (was missing vs parallel save_part_materials endpoint)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 13:50:05 +01:00
Hartmut 409fb92899 feat(P2): USD Foundation — canonical part identity + material overrides
M1 — USD exporter:
- render-worker/scripts/export_step_to_usd.py (631 lines)
  Full XCAF traversal, one UsdGeom.Mesh per leaf part,
  schaeffler:partKey on every prim, index-space sharpEdgeVertexPairs
- render-worker/Dockerfile: usd-core>=24.11 installed (USD 0.26.3)

M2 — usd_master MediaAsset + pipeline auto-chain:
- migrations 060 (usd_master enum), 061 (3 JSONB columns),
  062 (rename tessellation settings keys)
- generate_usd_master_task: runs export_step_to_usd.py, upserts
  usd_master MediaAsset, writes resolved_material_assignments to CadFile
- Auto-chained from generate_gltf_geometry_task after every GLB export
- step_tasks.py shim re-exports generate_usd_master_task

M3 — scene-manifest API:
- part_key_service.py: build_scene_manifest(), generate_part_key(),
  four-layer material priority resolution with provenance
- SceneManifest / PartEntry Pydantic models in products/schemas.py
- GET /api/cad/{id}/scene-manifest endpoint (graceful fallback to
  parsed_objects when USD not yet generated)
- POST /api/cad/{id}/generate-usd-master endpoint
- frontend/src/api/sceneManifest.ts: fetchSceneManifest(),
  triggerUsdMasterGeneration()

M4 — manual-material-overrides API:
- GET/PUT /api/cad/{id}/manual-material-overrides endpoints
- CadFile.manual_material_overrides JSONB column (migration 061)
- getManualOverrides() / saveManualOverrides() in cad.ts

M5 — ThreeDViewer partKey integration:
- export_step_to_gltf.py injects partKeyMap into GLB extras
- ThreeDViewer: partKeyMap extraction, resolvePartKey(), effectiveMaterials
  merges legacy partMaterials + new manualOverrides (server-side persistence)
- MaterialPanel: dual-path save (partKey vs legacy), provenance badge,
  reconciliation panel for unmatched/unassigned parts

Also:
- Admin.tsx: generate-missing-usd-masters + canonical scenes bulk actions
- ProductDetail.tsx: usd_master row in asset table
- vite-env.d.ts: fix ImportMeta.env TypeScript error
- GPUProbeResult: add timestamp/devices/render_time_s fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 13:11:09 +01:00
Hartmut 47b5d42bb5 refactor(P1): M1 dead code removal + M3 blender_render.py split
M1 — dead code removed:
- Delete blender-renderer/ and threejs-renderer/ source files
- Remove PIL/Pillow fallback block from step_processor.py
  (_generate_thumbnail_placeholder, _finalise_image JPG path)
- Remove stl_quality param from render_blender.py, render_still_task,
  render_turntable_task (was always "low"; hardcode deflection values)
- render_turntable_task now reads scene_linear/angular_deflection from
  system_settings (consistent with export_glb.py pipeline)

M3 — blender_render.py split from 263 → 68 lines:
- _blender_args.py: parse_args() — all 25 positional + named args
- _blender_scene_setup.py: setup_scene() — MODE A/B including USD import
- _blender_render_config.py: configure_and_render() — engine + output

Post-review fixes:
- _db_engine.dispose() after settings read in render_turntable_task
- _finalise_image() fmt param removed (always PNG; PIL never installed)
- _blender_import.py committed together with new submodules to satisfy
  import_usd_file dependency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 12:54:40 +01:00
Hartmut cd6c2f48e2 fix(gmsh): force preview-quality settings when GMSH fallback path is taken
Fine production settings (0.03mm/0.05rad) with GMSH → CharacteristicLengthMax
0.45mm → OOM kill on large assemblies even with per-solid iteration.

GMSH quality is algorithmic (conforming seams, no fan triangles) — a denser
mesh provides no extra UV-unwrap benefit. Cap GMSH tessellation at preview
settings (0.1mm/0.1rad) in the production fallback path.

Normal path (GMSH + valid geometry GLB) is unaffected — continues to reuse
_geometry.glb directly without re-tessellating.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 20:46:42 +01:00
Hartmut dbc032ec74 feat(gmsh): GMSH Frontal-Delaunay tessellation for clean cylinder seams
- Per-solid iteration prevents OOM on multi-part assemblies (25-part bearing:
  2.3GB RAM when processing compound → ~100MB per solid with per-solid approach)
- Fix CharacteristicLengthMax multiplier 5× → 15× and cap MinimumCirclePoints
  at 20 (prevents 63-pts/circle on angular_deflection=0.1rad → 231MB → 21MB)
- Geometry task timeout 120s → 600s for large assemblies
- Production task: reuse _geometry.glb when GMSH enabled (no re-tessellation)
  and cache _production_geom.glb for OCC (mtime vs STEP check)
- Viewer now prefers production GLB when available (shows correct GMSH mesh)
- GMSH OpenMP multithreading (min(cpu_count,16)) for 4.4× speedup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 20:45:27 +01:00
Hartmut af320bcdc8 feat(P3): add GMSH Frontal-Delaunay tessellation engine
Introduces GMSH as an alternative to OCC BRepMesh for STEP→GLB tessellation.
GMSH produces conforming meshes that eliminate fan triangles at cylinder seam
edges — a structural limitation of OCC BRepMesh that cannot be fixed via
deflection parameters.

Changes:
- render-worker/Dockerfile: install gmsh>=4.15.0 + libglu1-mesa + libxft2
- export_step_to_gltf.py: --tessellation_engine occ|gmsh CLI arg +
  _tessellate_with_gmsh() using BRep→GMSH→Poly_Triangulation write-back
- admin.py: tessellation_engine setting (SETTINGS_DEFAULTS, SettingsOut,
  SettingsUpdate, validation)
- export_glb.py: pass tessellation_engine to export_step_to_gltf.py CLI in
  both geometry and production GLB tasks
- Admin.tsx: radio button UI for OCC vs GMSH selection

Tested: 121 faces meshed, 0 BRepMesh fallback, 649K triangles on sample part.
Clean seam edges for UV unwrap — GMSH respects B-rep periodic face boundaries.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 19:17:26 +01:00
Hartmut ca62319688 feat: sharp edge pipeline V02, tessellation presets, media cache-bust, GMSH plan
Sharp Edge Pipeline V02:
- export_step_to_gltf.py: replace BRep_Tool.Polygon3D_s (returns None in XCAF) with
  GCPnts_UniformAbscissa curve sampling at 0.3mm step — extracts 17,129 segment pairs
- Inject sharp_edge_pairs + sharp_threshold_deg into GLB extras (scenes[0].extras)
  via binary GLB JSON-chunk patching (no extra dependency)
- export_gltf.py: read schaeffler_sharp_edge_pairs from Blender scene custom props,
  apply via KD-tree to mark edges sharp=True + seam=True (OCC mm Z-up → Blender transform)
- tools/restore_sharp_marks.py: dual-pass (dihedral angle + OCC pairs), updated coordinate
  transform (X, -Z, Y) * 0.001

Tessellation:
- Admin UI: Draft / Standard / Fine preset buttons with active-state highlighting
- Default angular deflection: preview 0.5→0.1 rad, production 0.2→0.05 rad
- export_glb.py: read updated defaults from system_settings

Media / Cache:
- media/service.py: get_download_url appends ?v={file_size_bytes} cache-buster
- media/router.py: Cache-Control: no-cache for all download/thumbnail endpoints

Render pipeline:
- still_render.py / turntable_render.py: shared GPU activation + camera improvements
- render_order_line.py: global render position support
- render_thumbnail.py: updated defaults

Frontend:
- InlineCadViewer: file_size_bytes-aware URL update triggers re-fetch on regeneration
- ThreeDViewer: material panel, part selection, PBR mode improvements
- Admin.tsx: tessellation preset cards, GMSH setting dropdown
- MediaBrowser, ProductDetail, OrderDetail, Orders: various UI improvements
- New: MaterialPanel, GlobalRenderPositionsPanel, StepIndicator components
- New: renderPositions.ts API client

Plans / Docs:
- plan.md: GMSH Frontal-Delaunay tessellation plan (6 tasks)
- LEARNINGS.md: OCC Polygon3D_s None issue + GCPnts fix
- .gitignore: add backend/core (core dump from root process)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 14:40:36 +01:00
Hartmut d1c7feacf6 fix(export_glb): upsert MediaAsset instead of DELETE+INSERT to preserve stable URLs
When re-generating a production or geometry GLB, the old approach deleted the
existing MediaAsset record and created a new one with a new UUID. Any page that
had the old download_url (/api/media/{old-id}/download) cached would then get
a 404 when trying to download, because the asset ID no longer existed in the DB.

Fix: update the existing MediaAsset record in-place (same UUID, new storage_key)
so existing download URLs remain valid after regeneration. Create a new record
only if no existing one is found.

Applies to both generate_gltf_geometry_task and generate_gltf_production_task.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 10:56:25 +01:00
Hartmut 22c29d5655 feat(azure-ai+gpu-ui): per-tenant Azure AI config + GPU health panel
- Per-tenant Azure AI config stored in tenants.tenant_config JSONB
- GET/PUT /api/tenants/{id}/ai-config + POST .../test connection
- api_key never returned to frontend (has_api_key: bool pattern)
- azure_ai.py resolves creds from tenant config when ai_enabled=True
- ai_tasks.py loads tenant config and passes it to validate_thumbnail
- Admin GPU Status section: probe button + status badge + last-checked time
- Notifications: _BELL_CHANNELS filter (notification+alert only in bell)
- Tenants.tsx: per-row Azure AI Config modal with URL auto-parse helper
- Remove duplicate in-memory /gpu-probe endpoints (kept DB-backed /probe/gpu)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 21:04:09 +01:00
Hartmut c6556434d6 refactor(section-a+c): decompose step_tasks.py into pipeline domain
Section A — 1172-line step_tasks.py split into 4 focused modules under
backend/app/domains/pipeline/tasks/:
- extract_metadata.py: process_step_file, reextract_cad_metadata,
  _auto_populate_materials_for_cad, bbox helpers + PipelineLogger
- render_thumbnail.py: render_step_thumbnail, regenerate_thumbnail
  + PipelineLogger instrumentation
- render_order_line.py: dispatch_order_line_render, render_order_line_task
  (full still+turntable paths) + PipelineLogger step tracking
- export_glb.py: generate_gltf_geometry_task, generate_gltf_production_task

All task names preserved (app.tasks.step_tasks.*) for backward compat.
step_tasks.py is now a 23-line import-only shim.

Section C — celery_app.py task routing:
- app.domains.pipeline.tasks.* → step_processing (primary route)
- All 4 new modules added to include list for Celery discovery
- app.tasks.step_tasks.* kept as legacy fallback for in-flight tasks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 20:43:56 +01:00
Hartmut b41e70cdad feat(phase7.3-ext): workflow executor + config validation
- workflow_schema.py: WorkflowConfig Pydantic model validates all node
  step values against StepName enum, edges reference declared node IDs,
  unique node IDs enforced; WorkflowEdge uses "from"/"to" aliases
- workflow_executor.py: dispatch_workflow() validates config, topological-
  sorts nodes (Kahn's algorithm, raises on cycles), maps StepName →
  Celery task name via STEP_TASK_MAP (all 15 StepName values covered),
  dispatches via celery_app.send_task()
- workflow_router.py: config validation on POST/PUT (422 on invalid);
  POST /{id}/dispatch?context_id= endpoint (PM+) dispatches workflow
  steps as Celery tasks for a given entity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 20:41:26 +01:00
Hartmut 1cc10d4bbb feat(phase7.4): order rejection + resubmit flow
- Migration 053: rejection_reason TEXT NULL on orders table
- POST /api/orders/{id}/reject (PM+): sets rejected status, cancels
  active renders, stores reason, notifies creator, broadcasts WS event
- POST /api/orders/{id}/resubmit (creator or PM+): resets to draft,
  clears rejection fields, notifies PMs
- OrderDetail: Reject button (PM+) + inline modal with reason textarea
  and notify-client checkbox; Resubmit button; rejection reason amber
  alert box shown below header when order is rejected
- Orders Kanban: rejection_reason shown as red italic note on card
- Order interface: rejected_at, rejection_reason fields

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 20:37:05 +01:00
Hartmut 1409be171c feat(phase7.3): workflow editor pipeline step nodes
- GET /api/workflows/pipeline-steps: returns all StepName enum values
  with category (input|processing|rendering|output) + descriptions;
  registered before /{workflow_id} to avoid path collision
- frontend/src/api/workflows.ts: getPipelineSteps(), PipelineStep
  and PipelineStepsResponse interfaces
- WorkflowEditor: PipelineStepsPanel showing steps grouped by category
  with collapsible accordion sections
- ConfigSidepanel: "Pipeline Step" select dropdown binds any node to a
  StepName; selected step description shown below dropdown
- Active workflow indicator: green dot next to is_active=true entries
- Improved empty state with descriptive copy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 20:24:17 +01:00
Hartmut c99976cc85 feat(phase7.2): media browser with server-side filters + pagination
- Migration 052: indexes on media_assets(asset_type, created_at) and
  products(category_key, lagertyp) for efficient filter queries
- GET /api/media/assets: JOINs media_assets→products→order_lines,
  filters by asset_type / category_key / render_status / q (ILIKE),
  paginated (page/page_size), returns total+pages count
- New schemas: MediaAssetBrowseItem, MediaAssetBrowseResponse
- frontend/src/api/media.ts: getMediaAssets(filters), typed interfaces
- MediaBrowser.tsx: rewritten with sticky filter bar (debounced search,
  type/category/status dropdowns), responsive grid, image previews,
  download buttons, pagination footer with page size selector
- Renamed legacy function to listMediaAssets for backward compat

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 20:24:03 +01:00
Hartmut 89c44b846f feat(phase5.1+6): fallback material cleanup + notification batch refactor
Phase 5.1 — MATERIAL_PALETTE removal:
- Remove MATERIAL_PALETTE + _material_to_color() from step_processor.py
- build_part_colors() now returns {part→material_name} for Blender resolver

Phase 6 — Notification Center Refactor:
- Migration 051: add channel (activity|notification|alert) to audit_log,
  add frequency (immediate|daily|never) to notification_configs
- Three notification channels: activity (per-render), notification (batch
  order summaries), alert (admin infrastructure)
- Per-render emit_notification_sync calls demoted to channel=activity
- New emit_batch_render_notification_sync(): single summary notification
  when all order lines reach terminal state ("47/50 succeeded, 3 failed")
- Beat task batch_render_notifications every 60s: safety-net for missed
  batch notifications after order completion
- GET /notifications: defaults to channel IN (notification, alert);
  accepts ?channel=activity for activity feed
- Unread count badge counts only notification+alert channels
- Notifications.tsx: three tabs (Notifications | Activity | Alerts)
- NotificationSettings.tsx: frequency dropdown per event type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 20:20:07 +01:00
Hartmut 9f54bc3ab1 feat(phase4+5): role hierarchy, tenant config, fallback material, dead code removal
Phase 4.1 — Role Hierarchy:
  - UserRole enum: add global_admin (platform operator) + tenant_admin
    (per-tenant admin); keep legacy 'admin' for backward compat
  - Role sets: ADMIN_ROLES, TENANT_ADMIN_ROLES, PM_ROLES, RLS_BYPASS_ROLES
  - New auth guards: require_global_admin(), require_tenant_admin_or_above(),
    require_pm_or_above(), is_admin(), is_privileged()
  - Legacy require_admin / require_admin_or_pm now check both old+new roles
  - Migration 049: ADD VALUE global_admin + tenant_admin with AUTOCOMMIT
    workaround; backfills admin → global_admin
  - Seed: new admin users created with global_admin role

Phase 4.3 — RLS bypass updated for global_admin in get_db + set_tenant_context

Phase 4.4 — Tenant Feature Flags:
  - Migration 050: tenant_config JSONB on tenants table
  - Tenant model: tenant_config field + get_config() accessor
  - Defaults: max_concurrent_renders=3, fallback_material, invoice_prefix etc.

Phase 5.1 — Fallback Material:
  - blender_render.py: replace PALETTE_LINEAR/PALETTE_HEX/_assign_palette_material
    with _assign_failed_material() → SCHAEFFLER_059999_FailedMaterial (magenta)
  - Unmatched parts now logged explicitly before rendering

Phase 5.2 — Remove EEVEE fallback:
  - render_blender.py: EEVEE→Cycles silent retry removed; hard failure on EEVEE error

Phase 5.3 — Remove Blender version check:
  - render_blender.py: deleted MIN_BLENDER_VERSION = (5, 0, 1) constant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 19:42:10 +01:00
Hartmut 966c3aed57 feat(refactor/phase2): pipeline modularity + dead code removal
Phase 2.3 — Fix render cancellation (real Celery task ID):
  - orders.py cancel endpoints: read celery_task_id from render_job_doc
    instead of synthetic "render-{line_id}" which was a no-op
  - render_order_line_still_task: creates RenderJobDocument at task start,
    stores self.request.id as celery_task_id, writes step-level state
    (RESOLVE_STEP_PATH → BLENDER_STILL) back to order_lines.render_job_doc

Phase 3.1 — Remove Pillow overlay dead code:
  - blender_render.py: deleted 55-line Pillow post-processing block
    (lines 798-851, green bar + model name label)
  - transparent_bg=True is always passed; the else branch was unreachable
  - Removed mention from script docstring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 19:27:33 +01:00
Hartmut ea31ed657c feat(refactor/phase1): foundation infrastructure for modular pipeline
Phase 1 of PLAN_REFACTOR.md — all four sub-tasks implemented:

1.1 PipelineLogger (backend/app/core/pipeline_logger.py)
  - Structured step_start/step_done/step_error/step_progress API
  - Publishes to Python logging AND Redis SSE via log_task_event
  - Context manager `pl.step("name")` for auto-timing

1.2 RenderJobDocument (backend/app/domains/rendering/job_document.py)
  - Pydantic JSONB schema: state machine + per-step records + timing
  - begin_step/finish_step/fail_step/skip_step helpers
  - Migration 048: adds render_job_doc JSONB column to order_lines
  - OrderLine model updated with render_job_doc field

1.3 TenantContextMiddleware (backend/app/core/middleware.py)
  - Decodes JWT, stores tenant_id + role in request.state
  - get_db updated to auto-apply RLS SET LOCAL from request.state
  - Registered in main.py (runs before every request)
  - JWT now embeds tenant_id claim via create_access_token()
  - Login endpoint passes tenant_id to token creation

1.4 ProcessStep Registry (backend/app/core/process_steps.py)
  - StepName StrEnum with all 20 pipeline step names
  - Single source of truth for log prefixes, DB records, UI labels

Also adds db_utils.py with set_tenant_sync() + get_sync_session()
for use inside Celery tasks (bypass-safe RLS helper).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 19:25:08 +01:00
Hartmut ee6eb34b4c feat: GPU rendering + material matching + perf improvements
- GPU: fix Cycles device activation order — set compute_device_type
  BEFORE engine init, re-set AFTER open_mainfile wipes preferences
- GPU: remove _mark_sharp_and_seams edit-mode loop (redundant with
  Blender 5.0 shade_smooth_by_angle), saves ~200s/render on 175 parts
- Material: fix _AFN suffix mismatch — build AF-stripped mat_map keys
  and add prefix fallback in _apply_material_library (blender_render.py)
- Material: production GLB now uses get_material_library_path() which
  checks active AssetLibrary instead of empty legacy system setting
- Admin: RenderTemplateTable multi-select output types (M2M frontend)
- Admin: MaterialLibraryPanel replaced with link to Asset Libraries
- UX: move Toaster to top-left to avoid dispatch button overlap
- SQLAlchemy: add .unique() to all RenderTemplate M2M collection queries
- Logging: flush=True on all Blender progress prints, stdout reconfigure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 19:05:03 +01:00
Hartmut 95cfe0aa93 refactor: replace STL intermediary with OCC-native STEP→GLB pipeline
- export_step_to_gltf.py: STEP→GLB via RWGltf_CafWriter + BRepBuilderAPI_Transform
  (mm→m pre-scaling, XCAFDoc_ShapeTool.GetComponents_s static method)
- Blender scripts (blender_render.py, still_render.py, turntable_render.py,
  export_gltf.py, export_blend.py): import GLB instead of STL, remove _scale_mm_to_m
- step_tasks.py: add generate_gltf_production_task, remove generate_stl_cache,
  replace _bbox_from_stl with _bbox_from_glb (trimesh), auto-queue geometry GLB
  after thumbnail render
- render_blender.py: replace _stl_from_cache_or_convert with _glb_from_step,
  remove convert_step_to_stl and export_per_part_stls
- domains/rendering/tasks.py: update render_turntable_task, export_gltf/blend tasks
  to use GLB instead of STL
- cad.py: remove STL download/generate endpoints, add generate-gltf-production
- admin.py: generate-missing-stls → generate-missing-geometry-glbs
- Frontend: replace STL cache UI with GLB generate buttons, remove stl_cached field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 16:49:18 +01:00
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
Hartmut 10ed1b5e91 feat(media): resolve thumbnail_url using product thumbnail priority (latest still → cad thumbnail) 2026-03-07 00:23:30 +01:00
Hartmut f5ca91ee02 feat: layout hamburger, media browser filters+previews, billing fixes
- Layout: mobile hamburger menu + overlay backdrop + close button; content area always full-width
- Media browser: filter chips (default still+turntable); advanced toggle for GLB/STL; thumbnail_url previews for non-image types; video hover-play for turntable
- Backend: asset_types multi-filter, thumbnail_url in MediaAssetOut, download proxy endpoint for MinIO/local files
- Admin: "Import Existing Media" button → POST /api/admin/import-media-assets
- Billing: fix invoice create 500 (MissingGreenlet — use selectinload after commit); PDF download uses axios blob instead of bare <a href> (auth header missing); fix storage.upload() accepting str|Path
- SSE task logs: task_logs.py core + router, LiveRenderLog component
- CadPreview: fix infinite loop when no gltf_geometry assets; loading screen before ThreeDViewer render
- render-worker: add trimesh layer to Dockerfile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 00:09:27 +01:00
Hartmut 382a18fd02 feat(O): UI-Vollständigkeit + v3-Workflows + OCC-Kantenanalyse
Backend:
- Phase I: notification_configs router (GET/PUT/{event}/{channel}/POST reset)
  war bereits in notifications.py — add-alias endpoint in uploads.py ergänzt
- OutputType schema: workflow_definition_id + workflow_name fields;
  PATCH unterstützt Workflow-Zuweisung; _enrich_workflow_names() batch query
- Dispatch-Integration: orders.py dispatch_renders() → dispatch_render_with_workflow()
  mit Legacy-Fallback; neues Logging
- uploads.py: POST /validations/{id}/add-alias für Material-Lücken

Pipeline:
- step_processor.py: extract_mesh_edge_data() via OCC — berechnet Dihedralwinkel
  aller Kanten, liefert suggested_smooth_angle + sharp_edge_midpoints
  Integriert in extract_cad_metadata() und process_cad_file()
- domains/rendering/tasks.py: apply_asset_library_materials_task (K3),
  export_gltf_for_order_line_task → Blender export_gltf.py (K4),
  export_blend_for_order_line_task → export_blend.py fix (K5)
- render-worker/scripts/still_render.py: _mark_sharp_and_seams() mit
  OCC midpoint KD-tree matching + UV-Seam-Markierung
- render-worker/scripts/blender_render.py: identische Funktion + mesh_attributes parsing

Frontend:
- Layout.tsx: Upload-Link in Sidebar (alle User); Asset Libraries Link (admin/PM)
- App.tsx: /asset-libraries Route
- AssetLibrary.tsx: neue Seite (Upload, Catalog-Anzeige, Refresh, Toggle, Delete)
- OutputTypeTable.tsx: Workflow-Dropdown + Legacy/Workflow Badge
- ProductDetail.tsx: Geometry-Karte (Volumen, Surface, BBox, Sharp-Winkel)
- api/outputTypes.ts + api/products.ts: neue Felder
- api/imports.ts: ImportValidation API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 23:20:55 +01:00
Hartmut f15b035b88 feat(L1): modular widget dashboard — 15 configurable widgets
Replaces monolithic AdminDashboard/ClientDashboard with a per-user
configurable widget grid. 15 widget types: ProductionStats, QueueStatus,
RecentRenders, CostOverview, WorkerStatus, KPISummary, OrderThroughput,
Revenue, ItemStatus, ProcessingTimes, RenderTimeByOutputType,
OutputTypeUsage, TopProducts, OrdersByUser, RenderBackendStats.

DashboardTimeframeContext provides shared timeframe state. Dashboard
config persisted in DB via GET/PUT /api/dashboard/config.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 23:11:13 +01:00
Hartmut a70cb55d01 feat(N): workflow pipeline, 3D viewer, worker management, QC tests
- workflow_builder.py: fix broken stubs, add render_order_line_still_task
  (resolves step_path from DB instead of passing order_line_id as step_path)
- domains/rendering/tasks.py: add render_order_line_still_task,
  export_gltf_for_order_line_task, export_blend_for_order_line_task,
  generate_gltf_geometry_task (trimesh STL→GLB, no Blender needed)
- tasks/step_tasks.py: add generate_gltf_geometry_task for CadFile GLB export
- cad router: POST /{id}/generate-gltf-geometry endpoint (admin/PM)
- worker router: GET /celery-workers + POST /scale (docker compose subprocess)
- Dockerfile: pip install -e "[dev]" to enable pytest
- docker-compose.yml: docker socket + compose file mount on backend
- ThreeDViewer.tsx: mode toggle (geometry/production), wireframe, env presets,
  download buttons (GLB + .blend)
- CadPreview.tsx: load gltf_geometry/gltf_production/blend_production assets
  from MediaAsset table and pass URLs to ThreeDViewer
- ProductDetail.tsx: "View 3D" button → /cad/:id, "Generate GLB" button
- media router/service: cad_file_id filter on GET /api/media
- WorkerManagement.tsx: new page with worker status, queue depth, scale controls
- App.tsx + Layout.tsx: /workers route + sidebar link (admin/PM)
- tests: test_rendering_service.py, test_orders_service.py (backend)
- tests: WorkerActivity.test.tsx, WorkerManagement.test.tsx (frontend)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 22:56:53 +01:00
Hartmut f1e02ded78 feat(F1): wire MinIO STL cache into render_still + render_turntable_to_file
Previously the cache_service was only used in the generate_stl_cache Celery task.
All render paths (render_still, render_turntable_to_file, render_turntable_task)
only checked for a local file and converted from scratch if missing.

Changes:
- render_blender.py: add _stl_from_cache_or_convert() helper that checks MinIO
  cache before falling back to local STEP→STL conversion. Wire into render_still()
  and render_turntable_to_file() (both STL conversion blocks).
- domains/rendering/tasks.py: wire MinIO cache check into render_turntable_task()
  inline before convert_step_to_stl(). All errors are non-fatal (falls back to
  fresh conversion).

Now a STEP file converted on one worker is available to all workers via MinIO,
avoiding redundant cadquery conversions on re-renders.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 21:54:03 +01:00