fix: Use SSE events instead of polling for preview status updates

httpBatchLink batches polling queries behind the long-running
startPreview mutation, so refetchInterval never fires independently.
Replace polling with preview: event invalidation via the existing
useLiveUpdates SSE subscription — preview:building/ready/stopped/failed
events now trigger listPreviews and getPreviewStatus invalidation.
This commit is contained in:
Lukas May
2026-03-05 22:01:16 +01:00
parent df84a877f2
commit cdb3de353d
3 changed files with 20 additions and 27 deletions

View File

@@ -53,20 +53,14 @@ export function InitiativeReview({ initiativeId, onCompleted }: InitiativeReview
const projectsQuery = trpc.getInitiativeProjects.useQuery({ initiativeId }); const projectsQuery = trpc.getInitiativeProjects.useQuery({ initiativeId });
const firstProjectId = projectsQuery.data?.[0]?.id ?? null; const firstProjectId = projectsQuery.data?.[0]?.id ?? null;
const previewsQuery = trpc.listPreviews.useQuery( const previewsQuery = trpc.listPreviews.useQuery({ initiativeId });
{ initiativeId },
{ refetchInterval: 3000 },
);
const existingPreview = previewsQuery.data?.find( const existingPreview = previewsQuery.data?.find(
(p) => p.initiativeId === initiativeId, (p) => p.initiativeId === initiativeId,
); );
const [activePreviewId, setActivePreviewId] = useState<string | null>(null); const [activePreviewId, setActivePreviewId] = useState<string | null>(null);
const previewStatusQuery = trpc.getPreviewStatus.useQuery( const previewStatusQuery = trpc.getPreviewStatus.useQuery(
{ previewId: activePreviewId ?? existingPreview?.id ?? "" }, { previewId: activePreviewId ?? existingPreview?.id ?? "" },
{ { enabled: !!(activePreviewId ?? existingPreview?.id) },
enabled: !!(activePreviewId ?? existingPreview?.id),
refetchInterval: 3000,
},
); );
const preview = previewStatusQuery.data ?? existingPreview; const preview = previewStatusQuery.data ?? existingPreview;

View File

@@ -45,14 +45,17 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
// Fetch phases for this initiative // Fetch phases for this initiative
const phasesQuery = trpc.listPhases.useQuery({ initiativeId }); const phasesQuery = trpc.listPhases.useQuery({ initiativeId });
const pendingReviewPhases = useMemo( const reviewablePhases = useMemo(
() => (phasesQuery.data ?? []).filter((p) => p.status === "pending_review"), () => (phasesQuery.data ?? []).filter((p) => p.status === "pending_review" || p.status === "completed"),
[phasesQuery.data], [phasesQuery.data],
); );
// Select first pending review phase // Select first pending review phase, falling back to completed phases
const [selectedPhaseId, setSelectedPhaseId] = useState<string | null>(null); const [selectedPhaseId, setSelectedPhaseId] = useState<string | null>(null);
const activePhaseId = selectedPhaseId ?? pendingReviewPhases[0]?.id ?? null; const defaultPhaseId = reviewablePhases.find((p) => p.status === "pending_review")?.id ?? reviewablePhases[0]?.id ?? null;
const activePhaseId = selectedPhaseId ?? defaultPhaseId;
const activePhase = reviewablePhases.find((p) => p.id === activePhaseId);
const isActivePhaseCompleted = activePhase?.status === "completed";
// Fetch projects for this initiative (needed for preview) // Fetch projects for this initiative (needed for preview)
const projectsQuery = trpc.getInitiativeProjects.useQuery({ initiativeId }); const projectsQuery = trpc.getInitiativeProjects.useQuery({ initiativeId });
@@ -78,20 +81,14 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
); );
// Preview state // Preview state
const previewsQuery = trpc.listPreviews.useQuery( const previewsQuery = trpc.listPreviews.useQuery({ initiativeId });
{ initiativeId },
{ refetchInterval: 3000 },
);
const existingPreview = previewsQuery.data?.find( const existingPreview = previewsQuery.data?.find(
(p) => p.phaseId === activePhaseId || p.initiativeId === initiativeId, (p) => p.phaseId === activePhaseId || p.initiativeId === initiativeId,
); );
const [activePreviewId, setActivePreviewId] = useState<string | null>(null); const [activePreviewId, setActivePreviewId] = useState<string | null>(null);
const previewStatusQuery = trpc.getPreviewStatus.useQuery( const previewStatusQuery = trpc.getPreviewStatus.useQuery(
{ previewId: activePreviewId ?? existingPreview?.id ?? "" }, { previewId: activePreviewId ?? existingPreview?.id ?? "" },
{ { enabled: !!(activePreviewId ?? existingPreview?.id) },
enabled: !!(activePreviewId ?? existingPreview?.id),
refetchInterval: 3000,
},
); );
const preview = previewStatusQuery.data ?? existingPreview; const preview = previewStatusQuery.data ?? existingPreview;
const sourceBranch = diffQuery.data?.sourceBranch ?? commitsQuery.data?.sourceBranch ?? ""; const sourceBranch = diffQuery.data?.sourceBranch ?? commitsQuery.data?.sourceBranch ?? "";
@@ -263,7 +260,7 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
const activePhaseName = const activePhaseName =
diffQuery.data?.phaseName ?? diffQuery.data?.phaseName ??
pendingReviewPhases.find((p) => p.id === activePhaseId)?.name ?? reviewablePhases.find((p) => p.id === activePhaseId)?.name ??
"Phase"; "Phase";
// All files from the full branch diff (for sidebar file list) // All files from the full branch diff (for sidebar file list)
@@ -285,7 +282,7 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
); );
} }
if (pendingReviewPhases.length === 0) { if (reviewablePhases.length === 0) {
return ( return (
<div className="flex h-64 items-center justify-center text-muted-foreground"> <div className="flex h-64 items-center justify-center text-muted-foreground">
<p>No phases pending review</p> <p>No phases pending review</p>
@@ -297,8 +294,9 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
<div className="rounded-lg border border-border bg-card"> <div className="rounded-lg border border-border bg-card">
{/* Header: phase selector + toolbar */} {/* Header: phase selector + toolbar */}
<ReviewHeader <ReviewHeader
phases={pendingReviewPhases.map((p) => ({ id: p.id, name: p.name }))} phases={reviewablePhases.map((p) => ({ id: p.id, name: p.name, status: p.status }))}
activePhaseId={activePhaseId} activePhaseId={activePhaseId}
isReadOnly={isActivePhaseCompleted}
onPhaseSelect={handlePhaseSelect} onPhaseSelect={handlePhaseSelect}
phaseName={activePhaseName} phaseName={activePhaseName}
sourceBranch={sourceBranch} sourceBranch={sourceBranch}

View File

@@ -29,10 +29,11 @@ function InitiativeDetailPage() {
// Single SSE stream for all live updates // Single SSE stream for all live updates
useLiveUpdates([ useLiveUpdates([
{ prefix: 'task:', invalidate: ['listPhases', 'listTasks', 'listInitiativeTasks'] }, { prefix: 'task:', invalidate: ['listPhases', 'listTasks', 'listInitiativeTasks'] },
{ prefix: 'phase:', invalidate: ['listPhases', 'listTasks', 'listInitiativePhaseDependencies'] }, { prefix: 'phase:', invalidate: ['listPhases', 'listTasks', 'listInitiativePhaseDependencies'] },
{ prefix: 'agent:', invalidate: ['listAgents', 'getActiveRefineAgent'] }, { prefix: 'agent:', invalidate: ['listAgents', 'getActiveRefineAgent'] },
{ prefix: 'page:', invalidate: ['listPages', 'getPage', 'getRootPage'] }, { prefix: 'page:', invalidate: ['listPages', 'getPage', 'getRootPage'] },
{ prefix: 'preview:', invalidate: ['listPreviews', 'getPreviewStatus'] },
]); ]);
// tRPC queries // tRPC queries