diff --git a/.claude/commands/visualtesting.md b/.claude/commands/visualtesting.md new file mode 100644 index 0000000..7f0b883 --- /dev/null +++ b/.claude/commands/visualtesting.md @@ -0,0 +1,140 @@ +You are an expert UX auditor, QA engineer, and frontend performance specialist. +Your task is to perform a thorough audit of the web service at http://localhost:5173. + +--- + +## YOUR AUDIT MISSION + +Systematically evaluate the service across 6 dimensions and produce a structured +report with prioritized, actionable recommendations. + +--- + +## AUDIT DIMENSIONS + +### 1. ๐ŸŽจ Visual Consistency & Theme Audit +- Navigate through ALL available themes (light, dark, high-contrast, custom, etc.) +- Switch between themes mid-session and check for visual glitches or FOUC (flash of unstyled content) +- Verify consistent use of: colors, typography, spacing, border-radius, icon style, shadow depth +- Check that ALL UI states (hover, focus, active, disabled, loading, error, empty) are themed correctly +- Look for hardcoded colors that break in certain themes +- Verify component library consistency โ€” do buttons, cards, inputs, and modals share a visual language? +- Flag any element that looks "out of place" or inconsistent with the design system + +### 2. ๐Ÿงช Functional QA & UI Integrity +- Test EVERY interactive element: buttons, dropdowns, toggles, sliders, modals, accordions, tabs +- Verify all tooltips: Do they appear? Are they cut off? Do they have correct content? +- Check for overlapping elements (z-index issues, overflow problems, popover collisions) +- Test all form inputs: validation messages, placeholder text, character limits, required fields +- Check loading states: spinners, skeletons โ€” do they appear and resolve correctly? +- Test error states: 404s, failed API calls, empty states โ€” are they handled gracefully? +- Verify that keyboard navigation works (Tab order, Enter/Space activation, Escape to close) +- Check focus trapping in modals and drawers + +### 3. ๐Ÿ“ฑ Mobile Friendliness Assessment +- Simulate viewport widths: 320px, 375px, 390px, 768px, 1024px +- Check for horizontal scroll at any breakpoint (major red flag) +- Verify touch target sizes (minimum 44x44px for interactive elements) +- Assess readability: font sizes, line lengths, contrast on small screens +- Check that dropdowns, modals, and overlays are usable on mobile +- Verify that navigation collapses properly (hamburger menus, bottom bars, etc.) +- Test forms on mobile: Are inputs large enough? Does the correct keyboard type appear? +- Check images: Do they scale and crop correctly? Are they too large to load on mobile? + +### 4. โšก User Flow Efficiency Analysis +For the following key user journeys, count the number of steps and assess whether +they can be streamlined: + - Onboarding / account creation + - Reaching the core value action (the main thing users come to do) + - Settings or preferences update + - Search and filtering to a specific result + - Error recovery (what happens when something goes wrong?) + +For each flow: +- Count total clicks/taps to complete +- Identify redundant confirmation steps +- Flag missing shortcuts or bulk actions +- Suggest progressive disclosure opportunities (hide complexity until needed) +- Note any missing defaults that force unnecessary user decisions + +### 5. ๐Ÿ’ก UX Improvement Suggestions +Based on all findings above, generate prioritized suggestions: +- **Quick wins** (low effort, high impact): e.g., better default values, + clearer button labels, improved empty states +- **Medium effort improvements**: e.g., combining steps, better feedback loops, + smarter form validation +- **Strategic improvements**: e.g., onboarding redesign, navigation restructure, + design system consolidation + +For each suggestion include: + - What the current behavior is + - What the improved behavior would be + - Why this matters (user impact) + - Estimated effort: Low / Medium / High + +### 6. ๐Ÿš€ Performance & Speed Improvement Opportunities +- Identify any UI-visible lag: slow renders, janky animations, delayed responses +- Check for layout shift (elements jumping after load) +- Note any unoptimized images (large files, no lazy loading) +- Look for blocking interactions (buttons unresponsive during loading) +- Suggest optimistic UI patterns where applicable (update UI before server confirms) +- Identify where skeleton loaders or progressive rendering would reduce perceived lag +- Flag any unnecessary full-page reloads that could be AJAX/SPA transitions +- Check if heavy operations give the user feedback (progress bars, estimated time) + +--- + +## OUTPUT FORMAT + +Structure your final report as follows: + +# Schaeffler Automat โ€” UX & Quality Audit Report +**Date**: [date] +**Overall Score**: [X/10] + +## Executive Summary +[3-5 sentence overview of the service's current state and top priorities] + +## Critical Issues ๐Ÿ”ด +[Issues that are broken, inaccessible, or severely harm the experience] + +## Major Improvements ๐ŸŸ  +[Significant friction points or inconsistencies] + +## Minor Refinements ๐ŸŸก +[Polish items that would elevate the quality] + +## Wins โœ… +[What's working well โ€” don't only focus on negatives] + +## Prioritized Recommendation List +| Priority | Area | Issue | Suggestion | Effort | +|----------|------|--------|------------|--------| +| 1 | ... | ... | ... | Low | + +## Theme & Visual Consistency Report +[Detailed findings from Dimension 1] + +## Functional QA Report +[Detailed findings from Dimension 2] + +## Mobile Report +[Detailed findings from Dimension 3] + +## User Flow Efficiency Report +[Step counts and flow diagrams for key journeys] + +## Performance Observations +[Detailed findings from Dimension 6] + +--- + +## AUDIT PRINCIPLES +- Be specific: reference exact UI elements, page names, and interaction states +- Be constructive: every criticism should come with a concrete suggestion +- Prioritize ruthlessly: not everything is equally important +- Think like a first-time user AND a power user โ€” they have different needs +- The goal is: less lag, better functionality, more clarity, easy-to-use options, + consistent UI, and perfectly working themes + +Write the report to `visual-audit-report.md` in the project root. diff --git a/backend/app/api/routers/orders.py b/backend/app/api/routers/orders.py index 088e323..7927c67 100644 --- a/backend/app/api/routers/orders.py +++ b/backend/app/api/routers/orders.py @@ -954,12 +954,14 @@ async def cancel_line_render( cancelled_backend = line.render_backend_used or "celery" errors: list[str] = [] - # Revoke Celery task (best-effort) + # Revoke Celery task (best-effort) using real task ID from job document try: from app.tasks.celery_app import celery_app - celery_app.control.revoke( - f"render-{line_id}", terminate=True, signal="SIGTERM" - ) + real_task_id = None + if line.render_job_doc: + real_task_id = line.render_job_doc.get("celery_task_id") + task_id = real_task_id or f"render-{line_id}" + celery_app.control.revoke(task_id, terminate=True, signal="SIGTERM") except Exception as exc: errors.append(f"Celery revoke failed: {str(exc)[:200]}") @@ -1025,11 +1027,13 @@ async def cancel_order_renders( errors: list[str] = [] for line in lines: - # Revoke Celery task (best-effort) + # Revoke Celery task using real task ID from job document try: - celery_app.control.revoke( - f"render-{line.id}", terminate=True, signal="SIGTERM" - ) + real_task_id = None + if line.render_job_doc: + real_task_id = line.render_job_doc.get("celery_task_id") + task_id = real_task_id or f"render-{line.id}" + celery_app.control.revoke(task_id, terminate=True, signal="SIGTERM") except Exception: pass diff --git a/backend/app/domains/rendering/tasks.py b/backend/app/domains/rendering/tasks.py index 05128b1..713ba11 100644 --- a/backend/app/domains/rendering/tasks.py +++ b/backend/app/domains/rendering/tasks.py @@ -395,14 +395,47 @@ def render_order_line_still_task(self, order_line_id: str, **params) -> dict: Wraps render_still_task logic but accepts order_line_id instead of step_path. On success, creates a MediaAsset record via publish_asset. """ + import asyncio + from app.domains.rendering.job_document import RenderJobDocument, JobState + from app.core.process_steps import StepName + log_task_event(self.request.id, f"Starting render_order_line_still_task: order_line={order_line_id}", "info") + + # Initialise job document and store real Celery task ID + job_doc = RenderJobDocument.new(order_line_id=order_line_id, celery_task_id=self.request.id) + job_doc.set_state(JobState.RUNNING) + + def _save_job_doc(): + async def _run(): + from app.database import AsyncSessionLocal + from app.domains.orders.models import OrderLine + from sqlalchemy import update as _upd + async with AsyncSessionLocal() as db: + await db.execute( + _upd(OrderLine) + .where(OrderLine.id == order_line_id) + .values(render_job_doc=job_doc.to_dict()) + ) + await db.commit() + try: + asyncio.get_event_loop().run_until_complete(_run()) + except Exception as _exc: + logger.debug("_save_job_doc failed: %s", _exc) + + _save_job_doc() + + job_doc.begin_step(StepName.RESOLVE_STEP_PATH) step_path_str, cad_file_id = _resolve_step_path_for_order_line(order_line_id) if not step_path_str: + job_doc.fail_step(StepName.RESOLVE_STEP_PATH, "product missing or has no linked CAD file") + job_doc.set_state(JobState.FAILED, error="Cannot resolve STEP path") + _save_job_doc() log_task_event(self.request.id, f"Failed: cannot resolve STEP path for order_line {order_line_id}", "error") raise RuntimeError( f"Cannot resolve STEP path for order_line {order_line_id}: " "product missing or has no linked CAD file" ) + job_doc.finish_step(StepName.RESOLVE_STEP_PATH, output={"step_path": step_path_str}) step = Path(step_path_str) output_dir = step.parent / "renders" @@ -410,12 +443,24 @@ def render_order_line_still_task(self, order_line_id: str, **params) -> dict: output_path = output_dir / f"line_{order_line_id}.png" try: + job_doc.begin_step(StepName.BLENDER_STILL) from app.services.render_blender import render_still result = render_still( step_path=step, output_path=output_path, **params, ) + job_doc.finish_step( + StepName.BLENDER_STILL, + output={"output_path": str(output_path), "duration_s": result.get("total_duration_s")}, + ) + job_doc.set_state(JobState.COMPLETED, result={ + "output_path": str(output_path), + "duration_s": result.get("total_duration_s"), + "engine_used": result.get("engine_used"), + }) + _save_job_doc() + publish_asset.delay( order_line_id, "still", @@ -438,6 +483,9 @@ def render_order_line_still_task(self, order_line_id: str, **params) -> dict: _update_workflow_run_status(order_line_id, "completed") return result except Exception as exc: + job_doc.fail_step(StepName.BLENDER_STILL, str(exc)) + job_doc.set_state(JobState.FAILED, error=str(exc)) + _save_job_doc() log_task_event(self.request.id, f"Failed: {exc}", "error") logger.error("render_order_line_still_task failed for %s: %s", order_line_id, exc) try: diff --git a/render-worker/scripts/blender_render.py b/render-worker/scripts/blender_render.py index dff8992..c02172b 100644 --- a/render-worker/scripts/blender_render.py +++ b/render-worker/scripts/blender_render.py @@ -14,7 +14,6 @@ Features: - Isometric-style angle (elevation 28ยฐ, azimuth 40ยฐ). - Dynamic clip planes. - Standard (non-Filmic) colour management โ†’ no grey tint. -- Schaeffler green top bar + model name label via Pillow post-processing. """ import sys import os @@ -795,59 +794,4 @@ sys.stdout.flush() bpy.ops.render.render(write_still=True) print("[blender_render] render done.", flush=True) -# โ”€โ”€ Pillow post-processing: green bar + model name label โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -# Skip overlay for transparent renders to keep clean alpha channel -if transparent_bg: - print("[blender_render] Transparent mode โ€” skipping Pillow overlay.") -else: - try: - from PIL import Image, ImageDraw, ImageFont - - img = Image.open(output_path).convert("RGBA") - draw = ImageDraw.Draw(img) - W, H = img.size - - # Schaeffler green top bar - bar_h = max(8, H // 32) - draw.rectangle([0, 0, W - 1, bar_h - 1], fill=(0, 137, 61, 255)) - - # Model name strip at bottom - model_name = os.path.splitext(os.path.basename(glb_path))[0] - label_h = max(20, H // 20) - img.alpha_composite( - Image.new("RGBA", (W, label_h), (30, 30, 30, 180)), - dest=(0, H - label_h), - ) - - font_size = max(10, label_h - 6) - font = None - for fp in [ - "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", - "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", - "/usr/share/fonts/truetype/freefont/FreeSansBold.ttf", - ]: - if os.path.exists(fp): - try: - font = ImageFont.truetype(fp, font_size) - break - except Exception: - pass - if font is None: - font = ImageFont.load_default() - - tb = draw.textbbox((0, 0), model_name, font=font) - text_w = tb[2] - tb[0] - draw.text( - ((W - text_w) // 2, H - label_h + (label_h - (tb[3] - tb[1])) // 2), - model_name, font=font, fill=(255, 255, 255, 255), - ) - - img.convert("RGB").save(output_path, format="PNG") - print(f"[blender_render] Pillow overlay applied.") - - except ImportError: - print("[blender_render] Pillow not in Blender Python โ€“ skipping overlay.") - except Exception as exc: - print(f"[blender_render] Pillow overlay failed (non-fatal): {exc}") - print("[blender_render] Done.")