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>
This commit is contained in:
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.")
|
||||
|
||||
Reference in New Issue
Block a user