refactor: rename thumbnail_rendering queue to asset_pipeline

The queue handles far more than thumbnails: OCC tessellation, USD master
generation, GLB production, order line renders, and workflow renders.
asset_pipeline better reflects its role as the render-worker's primary queue.

Updated all references in: task decorators, celery_app.py, beat_tasks.py,
docker-compose.yml worker command, worker.py MONITORED_QUEUES, admin.py,
CLAUDE.md, LEARNINGS.md, Dockerfile, helpTexts.ts, test files,
and all .claude/commands/*.md skill files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 22:28:38 +01:00
parent e7b70a35ea
commit 1321ef2bd4
39 changed files with 540 additions and 122 deletions
+24 -2
View File
@@ -138,6 +138,7 @@ export default function AdminPage() {
const [tessellationDraft, setTessellationDraft] = useState<Partial<Settings>>({})
const tess = { ...settings, ...tessellationDraft } as Settings
const [showAdvancedTess, setShowAdvancedTess] = useState(false)
const { data: rendererStatus, refetch: refetchStatus } = useQuery({
queryKey: ['renderer-status'],
@@ -1332,6 +1333,7 @@ export default function AdminPage() {
/>
<span className="text-sm text-content">Apply Laplacian smoothing on export</span>
</label>
<p className="text-xs text-content-muted mt-1">Smooths surface normals during GLB export for a less faceted look in the 3D viewer.</p>
</div>
</div>
@@ -1348,8 +1350,10 @@ export default function AdminPage() {
max="10000"
value={viewer3d.viewer_max_distance ?? 50}
onChange={e => setViewerDraft(d => ({ ...d, viewer_max_distance: parseFloat(e.target.value) }))}
title="Maximum camera distance from the model in the 3D viewer (in metres after mm→m conversion). Default: 50"
className="input w-full"
/>
<p className="text-xs text-content-muted mt-1">Maximum camera pull-back distance in the 3D viewer (metres).</p>
</div>
<div>
<label className="text-sm font-medium text-content-muted block mb-1">
@@ -1362,8 +1366,10 @@ export default function AdminPage() {
max="1"
value={viewer3d.viewer_min_distance ?? 0.001}
onChange={e => setViewerDraft(d => ({ ...d, viewer_min_distance: parseFloat(e.target.value) }))}
title="Minimum camera distance from the model in the 3D viewer (in metres). Default: 0.001. Prevents clipping into the geometry."
className="input w-full"
/>
<p className="text-xs text-content-muted mt-1">Closest the camera can zoom in (metres). Prevents clipping through geometry.</p>
</div>
</div>
@@ -1376,11 +1382,13 @@ export default function AdminPage() {
<select
value={viewer3d.gltf_material_quality ?? 'pbr_colors'}
onChange={e => setViewerDraft(d => ({ ...d, gltf_material_quality: e.target.value }))}
title="Controls what material data is embedded in exported GLB files. 'None' exports bare geometry; 'PBR Colors' bakes part colours into PBR materials."
className="input w-full"
>
<option value="none">None (geometry only)</option>
<option value="pbr_colors">PBR Colors (from part colors)</option>
</select>
<p className="text-xs text-content-muted mt-1">Material data embedded in exported GLB files.</p>
</div>
<div>
<label className="text-sm font-medium text-content-muted block mb-1">
@@ -1393,8 +1401,10 @@ export default function AdminPage() {
max="1"
value={viewer3d.gltf_pbr_roughness ?? 0.4}
onChange={e => setViewerDraft(d => ({ ...d, gltf_pbr_roughness: parseFloat(e.target.value) }))}
title="Surface roughness for GLB PBR materials (0 = mirror-smooth, 1 = fully matte). Default: 0.4 — appropriate for brushed metal."
className="input w-full"
/>
<p className="text-xs text-content-muted mt-1">0 = mirror-smooth, 1 = fully matte. Default 0.4 suits brushed metal.</p>
</div>
<div>
<label className="text-sm font-medium text-content-muted block mb-1">
@@ -1407,8 +1417,10 @@ export default function AdminPage() {
max="1"
value={viewer3d.gltf_pbr_metallic ?? 0.6}
onChange={e => setViewerDraft(d => ({ ...d, gltf_pbr_metallic: parseFloat(e.target.value) }))}
title="Metallic factor for GLB PBR materials (0 = dielectric/plastic, 1 = fully metallic). Default: 0.6 — suitable for steel parts."
className="input w-full"
/>
<p className="text-xs text-content-muted mt-1">0 = plastic/dielectric, 1 = fully metallic. Default 0.6 suits steel parts.</p>
</div>
</div>
@@ -1525,10 +1537,19 @@ export default function AdminPage() {
</div>
</div>
<button
onClick={() => setShowAdvancedTess(v => !v)}
className="text-xs text-accent hover:underline flex items-center gap-1 mt-1"
>
{showAdvancedTess ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
{showAdvancedTess ? 'Hide manual values' : 'Advanced: manual deflection values'}
</button>
{/* Manual inputs */}
{showAdvancedTess && (<>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-4">
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide">Scene / Viewer</p>
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide">Scene (USD Master)</p>
<div className="flex items-center gap-3">
<label className="text-sm text-content-secondary w-36 shrink-0">Linear deflection</label>
<input
@@ -1555,7 +1576,7 @@ export default function AdminPage() {
/>
<span className="text-sm text-content-muted">rad</span>
</div>
<p className="text-xs text-content-muted">Used for the 3D viewer (canonical scene). Smaller = smoother surfaces.</p>
<p className="text-xs text-content-muted">Used for the USD master + 3D viewer GLB (canonical scene). Smaller = smoother surfaces.</p>
</div>
<div className="space-y-4">
<p className="text-xs font-semibold text-content-secondary uppercase tracking-wide">Render output</p>
@@ -1588,6 +1609,7 @@ export default function AdminPage() {
<p className="text-xs text-content-muted">Used for final render output. Smaller = smoother surfaces, larger file sizes.</p>
</div>
</div>
</>)}
<div className="flex gap-2">
<button
onClick={() => { updateSettingsMut.mutate(tessellationDraft); setTessellationDraft({}) }}
+16 -7
View File
@@ -504,13 +504,22 @@ export default function MediaBrowserPage() {
<span className="text-sm">Loading assets</span>
</div>
) : items.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-content-muted gap-3">
<Image size={48} className="opacity-25" />
<p className="text-sm font-medium">No media assets found.</p>
<p className="text-xs text-center max-w-xs">
Renders will appear here once orders are completed. Try adjusting your filters.
</p>
</div>
(() => {
const hasActiveFilters = !!(q || assetType || categoryKey || renderStatus)
return (
<div className="flex flex-col items-center justify-center h-64 gap-3" style={{ color: 'var(--color-content-muted)' }}>
<Image size={48} style={{ opacity: 0.25 }} />
<p className="text-sm font-medium">
{hasActiveFilters ? 'No assets match your filters.' : 'No assets yet — upload a STEP file to get started'}
</p>
{hasActiveFilters && (
<p className="text-xs text-center max-w-xs">
Try adjusting your search or filter settings.
</p>
)}
</div>
)
})()
) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
{items.map(asset => (
+90 -4
View File
@@ -1,6 +1,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useParams, useNavigate, Link } from 'react-router-dom'
import { useState, useMemo, Fragment } from 'react'
import { createPortal } from 'react-dom'
import {
ArrowLeft, Send, Trash2,
FileBox, AlertTriangle, CheckCircle2, Image as ImageIcon, Unlink,
@@ -11,7 +12,7 @@ import {
XCircle, RotateCw, Info,
} from 'lucide-react'
import { toast } from 'sonner'
import { getOrder, submitOrder, deleteOrder, unlinkCadFile, regenerateItemThumbnail, patchOrderItem, removeOrderLine, dispatchRenders, cancelLineRender, cancelOrderRenders, splitMissingStep, generateLinesFromItems, downloadOrderRenders, rejectOrder, resubmitOrder } from '../api/orders'
import { getOrder, submitOrder, deleteOrder, unlinkCadFile, regenerateItemThumbnail, patchOrderItem, removeOrderLine, dispatchRenders, cancelLineRender, cancelOrderRenders, splitMissingStep, generateLinesFromItems, downloadOrderRenders, rejectOrder, resubmitOrder, rejectOrderLine } from '../api/orders'
import type { OrderItem, OrderLine } from '../api/orders'
import { listOutputTypes } from '../api/outputTypes'
import type { OutputType } from '../api/outputTypes'
@@ -827,6 +828,8 @@ function OrderLineRow({
}) {
const qc = useQueryClient()
const [showInfo, setShowInfo] = useState(false)
const [rejectLineModalOpen, setRejectLineModalOpen] = useState(false)
const [rejectLineReason, setRejectLineReason] = useState('')
const removeMut = useMutation({
mutationFn: () => removeOrderLine(orderId, line.id),
@@ -843,7 +846,19 @@ function OrderLineRow({
onError: (e: any) => toast.error(e.response?.data?.detail || 'Cancel failed'),
})
const rejectLineMut = useMutation({
mutationFn: () => rejectOrderLine(orderId, line.id, rejectLineReason),
onSuccess: () => {
toast.success('Line rejected')
setRejectLineModalOpen(false)
setRejectLineReason('')
qc.invalidateQueries({ queryKey: ['order', orderId] })
},
onError: (e: any) => toast.error(e.response?.data?.detail || 'Reject failed'),
})
const canCancel = isPrivileged && (line.render_status === 'processing' || line.render_status === 'pending') && line.output_type_id
const canRejectLine = isPrivileged && line.item_status !== 'rejected'
const renderStatusColor: Record<string, string> = {
pending: 'bg-surface-muted text-content-muted',
@@ -1003,7 +1018,21 @@ function OrderLineRow({
{/* Item Status */}
<td className="px-4 py-2">
<ItemStatusBadge status={line.item_status} />
<div className="flex items-center gap-1.5">
<ItemStatusBadge status={line.item_status} />
{canRejectLine && (
<button
onClick={(e) => {
e.stopPropagation()
setRejectLineModalOpen(true)
}}
className="text-content-muted hover:text-red-500 transition-colors"
title="Reject this line"
>
<XCircle size={12} />
</button>
)}
</div>
</td>
{/* Remove (draft only) */}
@@ -1027,6 +1056,61 @@ function OrderLineRow({
renderStartedAt={line.render_started_at}
renderCompletedAt={line.render_completed_at}
/>
{rejectLineModalOpen && createPortal(
<div
className="fixed inset-0 z-50 flex items-center justify-center"
style={{ backgroundColor: 'rgba(0,0,0,0.4)' }}
onClick={() => { setRejectLineModalOpen(false); setRejectLineReason('') }}
>
<div
className="rounded-xl shadow-2xl border border-border-default w-full max-w-md mx-4 p-6"
style={{ backgroundColor: 'var(--color-bg-surface)' }}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-3 mb-4">
<XCircle size={20} className="text-red-500 shrink-0" />
<h2 className="text-lg font-semibold text-content">Reject this item?</h2>
<button
onClick={() => { setRejectLineModalOpen(false); setRejectLineReason('') }}
className="ml-auto p-1 text-content-muted hover:text-content transition-colors rounded"
>
<X size={16} />
</button>
</div>
<div className="mb-4">
<p className="text-sm text-content-secondary mb-3">
Optionally provide a reason for rejecting this line.
</p>
<textarea
value={rejectLineReason}
onChange={(e) => setRejectLineReason(e.target.value)}
placeholder="Reason (optional)"
rows={3}
className="w-full rounded-lg border border-border-default px-3 py-2 text-sm text-content focus:outline-none focus:ring-2 focus:ring-accent resize-none"
style={{ backgroundColor: 'var(--color-bg-surface-alt)' }}
/>
</div>
<div className="flex justify-end gap-3">
<button
onClick={() => { setRejectLineModalOpen(false); setRejectLineReason('') }}
className="btn-secondary"
>
Cancel
</button>
<button
onClick={() => rejectLineMut.mutate()}
disabled={rejectLineMut.isPending}
className="px-4 py-2 rounded-lg text-sm font-semibold bg-red-600 hover:bg-red-700 text-white transition-colors disabled:opacity-50 flex items-center gap-2"
>
<XCircle size={15} />
{rejectLineMut.isPending ? 'Rejecting…' : 'Confirm Rejection'}
</button>
</div>
</div>
</div>,
document.body,
)}
</tr>
)
}
@@ -1611,10 +1695,12 @@ function SourceSpreadsheet({
{sorted.map((item) => (
<tr
key={item.id}
className={`hover:bg-surface-hover/30 group ${saving === item.id ? 'opacity-60' : ''}`}
className={`group ${saving === item.id ? 'opacity-60' : ''}`}
onMouseEnter={(e) => { (e.currentTarget as HTMLTableRowElement).style.backgroundColor = 'var(--color-bg-surface-hover)' }}
onMouseLeave={(e) => { (e.currentTarget as HTMLTableRowElement).style.backgroundColor = '' }}
>
{/* Row # */}
<td className="sticky left-0 z-10 bg-surface group-hover:bg-surface-hover/30 py-1.5 px-3 font-mono text-content-muted border-r border-b border-border-light">
<td className="sticky left-0 z-10 bg-surface py-1.5 px-3 font-mono text-content-muted border-r border-b border-border-light">
{item.row_index}
</td>
+13 -4
View File
@@ -326,10 +326,19 @@ export default function ProductLibraryPage() {
))}
</div>
) : !products?.length ? (
<div className="text-center py-16 text-content-muted">
<Library size={48} className="mx-auto mb-3 opacity-30" />
<p>No products found</p>
<p className="text-sm mt-1">Upload an Excel file to populate the library</p>
<div className="text-center py-16" style={{ color: 'var(--color-content-muted)' }}>
<Library size={48} className="mx-auto mb-3" style={{ opacity: 0.3 }} />
{debouncedSearch || categoryFilter || hasCadFilter || materialsFilter ? (
<>
<p className="font-medium">No products match your filters.</p>
<p className="text-sm mt-1">Try clearing the search or adjusting the filter criteria.</p>
</>
) : (
<>
<p className="font-medium">No products yet</p>
<p className="text-sm mt-1">Upload an Excel file to populate the library.</p>
</>
)}
</div>
) : view === 'grid' ? (
/* ── Grid view ─────────────────────────────────────────────────── */