From 3770ba185372edf6b9223ef3a26df3bc69b17832 Mon Sep 17 00:00:00 2001 From: Mark Henderson Date: Fri, 6 Mar 2026 14:18:03 -0800 Subject: [PATCH] feat: Infrastructure section with 6 sub-sections (Builds, Databases, Services, Environment, Domains, Logs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sidebar Infrastructure replaced with 6 named rows linking to /infrastructure?tab= - New /infrastructure page with left sub-nav and per-tab content panels: Builds — lists deployed Coolify apps with live status Databases — coming soon placeholder Services — coming soon placeholder Environment — variable table with masked values (scaffold) Domains — lists configured domains with SSL status Logs — dark terminal panel, ready to stream - Dim state on rows reflects whether data exists (e.g. no domains = dim) Made-with: Cursor --- .../[projectId]/infrastructure/page.tsx | 353 ++++++++++++++++++ components/layout/vibn-sidebar.tsx | 57 ++- 2 files changed, 395 insertions(+), 15 deletions(-) create mode 100644 app/[workspace]/project/[projectId]/infrastructure/page.tsx 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} + /> +