"use client"; import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { useSession } from "next-auth/react"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { OrchestratorChat } from "@/components/OrchestratorChat"; import { GitBranch, GitCommit, GitPullRequest, CircleDot, ExternalLink, Terminal, Rocket, Database, Loader2, CheckCircle2, XCircle, Clock, AlertCircle, Code2, RefreshCw, } from "lucide-react"; import Link from "next/link"; import { toast } from "sonner"; interface ContextSnapshot { lastCommit?: { sha: string; message: string; author?: string; timestamp?: string; url?: string; }; currentBranch?: string; recentCommits?: { sha: string; message: string; author?: string; timestamp?: string }[]; openPRs?: { number: number; title: string; url: string; from: string; into: string }[]; openIssues?: { number: number; title: string; url: string; labels?: string[] }[]; lastDeployment?: { status: string; url?: string; timestamp?: string; deploymentUuid?: string; }; updatedAt?: string; } interface Project { id: string; name: string; productName: string; productVision?: string; slug?: string; workspace?: string; status?: string; currentPhase?: string; projectType?: string; // Gitea giteaRepo?: string; giteaRepoUrl?: string; giteaCloneUrl?: string; giteaSshUrl?: string; giteaWebhookId?: number; giteaError?: string; // Coolify coolifyProjectUuid?: string; coolifyAppUuid?: string; coolifyDbUuid?: string; deploymentUrl?: string; // Theia theiaWorkspaceUrl?: string; // Context contextSnapshot?: ContextSnapshot; stats?: { sessions: number; costs: number }; createdAt?: string; updatedAt?: string; } function timeAgo(ts?: string): string { if (!ts) return "—"; const d = new Date(ts); if (isNaN(d.getTime())) return "—"; const diff = (Date.now() - d.getTime()) / 1000; if (diff < 60) return "just now"; if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; return `${Math.floor(diff / 86400)}d ago`; } function DeployBadge({ status }: { status?: string }) { if (!status) return No deployments; const map: Record = { finished: { label: "Deployed", icon: CheckCircle2, className: "bg-green-500/10 text-green-600 border-green-500/20" }, in_progress: { label: "Deploying", icon: Loader2, className: "bg-blue-500/10 text-blue-600 border-blue-500/20" }, queued: { label: "Queued", icon: Clock, className: "bg-yellow-500/10 text-yellow-600 border-yellow-500/20" }, failed: { label: "Failed", icon: XCircle, className: "bg-red-500/10 text-red-600 border-red-500/20" }, cancelled: { label: "Cancelled", icon: XCircle, className: "bg-gray-500/10 text-gray-500 border-gray-500/20" }, }; const cfg = map[status] ?? { label: status, icon: AlertCircle, className: "bg-gray-500/10 text-gray-500" }; const Icon = cfg.icon; return ( {cfg.label} ); } export default function ProjectOverviewPage() { const params = useParams(); const projectId = params.projectId as string; const workspace = params.workspace as string; const { status: authStatus } = useSession(); const [project, setProject] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [refreshing, setRefreshing] = useState(false); const [provisioning, setProvisioning] = useState(false); const fetchProject = async () => { try { const res = await fetch(`/api/projects/${projectId}`); if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Failed to load project"); } const data = await res.json(); setProject(data.project); setError(null); } catch (err) { setError(err instanceof Error ? err.message : "Unknown error"); } finally { setLoading(false); setRefreshing(false); } }; useEffect(() => { if (authStatus === "authenticated") fetchProject(); else if (authStatus === "unauthenticated") setLoading(false); }, [authStatus, projectId]); const handleRefresh = () => { setRefreshing(true); 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 (
); } if (error || !project) { return (

{error ?? "Project not found"}

); } const snap = project.contextSnapshot; const gitea_url = process.env.NEXT_PUBLIC_GITEA_URL ?? "https://git.vibnai.com"; return (
{/* ── Orchestrator Chat ── */} {/* ── Header ── */}

{project.productName}

{project.productVision && (

{project.productVision}

)}
{project.status ?? "active"} {project.currentPhase && ( {project.currentPhase} )}
{project.theiaWorkspaceUrl ? ( ) : ( )}
{/* ── Quick Stats ── */}
{[ { label: "Sessions", value: project.stats?.sessions ?? 0 }, { label: "AI Cost", value: `$${(project.stats?.costs ?? 0).toFixed(2)}` }, { label: "Open PRs", value: snap?.openPRs?.length ?? 0 }, { label: "Open Issues", value: snap?.openIssues?.length ?? 0 }, ].map(({ label, value }) => (

{value}

{label}

))}
{/* ── Code / Gitea ── */} Code Repository {project.giteaRepo ? ( <>
{snap?.currentBranch ?? "main"}
{project.giteaRepo}
{snap?.lastCommit ? (
{snap.lastCommit.sha.slice(0, 8)} · {timeAgo(snap.lastCommit.timestamp)} {snap.lastCommit.author && · {snap.lastCommit.author}}

{snap.lastCommit.message}

) : (

No commits yet — push to get started

)}

Clone

{project.giteaCloneUrl}

{project.giteaSshUrl && (

{project.giteaSshUrl}

)}
) : (

{project.giteaError ? `Repo provisioning failed: ${project.giteaError}` : "No repository linked"}

)}
{/* ── Deployment ── */} Deployment {snap?.lastDeployment ? ( <>
{timeAgo(snap.lastDeployment.timestamp)}
{snap.lastDeployment.url && ( {snap.lastDeployment.url} )} ) : (

No deployments yet

)}
{/* ── Open PRs ── */} Pull Requests {(snap?.openPRs?.length ?? 0) > 0 && ( {snap!.openPRs!.length} open )} {snap?.openPRs?.length ? ( ) : (

No open pull requests

)}
{/* ── Open Issues ── */} Issues {(snap?.openIssues?.length ?? 0) > 0 && ( {snap!.openIssues!.length} open )} {snap?.openIssues?.length ? ( ) : (

No open issues

)}
{/* ── Recent Commits ── */} {snap?.recentCommits && snap.recentCommits.length > 1 && ( Recent Commits
    {snap.recentCommits.map((c, i) => (
  • {c.sha.slice(0, 8)} {c.message} {c.author ?? ""} {timeAgo(c.timestamp)}
  • ))}
)} {/* ── Resources ── */} Resources Databases and services linked to this project {project.coolifyDbUuid ? (
Database provisioned {project.coolifyDbUuid}
) : (

No databases provisioned yet

)}
{/* ── Context snapshot freshness ── */} {snap?.updatedAt && (

Context updated {timeAgo(snap.updatedAt)} via webhooks

)}
); }