This repository has been archived on 2026-06-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files

182 lines
4.3 KiB
TypeScript

"use client";
/**
* Single-fetch anatomy hook shared by the Product / Hosting tabs.
* Hardened against silent failure: 10s timeout, error surfacing, and
* graceful unmount.
*
* Uses SWR to deduplicate requests across components (ProjectStagePill,
* ProjectHeaderUrls, Hosting page) so they share a single network request
* instead of hammering the API independently.
*/
import useSWR from "swr";
import { useCallback } from "react";
export interface Anatomy {
project: {
id: string;
name: string;
gitea?: string;
coolifyProjectUuid?: string;
};
codebasesReason?: "no_repo" | "empty_repo";
product: {
codebases: Array<{
id: string;
label: string;
path: string;
hint?: string;
}>;
images: Array<{
uuid: string;
name: string;
image: string;
version: string;
serviceType?: string;
status?: string;
}>;
};
hosting: {
live: Array<{
uuid: string;
name: string;
source: "repo" | "image";
sourceLabel: string;
status: string;
fqdn?: string;
domains: string[];
branch?: string;
buildPack?: string;
lastBuild?: { status: string; finishedAt?: string; commit?: string };
inFlightBuild?: { status: string; finishedAt?: string; commit?: string };
}>;
previews: Array<{
id: string;
name: string;
command?: string;
port: number;
url: string;
state: string;
startedAt: string;
}>;
};
infrastructure: {
databases: Array<{
uuid: string;
name: string;
type: string;
status: string;
isPublic: boolean;
publicPort?: number;
internalAddress?: string;
consumerEnvKey: string;
}>;
providers: Array<{
id: string;
category: "auth" | "email" | "payments" | "llm" | "storage";
vendor: string;
attachments: Array<{
resourceUuid: string;
resourceName: string;
resourceKind: "app" | "service";
keys: string[];
}>;
}>;
bundledStorage: {
status: "ready" | "pending" | "partial" | "error" | "unprovisioned";
bucketName?: string;
hmacAccessId?: string;
region?: string;
errorMessage?: string;
};
secrets: {
total: number;
byResource: Array<{
resourceUuid: string;
resourceName: string;
resourceKind: "app" | "service";
count: number;
keys: string[];
}>;
};
};
}
export interface UseAnatomyResult {
anatomy: Anatomy | null;
loading: boolean;
error: string | null;
reload: () => void;
}
export interface UseAnatomyOptions {
/** When set, re-fetch anatomy every N ms while the component is
* mounted. Used by the project-header status pill so it surfaces
* Coolify build state transitions live (e.g. queued → in_progress
* → success) without the user having to refresh. Pass undefined or
* 0 to disable polling. */
pollMs?: number;
}
const fetcher = async (url: string) => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10_000);
try {
const res = await fetch(url, {
credentials: "include",
signal: controller.signal,
cache: "no-store",
});
let body: unknown = {};
try {
body = await res.json();
} catch {
/* keep {} */
}
if (!res.ok) {
const msg =
(body as { error?: string }).error ||
`HTTP ${res.status} ${res.statusText}`.trim();
throw new Error(msg);
}
return body as Anatomy;
} catch (err: any) {
if (err.name === "AbortError") {
throw new Error("Request timed out after 10s.");
}
throw err;
} finally {
clearTimeout(timeout);
}
};
export function useAnatomy(
projectId: string,
options: UseAnatomyOptions = {},
): UseAnatomyResult {
const pollMs = options.pollMs && options.pollMs > 0 ? options.pollMs : 0;
const { data, error, isLoading, mutate } = useSWR<Anatomy, Error>(
projectId ? `/api/projects/${projectId}/anatomy` : null,
fetcher,
{
refreshInterval: pollMs,
dedupingInterval: 5000,
revalidateOnFocus: false,
revalidateOnReconnect: false,
},
);
const reload = useCallback(() => {
mutate();
}, [mutate]);
return {
anatomy: data ?? null,
loading: isLoading,
error: error?.message ?? null,
reload,
};
}