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>
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>
- 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>
- gpu_probe.py: Blender script that probes OPTIX/CUDA/HIP/ONEAPI and
exits 1 on no GPU — used at startup + on-demand from Admin UI
- blender_render.py, still_render.py, turntable_render.py: emit
RENDER_DEVICE_USED: engine=CYCLES device=GPU|CPU compute_type=...
after GPU activation; exit 2 when CYCLES_DEVICE=gpu and CPU fallback
- render_blender.py: parse RENDER_DEVICE_USED token into render_log
(device_used, compute_type, gpu_fallback); handle exit code 2 as
explicit GPU strict-mode failure
- check_version.py: check_gpu() runs gpu_probe.py at container startup;
CYCLES_DEVICE=gpu aborts startup if no GPU found
- docker-compose.yml: CYCLES_DEVICE=${CYCLES_DEVICE:-auto} env var
- gpu_tasks.py: probe_gpu Celery task on thumbnail_rendering queue;
saves result to system_settings.gpu_probe_last_result; beat every 30min
- worker.py: POST /probe/gpu (trigger) + GET /probe/gpu/result (last result)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Detect OrderLines stuck in render_status='processing' for >30min,
revoke their Celery task, mark failed, update render_job_doc, and
emit a single CHANNEL_ALERT batch notification to admins.
Runs every 5min via beat schedule alongside recover_stuck_cad_files.
Section B (tenant_id in JWT) confirmed already complete from Phase 4.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
asyncpg+alembic keeps a transaction open via begin_transaction(); calling
execution_options(isolation_level=AUTOCOMMIT) on an already-begun connection
raises InvalidRequestError. Replace with explicit COMMIT before ALTER TYPE
and BEGIN before the backfill UPDATE.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
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>
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>
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>
- 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>
OCC's RWGltf_CafWriter appends _AF0/_AF1 assembly-instance suffixes to
mesh object names when a part appears multiple times in an assembly.
The material matching in export_gltf.py only stripped Blender's .001
suffix, leaving 24/175 GLB objects without materials.
Fix: strip _AFN suffixes via while loop (handles nested _AF0_AF1),
add prefix fallback (longest key wins) as last resort before no-match.
Also improve build_materials_from_excel Jaccard matching:
- Strip _AFN and numeric hash suffixes (-21227) before tokenizing
- Add prefix-based fallback (step 3) before position fallback (step 4)
- Raise threshold 0.3 → 0.35 for better precision
- Guard prefix matches to len >= 5 to prevent trivial false positives
Result: Material substitution: 175/175 mesh objects assigned (was 151/175)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
InlineCadViewer: STL-derived GLBs have non-indexed geometry (unique vertex
per triangle face). computeVertexNormals() on non-indexed geometry produces
per-face normals (faceted shading). Fix: mergeVertices() first to create
shared/indexed geometry, then computeVertexNormals() averages across
adjacent faces → smooth shading. Indexed Blender GLBs are unaffected.
generate_gltf_geometry_task: asyncio.run() inside a Celery worker that
already runs asyncpg causes 'Future attached to a different loop'. Replace
async _store() with sync SQLAlchemy session (matching the rest of the task).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace trimesh-only GLB export with Blender headless pipeline using
export_gltf.py. The viewer GLB and downloadable GLB now receive:
- Material substitution via the Schaeffler asset library (.blend)
- OCC-derived sharp edge data (sharp_edge_midpoints from mesh_attributes)
marked as Blender sharp edges before export
Material map is resolved via resolve_material_map() to convert aliases
(e.g. "Steel--Stahl" → "SCHAEFFLER_010101_Steel-Bare") before passing
to Blender. Old gltf_geometry MediaAsset is replaced on each run to
avoid stale records accumulating.
Trimesh kept as fallback if Blender binary unavailable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- Remove max-w-* constraints from all data/table pages so content fills available width after sidebar (Billing, MediaBrowser, OrderDetail, WorkerManagement, WorkerActivity, Materials, Tenants, AssetLibrary, NewProductOrder, ProductDetail)
- Narrow form/settings pages keep their max-width (NotificationSettings, Preferences, NewOrder, Notifications)
- render_order_line_task: create MediaAsset record on render success so results immediately appear in Media Browser without requiring the retroactive import button
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
storage.upload() expects Path, not str — str has no .name attribute,
causing 'str object has no attribute name' warnings on every STL cache write.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fix(tasks): @shared_task → @celery_app.task so task uses configured redis://redis broker
(@shared_task without explicit app bound to default app → localhost:6379 → Connection refused)
- fix(tasks): import app.models before DB access so SQLAlchemy can resolve Material.creator → User
relationship (was: InvalidRequestError: 'User' failed to locate a name)
Verified: catalog refresh now finds and stores 35 materials from uploaded .blend file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fix(tasks): use RENDER_SCRIPTS_DIR env var for catalog_assets.py path
(was computing wrong path via __file__ parents → /render-worker/scripts/ which doesn't exist in container)
- fix(models): add AssetLibrary to app/models/__init__.py so alembic autogenerate discovers it
- fix(api): remove unused FileResponse import from asset_libraries.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Single chronological list sorted by updated_at (newest first)
- Type badges distinguish CAD processing (FileCode2) from Render jobs (Image)
- Render job rows now link directly to the order (/orders/:id)
- Remove separate "Render Jobs" and "CAD File Processing" sections
- Stat cards simplified to 4: CAD active/failed + Render active/failed
- Backend: add order_id to RenderJobEntry response
- Frontend: add order_id to RenderJobEntry interface, remove flamenco_job_id
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Change list/create route paths from '/' to '' on workflow_router so
GET /api/workflows and POST /api/workflows respond directly without
307 redirect. The redirect was causing axios to drop the Authorization
header, making the WorkflowEditor always show an empty list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docker-compose.yml: change render-worker build context from ./render-worker
to . (project root) so pyproject.toml is accessible; update dockerfile path
- render-worker/Dockerfile: update COPY paths for new build context;
install Python 3.11 via deadsnakes PPA (Ubuntu 22.04 ships 3.10 which
fails the >=3.11 requirement in pyproject.toml)
- 040_media_assets.py: rewrite upgrade() with raw idempotent SQL (CREATE TYPE
inside DO $$ EXCEPTION WHEN duplicate_object $$; CREATE TABLE IF NOT EXISTS;
CREATE INDEX IF NOT EXISTS) to handle pre-existing enum from partial runs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Migration 039: cad_files.mesh_attributes JSONB column.
domains/products/tasks.py: extract_mesh_attributes Celery task using pythonOCC.
still_render.py + turntable_render.py: _apply_mesh_attributes() sets auto-smooth
based on curved_ratio and topology threshold from OCC analysis.
render_blender.py: passes --mesh-attributes JSON arg to Blender subprocess.
render_still_task: loads mesh_attributes from DB and passes to renderer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>