feat: add live refresh to job detail page via SSE

The job detail page was only server-rendered with no live updates,
unlike the jobs list page. Add a lightweight JobDetailLive client
component that subscribes to the existing SSE endpoint and calls
router.refresh() on each update, keeping the page in sync while
a job is running.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 06:52:57 +01:00
parent 5fb24188e1
commit 3daa49ae6c
2 changed files with 49 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
"use client";
import { useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
interface JobDetailLiveProps {
jobId: string;
isTerminal: boolean;
}
export function JobDetailLive({ jobId, isTerminal }: JobDetailLiveProps) {
const router = useRouter();
const isTerminalRef = useRef(isTerminal);
isTerminalRef.current = isTerminal;
useEffect(() => {
if (isTerminalRef.current) return;
const eventSource = new EventSource(`/api/jobs/${jobId}/stream`);
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
router.refresh();
if (data.status === "success" || data.status === "failed" || data.status === "cancelled") {
eventSource.close();
}
} catch {
// ignore parse errors
}
};
eventSource.onerror = () => {
eventSource.close();
};
return () => {
eventSource.close();
};
}, [jobId, router]);
return null;
}

View File

@@ -1,3 +1,5 @@
export const dynamic = "force-dynamic";
import { notFound } from "next/navigation";
import Link from "next/link";
import { apiFetch, getMetadataBatchReport, getMetadataBatchResults, getMetadataRefreshReport, MetadataBatchReportDto, MetadataBatchResultDto, MetadataRefreshReportDto } from "../../../lib/api";
@@ -5,6 +7,7 @@ import {
Card, CardHeader, CardTitle, CardDescription, CardContent,
StatusBadge, JobTypeBadge, StatBox, ProgressBar
} from "../../components/ui";
import { JobDetailLive } from "../../components/JobDetailLive";
import { getServerTranslations } from "../../../lib/i18n/server";
interface JobDetailPageProps {
@@ -158,6 +161,7 @@ export default async function JobDetailPage({ params }: JobDetailPageProps) {
const isCompleted = job.status === "success";
const isFailed = job.status === "failed";
const isCancelled = job.status === "cancelled";
const isTerminal = isCompleted || isFailed || isCancelled;
const isExtractingPages = job.status === "extracting_pages";
const isThumbnailPhase = job.status === "generating_thumbnails";
const isPhase2 = isExtractingPages || isThumbnailPhase;
@@ -199,6 +203,7 @@ export default async function JobDetailPage({ params }: JobDetailPageProps) {
return (
<>
<JobDetailLive jobId={id} isTerminal={isTerminal} />
<div className="mb-6">
<Link
href="/jobs"