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

154 lines
4.5 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 (!last && !forceStart) {
return NextResponse.json({ status: "no_history" });
}
// 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.
// If forceStart is true but we have no history, default to Next.js start command.
const restartOpts = {
projectId: project.id,
projectSlug,
command: last?.command || "next dev -H 0.0.0.0 --no-turbopack",
port: last?.port || 3000,
workspace,
};
void (async () => {
try {
await ensureDevContainer({
projectId: project.id,
projectSlug,
projectName,
workspace,
});
const row = await startDevServer(restartOpts);
// Run the readiness probe in background so state transitions
// from 'starting' → 'running' (or 'failed') in the DB.
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: last?.preview_url ?? null,
command: restartOpts.command,
port: restartOpts.port,
});
}