import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { Activity, CheckCircle2, XCircle, Loader2, Clock, RefreshCw, ChevronDown, ChevronRight, RotateCcw, Terminal, Cpu, Image, Trash2, Ban, ListOrdered, FileCode2, } from 'lucide-react' import { Link } from 'react-router-dom' import { getWorkerActivity, reprocessCadFile, CadActivityEntry, RenderLog, RenderJobEntry, getQueueStatus, purgeQueue, cancelTask, QueueTask, } from '../api/worker' import LiveRenderLog from '../components/LiveRenderLog' type UnifiedEvent = | { kind: 'cad'; ts: number; entry: CadActivityEntry } | { kind: 'render'; ts: number; job: RenderJobEntry } export default function WorkerActivityPage() { const qc = useQueryClient() const [expanded, setExpanded] = useState>(new Set()) const { data, isLoading, dataUpdatedAt } = useQuery({ queryKey: ['worker-activity'], queryFn: getWorkerActivity, refetchInterval: 5000, }) const reprocessMut = useMutation({ mutationFn: reprocessCadFile, onSuccess: () => { toast.success('Re-queued for full reprocessing') qc.invalidateQueries({ queryKey: ['worker-activity'] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed'), }) const lastUpdated = dataUpdatedAt ? new Date(dataUpdatedAt).toLocaleTimeString('de-DE') : '—' const toggle = (id: string) => setExpanded((s) => { const n = new Set(s) n.has(id) ? n.delete(id) : n.add(id) return n }) // Merge + sort both event streams into one unified timeline const events: UnifiedEvent[] = [] if (data) { for (const entry of data.cad_processing) { events.push({ kind: 'cad', ts: new Date(entry.updated_at).getTime(), entry }) } for (const job of data.render_jobs) { events.push({ kind: 'render', ts: new Date(job.updated_at).getTime(), job }) } events.sort((a, b) => b.ts - a.ts) } const isEmpty = !isLoading && events.length === 0 return (

Worker Activity

Auto-refresh every 5 s · Last: {lastUpdated}
{/* Queue panel */} {/* 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'} />
)} {isLoading && (
Loading activity…
)} {isEmpty && (

No recent activity

STEP processing and render jobs will appear here.

)} {/* ── Unified timeline ─────────────────────────────────────────────── */} {events.length > 0 && (
{events.map((ev) => ev.kind === 'render' ? ( ) : ( toggle(ev.entry.cad_file_id)} onReprocess={() => reprocessMut.mutate(ev.entry.cad_file_id)} reprocessPending={reprocessMut.isPending} /> ) )}
)}
) } // ── CAD file row (extracted from inline JSX above) ──────────────────────────── function CadFileRow({ entry, expanded, onToggle, onReprocess, reprocessPending, }: { entry: CadActivityEntry expanded: boolean onToggle: () => void onReprocess: () => void reprocessPending: boolean }) { return (
{/* Type badge */} CAD {/* Expand toggle */} {expanded ? : }

{entry.original_name}

{entry.order_numbers.length > 0 && (
{entry.order_numbers.map((n) => ( {n} ))}
)} {entry.file_size != null && ( {formatBytes(entry.file_size)} )} {entry.render_log?.renderer && } {entry.render_log?.total_duration_s != null && ( {entry.render_log.total_duration_s}s )}
{entry.error_message && (

{entry.error_message}

)}

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

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

{expanded && (
)}
) } // ── Queue panel ────────────────────────────────────────────────────────────── function shortName(taskName: string): string { // "app.tasks.step_tasks.regenerate_thumbnail" → "regenerate_thumbnail" const parts = taskName.split('.') return parts[parts.length - 1] ?? taskName } function firstArg(task: QueueTask): string { const a = task.args?.[0] if (!a) return '—' const s = String(a) return s.length > 28 ? s.slice(0, 12) + '…' + s.slice(-8) : s } function QueuePanel() { const qc = useQueryClient() const { data: queue, isLoading } = useQuery({ queryKey: ['worker-queue'], queryFn: getQueueStatus, refetchInterval: 3000, }) const purgeMut = useMutation({ mutationFn: purgeQueue, onSuccess: (res) => { toast.success(res.message) qc.invalidateQueries({ queryKey: ['worker-queue'] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Purge failed'), }) const cancelMut = useMutation({ mutationFn: cancelTask, onSuccess: () => { toast.success('Task revoked') qc.invalidateQueries({ queryKey: ['worker-queue'] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Cancel failed'), }) const totalPending = queue?.pending_count ?? 0 const activeCount = queue?.active.length ?? 0 const reservedCount = queue?.reserved.length ?? 0 const isEmpty = totalPending === 0 && activeCount === 0 && reservedCount === 0 // Group pending by task name for summary const pendingGroups: Record = {} for (const t of queue?.pending ?? []) { const name = shortName(t.task_name) pendingGroups[name] = (pendingGroups[name] ?? 0) + 1 } return (
{/* Header */}

Celery Queue

{totalPending > 0 && ( )}
{/* Body */}
{/* Summary chips */}
0 ? 'text-status-warning-text' : 'text-content-muted'}`}> {totalPending} pending · 0 ? 'text-status-info-text' : 'text-content-muted'}`}> {activeCount} active · {reservedCount} reserved {queue?.queue_depths && Object.entries(queue.queue_depths).map(([q, n]) => n > 0 && ( {q}: {n} ))}
{isEmpty && !isLoading && (

Queue is empty — no pending or active tasks.

)} {/* Active tasks */} {(queue?.active.length ?? 0) > 0 && (

Active

{queue!.active.map((t) => (
{shortName(t.task_name)} {firstArg(t)} {t.worker && ( {t.worker.split('@')[0]} )}
))}
)} {/* Reserved tasks */} {(queue?.reserved.length ?? 0) > 0 && (

Reserved (prefetched)

{queue!.reserved.map((t) => (
{shortName(t.task_name)} {firstArg(t)}
))}
)} {/* Pending breakdown (grouped by task name) */} {totalPending > 0 && (

Pending ({totalPending}{totalPending > 100 ? ', showing first 100' : ''})

{Object.entries(pendingGroups).map(([name, count]) => ( {name} {count} ))}
)}
) } // ── Render job row ─────────────────────────────────────────────────────────── function RenderJobRow({ job }: { job: RenderJobEntry }) { 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 return ( <>
{/* Type badge */} Render
{job.product_name || 'Unknown product'} {job.output_type_name && ( {job.output_type_name} )}
{job.order_number && job.order_id ? ( {job.order_number} ) : job.order_number ? ( {job.order_number} ) : null} {job.render_backend_used && ( Celery )} {elapsed && ( {elapsed}s )}

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

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

) } // ── Render detail panel ────────────────────────────────────────────────────── function RenderDetails({ entry }: { entry: CadActivityEntry }) { const log = entry.render_log return (
{/* File info */}
} title="File"> {entry.order_numbers.length > 0 && ( )} {entry.error_message && (

Error

{entry.error_message}
)}
{/* Render settings */} {log && (
} title="Render settings"> {log.renderer === 'blender' && <> } {log.renderer === 'threejs' && ( )} {log.fallback && }
)} {/* Timing */} {log && (log.total_duration_s != null || log.stl_duration_s != null) && (
} title="Timing"> {log.total_duration_s != null && } {log.stl_duration_s != null && } {log.render_duration_s != null && } {log.stl_size_bytes != null && } {log.output_size_bytes != null && } {log.parts_count != null && }
)} {/* Blender log */} {log?.log_lines && log.log_lines.length > 0 && (
} title={`Blender log (${log.log_lines.length} lines)`}>
)}
) } function Section({ icon, title, children, }: { icon: React.ReactNode; title: string; children: React.ReactNode }) { return (

{icon}{title}

{children}
) } function KVGrid({ children }: { children: React.ReactNode }) { return (
{children}
) } function KV({ label, value, mono, highlight }: { label: string; value: string; mono?: boolean; highlight?: boolean }) { return (
{label} {value}
) } function BlenderLog({ lines }: { lines: string[] }) { return (
        {lines.map((l, i) => {
          const color =
            l.includes('ERROR') || l.includes('failed') ? 'text-red-400' :
            l.includes('WARNING') || l.includes('warn') ? 'text-yellow-300' :
            l.includes('Saved:') || l.includes('render done') ? 'text-green-400' :
            l.includes('separated into') || l.includes('parts_count') ? 'text-cyan-400' :
            'text-gray-200'
          return (
            {l}
          )
        })}
      
) } function RendererBadge({ log }: { log: RenderLog }) { if (log.renderer === 'blender') { const eng = log.engine_used ?? log.engine ?? '' const label = eng.includes('fallback') ? `Blender · Cycles (↩ fallback)` : `Blender · ${eng}` return ( {label} ) } if (log.renderer === 'threejs') { return ( Three.js ) } return ( {log.renderer} ) } function StatusIcon({ status }: { status: string }) { if (status === 'completed') return if (status === 'failed') return if (status === 'processing') return return } function StatCard({ label, value, color }: { label: string; value: number; color: string }) { return (

{value}

{label}

) } function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / (1024 * 1024)).toFixed(2)} MB` }