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 99ea784..02a9077 100644 --- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx +++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx @@ -19,7 +19,7 @@ function sandboxIframe(src: string, origin: string): boolean { } } -/** Elapsed time since an ISO string, formatted as "1m 23s". */ +/** Elapsed time since an ISO string, formatted as "1m 23s". Capped at 59m 59s. */ function useElapsed(sinceIso: string | undefined) { const [elapsed, setElapsed] = useState(""); useEffect(() => { @@ -29,7 +29,11 @@ function useElapsed(sinceIso: string | undefined) { 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`); + if (m > 59) { + setElapsed("> 1h"); + } else { + setElapsed(m > 0 ? `${m}m ${s % 60}s` : `${s}s`); + } }; update(); const id = setInterval(update, 1000); @@ -56,8 +60,14 @@ export default function PreviewTab() { (p) => p.port === 3000 && p.state === "running", ); // Also track a starting entry so we show the warm-up state instead of blank. + // Ignore ghosts older than 15 minutes. const primaryStarting = !primaryRunning - ? previews.find((p) => p.port === 3000 && p.state === "starting") + ? previews.find( + (p) => + p.port === 3000 && + p.state === "starting" && + Date.now() - new Date(p.startedAt).getTime() < 15 * 60 * 1000, + ) : undefined; // Derive in-flight / recently-failed build from prod apps. diff --git a/vibn-frontend/app/api/projects/[projectId]/dev-server/ensure/route.ts b/vibn-frontend/app/api/projects/[projectId]/dev-server/ensure/route.ts index baa8221..d2ad713 100644 --- a/vibn-frontend/app/api/projects/[projectId]/dev-server/ensure/route.ts +++ b/vibn-frontend/app/api/projects/[projectId]/dev-server/ensure/route.ts @@ -53,7 +53,7 @@ export async function POST( return NextResponse.json({ error: "Project not found" }, { status: 404 }); } - // 1. Is a dev server already running or starting? + // 1. Is a dev server already running or starting on the primary port? const running = await queryOne<{ id: string; state: string; @@ -63,7 +63,12 @@ export async function POST( }>( `SELECT id, state, preview_url, command, port FROM fs_dev_servers - WHERE project_id = $1 AND state IN ('running', 'starting') + WHERE project_id = $1 + AND port = 3000 + AND ( + state = 'running' OR + (state = 'starting' AND started_at > NOW() - INTERVAL '15 minutes') + ) ORDER BY started_at DESC LIMIT 1`, [projectId], ); @@ -78,6 +83,7 @@ export async function POST( } // 2. Do we have a previous config to restart from? + // (Limit to port 3000 since that's what the preview pane embeds) const last = await queryOne<{ command: string; port: number; @@ -85,7 +91,7 @@ export async function POST( }>( `SELECT command, port, preview_url FROM fs_dev_servers - WHERE project_id = $1 + WHERE project_id = $1 AND port = 3000 ORDER BY started_at DESC LIMIT 1`, [projectId], );