Theia rip-out: - Delete app/api/theia-auth/route.ts (Traefik ForwardAuth shim) - Delete app/api/projects/[projectId]/workspace/route.ts and app/api/projects/prewarm/route.ts (Cloud Run Theia provisioning) - Delete lib/cloud-run-workspace.ts and lib/coolify-workspace.ts - Strip provisionTheiaWorkspace + theiaWorkspaceUrl/theiaAppUuid/ theiaError from app/api/projects/create/route.ts response - Remove Theia callbackUrl branch in app/auth/page.tsx - Drop "Open in Theia" button + xterm/Theia PTY copy in build/page.tsx - Drop theiaWorkspaceUrl from deployment/page.tsx Project type - Strip Theia IDE line + theia-code-os from advisor + agent-chat context strings - Scrub Theia mention from lib/auth/workspace-auth.ts comment P5.1 (custom apex domains + DNS): - lib/coolify.ts + lib/opensrs.ts: nameserver normalization, OpenSRS XML auth, Cloud DNS plumbing - scripts/smoke-attach-e2e.ts: full prod GCP + sandbox OpenSRS + prod Coolify smoke covering register/zone/A/NS/PATCH/cleanup In-progress (Justine onboarding/build, MVP setup, agent telemetry): - New (justine)/stories, project (home) layouts, mvp-setup, run, tasks routes + supporting components - Project shell + sidebar + nav refactor for the Stackless palette - Agent session API hardening (sessions, events, stream, approve, retry, stop) + atlas-chat, advisor, design-surfaces refresh - New scripts/sync-db-url-from-coolify.mjs + scripts/prisma-db-push.mjs + docker-compose.local-db.yml for local Prisma workflows - lib/dev-bypass.ts, lib/chat-context-refs.ts, lib/prd-sections.ts - Misc: stories CSS, debug/prisma route, modal-theme, BuildLivePlanPanel Made-with: Cursor
359 lines
15 KiB
TypeScript
359 lines
15 KiB
TypeScript
"use client";
|
|
|
|
import { Suspense, useState, useEffect } from "react";
|
|
import { useParams, useSearchParams, useRouter } from "next/navigation";
|
|
import { JM } from "@/components/project-creation/modal-theme";
|
|
|
|
export type ProjectInfraRouteBase = "run" | "infrastructure";
|
|
|
|
export interface ProjectInfraPanelProps {
|
|
routeBase: ProjectInfraRouteBase;
|
|
/** Uppercase rail heading (e.g. Run vs Infrastructure) */
|
|
navGroupLabel: string;
|
|
}
|
|
|
|
// ── 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 (
|
|
<div style={{
|
|
flex: 1, display: "flex", flexDirection: "column",
|
|
alignItems: "center", justifyContent: "center",
|
|
padding: 60, textAlign: "center", gap: 16,
|
|
}}>
|
|
<div style={{
|
|
width: 56, height: 56, borderRadius: 14, background: "#f0ece4",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
fontSize: "1.5rem", color: "#b5b0a6",
|
|
}}>
|
|
{icon}
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: "1rem", fontWeight: 600, color: "#1a1a1a", marginBottom: 6 }}>{title}</div>
|
|
<div style={{ fontSize: "0.82rem", color: "#a09a90", maxWidth: 340, lineHeight: 1.6 }}>{description}</div>
|
|
</div>
|
|
<div style={{
|
|
marginTop: 8, padding: "8px 18px",
|
|
background: "#1a1a1a", color: "#fff",
|
|
borderRadius: 7, fontSize: "0.78rem", fontWeight: 500,
|
|
opacity: 0.4, cursor: "default",
|
|
}}>
|
|
Coming soon
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Builds tab ────────────────────────────────────────────────────────────────
|
|
|
|
function BuildsTab({ project }: { project: ProjectData | null }) {
|
|
const apps = project?.apps ?? [];
|
|
if (apps.length === 0) {
|
|
return (
|
|
<ComingSoonPanel
|
|
icon="⬡"
|
|
title="No deployments yet"
|
|
description="Once your apps are deployed via Coolify, build history and deployment logs will appear here."
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<div style={{ padding: 32, maxWidth: 720 }}>
|
|
<div style={{ fontSize: "0.72rem", fontWeight: 700, color: "#a09a90", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 16 }}>
|
|
Deployed Apps
|
|
</div>
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
|
|
{apps.map(app => (
|
|
<div key={app.name} style={{
|
|
background: "#fff", border: "1px solid #e8e4dc", borderRadius: 10,
|
|
padding: "14px 18px", display: "flex", alignItems: "center", justifyContent: "space-between",
|
|
}}>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
<span style={{ fontSize: "0.85rem", color: "#a09a90" }}>⬡</span>
|
|
<div>
|
|
<div style={{ fontSize: "0.82rem", fontWeight: 600, color: "#1a1a1a" }}>{app.name}</div>
|
|
{app.domain && (
|
|
<div style={{ fontSize: "0.72rem", color: "#a09a90", marginTop: 2 }}>{app.domain}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
<span style={{ width: 7, height: 7, borderRadius: "50%", background: "#2e7d32", display: "inline-block" }} />
|
|
<span style={{ fontSize: "0.73rem", color: "#6b6560" }}>Running</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Databases tab ─────────────────────────────────────────────────────────────
|
|
|
|
function DatabasesTab() {
|
|
return (
|
|
<ComingSoonPanel
|
|
icon="◫"
|
|
title="Databases"
|
|
description="Provision and manage PostgreSQL, Redis, and other databases for your project. Connection strings and credentials will be auto-injected into your environment."
|
|
/>
|
|
);
|
|
}
|
|
|
|
// ── Services tab ──────────────────────────────────────────────────────────────
|
|
|
|
function ServicesTab() {
|
|
return (
|
|
<ComingSoonPanel
|
|
icon="◎"
|
|
title="Services"
|
|
description="Background workers, email delivery, queues, file storage, and third-party integrations will be configured and monitored here."
|
|
/>
|
|
);
|
|
}
|
|
|
|
// ── Environment tab ───────────────────────────────────────────────────────────
|
|
|
|
function EnvironmentTab() {
|
|
return (
|
|
<div style={{ padding: 32, maxWidth: 720 }}>
|
|
<div style={{ fontSize: "0.72rem", fontWeight: 700, color: "#a09a90", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 16 }}>
|
|
Environment Variables & Secrets
|
|
</div>
|
|
<div style={{
|
|
background: "#fff", border: "1px solid #e8e4dc", borderRadius: 10,
|
|
overflow: "hidden", marginBottom: 20,
|
|
}}>
|
|
<div style={{
|
|
display: "grid", gridTemplateColumns: "1fr 1fr auto",
|
|
padding: "10px 18px", background: "#faf8f5",
|
|
borderBottom: "1px solid #e8e4dc",
|
|
fontSize: "0.68rem", fontWeight: 700, color: "#a09a90",
|
|
letterSpacing: "0.06em", textTransform: "uppercase",
|
|
}}>
|
|
<span>Key</span><span>Value</span><span />
|
|
</div>
|
|
{["DATABASE_URL", "NEXTAUTH_SECRET", "GITEA_API_TOKEN"].map(k => (
|
|
<div key={k} style={{
|
|
display: "grid", gridTemplateColumns: "1fr 1fr auto",
|
|
padding: "11px 18px", borderBottom: "1px solid #f0ece4",
|
|
alignItems: "center",
|
|
}}>
|
|
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.73rem", color: "#1a1a1a" }}>{k}</span>
|
|
<span style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.73rem", color: "#b5b0a6", letterSpacing: 2 }}>••••••••</span>
|
|
<button type="button" style={{ background: "none", border: "none", cursor: "pointer", color: "#a09a90", fontSize: "0.72rem", padding: "2px 6px" }}>Edit</button>
|
|
</div>
|
|
))}
|
|
<div style={{ padding: "11px 18px", borderTop: "1px solid #f0ece4" }}>
|
|
<button type="button" style={{
|
|
background: "none", border: "1px dashed #d4cfc8", borderRadius: 6,
|
|
padding: "6px 14px", fontSize: "0.75rem", color: "#a09a90",
|
|
cursor: "pointer", width: "100%",
|
|
}}>
|
|
+ Add variable
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div style={{ fontSize: "0.75rem", color: "#b5b0a6", lineHeight: 1.6 }}>
|
|
Variables are encrypted at rest and auto-injected into deployed containers. Secrets are never exposed in logs.
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Domains tab ───────────────────────────────────────────────────────────────
|
|
|
|
function DomainsTab({ project }: { project: ProjectData | null }) {
|
|
const apps = (project?.apps ?? []).filter(a => a.domain);
|
|
return (
|
|
<div style={{ padding: 32, maxWidth: 720 }}>
|
|
<div style={{ fontSize: "0.72rem", fontWeight: 700, color: "#a09a90", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 16 }}>
|
|
Domains & SSL
|
|
</div>
|
|
{apps.length > 0 ? (
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 10, marginBottom: 20 }}>
|
|
{apps.map(app => (
|
|
<div key={app.name} style={{
|
|
background: "#fff", border: "1px solid #e8e4dc", borderRadius: 10,
|
|
padding: "14px 18px", display: "flex", alignItems: "center", justifyContent: "space-between",
|
|
}}>
|
|
<div>
|
|
<div style={{ fontFamily: "IBM Plex Mono, monospace", fontSize: "0.8rem", color: "#1a1a1a", fontWeight: 500 }}>
|
|
{app.domain}
|
|
</div>
|
|
<div style={{ fontSize: "0.7rem", color: "#a09a90", marginTop: 3 }}>{app.name}</div>
|
|
</div>
|
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
<span style={{ width: 7, height: 7, borderRadius: "50%", background: "#2e7d32", display: "inline-block" }} />
|
|
<span style={{ fontSize: "0.73rem", color: "#6b6560" }}>SSL active</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<div style={{
|
|
background: "#fff", border: "1px dashed #d4cfc8", borderRadius: 10,
|
|
padding: "32px 24px", textAlign: "center", marginBottom: 20,
|
|
}}>
|
|
<div style={{ fontSize: "0.82rem", color: "#a09a90" }}>No custom domains configured</div>
|
|
<div style={{ fontSize: "0.73rem", color: "#b5b0a6", marginTop: 6 }}>Deploy an app first, then point a domain here.</div>
|
|
</div>
|
|
)}
|
|
<button type="button" style={{
|
|
background: "#1a1a1a", color: "#fff", border: "none",
|
|
borderRadius: 8, padding: "9px 20px",
|
|
fontSize: "0.78rem", fontWeight: 500, cursor: "pointer",
|
|
opacity: 0.5,
|
|
}}>
|
|
+ Add domain
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Logs tab ──────────────────────────────────────────────────────────────────
|
|
|
|
function LogsTab({ project }: { project: ProjectData | null }) {
|
|
const apps = project?.apps ?? [];
|
|
if (apps.length === 0) {
|
|
return (
|
|
<ComingSoonPanel
|
|
icon="≈"
|
|
title="No logs yet"
|
|
description="Runtime logs, request traces, and error reports from your deployed services will stream here."
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<div style={{ padding: 32, maxWidth: 900 }}>
|
|
<div style={{ fontSize: "0.72rem", fontWeight: 700, color: "#a09a90", letterSpacing: "0.08em", textTransform: "uppercase", marginBottom: 16 }}>
|
|
Runtime Logs
|
|
</div>
|
|
<div style={{
|
|
background: "#1e1e1e", borderRadius: 10, padding: "20px 24px",
|
|
fontFamily: "IBM Plex Mono, monospace", fontSize: "0.73rem", color: "#d4d4d4",
|
|
lineHeight: 1.6, minHeight: 200,
|
|
}}>
|
|
<div style={{ color: "#6a9955" }}>{"# Logs will stream here once connected to Coolify"}</div>
|
|
<div style={{ color: "#569cd6", marginTop: 8 }}>{"→ Select a service to tail its log output"}</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── Inner ───────────────────────────────────────────────────────────────────
|
|
|
|
function ProjectInfraPanelInner({ routeBase, navGroupLabel }: ProjectInfraPanelProps) {
|
|
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<ProjectData | null>(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}/${routeBase}?tab=${id}`, { scroll: false });
|
|
};
|
|
|
|
return (
|
|
<div style={{ display: "flex", height: "100%", fontFamily: JM.fontSans, overflow: "hidden" }}>
|
|
|
|
<div style={{
|
|
width: 190, flexShrink: 0,
|
|
borderRight: "1px solid #e8e4dc",
|
|
background: "#faf8f5",
|
|
display: "flex", flexDirection: "column",
|
|
padding: "16px 8px",
|
|
gap: 2,
|
|
overflow: "auto",
|
|
}}>
|
|
<div style={{
|
|
fontSize: "0.6rem", fontWeight: 700, color: "#b5b0a6",
|
|
letterSpacing: "0.1em", textTransform: "uppercase",
|
|
padding: "0 8px 10px",
|
|
}}>
|
|
{navGroupLabel}
|
|
</div>
|
|
{TABS.map(tab => {
|
|
const active = activeTab === tab.id;
|
|
return (
|
|
<button
|
|
key={tab.id}
|
|
type="button"
|
|
onClick={() => setTab(tab.id)}
|
|
style={{
|
|
display: "flex", alignItems: "center", gap: 9,
|
|
padding: "7px 10px", borderRadius: 6,
|
|
background: active ? "#f0ece4" : "transparent",
|
|
border: "none", cursor: "pointer", width: "100%", textAlign: "left",
|
|
color: active ? "#1a1a1a" : "#6b6560",
|
|
fontSize: "0.8rem", fontWeight: active ? 600 : 450,
|
|
transition: "background 0.1s",
|
|
fontFamily: JM.fontSans,
|
|
}}
|
|
onMouseEnter={e => { if (!active) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
|
|
onMouseLeave={e => { if (!active) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
|
>
|
|
<span style={{ fontSize: "0.75rem", opacity: 0.65, width: 16, textAlign: "center" }}>{tab.icon}</span>
|
|
{tab.label}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div style={{ flex: 1, overflow: "auto", display: "flex", flexDirection: "column" }}>
|
|
{activeTab === "builds" && <BuildsTab project={project} />}
|
|
{activeTab === "databases" && <DatabasesTab />}
|
|
{activeTab === "services" && <ServicesTab />}
|
|
{activeTab === "environment" && <EnvironmentTab />}
|
|
{activeTab === "domains" && <DomainsTab project={project} />}
|
|
{activeTab === "logs" && <LogsTab project={project} />}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function ProjectInfraPanel(props: ProjectInfraPanelProps) {
|
|
return (
|
|
<Suspense fallback={<div style={{ display: "flex", height: "100%", alignItems: "center", justifyContent: "center", color: JM.muted, fontFamily: JM.fontSans, fontSize: "0.85rem" }}>Loading…</div>}>
|
|
<ProjectInfraPanelInner {...props} />
|
|
</Suspense>
|
|
);
|
|
}
|