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

150 lines
4.3 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 { queryOne } from "@/lib/db-postgres";
import { getWorkspaceById } from "@/lib/workspaces";
import {
ensureDevContainer,
startDevServer,
probeDevServerReadiness,
} from "@/lib/dev-container";
export async function POST(
_req: 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 project = await queryOne<{
id: string;
slug: string;
name: string;
vibn_workspace_id: string | null;
data: Record<string, unknown>;
}>(
`SELECT p.id, p.slug, p.name, 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 (!project) {
return NextResponse.json({ error: "Project not found" }, { status: 404 });
}
// 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],
);
if (!last) {
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.
// The probe (up to 300s) runs in background; anatomy polling at 5s
// will surface state='starting' immediately, then 'running' when ready.
const restartOpts = {
projectId: project.id,
projectSlug: project.slug,
command: last.command,
port: last.port,
workspace,
};
void (async () => {
try {
await ensureDevContainer({
projectId: project.id,
projectSlug: project.slug,
projectName: project.name,
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,
command: last.command,
port: last.port,
});
}