diff --git a/.claude/settings.json b/.claude/settings.json index 97a832e..7853a91 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -10,7 +10,7 @@ "hooks": [ { "type": "command", - "command": "python3 .claude/hooks/pre_tool_use.py" + "command": "python3 /home/hartmut/Documents/Copilot/schaefflerautomat/.claude/hooks/pre_tool_use.py" } ] } diff --git a/backend/app/api/routers/chat.py b/backend/app/api/routers/chat.py index 3ca059b..08b64bf 100644 --- a/backend/app/api/routers/chat.py +++ b/backend/app/api/routers/chat.py @@ -99,12 +99,21 @@ async def send_message( raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc)) except Exception as exc: error_msg = str(exc) + error_code = None # Extract meaningful error from OpenAI exceptions if hasattr(exc, 'message'): error_msg = exc.message - elif hasattr(exc, 'body') and isinstance(exc.body, dict): - error_msg = exc.body.get('error', {}).get('message', error_msg) + if hasattr(exc, 'body') and isinstance(exc.body, dict): + err = exc.body.get('error', {}) + error_code = err.get('code') + error_msg = err.get('message', error_msg) logger.error("Chat error: %s", error_msg) + # Content filter violation → return 422 with user-friendly message + if error_code == 'content_filter': + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Deine Nachricht wurde vom Azure Content Filter blockiert. Bitte formuliere sie um.", + ) raise HTTPException(status_code=500, detail=f"AI error: {error_msg[:500]}") except Exception as exc: logger.exception("Chat error for user %s", user.id) diff --git a/backend/app/api/routers/worker.py b/backend/app/api/routers/worker.py index bec0910..3966099 100644 --- a/backend/app/api/routers/worker.py +++ b/backend/app/api/routers/worker.py @@ -212,6 +212,50 @@ async def stream_render_log( from fastapi import status as http_status +@router.post("/activity/dismiss-failed", status_code=http_status.HTTP_200_OK) +async def dismiss_all_failed( + user: User = Depends(require_admin_or_pm), + db: AsyncSession = Depends(get_db), +): + """Reset all failed render jobs and CAD files so they stop showing as failures.""" + from sqlalchemy import update as sql_update + + render_result = await db.execute( + sql_update(OrderLine) + .where(OrderLine.render_status == "failed") + .values(render_status="cancelled") + ) + cad_result = await db.execute( + sql_update(CadFile) + .where(CadFile.processing_status == ProcessingStatus.failed) + .values(processing_status=ProcessingStatus.pending) + ) + await db.commit() + return {"dismissed_renders": render_result.rowcount, "dismissed_cad": cad_result.rowcount} + + +@router.post("/activity/dismiss-render/{order_line_id}", status_code=http_status.HTTP_200_OK) +async def dismiss_single_failed_render( + order_line_id: str, + user: User = Depends(require_admin_or_pm), + db: AsyncSession = Depends(get_db), +): + """Dismiss a single failed render job by setting its status to 'cancelled'.""" + result = await db.execute( + select(OrderLine).where( + OrderLine.id == order_line_id, + OrderLine.render_status == "failed", + ) + ) + line = result.scalar_one_or_none() + if not line: + raise HTTPException(404, detail="Failed render job not found") + + line.render_status = "cancelled" + await db.commit() + return {"dismissed": order_line_id} + + @router.post("/activity/{cad_file_id}/reprocess", status_code=http_status.HTTP_202_ACCEPTED) async def reprocess_cad_file( cad_file_id: str, diff --git a/backend/app/domains/pipeline/tasks/render_order_line.py b/backend/app/domains/pipeline/tasks/render_order_line.py index 148835a..a222aca 100644 --- a/backend/app/domains/pipeline/tasks/render_order_line.py +++ b/backend/app/domains/pipeline/tasks/render_order_line.py @@ -443,8 +443,9 @@ def render_order_line_task(self, order_line_id: str): if is_cinematic: # ── Cinematic highlight animation path ────────────────────── - _cine_fps = 24 - _cine_frames = 480 + # Use frame_count/fps from output_type.render_settings (already extracted above) + _cine_fps = fps # extracted from render_settings, default 25 + _cine_frames = frame_count # extracted from render_settings, default 24 emit(order_line_id, f"Starting cinematic render: {_cine_frames} frames @ {_cine_fps}fps, {render_width or 1920}x{render_height or 1080}{tmpl_info}") pl.step_start("blender_cinematic", {"frame_count": _cine_frames, "fps": _cine_fps}) from app.services.render_blender import is_blender_available, render_cinematic_to_file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fbc55f4..fd5f6ef 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,7 +13,7 @@ "@tanstack/react-query": "^5.28.4", "@tanstack/react-table": "^8.14.0", "@xyflow/react": "^12.0.0", - "axios": "^1.6.8", + "axios": "1.13.6", "clsx": "^2.1.0", "get-stream": "^9.0.1", "lucide-react": "^0.363.0", diff --git a/frontend/package.json b/frontend/package.json index d1ed051..9f24190 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,7 +17,7 @@ "@tanstack/react-query": "^5.28.4", "@tanstack/react-table": "^8.14.0", "@xyflow/react": "^12.0.0", - "axios": "^1.6.8", + "axios": "1.13.6", "clsx": "^2.1.0", "get-stream": "^9.0.1", "lucide-react": "^0.363.0", diff --git a/frontend/src/api/worker.ts b/frontend/src/api/worker.ts index 0363e04..17fb02f 100644 --- a/frontend/src/api/worker.ts +++ b/frontend/src/api/worker.ts @@ -65,6 +65,15 @@ export async function reprocessCadFile(cad_file_id: string): Promise { await api.post(`/worker/activity/${cad_file_id}/reprocess`) } +export async function dismissAllFailed(): Promise<{ dismissed_renders: number; dismissed_cad: number }> { + const res = await api.post<{ dismissed_renders: number; dismissed_cad: number }>('/worker/activity/dismiss-failed') + return res.data +} + +export async function dismissSingleRender(orderLineId: string): Promise { + await api.post(`/worker/activity/dismiss-render/${orderLineId}`) +} + export interface RenderLogEntry { ts: number t: string diff --git a/frontend/src/components/LiveRenderLog.tsx b/frontend/src/components/LiveRenderLog.tsx index 82c609d..7881669 100644 --- a/frontend/src/components/LiveRenderLog.tsx +++ b/frontend/src/components/LiveRenderLog.tsx @@ -62,7 +62,7 @@ export default function LiveRenderLog({
{expanded && ( diff --git a/frontend/src/components/chat/ChatPanel.tsx b/frontend/src/components/chat/ChatPanel.tsx index 6fc773a..d0ea7e1 100644 --- a/frontend/src/components/chat/ChatPanel.tsx +++ b/frontend/src/components/chat/ChatPanel.tsx @@ -316,8 +316,9 @@ export default function ChatPanel({ open, onClose, contextType, contextId }: Cha {/* Error state */} {sendMut.isError && (
-

- Failed to send. Please try again. +

+ {(sendMut.error as { response?: { data?: { detail?: string } } })?.response?.data?.detail + || 'Failed to send. Please try again.'}

)} diff --git a/frontend/src/pages/AssetLibrary.tsx b/frontend/src/pages/AssetLibrary.tsx index 6dfe291..910698c 100644 --- a/frontend/src/pages/AssetLibrary.tsx +++ b/frontend/src/pages/AssetLibrary.tsx @@ -41,51 +41,51 @@ function UploadModal({ onClose }: { onClose: () => void }) { return (
-
-
-

Upload Asset Library

-
-
- + setDescription(e.target.value)} />
-
-
+
@@ -183,7 +183,7 @@ function LibraryCard({ lib }: { lib: AssetLibrary }) { disabled={toggleMut.isPending} title={lib.is_active ? 'Deactivate' : 'Activate'} className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none ${ - lib.is_active ? 'bg-green-500' : 'bg-gray-300' + lib.is_active ? 'bg-green-500' : 'bg-surface-muted' } disabled:opacity-50`} > !disabled && onChange(!enabled)} className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none ${ disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer' - } ${enabled ? 'bg-blue-600' : 'bg-gray-200'}`} + } ${enabled ? 'bg-accent' : 'bg-surface-muted'}`} > (null) // Table state const [filters, setFilters] = useState(EMPTY_FILTERS) @@ -81,9 +82,10 @@ export default function OrderDetailPage() { const { data: order, isLoading } = useQuery({ queryKey: ['order', id], queryFn: () => getOrder(id!), - // Poll while renders are active (pending/processing) — stop when all terminal + // Poll while renders are active, or for 15s after dispatch to catch initial queuing refetchInterval: (query) => { const rp = query.state.data?.render_progress + if (dispatchedAt && Date.now() - dispatchedAt < 15000) return 2000 if (!rp) return false return (rp.pending > 0 || rp.processing > 0) ? 3000 : false }, @@ -113,6 +115,7 @@ export default function OrderDetailPage() { mutationFn: () => dispatchRenders(id!), onSuccess: (data) => { toast.success(`${data.dispatched} render${data.dispatched !== 1 ? 's' : ''} dispatched`) + setDispatchedAt(Date.now()) qc.invalidateQueries({ queryKey: ['order', id] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Dispatch failed'), diff --git a/frontend/src/pages/WorkerActivity.tsx b/frontend/src/pages/WorkerActivity.tsx index 390b7e6..fe46f6a 100644 --- a/frontend/src/pages/WorkerActivity.tsx +++ b/frontend/src/pages/WorkerActivity.tsx @@ -10,6 +10,7 @@ import { Link } from 'react-router-dom' import { getWorkerActivity, reprocessCadFile, CadActivityEntry, RenderLog, RenderJobEntry, getQueueStatus, purgeQueue, cancelTask, QueueTask, + dismissAllFailed, dismissSingleRender, } from '../api/worker' import LiveRenderLog from '../components/LiveRenderLog' import ConfirmModal from '../components/ConfirmModal' @@ -37,6 +38,25 @@ export default function WorkerActivityPage() { onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed'), }) + const dismissAllMut = useMutation({ + mutationFn: dismissAllFailed, + onSuccess: (res) => { + const total = res.dismissed_renders + res.dismissed_cad + toast.success(`${total} failed job${total !== 1 ? 's' : ''} cleared`) + qc.invalidateQueries({ queryKey: ['worker-activity'] }) + }, + onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to dismiss'), + }) + + const dismissOneMut = useMutation({ + mutationFn: dismissSingleRender, + onSuccess: () => { + toast.success('Render dismissed') + qc.invalidateQueries({ queryKey: ['worker-activity'] }) + }, + onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to dismiss'), + }) + const lastUpdated = dataUpdatedAt ? new Date(dataUpdatedAt).toLocaleTimeString('de-DE') : '—' @@ -84,15 +104,32 @@ export default function WorkerActivityPage() { {/* Summary */} {data && ( -
- 0 ? 'text-status-info-text' : 'text-content-secondary'} /> - 0 ? 'text-red-600' : 'text-content-secondary'} /> - 0 ? 'text-status-info-text' : 'text-content-secondary'} /> - 0 ? 'text-red-600' : 'text-content-secondary'} /> +
+
+ 0 ? 'text-status-info-text' : 'text-content-secondary'} /> + 0 ? 'text-red-600' : 'text-content-secondary'} /> + 0 ? 'text-status-info-text' : 'text-content-secondary'} /> + 0 ? 'text-red-600' : 'text-content-secondary'} /> +
+ {(data.failed_count + data.render_failed_count) > 0 && ( +
+ +
+ )}
)} @@ -124,7 +161,12 @@ export default function WorkerActivityPage() {
{events.map((ev) => ev.kind === 'render' ? ( - + dismissOneMut.mutate(ev.job.order_line_id) : undefined} + dismissPending={dismissOneMut.isPending} + /> ) : ( void; dismissPending?: boolean }) { const elapsed = job.render_started_at && job.render_completed_at ? ((new Date(job.render_completed_at).getTime() - new Date(job.render_started_at).getTime()) / 1000).toFixed(1) : null @@ -473,9 +515,21 @@ function RenderJobRow({ job }: { job: RenderJobEntry }) {
-
-

{new Date(job.updated_at).toLocaleDateString('de-DE')}

-

{new Date(job.updated_at).toLocaleTimeString('de-DE')}

+
+ {onDismiss && ( + + )} +
+

{new Date(job.updated_at).toLocaleDateString('de-DE')}

+

{new Date(job.updated_at).toLocaleTimeString('de-DE')}

+
diff --git a/frontend/src/pages/WorkflowEditor.tsx b/frontend/src/pages/WorkflowEditor.tsx index 2b2e9c4..658eb2f 100644 --- a/frontend/src/pages/WorkflowEditor.tsx +++ b/frontend/src/pages/WorkflowEditor.tsx @@ -64,15 +64,15 @@ function BaseNode({ label, icon, color, description, selected, hasSource = true, }`} > {hasTarget && ( - + )}
{icon} {label}
- {description &&

{description}

} + {description &&

{description}

} {hasSource && ( - + )}
) diff --git a/renderproblems_tmp/tesselation_problem_01.jpg b/renderproblems_tmp/tesselation_problem_01.jpg new file mode 100644 index 0000000..c6c5d84 Binary files /dev/null and b/renderproblems_tmp/tesselation_problem_01.jpg differ diff --git a/renderproblems_tmp/tesselation_problem_02.jpg b/renderproblems_tmp/tesselation_problem_02.jpg new file mode 100644 index 0000000..426996b Binary files /dev/null and b/renderproblems_tmp/tesselation_problem_02.jpg differ diff --git a/renderproblems_tmp/tesselation_problem_03.png b/renderproblems_tmp/tesselation_problem_03.png new file mode 100644 index 0000000..602b4aa Binary files /dev/null and b/renderproblems_tmp/tesselation_problem_03.png differ diff --git a/renderproblems_tmp/tesselation_with_stepper_at_200_01.jpg b/renderproblems_tmp/tesselation_with_stepper_at_200_01.jpg new file mode 100644 index 0000000..096bef1 Binary files /dev/null and b/renderproblems_tmp/tesselation_with_stepper_at_200_01.jpg differ diff --git a/renderproblems_tmp/test_occ_surface_normals.usd b/renderproblems_tmp/test_occ_surface_normals.usd new file mode 100644 index 0000000..ce7b57f Binary files /dev/null and b/renderproblems_tmp/test_occ_surface_normals.usd differ diff --git a/renderproblems_tmp/test_surface_normals.usd b/renderproblems_tmp/test_surface_normals.usd new file mode 100644 index 0000000..367cd6a Binary files /dev/null and b/renderproblems_tmp/test_surface_normals.usd differ diff --git a/renderproblems_tmp/translation_problem_04.jpg b/renderproblems_tmp/translation_problem_04.jpg new file mode 100644 index 0000000..bc4d09a Binary files /dev/null and b/renderproblems_tmp/translation_problem_04.jpg differ diff --git a/review-report.md b/review-report.md deleted file mode 100644 index 2429445..0000000 --- a/review-report.md +++ /dev/null @@ -1,45 +0,0 @@ -# Review Report: Full UI/UX Cleanup & Simplification -Date: 2026-03-15 - -## Result: ✅ Approved - -## Checklist Results - -### Frontend / TypeScript -- [x] TypeScript compiles clean (`npx tsc --noEmit` — zero errors) -- [x] No `bg-surface/50` Tailwind opacity syntax with CSS vars -- [x] Loading states preserved (all existing `isPending` usage untouched) -- [x] Error feedback preserved (all existing `toast.error` calls untouched) -- [x] Role-dependent UI elements unchanged -- [x] No new API interfaces needed (frontend-only visual refactor) - -### Backend / Database / Render Pipeline -- [x] N/A — all changes are frontend-only (6 component/page files + plan.md) - -### Security -- [x] No credentials in code -- [x] No hardcoded tokens or secrets -- [x] English variable names and comments - -## Minor Notes (non-blocking) - -### `as any` usage in val()/set() helpers -**Severity**: Low -The RenderTemplateTable uses `(editDraft as any)[field]` and `(form as any)[field]` in the shared `renderEditFormGrid()` helper — same pattern as the reference OutputTypeTable implementation. This is a TypeScript limitation with dynamic field access on union types. Non-blocking; could be improved with generics later but works correctly. - -## Positives - -1. **Consistent pattern across all 4 admin tables**: OutputTypeTable, RenderTemplateTable, PricingTierTable, and GlobalRenderPositionsPanel all now use the identical expandable edit row pattern — display row always visible, edit form as full-width colSpan row below with accent border. - -2. **RenderTemplateTable column reduction**: Consolidated 4 boolean columns (Mat Replace, Lighting Only, Shadow Catcher, Camera Orbit) into a single "Flags" column with compact badges — reduces visual width from 11 to 8 columns. - -3. **Shared renderEditFormGrid()**: Both RenderTemplateTable and PricingTierTable use a shared helper for add/edit forms, keeping the pattern DRY. - -4. **OrderDetail material override UX**: The per-line override now shows a compact "+ override" link instead of always-visible dropdown — significantly reduces visual noise while keeping functionality accessible. - -5. **WorkerManagement controls**: Larger touch targets (p-2 rounded-lg) make the scale controls usable on touch devices. - -6. **Billing status indicator**: ChevronDown icon next to the status select makes the interactivity obvious without changing the badge aesthetic. - -## Recommendation -Approved — ready to merge. diff --git a/scripts/restart.sh b/scripts/restart.sh new file mode 100755 index 0000000..e40bdae --- /dev/null +++ b/scripts/restart.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +# Parse flags +BUILD="" +SERVICE="" +for arg in "$@"; do + case "$arg" in + --build) BUILD="--build" ;; + *) SERVICE="$arg" ;; + esac +done + +if [ -n "$SERVICE" ]; then + echo "Restarting $SERVICE${BUILD:+ (with rebuild)}..." + docker compose up -d $BUILD "$SERVICE" +else + echo "Restarting all services${BUILD:+ (with rebuild)}..." + docker compose down + docker compose up -d $BUILD +fi + +echo "" +echo "Waiting for health checks..." +sleep 3 + +BACKEND=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8888/docs 2>/dev/null || echo "000") +FRONTEND=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5173 2>/dev/null || echo "000") + +echo "" +docker compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" +echo "" + +if [ "$BACKEND" = "200" ] && [ "$FRONTEND" = "200" ]; then + echo "All services up." +else + echo "WARNING: Some services may not be ready yet." + [ "$BACKEND" != "200" ] && echo " Backend returned $BACKEND" + [ "$FRONTEND" != "200" ] && echo " Frontend returned $FRONTEND" +fi diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100755 index 0000000..9f215f1 --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +echo "Starting Schaeffler Automat..." +docker compose up -d + +echo "" +echo "Waiting for health checks..." +sleep 3 + +# Check services +BACKEND=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8888/docs 2>/dev/null || echo "000") +FRONTEND=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5173 2>/dev/null || echo "000") + +echo "" +docker compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" +echo "" + +if [ "$BACKEND" = "200" ] && [ "$FRONTEND" = "200" ]; then + echo "All services up." + echo " Backend: http://localhost:8888/docs" + echo " Frontend: http://localhost:5173" +else + echo "WARNING: Some services may not be ready yet." + [ "$BACKEND" != "200" ] && echo " Backend returned $BACKEND" + [ "$FRONTEND" != "200" ] && echo " Frontend returned $FRONTEND" + echo " Run 'docker compose logs -f' to investigate." +fi diff --git a/scripts/stop.sh b/scripts/stop.sh new file mode 100755 index 0000000..41727dc --- /dev/null +++ b/scripts/stop.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +echo "Stopping Schaeffler Automat..." +docker compose down + +echo "" +echo "All services stopped."