The guard forced all cinematic output types to legacy dispatch because
BLENDER_CINEMATIC did not exist in the workflow graph. M1 added that node,
so the guard is now stale — it was preventing cinematic output types from
ever using graph mode even after an admin sets workflow_rollout_mode=graph.
The normal gate path handles it correctly: workflow_rollout_mode=legacy_only
keeps it on legacy, and find_unsupported_graph_nodes preflight catches any
workflow that doesn't actually contain a BLENDER_CINEMATIC node.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dispatch_order_line_render now routes still renders (non-animation,
non-cinematic) to a secondary GPU queue when MULTI_GPU_LIGHT_RENDER_QUEUE
is configured and that queue has active Celery workers.
- config.py: multi_gpu_light_render_queue setting (default "" = disabled)
- render_order_line.py: implement the routing stub — load output_type via
selectinload, check render_settings.animation + .cinematic flags, call
_inspect_active_worker_queues (reused from workflow_graph_runtime) with
0.5s timeout to check if the light queue is live
No behaviour change when MULTI_GPU_LIGHT_RENDER_QUEUE is not set.
Enable by setting it to "asset_pipeline_light" and adding a
render-worker-light service with concurrency=1 to docker-compose.
docs: learnings erfasst — multi-GPU queue routing M2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
L18: remove comingSoon from email notification channel (SMTP backend already exists).
L19: remove console.debug useEffect from InlineCadViewer.tsx.
L20: sync ROADMAP.md priority status sections (P1/P2/P3/P8) with status snapshot — all Done.
L22: replace 8 bare except blocks in step_processor.py with logger.debug/warning
so geometry calculation failures are visible in logs instead of silently discarded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
M3: cors_origins setting in config.py (env CORS_ORIGINS); main.py reads from settings.
M4: add build_order_line_step_render_dir() to render_paths.py; tasks.py drops placeholder.mp4 trick.
M5: unknown workflow graph nodes now fail the run (status="failed" + logger.error) instead of silently skipping.
M6: invoice line description is now "{product} — {output_type}" instead of bare UUID; eager-loads relations.
M7: order_number_prefix setting in config.py (env ORDER_NUMBER_PREFIX, default "SA").
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
H1: model_validator in Settings raises ValueError when jwt_secret_key is
"changeme" and the process is running inside a container — fast-fail
prevents insecure deployments; local dev outside Docker is unaffected.
H7: generate_order_number now acquires pg_advisory_xact_lock before SELECT MAX,
matching the same pattern used in generate_invoice_number (billing/service.py).
Concurrent order creation can no longer race to produce duplicate numbers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
H4: add internal_api_base_url setting to config.py (default http://localhost:8888,
env-overridable via INTERNAL_API_BASE_URL); replace all 5 hardcoded base_url
strings in chat_service.py.
H6: add post-read size check in both Excel and STEP upload handlers;
raises HTTP 413 when content exceeds settings.max_upload_size_mb.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
H2: list_invoices now passes tenant_id (from current_user) to get_invoices;
get_invoices applies WHERE tenant_id filter for non-global-admin users;
get_invoice_endpoint returns 404 when tenant mismatch for non-admins.
H3: InvoiceStatusUpdate.status changed to Literal["draft","sent","paid","cancelled"]
for schema-level validation; guard also added in update_invoice_status service.
H5: _settings_to_out masks smtp_password as "***" when set, "" when empty;
update_settings skips writing when value is the "***" sentinel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_render_turntable: Option B — resolved objects as params (render_invocation, step_path,
output_path, template, emit, pl). Session and PipelineLogger stay in the caller so
no second DB connection is opened and log steps roll up to the main task.
_handle_render_task_exhausted: extracted 68-line mark-as-failed block; retry/raise
logic with Celery self.retry stays in render_order_line_task since it needs the
bound-task context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Save button disabled for non-admins (canSave prop threaded through
WorkflowEditor → WorkflowCanvas → WorkflowCanvasToolbar); tooltip
explains why when hovered
- Optimistic concurrency: migration 072 adds updated_at to
workflow_definitions; PUT /workflows/:id returns 409 when client sends
a stale updated_at; frontend shows a specific reload-prompt toast
- _legacy_dispatch now routes through dispatch_order_line_render instead
of calling render_order_line_task directly, so cancelled/rejected
order lines are skipped before queueing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
canonicalize_workflow_config was silently rebuilding any workflow with
ui.blueprint or ui.preset == still_graph from the canonical template on
every save, preflight, and read — making blueprint-based workflows
effectively read-only.
Add preserve_user_graph=True flag that skips the rebuild blocks. Only
create_workflow still uses preserve_user_graph=False so blueprints are
correctly expanded at creation time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds app_name and app_subtitle as system settings with a dedicated
Branding tab in the Admin panel. Both values are served via a public
GET /api/admin/branding endpoint (no auth) so the login page can also
show the configured name before the user signs in.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration 070: seeds 'Cinematic Highlight' output type (legacy_only, no
workflow link) for fresh installs
- Migration 071: patches existing cinematic output types — clears
workflow_definition_id and forces legacy_only rollout mode. The row
existed since 2026-03 with shadow mode + a linked workflow def;
no BLENDER_CINEMATIC graph node exists so shadow execution would fail.
- API guard in output_types POST + PATCH: cinematic output types cannot
receive a workflow_definition_id (HTTP 400)
- Defense-in-depth in dispatch_service: early legacy exit if
render_settings.cinematic is true, regardless of rollout mode
- docs: learning erfasst — cinematic rollout mode footgun
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drei Bugs aus Cinematic-Render-Session dokumentiert:
- is_cinematic immer False wegen Invocation-Override-Filter
- usd_path.exists() auf str (fehlender str→Path-Cast in turntable/cinematic)
- NameError stdout/stderr in cinematic error path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs causing all cinematic renders to silently fall through to the
turntable path and then immediately crash:
1. is_cinematic read from filtered invocation-override dict — 'cinematic' key
is not in the allowed override key list for turntable_video, so it gets
stripped. Fixed: read directly from output_type.render_settings.
2. render_turntable_to_file and render_cinematic_to_file both call
usd_path.exists() but receive a str from the caller. Added the same
isinstance str→Path conversion that render_still_to_file already had.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Completed renders no longer overwrite a cancelled status — a Blender job
that finishes after a cancel is issued is now silently dropped instead of
flipping the line back to completed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- `render_cinematic_to_file` referenced undefined `stdout`/`stderr` in the
Blender error path; replaced with `log_lines`/`stderr_lines` so the real
Blender error is actually stored in render_log instead of a Python NameError
- Progress callback hardcoded 480 frames; now uses the `frame_count` variable
- Updated docstring to reflect actual 250 frames @ 25fps (not 480 @ 24fps)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sidebar: Hart.O.Mat + Hartomatisierung 2.0
- Logo badge: S → H
- Mobile header: Hart.O.Mat
- Page title: Hart.O.Mat — Hartomatisierung 2.0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Layout: main content gets mr-96 (margin-right) when chat is open,
pushing the page content left so the chat panel sits alongside it
without overlapping. Smooth 300ms transition.
ChatPanel: internal links (/products/..., /orders/...) now use
React Router navigate() instead of target="_blank" — clicking a
product link in the chat navigates without reloading the page,
keeping the chat panel open.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added product_material filter to find_product_renders:
- Searches cad_part_materials JSONB for materials assigned to CAD parts
- find_product_renders(transparent_only=true, product_material="Durotect")
→ finds 9 products that naturally have Durotect parts with transparent renders
Two material levels explained in system prompt rule 13:
- product_material: materials from STEP/Excel (Durotect_M, Stahl, Bronze)
- material_override: single material forced on ALL parts at render time
AI now searches product_material FIRST when user asks "with Durotect material"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rule 13: when searching with multiple criteria (transparent + Durotect),
decompose the search. Show what exists (1220 transparent renders) and
explain what's missing (no Durotect material applied). Never say
"no renders found" when transparent renders DO exist.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
find_product_renders now reads actual render metadata from the
render_log JSONB (stored at render time) via COALESCE fallback:
- engine: render_log.engine_used > render_log.engine > output_type setting
- width/height: render_log > output_type.render_settings
- samples: render_log > output_type.render_settings
- render_type: still/turntable/cinematic from render_log.type
- has_template: whether a .blend template was used
All past renderings now have correct metadata without re-processing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
find_product_renders now returns full render job metadata:
- material_override (effective: line override > output type override)
- output_format (with render_overrides applied)
- resolution (width x height)
- engine, samples
- order_number + order_id (for linking)
- is_animation flag
New material_override filter: search renders by material name
(e.g. transparent_only=true, material_override="Durotect" finds
renders with Durotect material on transparent background)
AI can now answer: "Show me a transparent Durotect render" by filtering
both transparency AND material in one query.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Explicitly listed all write tools that require confirmation:
create_order, dispatch_renders, set_material_override, set_render_overrides
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split rules into read-only vs write actions:
- READ (search, list, show images, status): execute immediately
- WRITE (create orders, dispatch, set overrides): ALWAYS ask for
confirmation before executing
Prevents accidental render job creation from casual questions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
System prompt rule 12: always format product mentions as
[ProductName](/products/UUID) and orders as [OrderNumber](/orders/UUID).
ReactMarkdown in ChatPanel already renders these as clickable links
with accent color styling, so users can navigate directly from chat.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added list_materials tool to the chat agent:
- Searches SCHAEFFLER library materials by name, description, or alias
- Returns material name + schaeffler_code + aliases
- Enables: "zeig mir ein Bild mit Durotect-Material" → agent searches
for "durotect" → finds SCHAEFFLER_020101_Durotect-Blue → uses as
material_override
System prompt updated with rules 10-11:
- Explains alias → library material mapping
- Always use full SCHAEFFLER name for material_override
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
For 16:9 (1280x720), vertical FOV is narrower than horizontal —
products could be clipped top/bottom. Now applies an aspect ratio
correction factor to camera distance: wider formats push camera
further back proportionally.
Still render (_blender_camera.py) already handled this via
min(fov_h, fov_v); cinematic now has equivalent correction.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Blender 5.0 renamed Action.fcurves. Now tries fcurves first, falls
back to channels, and wraps in try/except so the render proceeds
even if LINEAR interpolation can't be set.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Old frame PNGs from previous render attempts persisted in the frames
directory, causing FFmpeg to stitch the wrong number of frames.
Now rmtree's the directory before creating it fresh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changes per user feedback:
- Keyframe interpolation: BEZIER → LINEAR (all fcurves set to LINEAR)
- Removed segment 4 (closeup) — now 3 segments only
- Frame count: 480 → 250 (10 seconds at 25fps)
- FPS: 24 → 25
- Easing removed — pure linear interpolation between segment params
- White background by default (World node Color = white)
- Transparent bg still available as override
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes for frame progress not appearing in frontend:
1. Added flush=True to all print() calls in cinematic_render.py
2. Set PYTHONUNBUFFERED=1 in subprocess environment
Without these, Python buffers stdout inside Blender, so all frame
progress lines arrive in a batch after the process exits instead
of streaming line-by-line during rendering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The filter only matched [cinematic_render] tag but Blender outputs its
own "Saved: frame_NNNN.png" lines per frame. Now also matches "Saved:"
lines, extracts the frame number, and formats as:
[cinematic_render] Frame 84/480 rendered
This shows live frame progress in the LiveRenderLog on the frontend.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
The service was sending (glb, frames_dir, width, height, engine, samples...)
but the script expected turntable order (glb, frames_dir, frame_count, degrees,
width, height, engine, samples...). Fixed by adding frame_count and degrees
placeholders to match the expected positional layout.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The kanban board kept showing deleted orders until manual reload.
Added qc.invalidateQueries for 'orders' and qc.removeQueries for
the deleted order's detail cache on delete success.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Session ID saved to localStorage (schaeffler-chat-session).
On mount, restores the last session and loads messages from DB.
"New Chat" clears the stored session.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
System prompt rewritten with explicit RULES:
- Never ask user for info you can query yourself
- "Any product" / "beliebig" → just pick one, don't ask back
- Execute immediately, no confirmation needed
- Be concise, short answers preferred
find_product_renders tool:
- No longer requires product_name or product_id
- Call with empty params → returns any recent renders
- Enables "show me any render with transparent bg"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added find_product_renders tool to the AI agent:
- Searches completed renders by product name/ID
- Returns viewable image URLs (relative paths)
- Supports transparent_only filter
- AI formats results as Markdown images/links in chat
Frontend:
- ChatPanel ReactMarkdown renders <img> and <a> tags
- Images shown inline (max 200px height, rounded, bordered)
- Links open in new tab with accent color
System prompt updated to instruct AI to use Markdown image syntax
when showing renders: 
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
- All httpx tool calls use real user_id instead of uuid(int=0) for
service token — fixes 401 Unauthorized on dispatch/override
- create_order auto-submits and auto-dispatches in one step
- System prompt updated: execute immediately without asking for
confirmation, respond in user's language
- Product search returns CAD dimensions (dim_x/y/z_mm)
- query_database tool description includes cad_files schema
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- search_products now returns dim_x_mm, dim_y_mm, dim_z_mm from
cad_files.mesh_attributes->'dimensions_mm'
- query_database tool description updated with cad_files schema
including mesh_attributes->'dimensions_mm' path
- AI can now answer "what's the biggest product?" using real dimensions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Frontend: Layout extracts order/product UUID from URL path and passes
to ChatPanel as contextType/contextId. When on /orders/{uuid}, the
chat knows which order you're viewing.
Backend: System prompt now loads actual entity data for the context:
- Order: order_number, status, line counts (completed/processing/failed)
- Product: name, PIM-ID, category, STEP file status
The AI understands "this order", "current product" etc. Example:
"What's the status of this order?" → agent knows you mean SA-2026-00164
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a tool like query_database fails (e.g., bad column name), the
SQLAlchemy session enters a failed transaction state. Subsequent
operations (like saving the assistant response) then also fail with
InFailedSQLTransactionError.
Fix: rollback the session in the except block of _execute_tool().
Also improved query_database tool description with correct column
names (category_key not category) to help the AI write valid SQL.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added react-markdown to render assistant messages with proper formatting:
- **bold** text renders as bold
- Bullet/numbered lists render correctly
- Inline code renders with monospace background
- User messages stay as plain text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Frontend interface expected 'message' and 'response' but API returns
'user_message' and 'assistant_message'. Field name mismatch caused
undefined access and page crash.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Catch all exceptions (not just ValueError) and return meaningful
error messages from OpenAI API errors instead of generic 500.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>