diff --git a/vibn-frontend/app/api/projects/[projectId]/anatomy/route.ts b/vibn-frontend/app/api/projects/[projectId]/anatomy/route.ts index b78f4fb1..5339dc21 100644 --- a/vibn-frontend/app/api/projects/[projectId]/anatomy/route.ts +++ b/vibn-frontend/app/api/projects/[projectId]/anatomy/route.ts @@ -792,7 +792,60 @@ async function loadPreviews(projectId: string): Promise { WHERE project_id = $1 AND state != 'stopped'`, [projectId], ); - return sortDevPreviewsFrontendFirst(rows).map((r) => ({ + + // Filter out zombies: if a server is marked 'running' but the URL returns a 50x + // Gateway error or times out, the process died. We mark it stopped so the + // UI can trigger an auto-restart. + const activePreviews: typeof rows = []; + + await Promise.all( + rows.map(async (r) => { + if (r.state !== "running") { + activePreviews.push(r); + return; + } + + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 2500); // Fast 2.5s timeout + const ping = await fetch(r.preview_url, { + method: "HEAD", + signal: controller.signal, + }); + clearTimeout(timeout); + + // 502/503/504 means Traefik is up but the container isn't answering. + // 404 means Traefik doesn't even know about the route. + if ( + ping.status === 502 || + ping.status === 503 || + ping.status === 504 || + ping.status === 404 + ) { + console.warn( + `[anatomy] Preview zombie detected for ${r.preview_url} (HTTP ${ping.status}). Marking stopped.`, + ); + await query( + `UPDATE fs_dev_servers SET state = 'stopped' WHERE id = $1`, + [r.id], + ).catch(() => {}); + } else { + activePreviews.push(r); + } + } catch (e: any) { + // If the fetch completely fails (e.g. timeout, DNS failure), it's dead. + console.warn( + `[anatomy] Preview zombie detected for ${r.preview_url} (${e.message}). Marking stopped.`, + ); + await query( + `UPDATE fs_dev_servers SET state = 'stopped' WHERE id = $1`, + [r.id], + ).catch(() => {}); + } + }), + ); + + return sortDevPreviewsFrontendFirst(activePreviews).map((r) => ({ id: r.id, name: r.name, command: r.command ?? undefined,