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:
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ function InitiativeDetailPage() {
|
|||||||
{ 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
|
||||||
|
|||||||
Reference in New Issue
Block a user