diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx index 775c30c..cebec62 100644 --- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx @@ -155,6 +155,8 @@ export default function PreviewTab() { const refreshKey = usePreviewToolbarStore((s) => s.refreshKey); const currentPath = usePreviewToolbarStore((s) => s.currentPath); + const [isForceStarting, setIsForceStarting] = useState(false); + // When the user clicks the manual refresh button in the toolbar, we don't // just want to reload the iframe — we also want to trigger the same ghost/zombie // check as the initial mount, in case the server died while they were looking at it. @@ -163,9 +165,13 @@ export default function PreviewTab() { if (refreshKey === prevRefreshKeyRef.current) return; prevRefreshKeyRef.current = refreshKey; - // Reset the ensure called flag so the ensure effect (below) fires again. - ensureCalledRef.current = false; - }, [refreshKey]); + // We only reset the ensure flag if we aren't currently waiting for a forced start. + // If they hit refresh while it's already booting, don't break the state machine. + if (!isForceStarting) { + ensureCalledRef.current = false; + setEnsureStatus("idle"); + } + }, [refreshKey, isForceStarting]); useLayoutEffect(() => { if (!primaryRunning?.url) { @@ -184,29 +190,26 @@ export default function PreviewTab() { bridge.registerPreviewIframe(iframeDomRef.current, iframeSrc); }, [bridge, iframeSrc]); - const [isForceStarting, setIsForceStarting] = useState(false); - // Determine which empty state to show. const emptyContent = (() => { if (loading && !anatomy) return ; if (inFlightApp) return ; if (failedApp) return ; + // Dev server is in the process of booting (either picked up from anatomy // or we just fired the ensure endpoint and are waiting for the DB row). - if (primaryStarting) { + // If isForceStarting is true, we know we clicked the manual start button + // and are waiting for the DB to reflect 'starting'. + if (primaryStarting || ensureStatus === "starting" || isForceStarting) { return ( ); } - if ( - ensureStatus === "calling" || - ensureStatus === "starting" || - isForceStarting - ) { + if (ensureStatus === "calling") { return ( setIsForceStarting(false)); + ) + .then((res) => { + if (!res.ok) throw new Error("Failed to start"); + }) + .catch(() => setIsForceStarting(false)); }} /> );