Files
vibn-agent-runner/vibn-frontend/app/api/projects/[projectId]/dev-server/ensure/route.ts

164 lines
5.0 KiB
TypeScript

/**
* POST /api/projects/[projectId]/dev-server/ensure
*
* Lightweight endpoint called by the preview pane when it loads and finds
* no running dev server. Checks for a previous server config and restarts
* it in the background, returning immediately so the UI isn't blocked.
*
* Response shapes:
* { status: 'running', previewUrl } — already up, nothing to do
* { status: 'starting', previewUrl } — was down, restart fired
* { status: 'no_history' } — never started before, AI needs to do it
* { status: 'no_container' } — dev container doesn't exist yet
*/
import { NextResponse } from "next/server";
import { authSession } from "@/lib/auth/session-server";
import { query, queryOne } from "@/lib/db-postgres";
import { getWorkspaceById } from "@/lib/workspaces";
import {
ensureDevContainer,
startDevServer,
probeDevServerReadiness,
} from "@/lib/dev-container";
export async function POST(
request: Request,
{ params }: { params: Promise<{ projectId: string }> },
) {
const { projectId } = await params;
const session = await authSession();
if (!session?.user?.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Load project — verify ownership
const projectRows = await query<{
id: string;
vibn_workspace_id: string | null;
data: Record<string, unknown>;
}>(
`SELECT p.id, p.vibn_workspace_id, p.data
FROM fs_projects p
JOIN fs_users u ON u.id = p.user_id
WHERE p.id = $1 AND u.data->>'email' = $2
LIMIT 1`,
[projectId, session.user.email],
);
if (projectRows.length === 0) {
return NextResponse.json({ error: "Project not found" }, { status: 404 });
}
const project = projectRows[0];
const projectSlug = (project.data?.slug as string) || project.id;
const projectName = (project.data?.name as string) || "Project";
// 1. Is a dev server already running or starting on the primary port?
const running = await queryOne<{
id: string;
state: string;
preview_url: string;
command: string;
port: number;
}>(
`SELECT id, state, preview_url, command, port
FROM fs_dev_servers
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],
);
if (running) {
return NextResponse.json({
status: running.state === "running" ? "running" : "starting",
previewUrl: running.preview_url,
command: running.command,
port: running.port,
});
}
// 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;
preview_url: string;
}>(
`SELECT command, port, preview_url
FROM fs_dev_servers
WHERE project_id = $1 AND port = 3000
ORDER BY started_at DESC LIMIT 1`,
[projectId],
);
const forceStart =
new URL(request.url).searchParams.get("forceStart") === "true";
// If there's no history, we STILL want to auto-start! We just assume it's a standard
// Next.js app on port 3000. Forcing the user to hit "Start Preview" on a new project
// is unnecessary friction.
const commandToRun = last?.command || "npx next dev -H 0.0.0.0 --webpack";
const portToRun = last?.port || 3000;
const previewUrlToUse = last?.preview_url ?? null;
// 3. Load workspace
if (!project.vibn_workspace_id) {
return NextResponse.json({ status: "no_container" });
}
const workspace = await getWorkspaceById(project.vibn_workspace_id);
if (!workspace) {
return NextResponse.json({ status: "no_container" });
}
// 4. Fire restart in background — don't block the response.
const restartOpts = {
projectId: project.id,
projectSlug,
command: commandToRun,
port: portToRun,
workspace,
};
void (async () => {
try {
await ensureDevContainer({
projectId: project.id,
projectSlug,
projectName,
workspace,
});
const row = await startDevServer(restartOpts);
// We immediately set it to running instead of waiting for the probe.
// The probe has been flaky on Webpack cold compiles and causing the
// frontend to get stuck in a "Preview not running" loop.
await query(`UPDATE fs_dev_servers SET state = 'running' WHERE id = $1`, [
row.id,
]);
// Still run the probe in the background just to log any catastrophic failures,
// but the UI won't be blocked by it.
probeDevServerReadiness(project.id, row.id, row.port).catch((err) => {
console.error("[dev-server/ensure] probe failed:", err?.message);
});
} catch (err) {
console.error("[dev-server/ensure] restart failed:", err);
}
})();
return NextResponse.json({
status: "starting",
previewUrl: previewUrlToUse,
command: restartOpts.command,
port: restartOpts.port,
});
}