Files
vibn-frontend/app/api/projects/[projectId]/analyze/route.ts
Mark Henderson 9c277fd8e3 feat: add GitHub import flow, project delete fix, and analyze API
- Mirror GitHub repos to Gitea as-is on import (skip scaffold)
- Auto-trigger ImportAnalyzer agent after successful mirror
- Add POST/GET /api/projects/[projectId]/analyze route
- Fix project delete button visibility (was permanently opacity:0)
- Store isImport, importAnalysisStatus, importAnalysisJobId on projects

Made-with: Cursor
2026-03-09 11:30:51 -07:00

122 lines
3.9 KiB
TypeScript

import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth/authOptions';
import { query } from '@/lib/db-postgres';
const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? 'http://localhost:3333';
// GET — check the current analysis status for a project
export async function GET(
_req: Request,
{ params }: { params: Promise<{ projectId: string }> }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { projectId } = await params;
const rows = await query<{ data: any }>(
`SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
[projectId]
);
if (!rows.length) return NextResponse.json({ error: 'Project not found' }, { status: 404 });
const project = rows[0].data;
if (!project.isImport) {
return NextResponse.json({ isImport: false });
}
const jobId = project.importAnalysisJobId;
let jobStatus: Record<string, unknown> | null = null;
// Fetch live job status from agent runner if we have a job ID
if (jobId) {
try {
const jobRes = await fetch(`${AGENT_RUNNER_URL}/api/jobs/${jobId}`);
if (jobRes.ok) {
jobStatus = await jobRes.json() as Record<string, unknown>;
// Sync terminal status back to the project record
const runnerStatus = jobStatus.status as string | undefined;
if (runnerStatus && runnerStatus !== project.importAnalysisStatus) {
await query(
`UPDATE fs_projects SET data = jsonb_set(data, '{importAnalysisStatus}', $1::jsonb) WHERE id = $2`,
[JSON.stringify(runnerStatus), projectId]
);
}
}
} catch {
// Agent runner unreachable — return last known status
}
}
return NextResponse.json({
isImport: true,
status: project.importAnalysisStatus ?? 'pending',
jobId,
job: jobStatus,
githubRepoUrl: project.githubRepoUrl,
giteaRepo: project.giteaRepo,
});
}
// POST — (re-)trigger an analysis job for a project
export async function POST(
_req: Request,
{ params }: { params: Promise<{ projectId: string }> }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.email) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { projectId } = await params;
const rows = await query<{ data: any }>(
`SELECT data FROM fs_projects WHERE id = $1 LIMIT 1`,
[projectId]
);
if (!rows.length) return NextResponse.json({ error: 'Project not found' }, { status: 404 });
const project = rows[0].data;
if (!project.giteaRepo) {
return NextResponse.json({ error: 'Project has no Gitea repo' }, { status: 400 });
}
try {
const jobRes = await fetch(`${AGENT_RUNNER_URL}/api/agent/run`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent: 'ImportAnalyzer',
task: `Analyze this codebase${project.githubRepoUrl ? ` (originally from ${project.githubRepoUrl})` : ''} and produce CODEBASE_MAP.md and MIGRATION_PLAN.md as described in your instructions.`,
repo: project.giteaRepo,
}),
});
if (!jobRes.ok) {
const detail = await jobRes.text();
return NextResponse.json({ error: 'Failed to start analysis', details: detail }, { status: 500 });
}
const jobData = await jobRes.json() as { jobId?: string };
const jobId = jobData.jobId ?? null;
await query(
`UPDATE fs_projects SET data = jsonb_set(jsonb_set(data, '{importAnalysisJobId}', $1::jsonb), '{importAnalysisStatus}', '"running"') WHERE id = $2`,
[JSON.stringify(jobId), projectId]
);
return NextResponse.json({ success: true, jobId, status: 'running' });
} catch (err) {
return NextResponse.json(
{ error: 'Failed to start analysis', details: err instanceof Error ? err.message : String(err) },
{ status: 500 }
);
}
}