diff --git a/app/[workspace]/project/[projectId]/(home)/preview/page.tsx b/app/[workspace]/project/[projectId]/(home)/preview/page.tsx index c5e88069..78e9deac 100644 --- a/app/[workspace]/project/[projectId]/(home)/preview/page.tsx +++ b/app/[workspace]/project/[projectId]/(home)/preview/page.tsx @@ -19,15 +19,47 @@ function sandboxIframe(src: string, origin: string): boolean { } } +/** How long a deployment has been running, formatted as "1m 23s" */ +function useElapsed(sinceIso: string | undefined) { + const [elapsed, setElapsed] = useState(""); + useEffect(() => { + // No ISO timestamp — nothing to tick. The caller won't render + // the elapsed string when sinceIso is undefined anyway. + if (!sinceIso) return; + const update = () => { + const ms = Date.now() - new Date(sinceIso).getTime(); + if (ms < 0) return; + const s = Math.floor(ms / 1000); + const m = Math.floor(s / 60); + setElapsed(m > 0 ? `${m}m ${s % 60}s` : `${s}s`); + }; + update(); + const id = setInterval(update, 1000); + return () => { + clearInterval(id); + setElapsed(""); + }; + }, [sinceIso]); + return elapsed; +} + export default function PreviewTab() { const params = useParams(); const projectId = params.projectId as string; - const { anatomy, loading } = useAnatomy(projectId, { pollMs: 0 }); + + // Poll every 5 s so build-state transitions surface without a manual refresh. + const { anatomy, loading } = useAnatomy(projectId, { pollMs: 5000 }); const previews = anatomy?.hosting.previews ?? []; - // Find the port 3000 preview if it exists, otherwise fall back to null const primaryPreview = previews.find((p) => p.port === 3000); + // Derive in-flight / recently-failed build from prod apps. + const liveApps = anatomy?.hosting.live ?? []; + const inFlightApp = liveApps.find((a) => a.inFlightBuild); + const failedApp = !inFlightApp + ? liveApps.find((a) => a.lastBuild?.status === "failed") + : undefined; + const [iframeSrc, setIframeSrc] = useState(null); const iframeDomRef = useRef(null); const bridge = usePreviewBridge(); @@ -45,6 +77,14 @@ export default function PreviewTab() { bridge.registerPreviewIframe(iframeDomRef.current, iframeSrc); }, [bridge, iframeSrc]); + // Derive content for the empty state. + const emptyContent = (() => { + if (loading && !anatomy) return ; + if (inFlightApp) return ; + if (failedApp) return ; + return ; + })(); + return (
{deviceMode === "mobile" && } - {loading && !iframeSrc ? ( -
- -
- ) : iframeSrc ? ( + {iframeSrc ? (