Anatomy + UI rewrite — locked the conceptual model after user feedback:
Product = "what makes up the thing you're shipping":
- Codebases (Gitea repos)
- Images (Coolify services backed by upstream Docker images: Twenty
CRM, n8n, etc.)
- Dev containers no longer surface here. The vibn-dev-* container is
the AI's workshop, not a product surface; previews it serves still
appear under Hosting → Previews.
Hosting = "where it lives + how it gets there", unified:
- Live: every running endpoint as one list. Each item carries a
source badge ("repo" | "image"), status dot, attached domain, and
last-build summary inline. No separate Build, Domains or Services
categories — those are properties on each Live item.
- Previews: dev container preview URLs (unchanged).
Anatomy endpoint reshaped accordingly:
- product.{codebases, images}
- hosting.{live, previews} (was production/services/previewUrls/domains)
- lastBuild summary fetched per repo-app via listApplicationDeployments
in parallel.
ProjectStagePill rewired to derive Live/Down/Building from hosting.live
+ hosting.previews. dev-container-detail.tsx removed.
services.* MCP tools added so AI agents can manage Coolify services
(Twenty CRM, n8n, …) the same way they manage apps:
- services.list, services.get
- services.start, services.stop
- services.envs.list, services.envs.upsert
All tenant-scoped via getServiceInWorkspace + getOwnedCoolifyProjectUuids.
vibn-dev-* containers stay hidden from services.list.
Made-with: Cursor
68 lines
2.6 KiB
TypeScript
68 lines
2.6 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Lives in the project header. Shows the project's *real* stage
|
|
* derived from hosting reality, not the legacy `data.status` field
|
|
* (which historically lied).
|
|
*
|
|
* - any running production app → "Live" (green)
|
|
* - any failed production app → "Down" (red)
|
|
* - any service / preview URL → "Building" (blue)
|
|
* - else → fallbackStage from data.status
|
|
* (typically "Defining" or "Planning")
|
|
*/
|
|
|
|
import { useAnatomy } from "./use-anatomy";
|
|
|
|
interface ProjectStagePillProps {
|
|
projectId: string;
|
|
/** Stage value pulled from fs_projects.data.status — used only as
|
|
* a fallback if no live infra exists yet. */
|
|
fallbackStage: "discovery" | "architecture" | "building" | "active";
|
|
}
|
|
|
|
export function ProjectStagePill({ projectId, fallbackStage }: ProjectStagePillProps) {
|
|
const { anatomy, loading } = useAnatomy(projectId);
|
|
|
|
if (loading && !anatomy) return <Pill {...PRESETS[fallbackStage]} />;
|
|
|
|
const live = anatomy?.hosting.live ?? [];
|
|
const previews = anatomy?.hosting.previews ?? [];
|
|
|
|
const anyRunning = live.some(l => /running|healthy/i.test(l.status));
|
|
const anyFailed = live.some(l => /failed|exited|unhealthy/i.test(l.status));
|
|
const buildingNow = !anyRunning && (live.length > 0 || previews.length > 0);
|
|
|
|
if (anyFailed) return <Pill label="Down" color="#c5392b" bg="#c5392b14" />;
|
|
if (anyRunning) return <Pill label="Live" color="#2e7d32" bg="#2e7d3210" />;
|
|
if (buildingNow) return <Pill label="Building" color="#3d5afe" bg="#3d5afe10" />;
|
|
|
|
return <Pill {...PRESETS[fallbackStage]} />;
|
|
}
|
|
|
|
// ──────────────────────────────────────────────────
|
|
|
|
const PRESETS: Record<
|
|
"discovery" | "architecture" | "building" | "active",
|
|
{ label: string; color: string; bg: string }
|
|
> = {
|
|
discovery: { label: "Defining", color: "#9a7b3a", bg: "#d4a04a14" },
|
|
architecture: { label: "Planning", color: "#3d5afe", bg: "#3d5afe10" },
|
|
building: { label: "Building", color: "#3d5afe", bg: "#3d5afe10" },
|
|
active: { label: "Live", color: "#2e7d32", bg: "#2e7d3210" },
|
|
};
|
|
|
|
function Pill({ label, color, bg }: { label: string; color: string; bg: string }) {
|
|
return (
|
|
<span style={{
|
|
display: "inline-flex", alignItems: "center", gap: 6,
|
|
padding: "4px 10px", borderRadius: 4,
|
|
fontSize: "0.7rem", fontWeight: 600, letterSpacing: "0.02em",
|
|
color, background: bg, whiteSpace: "nowrap",
|
|
}}>
|
|
<span style={{ width: 7, height: 7, borderRadius: "50%", background: color }} />
|
|
{label}
|
|
</span>
|
|
);
|
|
}
|