From 346cd681f9d0a1ecc7e25bc1ed4f1d3c81f94704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20F=C3=B6rtsch?= Date: Wed, 15 Apr 2026 07:06:20 +0200 Subject: [PATCH] details: surface job status, command, log, and run/cancel inline Co-Authored-By: Claude Sonnet 4.6 --- src/features/review/AudioDetailPage.tsx | 111 ++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/src/features/review/AudioDetailPage.tsx b/src/features/review/AudioDetailPage.tsx index f3f45bd..ebaa783 100644 --- a/src/features/review/AudioDetailPage.tsx +++ b/src/features/review/AudioDetailPage.tsx @@ -206,6 +206,114 @@ function TitleInput({ value, onCommit }: { value: string; onCommit: (v: string) ); } +// ─── Job section ───────────────────────────────────────────────────────────── + +interface JobSectionProps { + job: Job; + onMutate: () => void; +} + +function JobSection({ job, onMutate }: JobSectionProps) { + const [showCmd, setShowCmd] = useState(false); + const [showLog, setShowLog] = useState(job.status === "error"); + const [liveStatus, setLiveStatus] = useState(job.status); + const [liveOutput, setLiveOutput] = useState(job.output ?? ""); + const [progress, setProgress] = useState<{ seconds: number; total: number } | null>(null); + + useEffect(() => { + setLiveStatus(job.status); + setLiveOutput(job.output ?? ""); + }, [job.status, job.output, job.id]); + + useEffect(() => { + const es = new EventSource("/api/execute/events"); + es.addEventListener("job_update", (e) => { + const d = JSON.parse((e as MessageEvent).data) as { id: number; status: string; output?: string }; + if (d.id !== job.id) return; + setLiveStatus(d.status as Job["status"]); + if (d.output !== undefined) setLiveOutput(d.output); + if (d.status === "done" || d.status === "error") onMutate(); + }); + es.addEventListener("job_progress", (e) => { + const d = JSON.parse((e as MessageEvent).data) as { id: number; seconds: number; total: number }; + if (d.id !== job.id) return; + setProgress({ seconds: d.seconds, total: d.total }); + }); + return () => es.close(); + }, [job.id, onMutate]); + + const runJob = async () => { + await api.post(`/api/execute/job/${job.id}/run`); + onMutate(); + }; + const cancelJob = async () => { + await api.post(`/api/execute/job/${job.id}/cancel`); + onMutate(); + }; + const stopJob = async () => { + await api.post("/api/execute/stop"); + onMutate(); + }; + + const typeLabel = job.job_type === "transcode" ? "Audio Transcode" : "Audio Remux"; + const exitBadge = job.exit_code != null && job.exit_code !== 0 ? job.exit_code : null; + + return ( +
+
Job
+
+ {liveStatus} + {typeLabel} + {exitBadge != null && exit {exitBadge}} + {job.started_at && started {job.started_at}} + {job.completed_at && completed {job.completed_at}} +
+ + {liveOutput && ( + + )} + {liveStatus === "pending" && ( + <> + + + + )} + {liveStatus === "running" && ( + + )} +
+ {liveStatus === "running" && progress && progress.total > 0 && ( +
+
+
+ )} + {showCmd && ( +
+ {job.command} +
+ )} + {showLog && liveOutput && ( +
+ {liveOutput} +
+ )} +
+ ); +} + // ─── Detail page ────────────────────────────────────────────────────────────── export function AudioDetailPage() { @@ -348,6 +456,9 @@ export function AudioDetailPage() {
)} + {/* Job */} + {data.job && } + {/* Actions */} {plan?.status === "pending" && !plan.is_noop && (