diff --git a/app/[workspace]/project/[projectId]/overview/page.tsx b/app/[workspace]/project/[projectId]/overview/page.tsx index 28da1fc..599b641 100644 --- a/app/[workspace]/project/[projectId]/overview/page.tsx +++ b/app/[workspace]/project/[projectId]/overview/page.tsx @@ -124,6 +124,7 @@ export default function ProjectOverviewPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [refreshing, setRefreshing] = useState(false); + const [provisioning, setProvisioning] = useState(false); const fetchProject = async () => { try { @@ -153,6 +154,24 @@ export default function ProjectOverviewPage() { fetchProject(); }; + const handleProvisionWorkspace = async () => { + setProvisioning(true); + try { + const res = await fetch(`/api/projects/${projectId}/workspace`, { method: 'POST' }); + const data = await res.json(); + if (res.ok && data.workspaceUrl) { + toast.success('Workspace provisioned — starting up…'); + await fetchProject(); + } else { + toast.error(data.error || 'Failed to provision workspace'); + } + } catch { + toast.error('An error occurred'); + } finally { + setProvisioning(false); + } + }; + if (loading) { return (
@@ -200,16 +219,21 @@ export default function ProjectOverviewPage() { Refresh - + {project.theiaWorkspaceUrl ? ( + + ) : ( + + )}
diff --git a/app/api/projects/[projectId]/workspace/route.ts b/app/api/projects/[projectId]/workspace/route.ts new file mode 100644 index 0000000..bf1b006 --- /dev/null +++ b/app/api/projects/[projectId]/workspace/route.ts @@ -0,0 +1,71 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; +import { query } from '@/lib/db-postgres'; +import { provisionTheiaWorkspace } from '@/lib/cloud-run-workspace'; + +export async function POST( + _request: Request, + { params }: { params: Promise<{ projectId: string }> }, +) { + try { + const { projectId } = await params; + + const session = await getServerSession(authOptions); + if (!session?.user?.email) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // Verify ownership + const rows = await query<{ id: string; data: any }>(` + SELECT p.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 (rows.length === 0) { + return NextResponse.json({ error: 'Project not found' }, { status: 404 }); + } + + const project = rows[0].data; + + if (project.theiaWorkspaceUrl) { + return NextResponse.json({ + success: true, + workspaceUrl: project.theiaWorkspaceUrl, + message: 'Workspace already provisioned', + }); + } + + const slug = project.slug; + if (!slug) { + return NextResponse.json({ error: 'Project has no slug — cannot provision workspace' }, { status: 400 }); + } + + // Provision Cloud Run workspace + const workspace = await provisionTheiaWorkspace(slug, projectId, project.giteaRepo ?? null); + + // Save URL back to project record + await query(` + UPDATE fs_projects + SET data = data || jsonb_build_object( + 'theiaWorkspaceUrl', $1::text, + 'theiaAppUuid', $2::text + ) + WHERE id = $3 + `, [workspace.serviceUrl, workspace.serviceName, projectId]); + + return NextResponse.json({ + success: true, + workspaceUrl: workspace.serviceUrl, + }); + } catch (error) { + console.error('[POST /api/projects/:id/workspace] Error:', error); + return NextResponse.json( + { error: 'Failed to provision workspace', details: error instanceof Error ? error.message : String(error) }, + { status: 500 }, + ); + } +}