diff --git a/backend/app/api/routers/worker.py b/backend/app/api/routers/worker.py index c6600ff..a23c235 100644 --- a/backend/app/api/routers/worker.py +++ b/backend/app/api/routers/worker.py @@ -33,6 +33,7 @@ class CadActivityEntry(BaseModel): class RenderJobEntry(BaseModel): order_line_id: str + order_id: str | None order_number: str | None product_name: str | None output_type_name: str | None @@ -134,6 +135,7 @@ async def get_worker_activity( for rl in render_lines: render_entries.append(RenderJobEntry( order_line_id=str(rl.id), + order_id=str(rl.order_id) if rl.order_id else None, order_number=rl.order.order_number if rl.order else None, product_name=rl.product.name if rl.product else None, output_type_name=rl.output_type.name if rl.output_type else None, diff --git a/frontend/src/api/worker.ts b/frontend/src/api/worker.ts index c676063..e9a5f3b 100644 --- a/frontend/src/api/worker.ts +++ b/frontend/src/api/worker.ts @@ -37,12 +37,12 @@ export interface CadActivityEntry { export interface RenderJobEntry { order_line_id: string + order_id: string | null order_number: string | null product_name: string | null output_type_name: string | null render_status: 'processing' | 'completed' | 'failed' | string render_backend_used: string | null - flamenco_job_id: string | null render_started_at: string | null render_completed_at: string | null updated_at: string diff --git a/frontend/src/pages/WorkerActivity.tsx b/frontend/src/pages/WorkerActivity.tsx index bc4e057..3b75376 100644 --- a/frontend/src/pages/WorkerActivity.tsx +++ b/frontend/src/pages/WorkerActivity.tsx @@ -4,7 +4,7 @@ import { toast } from 'sonner' import { Activity, CheckCircle2, XCircle, Loader2, Clock, RefreshCw, ChevronDown, ChevronRight, RotateCcw, Terminal, Cpu, Image, - ExternalLink, Trash2, Ban, ListOrdered, + Trash2, Ban, ListOrdered, FileCode2, } from 'lucide-react' import { Link } from 'react-router-dom' import { @@ -13,6 +13,10 @@ import { } 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()) @@ -43,6 +47,20 @@ export default function WorkerActivityPage() { 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 (
@@ -65,13 +83,11 @@ 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'} /> )} - {data && data.cad_processing.length === 0 && !isLoading && ( + {isEmpty && (

No recent activity

-

STEP file processing jobs will appear here.

+

STEP processing and render jobs will appear here.

)} - {/* ── Render Jobs ─────────────────────────────────────────────────── */} - {data && data.render_jobs.length > 0 && ( -
-

Render Jobs

-
- {data.render_jobs.map((job) => ( - - ))} -
-
- )} - - {/* ── CAD File Processing ──────────────────────────────────────────── */} - {data && data.cad_processing.length > 0 && ( -
-

CAD File Processing

-
- )} - {data && data.cad_processing.length > 0 && ( + {/* ── Unified timeline ─────────────────────────────────────────────── */} + {events.length > 0 && (
- {data.cad_processing.map((entry) => ( -
- {/* ── Main row ── */} -
toggle(entry.cad_file_id)} - > - {/* Expand toggle */} - - {expanded.has(entry.cad_file_id) - ? - : } - + {events.map((ev) => + ev.kind === 'render' ? ( + + ) : ( + toggle(ev.entry.cad_file_id)} + onReprocess={() => reprocessMut.mutate(ev.entry.cad_file_id)} + reprocessPending={reprocessMut.isPending} + /> + ) + )} +
+ )} +
+ ) +} - {/* Status icon */} - +// ── CAD file row (extracted from inline JSX above) ──────────────────────────── - {/* File name */} -
-

- {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 total - - )} -
- {entry.error_message && ( -

- {entry.error_message} -

- )} -
+function CadFileRow({ + entry, expanded, onToggle, onReprocess, reprocessPending, +}: { + entry: CadActivityEntry + expanded: boolean + onToggle: () => void + onReprocess: () => void + reprocessPending: boolean +}) { + return ( +
+
+ {/* Type badge */} + + CAD + - {/* Timestamp */} -
-

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

-

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

-
+ {/* Expand toggle */} + + {expanded ? : } + - {/* Re-process button */} - + + +
+

+ {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} +

+ )} +
- {/* ── Expanded detail panel ── */} - {expanded.has(entry.cad_file_id) && ( -
- -
- )} -
- ))} +
+

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

+

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

+
+ + +
+ + {expanded && ( +
+
)}
@@ -393,6 +411,11 @@ function RenderJobRow({ job }: { job: RenderJobEntry }) { return ( <>
+ {/* Type badge */} + + Render + +
@@ -407,32 +430,22 @@ function RenderJobRow({ job }: { job: RenderJobEntry }) { )}
- {job.order_number && ( + {job.order_number && job.order_id ? ( {job.order_number} - )} - {job.render_backend_used && ( - - {job.render_backend_used === 'flamenco' ? 'Flamenco' : 'Celery'} + ) : job.order_number ? ( + + {job.order_number} + + ) : null} + {job.render_backend_used && ( + + Celery - )} - {job.flamenco_job_id && ( - - Flamenco - )} {elapsed && (