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