feat(project): split dev containers into Product; convert Hosting to tile-rail
The vibn-dev-* services that the AI authors code in conceptually belong to Product (build surface), not Hosting (runtime + reach). Anatomy endpoint now splits Coolify services by name prefix: - vibn-dev-* → product.devContainers[] - everything else → hosting.services[] Product tab gains a "Workspace" section above the codebases stack with a single dev-container tile. Selecting it shows status + active dev servers in the right pane. Codebase + file selection behaves the same as before. Hosting tab restructured from a stack of always-visible cards to the same tile-rail pattern Product uses: left rail has 4 always- present categories (Production / Services / Previews / Domains) each with a count badge, items inside are clickable tiles, right pane shows details for the selected item. Empty categories show a one-liner explaining what would appear there — teaches the user the model on a brand-new project without being preachy. Made-with: Cursor
This commit is contained in:
@@ -50,6 +50,13 @@ interface DevService {
|
||||
status?: string;
|
||||
}
|
||||
|
||||
/** Dev container = the vibn-dev-* Coolify service this project edits in. */
|
||||
interface DevContainer {
|
||||
uuid: string;
|
||||
name: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface PreviewUrl {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -68,6 +75,9 @@ interface Anatomy {
|
||||
project: { id: string; name: string; gitea?: string; coolifyProjectUuid?: string };
|
||||
codebases: Codebase[];
|
||||
codebasesReason?: "no_repo" | "empty_repo";
|
||||
product: {
|
||||
devContainers: DevContainer[];
|
||||
};
|
||||
hosting: {
|
||||
production: ProductionApp[];
|
||||
services: DevService[];
|
||||
@@ -188,22 +198,22 @@ async function loadProductionApps(giteaRepo: string | undefined): Promise<Produc
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDevServices(coolifyProjectUuid: string | undefined): Promise<DevService[]> {
|
||||
/** Returns ALL services in the Coolify project. Caller splits dev
|
||||
* containers from deployed services by name prefix. */
|
||||
async function loadAllServices(coolifyProjectUuid: string | undefined): Promise<CoolifyService[]> {
|
||||
if (!coolifyProjectUuid) return [];
|
||||
try {
|
||||
const services = await listServicesInProject(coolifyProjectUuid);
|
||||
return services.map((s: CoolifyService) => ({
|
||||
uuid: s.uuid,
|
||||
name: s.name,
|
||||
serviceType: s.service_type,
|
||||
status: s.status,
|
||||
}));
|
||||
return await listServicesInProject(coolifyProjectUuid);
|
||||
} catch (err) {
|
||||
console.error("[anatomy] listServicesInProject failed:", err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function isDevContainer(svc: CoolifyService): boolean {
|
||||
return svc.name.startsWith("vibn-dev-");
|
||||
}
|
||||
|
||||
async function loadPreviewUrls(projectId: string): Promise<PreviewUrl[]> {
|
||||
try {
|
||||
const rows = await query<{
|
||||
@@ -291,7 +301,7 @@ export async function GET(
|
||||
"Project";
|
||||
|
||||
// Run the slow bits in parallel
|
||||
const [codebasesResult, production, services, previews] = await Promise.all([
|
||||
const [codebasesResult, production, allServices, previews] = await Promise.all([
|
||||
giteaRepo
|
||||
? discoverCodebases(giteaRepo).catch(err => {
|
||||
console.error("[anatomy] discoverCodebases failed:", err);
|
||||
@@ -299,10 +309,27 @@ export async function GET(
|
||||
})
|
||||
: Promise.resolve({ codebases: [] as Codebase[], reason: undefined as undefined }),
|
||||
loadProductionApps(giteaRepo),
|
||||
loadDevServices(coolifyProjectUuid),
|
||||
loadAllServices(coolifyProjectUuid),
|
||||
loadPreviewUrls(projectId),
|
||||
]);
|
||||
|
||||
// Split services: vibn-dev-* belong to Product (the dev workbench).
|
||||
// Everything else is a deployed service that belongs in Hosting.
|
||||
const devContainers: DevContainer[] = [];
|
||||
const deployedServices: DevService[] = [];
|
||||
for (const s of allServices) {
|
||||
if (isDevContainer(s)) {
|
||||
devContainers.push({ uuid: s.uuid, name: s.name, status: s.status });
|
||||
} else {
|
||||
deployedServices.push({
|
||||
uuid: s.uuid,
|
||||
name: s.name,
|
||||
serviceType: s.service_type,
|
||||
status: s.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const codebasesReason: "no_repo" | "empty_repo" | undefined = !giteaRepo
|
||||
? "no_repo"
|
||||
: codebasesResult.reason;
|
||||
@@ -316,9 +343,10 @@ export async function GET(
|
||||
},
|
||||
codebases: codebasesResult.codebases,
|
||||
codebasesReason,
|
||||
product: { devContainers },
|
||||
hosting: {
|
||||
production,
|
||||
services,
|
||||
services: deployedServices,
|
||||
previewUrls: previews,
|
||||
domains: dedupeDomains(production, previews),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user