From 14835e2e0a5a56df08c30729457f124c9ca0b517 Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Tue, 21 Apr 2026 11:12:20 -0700 Subject: [PATCH] Revert "fix(gitea-bot): add write:organization scope so bot can create repos" This reverts commit 6f79a88abd8a20ec751b6ca311b941711f3f7c4e. Made-with: Cursor --- app/(justine)/pricing/page.tsx | 2 +- .../[projectId]/(workspace)/build/page.tsx | 458 +++++------- .../(workspace)/infrastructure/page.tsx | 350 ++++++++- .../[projectId]/(workspace)/overview/page.tsx | 20 +- .../[projectId]/(workspace)/prd/page.tsx | 466 +++++++++++- .../project/[projectId]/layout.tsx | 80 ++- app/[workspace]/projects/page.tsx | 2 +- app/api/github/connect/route.ts | 9 +- app/api/projects/[projectId]/advisor/route.ts | 5 +- .../projects/[projectId]/agent-chat/route.ts | 7 +- .../sessions/[sessionId]/approve/route.ts | 5 +- .../sessions/[sessionId]/events/route.ts | 5 +- .../[sessionId]/events/stream/route.ts | 5 +- .../agent/sessions/[sessionId]/retry/route.ts | 5 +- .../agent/sessions/[sessionId]/route.ts | 5 +- .../agent/sessions/[sessionId]/stop/route.ts | 5 +- .../[projectId]/agent/sessions/route.ts | 7 +- .../[projectId]/analysis-status/route.ts | 5 +- .../[projectId]/analyze-chats/route.ts | 5 +- .../[projectId]/analyze-repo/route.ts | 5 +- app/api/projects/[projectId]/analyze/route.ts | 7 +- app/api/projects/[projectId]/apps/route.ts | 7 +- .../[projectId]/architecture/route.ts | 9 +- .../projects/[projectId]/atlas-chat/route.ts | 163 ++--- .../[projectId]/design-surfaces/route.ts | 7 +- app/api/projects/[projectId]/file/route.ts | 5 +- .../generate-migration-plan/route.ts | 5 +- .../knowledge/import-ai-chat/route.ts | 5 +- .../[projectId]/knowledge/items/route.ts | 5 +- .../projects/[projectId]/knowledge/route.ts | 9 +- .../projects/[projectId]/preview-url/route.ts | 5 +- app/api/projects/[projectId]/route.ts | 7 +- .../projects/[projectId]/save-phase/route.ts | 7 +- app/api/projects/[projectId]/vision/route.ts | 5 +- .../projects/[projectId]/workspace/route.ts | 5 +- app/api/projects/delete/route.ts | 5 +- app/api/projects/deploy/route.ts | 5 +- app/api/projects/prewarm/route.ts | 5 +- app/api/projects/route.ts | 5 +- app/api/sessions/route.ts | 5 +- app/api/user/api-key/route.ts | 5 +- app/auth/layout.tsx | 26 +- app/auth/page.tsx | 8 +- app/components/NextAuthComponent.tsx | 226 ++---- app/layout.tsx | 11 +- app/styles/justine/01-homepage.css | 3 +- components/AtlasChat.tsx | 670 +++++------------- components/layout/project-shell.tsx | 14 +- components/layout/vibn-sidebar.tsx | 2 +- .../project-creation/ChatImportSetup.tsx | 17 +- .../project-creation/CodeImportSetup.tsx | 25 +- .../project-creation/CreateProjectFlow.tsx | 27 +- .../project-creation/FreshIdeaSetup.tsx | 56 +- components/project-creation/MigrateSetup.tsx | 41 +- components/project-creation/TypeSelector.tsx | 74 +- components/project-creation/setup-shared.tsx | 149 +--- components/project-main/ChatImportMain.tsx | 6 +- components/project-main/FreshIdeaMain.tsx | 611 +++------------- .../components/justine/JustineHomePage.tsx | 4 +- marketing/components/justine/JustineNav.tsx | 4 +- marketing/components/justine/index.ts | 1 - next.config.ts | 17 +- package.json | 10 +- public/sw.js | 42 +- 64 files changed, 1708 insertions(+), 2068 deletions(-) diff --git a/app/(justine)/pricing/page.tsx b/app/(justine)/pricing/page.tsx index 9c6d422..6b2883d 100644 --- a/app/(justine)/pricing/page.tsx +++ b/app/(justine)/pricing/page.tsx @@ -58,7 +58,7 @@ export default function PricingPage() { {/* Pro Tier */} - +
Popular
diff --git a/app/[workspace]/project/[projectId]/(workspace)/build/page.tsx b/app/[workspace]/project/[projectId]/(workspace)/build/page.tsx index f2ed005..ab6f860 100644 --- a/app/[workspace]/project/[projectId]/(workspace)/build/page.tsx +++ b/app/[workspace]/project/[projectId]/(workspace)/build/page.tsx @@ -3,15 +3,7 @@ import { Suspense, useState, useEffect, useCallback, useRef } from "react"; import { useParams, useSearchParams, useRouter } from "next/navigation"; import { useSession } from "next-auth/react"; -import { isClientDevProjectBypass } from "@/lib/dev-bypass"; import Link from "next/link"; -import { JM } from "@/components/project-creation/modal-theme"; -import { AtlasChat } from "@/components/AtlasChat"; -import { PRD_PLAN_SECTIONS } from "@/lib/prd-sections"; -import { - type ChatContextRef, - contextRefKey, -} from "@/lib/chat-context-refs"; // ── Types ───────────────────────────────────────────────────────────────────── @@ -25,6 +17,15 @@ interface TreeNode { // ── Constants ───────────────────────────────────────────────────────────────── +const INFRA_ITEMS = [ + { id: "builds", label: "Builds", icon: "⬡" }, + { id: "databases", label: "Databases", icon: "◫" }, + { id: "services", label: "Services", icon: "◎" }, + { id: "environment", label: "Environment", icon: "≡" }, + { id: "domains", label: "Domains", icon: "◬" }, + { id: "logs", label: "Logs", icon: "≈" }, +]; + const SURFACE_LABELS: Record = { webapp: "Web App", marketing: "Marketing Site", admin: "Admin Panel", }; @@ -32,74 +33,6 @@ const SURFACE_ICONS: Record = { webapp: "◈", marketing: "◌", admin: "◫", }; -/** Growth page–style left rail: cream panel, icon + label rows */ -const BUILD_LEFT_BG = "#faf8f5"; -const BUILD_LEFT_BORDER = "#e8e4dc"; - -const BUILD_NAV_GROUP: React.CSSProperties = { - fontSize: "0.6rem", - fontWeight: 700, - color: "#b5b0a6", - letterSpacing: "0.09em", - textTransform: "uppercase", - padding: "14px 12px 6px", - fontFamily: JM.fontSans, -}; - -const BUILD_PRIMARY = [ - { id: "chat", label: "Chat", icon: "◆" }, - { id: "code", label: "Code", icon: "◇" }, - { id: "layouts", label: "Layouts", icon: "◈" }, - { id: "tasks", label: "Agent", icon: "◎" }, - { id: "preview", label: "Preview", icon: "▢" }, -] as const; - -function BuildGrowthNavRow({ - icon, - label, - active, - onClick, -}: { - icon: string; - label: string; - active: boolean; - onClick: () => void; -}) { - return ( - - ); -} - // ── Language / syntax helpers ───────────────────────────────────────────────── function langFromName(name: string): string { @@ -170,11 +103,28 @@ function TreeRow({ node, depth, selectedPath, onSelect, onToggle }: { // ── Left nav shared styles ──────────────────────────────────────────────────── const NAV_GROUP_LABEL: React.CSSProperties = { - fontSize: "0.6rem", fontWeight: 700, color: JM.muted, + fontSize: "0.6rem", fontWeight: 700, color: "#b5b0a6", letterSpacing: "0.09em", textTransform: "uppercase", - padding: "12px 12px 5px", fontFamily: JM.fontSans, + padding: "12px 12px 5px", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", }; +function NavItem({ label, active, onClick, indent = false }: { label: string; active: boolean; onClick: () => void; indent?: boolean }) { + return ( + + ); +} + // ── Placeholder panel ───────────────────────────────────────────────────────── function Placeholder({ icon, title, desc }: { icon: string; title: string; desc: string }) { @@ -190,6 +140,30 @@ function Placeholder({ icon, title, desc }: { icon: string; title: string; desc: ); } +// ── Infra content ───────────────────────────────────────────────────────────── + +function InfraContent({ tab, projectId, workspace }: { tab: string; projectId: string; workspace: string }) { + const base = `/${workspace}/project/${projectId}/infrastructure`; + const descriptions: Record = { + databases: { icon: "◫", title: "Databases", desc: "PostgreSQL, Redis, and other databases — provisioned and managed with connection strings auto-injected." }, + services: { icon: "◎", title: "Services", desc: "Background workers, queues, email delivery, file storage, and third-party integrations." }, + environment: { icon: "≡", title: "Environment", desc: "Environment variables and secrets, encrypted at rest and auto-injected into your containers." }, + domains: { icon: "◬", title: "Domains", desc: "Custom domains and SSL certificates for all your deployed services." }, + logs: { icon: "≈", title: "Logs", desc: "Runtime logs, request traces, and error reports streaming from deployed services." }, + builds: { icon: "⬡", title: "Builds", desc: "Deployment history, build logs, and rollback controls for all your apps." }, + }; + const d = descriptions[tab]; + return ( +
+
+
{tab}
+ Open full view → +
+ {d && } +
+ ); +} + // ── Layouts content ─────────────────────────────────────────────────────────── function LayoutsContent({ surfaces, projectId, workspace, activeSurfaceId, onSelectSurface }: { @@ -982,8 +956,7 @@ function FileTree({ projectId, rootPath, selectedPath, onSelectFile }: { }, [projectId]); useEffect(() => { - if (!rootPath) return; - if (!isClientDevProjectBypass() && status !== "authenticated") return; + if (!rootPath || status !== "authenticated") return; setTree([]); setTreeLoading(true); fetchDir(rootPath).then(nodes => { setTree(nodes); setTreeLoading(false); }).catch(() => setTreeLoading(false)); }, [rootPath, status, fetchDir]); @@ -1073,6 +1046,21 @@ interface PreviewApp { name: string; url: string | null; status: string; } // ── PRD Content ─────────────────────────────────────────────────────────────── +const PRD_SECTIONS = [ + { id: "executive_summary", label: "Executive Summary", phaseId: "big_picture" }, + { id: "problem_statement", label: "Problem Statement", phaseId: "big_picture" }, + { id: "vision_metrics", label: "Vision & Success Metrics", phaseId: "big_picture" }, + { id: "users_personas", label: "Users & Personas", phaseId: "users_personas" }, + { id: "user_flows", label: "User Flows", phaseId: "users_personas" }, + { id: "feature_requirements", label: "Feature Requirements", phaseId: "features_scope" }, + { id: "screen_specs", label: "Screen Specs", phaseId: "screens_data" }, + { id: "business_model", label: "Business Model", phaseId: "business_model" }, + { id: "integrations", label: "Integrations & Dependencies", phaseId: "features_scope" }, + { id: "non_functional", label: "Non-Functional Reqs", phaseId: null }, + { id: "risks", label: "Risks & Mitigations", phaseId: "risks_questions" }, + { id: "open_questions", label: "Open Questions", phaseId: "risks_questions" }, +]; + interface SavedPhase { phase: string; title: string; summary: string; data: Record; saved_at: string; } function PrdContent({ projectId }: { projectId: string }) { @@ -1098,7 +1086,7 @@ function PrdContent({ projectId }: { projectId: string }) { const phaseMap = new Map(savedPhases.map(p => [p.phase, p])); const savedPhaseIds = new Set(savedPhases.map(p => p.phase)); - const sections = PRD_PLAN_SECTIONS.map(s => ({ + const sections = PRD_SECTIONS.map(s => ({ ...s, savedPhase: s.phaseId ? phaseMap.get(s.phaseId) ?? null : null, isDone: s.phaseId ? savedPhaseIds.has(s.phaseId) : false, @@ -1284,11 +1272,10 @@ function BuildHubInner() { const projectId = params.projectId as string; const workspace = params.workspace as string; - const section = searchParams.get("section") ?? "chat"; - const tasksSubTabRaw = searchParams.get("tab") ?? "tasks"; - const tasksSubTab = tasksSubTabRaw === "prd" ? "requirements" : tasksSubTabRaw; + const section = searchParams.get("section") ?? "code"; const activeApp = searchParams.get("app") ?? ""; const activeRoot = searchParams.get("root") ?? ""; + const activeInfra = searchParams.get("tab") ?? "builds"; const activeSurfaceParam = searchParams.get("surface") ?? ""; const [apps, setApps] = useState([]); @@ -1305,32 +1292,6 @@ function BuildHubInner() { const [fileLoading, setFileLoading] = useState(false); const [fileName, setFileName] = useState(null); - const [chatContextRefs, setChatContextRefs] = useState([]); - - const addAppToChat = useCallback((app: AppEntry) => { - setChatContextRefs(prev => { - const next: ChatContextRef = { kind: "app", label: app.name, path: app.path }; - const k = contextRefKey(next); - if (prev.some(r => contextRefKey(r) === k)) return prev; - return [...prev, next]; - }); - }, []); - - const removeChatContextRef = useCallback((key: string) => { - setChatContextRefs(prev => prev.filter(r => contextRefKey(r) !== key)); - }, []); - - useEffect(() => { - if (searchParams.get("section") !== "infrastructure") return; - const t = searchParams.get("tab") ?? "builds"; - router.replace(`/${workspace}/project/${projectId}/run?tab=${encodeURIComponent(t)}`, { scroll: false }); - }, [searchParams, workspace, projectId, router]); - - useEffect(() => { - if (searchParams.get("section") !== "mvp") return; - router.replace(`/${workspace}/project/${projectId}/mvp-setup/launch`, { scroll: false }); - }, [searchParams, workspace, projectId, router]); - useEffect(() => { fetch(`/api/projects/${projectId}/preview-url`) .then(r => r.json()) @@ -1375,114 +1336,82 @@ function BuildHubInner() { router.push(`/${workspace}/project/${projectId}/build?${sp.toString()}`, { scroll: false }); }; - const workspaceAppActive = (app: AppEntry) => { - if (section === "chat") { - return chatContextRefs.some(r => r.kind === "app" && r.path === app.path); - } - if (section === "code" || (section === "tasks" && tasksSubTab !== "requirements")) { - return activeApp === app.name; - } - return false; - }; - - const onWorkspaceApp = (app: AppEntry) => { - if (section === "chat") addAppToChat(app); - else if (section === "code") navigate({ section: "code", app: app.name, root: app.path }); - else if (section === "tasks" && tasksSubTab !== "requirements") { - navigate({ section: "tasks", tab: "tasks", app: app.name, root: app.path }); - } - else navigate({ section: "code", app: app.name, root: app.path }); - }; - return ( -
- {/* Growth-style left rail */} -
-
Build
- {BUILD_PRIMARY.map(p => ( - { - if (p.id === "tasks") navigate({ section: "tasks", tab: "tasks" }); - else navigate({ section: p.id }); - }} - /> - ))} +
-
- {section === "chat" && ( -
- Attach monorepo apps from Workspace below. Live preview stays on the right when deployed. -
- )} + {/* ── Build content ── */} +
- {section === "code" && activeApp && activeRoot && ( -
-
Files
-
- {activeApp} + {/* Inner nav — contextual items driven by top-bar tool icon */} +
+ + {/* Code: app list + file tree */} + {section === "code" && ( +
+
+
Apps
+ {apps.length > 0 ? apps.map(app => ( + navigate({ section: "code", app: app.name, root: app.path })} + /> + )) : ( +
No apps yet
+ )}
-
- -
-
- )} - - {section === "layouts" && ( -
-
Surfaces
- {surfaces.length > 0 ? surfaces.map(s => ( - { setActiveSurfaceId(s.id); navigate({ section: "layouts", surface: s.id }); }} - /> - )) : ( -
Not configured
+ {activeApp && activeRoot && ( +
+
+ Files + {activeApp} +
+ +
)}
)} + {/* Layouts: surface list */} + {section === "layouts" && ( +
+
Surfaces
+ {surfaces.length > 0 ? surfaces.map(s => ( + { setActiveSurfaceId(s.id); navigate({ section: "layouts", surface: s.id }); }} + /> + )) : ( +
Not configured
+ )} +
+ )} + + {/* Infrastructure: item list */} + {section === "infrastructure" && ( +
+
Infrastructure
+ {INFRA_ITEMS.map(item => ( + navigate({ section: "infrastructure", tab: item.id })} + /> + ))} +
+ )} + + {/* Tasks: sub-nav + app list */} {section === "tasks" && ( -
-
- {[{ id: "tasks", label: "Runs" }, { id: "requirements", label: "Requirements" }].map(item => { - const isActive = tasksSubTab === item.id; +
+ {/* Tasks | PRD sub-nav */} +
+ {[{ id: "tasks", label: "Tasks" }, { id: "prd", label: "PRD" }].map(item => { + const isActive = (searchParams.get("tab") ?? "tasks") === item.id; return ( -
+ {/* App list (only in tasks tab) */} + {(searchParams.get("tab") ?? "tasks") !== "prd" && ( + <> +
Apps
+ {apps.length > 0 ? apps.map(app => ( + navigate({ section: "tasks", tab: "tasks", app: app.name, root: app.path })} + /> + )) : ( +
No apps yet
+ )} + + )}
)} + {/* Preview: deployed apps list */} {section === "preview" && ( -
-
Deployed
+
+
Apps
{previewApps.length > 0 ? previewApps.map(app => ( - setActivePreviewApp(app)} /> )) : ( -
No deployments yet
+
No deployments yet
)}
)}
-
- {apps.length > 0 && ( - <> -
Workspace
- {apps.map(app => ( - onWorkspaceApp(app)} - /> - ))} - - )} - {section === "chat" && previewApps.length > 0 && ( - <> -
Live
- {previewApps.map(app => ( - setActivePreviewApp(app)} - /> - ))} - - )} - - Open Tasks → - -
-
- - {/* Main content */} -
- {section === "chat" && ( -
-
- -
-
- -
-
- )} + {/* Main content panel */} {section === "code" && (
{ setActiveSurfaceId(id); navigate({ section: "layouts", surface: id }); }} /> )} - {section === "tasks" && tasksSubTab !== "requirements" && ( + {section === "infrastructure" && ( + + )} + {section === "tasks" && (searchParams.get("tab") ?? "tasks") !== "prd" && ( )} - {section === "tasks" && tasksSubTab === "requirements" && ( + {section === "tasks" && searchParams.get("tab") === "prd" && ( )} {section === "preview" && ( @@ -1626,7 +1492,7 @@ function BuildHubInner() { export default function BuildPage() { return ( - Loading…
}> + Loading…
}> ); diff --git a/app/[workspace]/project/[projectId]/(workspace)/infrastructure/page.tsx b/app/[workspace]/project/[projectId]/(workspace)/infrastructure/page.tsx index fffb997..46e30b8 100644 --- a/app/[workspace]/project/[projectId]/(workspace)/infrastructure/page.tsx +++ b/app/[workspace]/project/[projectId]/(workspace)/infrastructure/page.tsx @@ -1,7 +1,353 @@ -import { ProjectInfraPanel } from "@/components/project-main/ProjectInfraPanel"; +"use client"; + +import { Suspense, useState, useEffect } from "react"; +import { useParams, useSearchParams, useRouter } from "next/navigation"; + +// ── Types ───────────────────────────────────────────────────────────────────── + +interface InfraApp { + name: string; + domain?: string | null; + coolifyServiceUuid?: string | null; +} + +interface ProjectData { + giteaRepo?: string; + giteaRepoUrl?: string; + apps?: InfraApp[]; +} + +// ── Tab definitions ─────────────────────────────────────────────────────────── + +const TABS = [ + { id: "builds", label: "Builds", icon: "⬡" }, + { id: "databases", label: "Databases", icon: "◫" }, + { id: "services", label: "Services", icon: "◎" }, + { id: "environment", label: "Environment", icon: "≡" }, + { id: "domains", label: "Domains", icon: "◬" }, + { id: "logs", label: "Logs", icon: "≈" }, +] as const; + +type TabId = typeof TABS[number]["id"]; + +// ── Shared empty state ──────────────────────────────────────────────────────── + +function ComingSoonPanel({ icon, title, description }: { icon: string; title: string; description: string }) { + return ( +
+
+ {icon} +
+
+
{title}
+
{description}
+
+
+ Coming soon +
+
+ ); +} + +// ── Builds tab ──────────────────────────────────────────────────────────────── + +function BuildsTab({ project }: { project: ProjectData | null }) { + const apps = project?.apps ?? []; + if (apps.length === 0) { + return ( + + ); + } + return ( +
+
+ Deployed Apps +
+
+ {apps.map(app => ( +
+
+ +
+
{app.name}
+ {app.domain && ( +
{app.domain}
+ )} +
+
+
+ + Running +
+
+ ))} +
+
+ ); +} + +// ── Databases tab ───────────────────────────────────────────────────────────── + +function DatabasesTab() { + return ( + + ); +} + +// ── Services tab ────────────────────────────────────────────────────────────── + +function ServicesTab() { + return ( + + ); +} + +// ── Environment tab ─────────────────────────────────────────────────────────── + +function EnvironmentTab({ project }: { project: ProjectData | null }) { + return ( +
+
+ Environment Variables & Secrets +
+
+ {/* Header row */} +
+ KeyValue +
+ {/* Placeholder rows */} + {["DATABASE_URL", "NEXTAUTH_SECRET", "GITEA_API_TOKEN"].map(k => ( +
+ {k} + •••••••• + +
+ ))} +
+ +
+
+
+ Variables are encrypted at rest and auto-injected into deployed containers. Secrets are never exposed in logs. +
+
+ ); +} + +// ── Domains tab ─────────────────────────────────────────────────────────────── + +function DomainsTab({ project }: { project: ProjectData | null }) { + const apps = (project?.apps ?? []).filter(a => a.domain); + return ( +
+
+ Domains & SSL +
+ {apps.length > 0 ? ( +
+ {apps.map(app => ( +
+
+
+ {app.domain} +
+
{app.name}
+
+
+ + SSL active +
+
+ ))} +
+ ) : ( +
+
No custom domains configured
+
Deploy an app first, then point a domain here.
+
+ )} + +
+ ); +} + +// ── Logs tab ────────────────────────────────────────────────────────────────── + +function LogsTab({ project }: { project: ProjectData | null }) { + const apps = project?.apps ?? []; + if (apps.length === 0) { + return ( + + ); + } + return ( +
+
+ Runtime Logs +
+
+
{"# Logs will stream here once connected to Coolify"}
+
{"→ Select a service to tail its log output"}
+
+
+ ); +} + +// ── Inner page ──────────────────────────────────────────────────────────────── + +function InfrastructurePageInner() { + const params = useParams(); + const searchParams = useSearchParams(); + const router = useRouter(); + const projectId = params.projectId as string; + const workspace = params.workspace as string; + + const activeTab = (searchParams.get("tab") ?? "builds") as TabId; + const [project, setProject] = useState(null); + + useEffect(() => { + fetch(`/api/projects/${projectId}/apps`) + .then(r => r.json()) + .then(d => setProject({ apps: d.apps ?? [], giteaRepo: d.giteaRepo, giteaRepoUrl: d.giteaRepoUrl })) + .catch(() => {}); + }, [projectId]); + + const setTab = (id: TabId) => { + router.push(`/${workspace}/project/${projectId}/infrastructure?tab=${id}`, { scroll: false }); + }; + + return ( +
+ + {/* ── Left sub-nav ── */} +
+
+ Infrastructure +
+ {TABS.map(tab => { + const active = activeTab === tab.id; + return ( + + ); + })} +
+ + {/* ── Content ── */} +
+ {activeTab === "builds" && } + {activeTab === "databases" && } + {activeTab === "services" && } + {activeTab === "environment" && } + {activeTab === "domains" && } + {activeTab === "logs" && } +
+
+ ); +} + +// ── Export ──────────────────────────────────────────────────────────────────── export default function InfrastructurePage() { return ( - + Loading…
}> + + ); } diff --git a/app/[workspace]/project/[projectId]/(workspace)/overview/page.tsx b/app/[workspace]/project/[projectId]/(workspace)/overview/page.tsx index de690b6..c667095 100644 --- a/app/[workspace]/project/[projectId]/(workspace)/overview/page.tsx +++ b/app/[workspace]/project/[projectId]/(workspace)/overview/page.tsx @@ -3,9 +3,7 @@ import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { useSession } from "next-auth/react"; -import { isClientDevProjectBypass } from "@/lib/dev-bypass"; import { Loader2 } from "lucide-react"; -import { JM } from "@/components/project-creation/modal-theme"; import { FreshIdeaMain } from "@/components/project-main/FreshIdeaMain"; import { ChatImportMain } from "@/components/project-main/ChatImportMain"; import { CodeImportMain } from "@/components/project-main/CodeImportMain"; @@ -37,12 +35,10 @@ export default function ProjectOverviewPage() { const [loading, setLoading] = useState(true); useEffect(() => { - const bypass = isClientDevProjectBypass(); - if (!bypass && authStatus !== "authenticated") { + if (authStatus !== "authenticated") { if (authStatus === "unauthenticated") setLoading(false); return; } - if (!bypass && authStatus === "loading") return; fetch(`/api/projects/${projectId}`) .then(r => r.json()) .then(d => setProject(d.project)) @@ -52,23 +48,15 @@ export default function ProjectOverviewPage() { if (loading) { return ( -
- +
+
); } if (!project) { return ( -
+
Project not found.
); diff --git a/app/[workspace]/project/[projectId]/(workspace)/prd/page.tsx b/app/[workspace]/project/[projectId]/(workspace)/prd/page.tsx index 72641e1..4b26990 100644 --- a/app/[workspace]/project/[projectId]/(workspace)/prd/page.tsx +++ b/app/[workspace]/project/[projectId]/(workspace)/prd/page.tsx @@ -1,11 +1,459 @@ -import { redirect } from "next/navigation"; +"use client"; -/** Legacy URL — project work now lives under Tasks (PRD is the first task). */ -export default async function PrdRedirectPage({ - params, -}: { - params: Promise<{ workspace: string; projectId: string }>; -}) { - const { workspace, projectId } = await params; - redirect(`/${workspace}/project/${projectId}/tasks`); +import { useEffect, useState } from "react"; +import { useParams, useRouter } from "next/navigation"; + +// Maps each PRD section to the discovery phase that populates it +const PRD_SECTIONS = [ + { id: "executive_summary", label: "Executive Summary", phaseId: "big_picture" }, + { id: "problem_statement", label: "Problem Statement", phaseId: "big_picture" }, + { id: "vision_metrics", label: "Vision & Success Metrics", phaseId: "big_picture" }, + { id: "users_personas", label: "Users & Personas", phaseId: "users_personas" }, + { id: "user_flows", label: "User Flows", phaseId: "users_personas" }, + { id: "feature_requirements", label: "Feature Requirements", phaseId: "features_scope" }, + { id: "screen_specs", label: "Screen Specs", phaseId: "screens_data" }, + { id: "business_model", label: "Business Model", phaseId: "business_model" }, + { id: "integrations", label: "Integrations & Dependencies", phaseId: "features_scope" }, + { id: "non_functional", label: "Non-Functional Reqs", phaseId: "features_scope" }, + { id: "risks", label: "Risks & Mitigations", phaseId: "risks_questions" }, + { id: "open_questions", label: "Open Questions", phaseId: "risks_questions" }, +]; + +interface SavedPhase { + phase: string; + title: string; + summary: string; + data: Record; + saved_at: string; +} + +function formatValue(v: unknown): string { + if (v === null || v === undefined) return "—"; + if (Array.isArray(v)) return v.map(item => typeof item === "object" ? JSON.stringify(item) : String(item)).join(", "); + return String(v); +} + +function PhaseDataCard({ phase }: { phase: SavedPhase }) { + const [expanded, setExpanded] = useState(false); + const entries = Object.entries(phase.data).filter(([, v]) => v !== null && v !== undefined && v !== ""); + return ( +
+ + {expanded && entries.length > 0 && ( +
+ {entries.map(([k, v]) => ( +
+
+ {k.replace(/_/g, " ")} +
+
+ {formatValue(v)} +
+
+ ))} +
+ )} +
+ ); +} + +interface ArchApp { name: string; type: string; description: string; tech?: string[]; screens?: string[] } +interface ArchInfra { name: string; reason: string } +interface ArchPackage { name: string; description: string } +interface ArchIntegration { name: string; required?: boolean; notes?: string } +interface Architecture { + productName?: string; + productType?: string; + summary?: string; + apps?: ArchApp[]; + packages?: ArchPackage[]; + infrastructure?: ArchInfra[]; + integrations?: ArchIntegration[]; + designSurfaces?: string[]; + riskNotes?: string[]; +} + +function ArchitectureView({ arch }: { arch: Architecture }) { + const Section = ({ title, children }: { title: string; children: React.ReactNode }) => ( +
+
{title}
+ {children} +
+ ); + const Card = ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ); + const Tag = ({ label }: { label: string }) => ( + {label} + ); + + return ( +
+ {arch.summary && ( +
+ {arch.summary} +
+ )} + {(arch.apps ?? []).length > 0 && ( +
+ {arch.apps!.map(a => ( + +
+ {a.name} + {a.type} +
+
{a.description}
+ {a.tech?.map(t => )} + {a.screens && a.screens.length > 0 && ( +
Screens: {a.screens.join(", ")}
+ )} +
+ ))} +
+ )} + {(arch.packages ?? []).length > 0 && ( +
+ {arch.packages!.map(p => ( + +
+ {p.name} + {p.description} +
+
+ ))} +
+ )} + {(arch.infrastructure ?? []).length > 0 && ( +
+ {arch.infrastructure!.map(i => ( + +
{i.name}
+
{i.reason}
+
+ ))} +
+ )} + {(arch.integrations ?? []).length > 0 && ( +
+ {arch.integrations!.map(i => ( + +
+ {i.name} + {i.required && required} +
+ {i.notes &&
{i.notes}
} +
+ ))} +
+ )} + {(arch.riskNotes ?? []).length > 0 && ( +
+ {arch.riskNotes!.map((r, i) => ( +
+ + {r} +
+ ))} +
+ )} +
+ ); +} + +export default function PRDPage() { + const params = useParams(); + const projectId = params.projectId as string; + const workspace = params.workspace as string; + const [prd, setPrd] = useState(null); + const [architecture, setArchitecture] = useState(null); + const [savedPhases, setSavedPhases] = useState([]); + const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState<"prd" | "architecture">("prd"); + const [archGenerating, setArchGenerating] = useState(false); + const [archError, setArchError] = useState(null); + + useEffect(() => { + Promise.all([ + fetch(`/api/projects/${projectId}`).then(r => r.json()).catch(() => ({})), + fetch(`/api/projects/${projectId}/save-phase`).then(r => r.json()).catch(() => ({ phases: [] })), + ]).then(([projectData, phaseData]) => { + setPrd(projectData?.project?.prd ?? null); + setArchitecture(projectData?.project?.architecture ?? null); + setSavedPhases(phaseData?.phases ?? []); + setLoading(false); + }); + }, [projectId]); + + const router = useRouter(); + + const handleGenerateArchitecture = async () => { + setArchGenerating(true); + setArchError(null); + try { + const res = await fetch(`/api/projects/${projectId}/architecture`, { method: "POST" }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error ?? "Generation failed"); + setArchitecture(data.architecture); + setActiveTab("architecture"); + } catch (e) { + setArchError(e instanceof Error ? e.message : "Something went wrong"); + } finally { + setArchGenerating(false); + } + }; + + const phaseMap = new Map(savedPhases.map(p => [p.phase, p])); + const savedPhaseIds = new Set(savedPhases.map(p => p.phase)); + + const sections = PRD_SECTIONS.map(s => ({ + ...s, + savedPhase: s.phaseId ? phaseMap.get(s.phaseId) ?? null : null, + isDone: s.phaseId ? savedPhaseIds.has(s.phaseId) : false, + })); + + const doneCount = sections.filter(s => s.isDone).length; + const totalPct = Math.round((doneCount / sections.length) * 100); + + if (loading) { + return ( +
+ Loading… +
+ ); + } + + const tabs = [ + { id: "prd" as const, label: "PRD", available: true }, + { id: "architecture" as const, label: "Architecture", available: !!architecture }, + ]; + + return ( +
+ + {/* Tab bar — only when at least one doc exists */} + {(prd || architecture) && ( +
+ {tabs.map(t => { + const isActive = activeTab === t.id; + return ( + + ); + })} +
+ )} + + {/* Next step banner — PRD done but no architecture yet */} + {prd && !architecture && activeTab === "prd" && ( +
+
+
+ Next: Generate technical architecture +
+
+ The AI will read your PRD and recommend the apps, services, and infrastructure your product needs. Takes ~30 seconds. +
+ {archError && ( +
⚠ {archError}
+ )} +
+ +
+ )} + + {/* Architecture tab */} + {activeTab === "architecture" && architecture && ( + + )} + + {/* PRD tab — finalized */} + {activeTab === "prd" && prd && ( +
+
+

+ Product Requirements +

+ + PRD complete + +
+
+ {prd} +
+
+ )} + + {/* PRD tab — section progress (no finalized PRD yet) */} + {activeTab === "prd" && !prd && ( + /* ── Section progress view ── */ +
+ {/* Progress bar */} +
+
+ {totalPct}% +
+
+
+
+
+
+ + {doneCount}/{sections.length} sections + +
+ + {/* Sections */} + {sections.map((s, i) => ( +
+
+ {/* Status icon */} +
+ {s.isDone ? "✓" : "○"} +
+ + + {s.label} + + + {s.isDone && s.savedPhase && ( + + saved + + )} + {!s.isDone && !s.phaseId && ( + + generated + + )} +
+ + {/* Expandable phase data */} + {s.isDone && s.savedPhase && ( + + )} + + {/* Pending hint */} + {!s.isDone && ( +
+ {s.phaseId + ? `Complete the ${s.savedPhase ? s.savedPhase.title : "discovery"} phase in Vibn` + : "Will be generated when PRD is finalized"} +
+ )} +
+ ))} + + {doneCount === 0 && ( +

+ Continue chatting with Vibn — saved phases will appear here automatically. +

+ )} +
+ )} +
+ ); } diff --git a/app/[workspace]/project/[projectId]/layout.tsx b/app/[workspace]/project/[projectId]/layout.tsx index fc8d862..2e27008 100644 --- a/app/[workspace]/project/[projectId]/layout.tsx +++ b/app/[workspace]/project/[projectId]/layout.tsx @@ -1,12 +1,72 @@ -/** - * Passthrough layout for the project route. - * - * Two sibling route groups provide their own scaffolds: - * - (home)/ — VIBNSidebar scaffold for the project home page. - * - (workspace)/ — ProjectShell (top tab nav) for overview/build/run/etc. - */ -import { ReactNode } from "react"; +import { ProjectShell } from "@/components/layout/project-shell"; +import { query } from "@/lib/db-postgres"; -export default function ProjectRootLayout({ children }: { children: ReactNode }) { - return <>{children}; +interface ProjectData { + name: string; + description?: string; + status?: string; + progress?: number; + discoveryPhase?: number; + capturedData?: Record; + createdAt?: string; + updatedAt?: string; + featureCount?: number; + creationMode?: "fresh" | "chat-import" | "code-import" | "migration"; +} + +async function getProjectData(projectId: string): Promise { + try { + const rows = await query<{ data: any; created_at?: string; updated_at?: string }>( + `SELECT data, created_at, updated_at FROM fs_projects WHERE id = $1 LIMIT 1`, + [projectId] + ); + if (rows.length > 0) { + const { data, created_at, updated_at } = rows[0]; + return { + name: data?.productName || data?.name || "Project", + description: data?.productVision || data?.description, + status: data?.status, + progress: data?.progress ?? 0, + discoveryPhase: data?.discoveryPhase ?? 0, + capturedData: data?.capturedData ?? {}, + createdAt: created_at, + updatedAt: updated_at, + featureCount: Array.isArray(data?.features) ? data.features.length : (data?.featureCount ?? 0), + creationMode: data?.creationMode ?? "fresh", + }; + } + } catch (error) { + console.error("Error fetching project:", error); + } + return { name: "Project" }; +} + +export default async function ProjectLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise<{ workspace: string; projectId: string }>; +}) { + const { workspace, projectId } = await params; + const project = await getProjectData(projectId); + + return ( + + {children} + + ); } diff --git a/app/[workspace]/projects/page.tsx b/app/[workspace]/projects/page.tsx index 355fd87..5aa3b8f 100644 --- a/app/[workspace]/projects/page.tsx +++ b/app/[workspace]/projects/page.tsx @@ -184,7 +184,7 @@ export default function ProjectsPage() { style={{ position: "relative", animationDelay: `${i * 0.05}s` }} > } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } @@ -189,7 +190,7 @@ export async function DELETE( _req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/agent/sessions/[sessionId]/approve/route.ts b/app/api/projects/[projectId]/agent/sessions/[sessionId]/approve/route.ts index 1792665..23cfe66 100644 --- a/app/api/projects/[projectId]/agent/sessions/[sessionId]/approve/route.ts +++ b/app/api/projects/[projectId]/agent/sessions/[sessionId]/approve/route.ts @@ -8,7 +8,8 @@ * Body: { commitMessage: string } */ import { NextResponse } from "next/server"; -import { authSession } from "@/lib/auth/session-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"; @@ -28,7 +29,7 @@ export async function POST( ) { try { const { projectId, sessionId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/agent/sessions/[sessionId]/events/route.ts b/app/api/projects/[projectId]/agent/sessions/[sessionId]/events/route.ts index 007e843..1125d03 100644 --- a/app/api/projects/[projectId]/agent/sessions/[sessionId]/events/route.ts +++ b/app/api/projects/[projectId]/agent/sessions/[sessionId]/events/route.ts @@ -6,7 +6,8 @@ * Batch append from vibn-agent-runner (x-agent-runner-secret). */ import { NextResponse } from "next/server"; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth/authOptions"; import { query, getPool } from "@/lib/db-postgres"; export interface AgentSessionEventRow { @@ -22,7 +23,7 @@ export async function GET( ) { try { const { projectId, sessionId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/agent/sessions/[sessionId]/events/stream/route.ts b/app/api/projects/[projectId]/agent/sessions/[sessionId]/events/stream/route.ts index 73d5883..1c2171b 100644 --- a/app/api/projects/[projectId]/agent/sessions/[sessionId]/events/stream/route.ts +++ b/app/api/projects/[projectId]/agent/sessions/[sessionId]/events/stream/route.ts @@ -2,7 +2,8 @@ * GET /api/projects/.../agent/sessions/.../events/stream?afterSeq=0 * Server-Sent Events: tail agent_session_events while the session is active. */ -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth/authOptions"; import { query, queryOne } from "@/lib/db-postgres"; export const dynamic = "force-dynamic"; @@ -16,7 +17,7 @@ export async function GET( req: Request, { params }: { params: Promise<{ projectId: string; sessionId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return new Response("Unauthorized", { status: 401 }); } diff --git a/app/api/projects/[projectId]/agent/sessions/[sessionId]/retry/route.ts b/app/api/projects/[projectId]/agent/sessions/[sessionId]/retry/route.ts index 9841098..539ba19 100644 --- a/app/api/projects/[projectId]/agent/sessions/[sessionId]/retry/route.ts +++ b/app/api/projects/[projectId]/agent/sessions/[sessionId]/retry/route.ts @@ -9,7 +9,8 @@ * understands what was already tried */ import { NextResponse } from "next/server"; -import { authSession } from "@/lib/auth/session-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"; @@ -20,7 +21,7 @@ export async function POST( ) { try { const { projectId, sessionId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/agent/sessions/[sessionId]/route.ts b/app/api/projects/[projectId]/agent/sessions/[sessionId]/route.ts index 85e09a7..3a1e644 100644 --- a/app/api/projects/[projectId]/agent/sessions/[sessionId]/route.ts +++ b/app/api/projects/[projectId]/agent/sessions/[sessionId]/route.ts @@ -7,7 +7,8 @@ * (handled in /stop/route.ts) */ import { NextResponse } from "next/server"; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth/authOptions"; import { query } from "@/lib/db-postgres"; export async function GET( @@ -16,7 +17,7 @@ export async function GET( ) { try { const { projectId, sessionId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/agent/sessions/[sessionId]/stop/route.ts b/app/api/projects/[projectId]/agent/sessions/[sessionId]/stop/route.ts index 75755db..5a868c9 100644 --- a/app/api/projects/[projectId]/agent/sessions/[sessionId]/stop/route.ts +++ b/app/api/projects/[projectId]/agent/sessions/[sessionId]/stop/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; -import { authSession } from "@/lib/auth/session-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"; @@ -10,7 +11,7 @@ export async function POST( ) { try { const { projectId, sessionId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/agent/sessions/route.ts b/app/api/projects/[projectId]/agent/sessions/route.ts index 951f431..06adfd0 100644 --- a/app/api/projects/[projectId]/agent/sessions/route.ts +++ b/app/api/projects/[projectId]/agent/sessions/route.ts @@ -9,7 +9,8 @@ * List all sessions for a project, newest first. */ import { NextResponse } from "next/server"; -import { authSession } from "@/lib/auth/session-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"; @@ -32,7 +33,7 @@ export async function POST( ) { try { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } @@ -130,7 +131,7 @@ export async function GET( ) { try { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/analysis-status/route.ts b/app/api/projects/[projectId]/analysis-status/route.ts index 188cc4d..72d20ce 100644 --- a/app/api/projects/[projectId]/analysis-status/route.ts +++ b/app/api/projects/[projectId]/analysis-status/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; export async function GET( @@ -8,7 +9,7 @@ export async function GET( ) { try { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/analyze-chats/route.ts b/app/api/projects/[projectId]/analyze-chats/route.ts index 8aad66d..3585d04 100644 --- a/app/api/projects/[projectId]/analyze-chats/route.ts +++ b/app/api/projects/[projectId]/analyze-chats/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; export const maxDuration = 60; @@ -36,7 +37,7 @@ export async function POST( ) { try { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/analyze-repo/route.ts b/app/api/projects/[projectId]/analyze-repo/route.ts index d7d7414..2e500d5 100644 --- a/app/api/projects/[projectId]/analyze-repo/route.ts +++ b/app/api/projects/[projectId]/analyze-repo/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; import { execSync } from 'child_process'; import { existsSync, readdirSync, readFileSync, statSync, rmSync } from 'fs'; @@ -78,7 +79,7 @@ export async function POST( ) { try { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/analyze/route.ts b/app/api/projects/[projectId]/analyze/route.ts index 92fff72..85f8bc4 100644 --- a/app/api/projects/[projectId]/analyze/route.ts +++ b/app/api/projects/[projectId]/analyze/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-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'; @@ -9,7 +10,7 @@ export async function GET( _req: Request, { params }: { params: Promise<{ projectId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } @@ -67,7 +68,7 @@ export async function POST( _req: Request, { params }: { params: Promise<{ projectId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/apps/route.ts b/app/api/projects/[projectId]/apps/route.ts index a1f1de7..d0d2701 100644 --- a/app/api/projects/[projectId]/apps/route.ts +++ b/app/api/projects/[projectId]/apps/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; const GITEA_API_URL = process.env.GITEA_API_URL ?? 'https://git.vibnai.com'; @@ -24,7 +25,7 @@ export async function GET( ) { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } @@ -124,7 +125,7 @@ export async function PATCH( ) { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/architecture/route.ts b/app/api/projects/[projectId]/architecture/route.ts index f4f003d..0db21cd 100644 --- a/app/api/projects/[projectId]/architecture/route.ts +++ b/app/api/projects/[projectId]/architecture/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/lib/auth/authOptions"; import { query } from "@/lib/db-postgres"; const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333"; @@ -12,7 +13,7 @@ export async function GET( _req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } @@ -42,7 +43,7 @@ export async function POST( req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } @@ -183,7 +184,7 @@ export async function PATCH( _req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/atlas-chat/route.ts b/app/api/projects/[projectId]/atlas-chat/route.ts index da5b5bf..527b429 100644 --- a/app/api/projects/[projectId]/atlas-chat/route.ts +++ b/app/api/projects/[projectId]/atlas-chat/route.ts @@ -1,47 +1,18 @@ import { NextRequest, NextResponse } from "next/server"; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/lib/auth/authOptions"; import { query } from "@/lib/db-postgres"; -import { - augmentAtlasMessage, - parseContextRefs, -} from "@/lib/chat-context-refs"; const AGENT_RUNNER_URL = process.env.AGENT_RUNNER_URL ?? "http://localhost:3333"; -const ALLOWED_SCOPES = new Set(["overview", "build"]); - -function normalizeScope(raw: string | null | undefined): "overview" | "build" { - const s = (raw ?? "overview").trim(); - return ALLOWED_SCOPES.has(s) ? (s as "overview" | "build") : "overview"; -} - -function runnerSessionId(projectId: string, scope: "overview" | "build"): string { - return scope === "overview" ? `atlas_${projectId}` : `atlas_${projectId}__build`; -} - // --------------------------------------------------------------------------- -// DB — atlas_chat_threads (project_id + scope); legacy atlas_conversations → overview +// DB helpers — atlas_conversations table // --------------------------------------------------------------------------- -let threadsTableReady = false; -let legacyTableChecked = false; +let tableReady = false; -async function ensureThreadsTable() { - if (threadsTableReady) return; - await query(` - CREATE TABLE IF NOT EXISTS atlas_chat_threads ( - project_id TEXT NOT NULL, - scope TEXT NOT NULL, - messages JSONB NOT NULL DEFAULT '[]'::jsonb, - updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (project_id, scope) - ) - `); - threadsTableReady = true; -} - -async function ensureLegacyConversationsTable() { - if (legacyTableChecked) return; +async function ensureTable() { + if (tableReady) return; await query(` CREATE TABLE IF NOT EXISTS atlas_conversations ( project_id TEXT PRIMARY KEY, @@ -49,47 +20,31 @@ async function ensureLegacyConversationsTable() { updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ) `); - legacyTableChecked = true; + tableReady = true; } -async function loadAtlasHistory(projectId: string, scope: "overview" | "build"): Promise { +async function loadAtlasHistory(projectId: string): Promise { try { - await ensureThreadsTable(); + await ensureTable(); const rows = await query<{ messages: any[] }>( - `SELECT messages FROM atlas_chat_threads WHERE project_id = $1 AND scope = $2`, - [projectId, scope] + `SELECT messages FROM atlas_conversations WHERE project_id = $1`, + [projectId] ); - if (rows.length > 0) { - const fromThreads = rows[0]?.messages; - return Array.isArray(fromThreads) ? fromThreads : []; - } - if (scope === "overview") { - await ensureLegacyConversationsTable(); - const leg = await query<{ messages: any[] }>( - `SELECT messages FROM atlas_conversations WHERE project_id = $1`, - [projectId] - ); - const legacyMsgs = leg[0]?.messages ?? []; - if (Array.isArray(legacyMsgs) && legacyMsgs.length > 0) { - await saveAtlasHistory(projectId, scope, legacyMsgs); - return legacyMsgs; - } - } - return []; + return rows[0]?.messages ?? []; } catch { return []; } } -async function saveAtlasHistory(projectId: string, scope: "overview" | "build", messages: any[]): Promise { +async function saveAtlasHistory(projectId: string, messages: any[]): Promise { try { - await ensureThreadsTable(); + await ensureTable(); await query( - `INSERT INTO atlas_chat_threads (project_id, scope, messages, updated_at) - VALUES ($1, $2, $3::jsonb, NOW()) - ON CONFLICT (project_id, scope) DO UPDATE - SET messages = $3::jsonb, updated_at = NOW()`, - [projectId, scope, JSON.stringify(messages)] + `INSERT INTO atlas_conversations (project_id, messages, updated_at) + VALUES ($1, $2::jsonb, NOW()) + ON CONFLICT (project_id) DO UPDATE + SET messages = $2::jsonb, updated_at = NOW()`, + [projectId, JSON.stringify(messages)] ); } catch (e) { console.error("[atlas-chat] Failed to save history:", e); @@ -111,36 +66,21 @@ async function savePrd(projectId: string, prdContent: string): Promise { } } -/** Replace the latest user message content so DB/UI never show the internal ref prefix. */ -function scrubLastUserMessageContent(history: unknown[], cleanText: string): unknown[] { - if (!Array.isArray(history) || history.length === 0) return history; - const h = history.map(m => (m && typeof m === "object" ? { ...(m as object) } : m)); - for (let i = h.length - 1; i >= 0; i--) { - const m = h[i] as { role?: string; content?: string }; - if (m?.role === "user" && typeof m.content === "string") { - h[i] = { ...m, content: cleanText }; - break; - } - } - return h; -} - // --------------------------------------------------------------------------- // GET — load stored conversation messages for display // --------------------------------------------------------------------------- export async function GET( - req: NextRequest, + _req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { projectId } = await params; - const scope = normalizeScope(req.nextUrl.searchParams.get("scope")); - const history = await loadAtlasHistory(projectId, scope); + const history = await loadAtlasHistory(projectId); // Filter to only user/assistant messages (no system prompts) for display const messages = history @@ -158,50 +98,43 @@ export async function POST( req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { projectId } = await params; - const body = await req.json(); - const message = body?.message as string | undefined; - const contextRefs = parseContextRefs(body?.contextRefs); + const { message } = await req.json(); if (!message?.trim()) { return NextResponse.json({ error: "message is required" }, { status: 400 }); } - const scope = normalizeScope(body?.scope as string | undefined); - const sessionId = runnerSessionId(projectId, scope); - const cleanUserText = message.trim(); + const sessionId = `atlas_${projectId}`; // Load conversation history from DB to persist across agent runner restarts. // Strip tool_call / tool_response messages — replaying them across sessions // causes Gemini to reject the request with a turn-ordering error. - const rawHistory = await loadAtlasHistory(projectId, scope); + const rawHistory = await loadAtlasHistory(projectId); const history = rawHistory.filter((m: any) => (m.role === "user" || m.role === "assistant") && m.content ); // __init__ is a special internal trigger used only when there is no existing history. // If history already exists, ignore the init request (conversation already started). - const isInit = cleanUserText === "__atlas_init__"; + const isInit = message.trim() === "__atlas_init__"; if (isInit && history.length > 0) { return NextResponse.json({ reply: null, alreadyStarted: true }); } - const runnerMessage = isInit - ? scope === "build" - ? "Begin as Vibn in build mode. The user is working in their monorepo. Ask what they want to ship or fix next, and offer concrete implementation guidance. Do not acknowledge this as an internal trigger." - : "Begin the conversation. Introduce yourself as Vibn and ask what the user is building. Do not acknowledge this as an internal trigger." - : augmentAtlasMessage(cleanUserText, contextRefs); - try { const res = await fetch(`${AGENT_RUNNER_URL}/atlas/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - message: runnerMessage, + // For init, send the greeting prompt but don't store it as a user message + message: isInit + ? "Begin the conversation. Introduce yourself as Vibn and ask what the user is building. Do not acknowledge this as an internal trigger." + : message, session_id: sessionId, history, is_init: isInit, @@ -220,16 +153,11 @@ export async function POST( const data = await res.json(); - let historyOut = data.history ?? []; - // Store the user's line without the internal reference block (UI shows clean text). - if (!isInit && cleanUserText !== "__atlas_init__") { - historyOut = scrubLastUserMessageContent(historyOut, cleanUserText); - } + // Persist updated history + await saveAtlasHistory(projectId, data.history ?? []); - await saveAtlasHistory(projectId, scope, historyOut); - - // If Atlas finalized the PRD, save it to the project (discovery / overview) - if (data.prdContent && scope === "overview") { + // If Atlas finalized the PRD, save it to the project + if (data.prdContent) { await savePrd(projectId, data.prdContent); } @@ -253,35 +181,24 @@ export async function POST( // --------------------------------------------------------------------------- export async function DELETE( - req: NextRequest, + _req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const { projectId } = await params; - const scope = normalizeScope(req.nextUrl.searchParams.get("scope")); - const sessionId = runnerSessionId(projectId, scope); + const sessionId = `atlas_${projectId}`; try { - await fetch(`${AGENT_RUNNER_URL}/atlas/sessions/${encodeURIComponent(sessionId)}`, { method: "DELETE" }); + await fetch(`${AGENT_RUNNER_URL}/atlas/sessions/${sessionId}`, { method: "DELETE" }); } catch { /* runner may be down */ } try { - await ensureThreadsTable(); - await query( - `DELETE FROM atlas_chat_threads WHERE project_id = $1 AND scope = $2`, - [projectId, scope] - ); + await query(`DELETE FROM atlas_conversations WHERE project_id = $1`, [projectId]); } catch { /* table may not exist yet */ } - if (scope === "overview") { - try { - await query(`DELETE FROM atlas_conversations WHERE project_id = $1`, [projectId]); - } catch { /* legacy */ } - } - return NextResponse.json({ cleared: true }); } diff --git a/app/api/projects/[projectId]/design-surfaces/route.ts b/app/api/projects/[projectId]/design-surfaces/route.ts index 6f75efd..785e83e 100644 --- a/app/api/projects/[projectId]/design-surfaces/route.ts +++ b/app/api/projects/[projectId]/design-surfaces/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; /** @@ -11,7 +12,7 @@ export async function GET( ) { try { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); const rows = await query<{ data: Record }>( @@ -48,7 +49,7 @@ export async function PATCH( ) { try { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); // Step 1: read current data — explicit ::text casts on every param diff --git a/app/api/projects/[projectId]/file/route.ts b/app/api/projects/[projectId]/file/route.ts index 4e6306c..03a6eef 100644 --- a/app/api/projects/[projectId]/file/route.ts +++ b/app/api/projects/[projectId]/file/route.ts @@ -6,7 +6,8 @@ * Response for file: { type: "file", content: string, encoding: "utf8" | "base64" } */ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; const GITEA_API_URL = process.env.GITEA_API_URL ?? 'https://git.vibnai.com'; @@ -38,7 +39,7 @@ export async function GET( ) { try { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/generate-migration-plan/route.ts b/app/api/projects/[projectId]/generate-migration-plan/route.ts index 44d963d..19b4189 100644 --- a/app/api/projects/[projectId]/generate-migration-plan/route.ts +++ b/app/api/projects/[projectId]/generate-migration-plan/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; export const maxDuration = 120; @@ -27,7 +28,7 @@ export async function POST( ) { try { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/knowledge/import-ai-chat/route.ts b/app/api/projects/[projectId]/knowledge/import-ai-chat/route.ts index 7987edd..9b113d5 100644 --- a/app/api/projects/[projectId]/knowledge/import-ai-chat/route.ts +++ b/app/api/projects/[projectId]/knowledge/import-ai-chat/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; import { createKnowledgeItem } from '@/lib/server/knowledge'; import type { KnowledgeSourceMeta } from '@/lib/types/knowledge'; @@ -33,7 +34,7 @@ export async function POST( return NextResponse.json({ error: 'transcript is required' }, { status: 400 }); } - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/knowledge/items/route.ts b/app/api/projects/[projectId]/knowledge/items/route.ts index 0c30116..64acbee 100644 --- a/app/api/projects/[projectId]/knowledge/items/route.ts +++ b/app/api/projects/[projectId]/knowledge/items/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; export async function GET( @@ -9,7 +10,7 @@ export async function GET( try { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/knowledge/route.ts b/app/api/projects/[projectId]/knowledge/route.ts index 4585665..dcc4788 100644 --- a/app/api/projects/[projectId]/knowledge/route.ts +++ b/app/api/projects/[projectId]/knowledge/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/lib/auth/authOptions"; import { query } from "@/lib/db-postgres"; async function assertOwnership(projectId: string, email: string): Promise { @@ -17,7 +18,7 @@ export async function GET( _req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); const { projectId } = await params; @@ -40,7 +41,7 @@ export async function POST( req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); const { projectId } = await params; @@ -82,7 +83,7 @@ export async function DELETE( req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); const { projectId } = await params; diff --git a/app/api/projects/[projectId]/preview-url/route.ts b/app/api/projects/[projectId]/preview-url/route.ts index 94e9028..accd86d 100644 --- a/app/api/projects/[projectId]/preview-url/route.ts +++ b/app/api/projects/[projectId]/preview-url/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; import { listApplications, CoolifyApplication } from '@/lib/coolify'; @@ -19,7 +20,7 @@ export async function GET( ) { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/route.ts b/app/api/projects/[projectId]/route.ts index 4c6f2f1..3b08da5 100644 --- a/app/api/projects/[projectId]/route.ts +++ b/app/api/projects/[projectId]/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; export async function GET( @@ -9,7 +10,7 @@ export async function GET( try { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } @@ -44,7 +45,7 @@ export async function PATCH( const { projectId } = await params; const body = await request.json(); - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/save-phase/route.ts b/app/api/projects/[projectId]/save-phase/route.ts index 374dc69..016c0d1 100644 --- a/app/api/projects/[projectId]/save-phase/route.ts +++ b/app/api/projects/[projectId]/save-phase/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/lib/auth/authOptions"; import { query } from "@/lib/db-postgres"; // --------------------------------------------------------------------------- @@ -10,7 +11,7 @@ export async function POST( req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } @@ -84,7 +85,7 @@ export async function GET( _req: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/vision/route.ts b/app/api/projects/[projectId]/vision/route.ts index 3c500d5..2e4b361 100644 --- a/app/api/projects/[projectId]/vision/route.ts +++ b/app/api/projects/[projectId]/vision/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; export async function POST( @@ -8,7 +9,7 @@ export async function POST( ) { try { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/[projectId]/workspace/route.ts b/app/api/projects/[projectId]/workspace/route.ts index 4ee1000..bf1b006 100644 --- a/app/api/projects/[projectId]/workspace/route.ts +++ b/app/api/projects/[projectId]/workspace/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-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'; @@ -10,7 +11,7 @@ export async function POST( try { const { projectId } = await params; - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/delete/route.ts b/app/api/projects/delete/route.ts index 4012870..be70b6f 100644 --- a/app/api/projects/delete/route.ts +++ b/app/api/projects/delete/route.ts @@ -1,10 +1,11 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; export async function POST(request: Request) { try { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/deploy/route.ts b/app/api/projects/deploy/route.ts index bbe2d27..4ea0001 100644 --- a/app/api/projects/deploy/route.ts +++ b/app/api/projects/deploy/route.ts @@ -8,13 +8,14 @@ */ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; import { deployApplication } from '@/lib/coolify'; export async function POST(request: Request) { try { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/prewarm/route.ts b/app/api/projects/prewarm/route.ts index 5d6d45b..84b62a2 100644 --- a/app/api/projects/prewarm/route.ts +++ b/app/api/projects/prewarm/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { prewarmWorkspace } from '@/lib/cloud-run-workspace'; /** @@ -11,7 +12,7 @@ import { prewarmWorkspace } from '@/lib/cloud-run-workspace'; * to avoid CORS issues with run.app domains. */ export async function POST(req: NextRequest) { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts index ffcf749..ac3f37f 100644 --- a/app/api/projects/route.ts +++ b/app/api/projects/route.ts @@ -1,10 +1,11 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; export async function GET() { try { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } diff --git a/app/api/sessions/route.ts b/app/api/sessions/route.ts index eddd0ca..9278ae9 100644 --- a/app/api/sessions/route.ts +++ b/app/api/sessions/route.ts @@ -1,10 +1,11 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; export async function GET(request: Request) { try { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json([], { status: 200 }); } diff --git a/app/api/user/api-key/route.ts b/app/api/user/api-key/route.ts index ce53321..3296818 100644 --- a/app/api/user/api-key/route.ts +++ b/app/api/user/api-key/route.ts @@ -1,11 +1,12 @@ import { NextResponse } from 'next/server'; -import { authSession } from "@/lib/auth/session-server"; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth/authOptions'; import { query } from '@/lib/db-postgres'; import { v4 as uuidv4 } from 'uuid'; export async function GET(request: Request) { try { - const session = await authSession(); + const session = await getServerSession(authOptions); if (!session?.user?.email) { return NextResponse.json({ error: 'No authorization token provided' }, { status: 401 }); } diff --git a/app/auth/layout.tsx b/app/auth/layout.tsx index 6f0908c..64f6233 100644 --- a/app/auth/layout.tsx +++ b/app/auth/layout.tsx @@ -1,20 +1,4 @@ -import type { Metadata } from "next"; -import { Plus_Jakarta_Sans } from "next/font/google"; import { Toaster } from "sonner"; -import { JustineAuthShell } from "@/marketing/components/justine/JustineAuthShell"; -import "../styles/justine/02-signup.css"; - -const justineJakarta = Plus_Jakarta_Sans({ - subsets: ["latin"], - weight: ["400", "500", "600", "700", "800"], - variable: "--font-justine-jakarta", - display: "swap", -}); - -export const metadata: Metadata = { - title: "Sign in · vibn", - description: "Sign in to your vibn workspace with Google.", -}; export default function AuthLayout({ children, @@ -22,12 +6,10 @@ export default function AuthLayout({ children: React.ReactNode; }) { return ( -
- {children} + <> + {children} -
+ ); } + diff --git a/app/auth/page.tsx b/app/auth/page.tsx index d6af17b..1d9c671 100644 --- a/app/auth/page.tsx +++ b/app/auth/page.tsx @@ -29,10 +29,10 @@ function AuthPageInner() { if (status === "loading") { return ( -
-
-
-

Loading authentication…

+
+
+
+

Loading authentication...

); diff --git a/app/components/NextAuthComponent.tsx b/app/components/NextAuthComponent.tsx index ca8d746..e0af278 100644 --- a/app/components/NextAuthComponent.tsx +++ b/app/components/NextAuthComponent.tsx @@ -1,190 +1,84 @@ "use client"; import { signIn } from "next-auth/react"; -import Link from "next/link"; -import { useSearchParams } from "next/navigation"; -import { Suspense, useState } from "react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -function authErrorMessage(code: string | null): string | null { - if (!code) return null; - if (code === "Callback") { - return ( - "Google could not complete sign-in. Most often: DATABASE_URL in vibn-frontend/.env.local must reach Postgres from " + - "this machine (Coolify internal hostnames only work inside Docker). Use a public host/port, tunnel, or proxy; " + - "then run npx prisma db push. Also confirm NEXTAUTH_URL matches the browser (http://localhost:3000) and " + - "Google redirect URI http://localhost:3000/api/auth/callback/google. Dev check: GET /api/debug/prisma — see terminal for [next-auth] logs." - ); - } - if (code === "Configuration") { - return "Auth is misconfigured (check GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, NEXTAUTH_SECRET)."; - } - if (code === "AccessDenied") { - return "Access was denied. You may need to be added as a test user if the OAuth app is in testing mode."; - } - return `Sign-in error: ${code}`; -} - -const showDevLocalSignIn = - process.env.NODE_ENV === "development" && - Boolean(process.env.NEXT_PUBLIC_DEV_LOCAL_AUTH_EMAIL?.trim()); - -function NextAuthForm() { +export default function NextAuthComponent() { const [isLoading, setIsLoading] = useState(false); - const [devSecret, setDevSecret] = useState(""); - const searchParams = useSearchParams(); - const callbackUrl = searchParams.get("callbackUrl") ?? "/auth"; - const errorCode = searchParams.get("error"); - const errorHint = authErrorMessage(errorCode); const handleGoogleSignIn = async () => { setIsLoading(true); try { - await signIn("google", { callbackUrl }); + // Sign in with Google using NextAuth + await signIn("google", { + callbackUrl: "/auth", + }); } catch (error) { console.error("Google sign-in error:", error); setIsLoading(false); } }; - const handleDevLocalSignIn = async () => { - setIsLoading(true); - try { - await signIn("dev-local", { - callbackUrl, - password: devSecret, - redirect: true, - }); - } catch (error) { - console.error("Dev local sign-in error:", error); - setIsLoading(false); - } - }; - return ( -
-
-

Welcome back

-

Sign in with Google to open your workspace.

+
+
+ {/* Logo */} +
+ Vib'n +
- {errorHint && ( -
- {errorHint} -
- )} - - - - {showDevLocalSignIn && ( -
-

- Local only: sign in without Google as{" "} - {process.env.NEXT_PUBLIC_DEV_LOCAL_AUTH_EMAIL} -

-
{ - e.preventDefault(); - void handleDevLocalSignIn(); - }} + {/* Auth Card */} + + + + Welcome to Vib'n + + + Sign in to continue + + + + - -
- )} + + + + + + + {isLoading ? "Signing in..." : "Continue with Google"} + + + -

- By continuing, you agree to our{" "} - Terms and Privacy Policy. + {/* Footer */} +

+ By continuing, you agree to our Terms of Service and Privacy Policy.

); } - -function NextAuthFallback() { - return ( -
-
-
-

Loading…

-
-
- ); -} - -export default function NextAuthComponent() { - return ( - }> - - - ); -} diff --git a/app/layout.tsx b/app/layout.tsx index 42258ef..eb88285 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -58,20 +58,13 @@ export default function RootLayout({ {children} - {/* Service worker breaks Next dev (RSC / __nextjs_* fetches need a real Response). Prod only. */} - {process.env.NODE_ENV === "production" ? ( -