From cdb3de353d2dd496e735c1d5f64a360cacd65a07 Mon Sep 17 00:00:00 2001 From: Lukas May Date: Thu, 5 Mar 2026 22:01:16 +0100 Subject: [PATCH] fix: Use SSE events instead of polling for preview status updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../components/review/InitiativeReview.tsx | 10 ++----- apps/web/src/components/review/ReviewTab.tsx | 28 +++++++++---------- apps/web/src/routes/initiatives/$id.tsx | 9 +++--- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/apps/web/src/components/review/InitiativeReview.tsx b/apps/web/src/components/review/InitiativeReview.tsx index 2b7709c..fc0cd7a 100644 --- a/apps/web/src/components/review/InitiativeReview.tsx +++ b/apps/web/src/components/review/InitiativeReview.tsx @@ -53,20 +53,14 @@ export function InitiativeReview({ initiativeId, onCompleted }: InitiativeReview const projectsQuery = trpc.getInitiativeProjects.useQuery({ initiativeId }); const firstProjectId = projectsQuery.data?.[0]?.id ?? null; - const previewsQuery = trpc.listPreviews.useQuery( - { initiativeId }, - { refetchInterval: 3000 }, - ); + const previewsQuery = trpc.listPreviews.useQuery({ initiativeId }); const existingPreview = previewsQuery.data?.find( (p) => p.initiativeId === initiativeId, ); const [activePreviewId, setActivePreviewId] = useState(null); const previewStatusQuery = trpc.getPreviewStatus.useQuery( { previewId: activePreviewId ?? existingPreview?.id ?? "" }, - { - enabled: !!(activePreviewId ?? existingPreview?.id), - refetchInterval: 3000, - }, + { enabled: !!(activePreviewId ?? existingPreview?.id) }, ); const preview = previewStatusQuery.data ?? existingPreview; diff --git a/apps/web/src/components/review/ReviewTab.tsx b/apps/web/src/components/review/ReviewTab.tsx index 2b8b660..7a28e3a 100644 --- a/apps/web/src/components/review/ReviewTab.tsx +++ b/apps/web/src/components/review/ReviewTab.tsx @@ -45,14 +45,17 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) { // Fetch phases for this initiative const phasesQuery = trpc.listPhases.useQuery({ initiativeId }); - const pendingReviewPhases = useMemo( - () => (phasesQuery.data ?? []).filter((p) => p.status === "pending_review"), + const reviewablePhases = useMemo( + () => (phasesQuery.data ?? []).filter((p) => p.status === "pending_review" || p.status === "completed"), [phasesQuery.data], ); - // Select first pending review phase + // Select first pending review phase, falling back to completed phases const [selectedPhaseId, setSelectedPhaseId] = useState(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) const projectsQuery = trpc.getInitiativeProjects.useQuery({ initiativeId }); @@ -78,20 +81,14 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) { ); // Preview state - const previewsQuery = trpc.listPreviews.useQuery( - { initiativeId }, - { refetchInterval: 3000 }, - ); + const previewsQuery = trpc.listPreviews.useQuery({ initiativeId }); const existingPreview = previewsQuery.data?.find( (p) => p.phaseId === activePhaseId || p.initiativeId === initiativeId, ); const [activePreviewId, setActivePreviewId] = useState(null); const previewStatusQuery = trpc.getPreviewStatus.useQuery( { previewId: activePreviewId ?? existingPreview?.id ?? "" }, - { - enabled: !!(activePreviewId ?? existingPreview?.id), - refetchInterval: 3000, - }, + { enabled: !!(activePreviewId ?? existingPreview?.id) }, ); const preview = previewStatusQuery.data ?? existingPreview; const sourceBranch = diffQuery.data?.sourceBranch ?? commitsQuery.data?.sourceBranch ?? ""; @@ -263,7 +260,7 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) { const activePhaseName = diffQuery.data?.phaseName ?? - pendingReviewPhases.find((p) => p.id === activePhaseId)?.name ?? + reviewablePhases.find((p) => p.id === activePhaseId)?.name ?? "Phase"; // 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 (

No phases pending review

@@ -297,8 +294,9 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
{/* Header: phase selector + toolbar */} ({ id: p.id, name: p.name }))} + phases={reviewablePhases.map((p) => ({ id: p.id, name: p.name, status: p.status }))} activePhaseId={activePhaseId} + isReadOnly={isActivePhaseCompleted} onPhaseSelect={handlePhaseSelect} phaseName={activePhaseName} sourceBranch={sourceBranch} diff --git a/apps/web/src/routes/initiatives/$id.tsx b/apps/web/src/routes/initiatives/$id.tsx index 7ff848e..6662f18 100644 --- a/apps/web/src/routes/initiatives/$id.tsx +++ b/apps/web/src/routes/initiatives/$id.tsx @@ -29,10 +29,11 @@ function InitiativeDetailPage() { // Single SSE stream for all live updates useLiveUpdates([ - { prefix: 'task:', invalidate: ['listPhases', 'listTasks', 'listInitiativeTasks'] }, - { prefix: 'phase:', invalidate: ['listPhases', 'listTasks', 'listInitiativePhaseDependencies'] }, - { prefix: 'agent:', invalidate: ['listAgents', 'getActiveRefineAgent'] }, - { prefix: 'page:', invalidate: ['listPages', 'getPage', 'getRootPage'] }, + { prefix: 'task:', invalidate: ['listPhases', 'listTasks', 'listInitiativeTasks'] }, + { prefix: 'phase:', invalidate: ['listPhases', 'listTasks', 'listInitiativePhaseDependencies'] }, + { prefix: 'agent:', invalidate: ['listAgents', 'getActiveRefineAgent'] }, + { prefix: 'page:', invalidate: ['listPages', 'getPage', 'getRootPage'] }, + { prefix: 'preview:', invalidate: ['listPreviews', 'getPreviewStatus'] }, ]); // tRPC queries