diff --git a/app/[workspace]/project/[projectId]/infrastructure/page.tsx b/app/[workspace]/project/[projectId]/infrastructure/page.tsx new file mode 100644 index 0000000..40dfb02 --- /dev/null +++ b/app/[workspace]/project/[projectId]/infrastructure/page.tsx @@ -0,0 +1,353 @@ +"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/components/layout/vibn-sidebar.tsx b/components/layout/vibn-sidebar.tsx index 2e75c97..964bc43 100644 --- a/components/layout/vibn-sidebar.tsx +++ b/components/layout/vibn-sidebar.tsx @@ -350,21 +350,48 @@ export function VIBNSidebar({ workspace }: VIBNSidebarProps) { {/* ── Infrastructure ── */} - {infraApps.length > 0 ? ( - infraApps.map(app => ( - - )) - ) : project?.giteaRepo ? ( - - ) : ( - - )} + + + + + a.domain)} + collapsed={collapsed} + /> +