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}
+ />
+