"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 (
);
}
// ── 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 ? (
) : (
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…}>
);
}