import { RotateCcw } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { toast } from "sonner"; import { api } from "../api"; import { eventTitle } from "../constants"; import { DateTime } from "../components/DateTime"; import { EmptyState } from "../components/EmptyState"; import { StatusBadge } from "../components/StatusBadge"; import type { Job, JobEvent } from "../types"; type TimelineEvent = JobEvent & { firstId: number; lastId: number; repeatCount: number; }; function buildTimeline(events: JobEvent[]) { const grouped: TimelineEvent[] = []; [...events] .sort((a, b) => a.id - b.id) .forEach((event) => { const previous = grouped.at(-1); const isSameRun = previous && previous.type === event.type && previous.attempt === event.attempt && previous.worker_id === event.worker_id; if (isSameRun) { previous.lastId = event.id; previous.repeatCount += 1; previous.created_at = event.created_at; previous.message = event.message; previous.data = event.data; return; } grouped.push({ ...event, firstId: event.id, lastId: event.id, repeatCount: 1 }); }); return grouped.reverse(); } export function JobDetailPage() { const { jobId } = useParams(); const [job, setJob] = useState(null); const [events, setEvents] = useState([]); const refresh = useCallback(async () => { if (!jobId) return; const [jobData, eventData] = await Promise.all([api.getJob(jobId), api.listJobEvents(jobId)]); setJob(jobData); setEvents(eventData); }, [jobId]); useEffect(() => { void refresh().catch((caught) => toast.error(caught instanceof Error ? caught.message : String(caught))); const id = window.setInterval(() => void refresh().catch(() => undefined), 1000); return () => window.clearInterval(id); }, [refresh]); const timelineEvents = useMemo(() => buildTimeline(events), [events]); async function retry() { if (!job) return; try { await api.retryJob(job.id); await refresh(); toast.success("Job returned to the queue."); } catch (caught) { toast.error(caught instanceof Error ? caught.message : String(caught)); } } if (!job) { return (
); } return (
Job detail

{job.type}

Back
Status
Priority {job.priority}
Attempts {job.attempts}/{job.max_attempts}
Locked by {job.locked_by ?? "-"}

Metadata

ID {job.id}
Available
Locked until
Finished
Idempotency {job.idempotency_key ?? "-"}

Payload

{JSON.stringify(job.payload, null, 2)}

Result

{JSON.stringify(job.result ?? null, null, 2)}
{job.last_error &&

{job.last_error}

}

Event Timeline

{timelineEvents.map((event) => (
))} {!timelineEvents.length && }
); }