Stop button fix: - Plumb AbortSignal end-to-end: callVibnChat → Gemini SDK (config.abortSignal) / OpenAI fetch → executeMcpTool (/api/mcp fetch) - Treat abort as clean user stop (not fatal error); partial reply persisted with '(stopped by user)' Classifier fix: - Add timeout/gateway/5xx/connection-error vocabulary to diagnose intent - Prevents 'I get a gateway timeout' from falling through to feature_build (40 rounds) and looping Prompt / agent behaviour: - Render verification is now scope-aware: small edits stop at green healthCheck; no browser_console/curl audit on healthy server - Sanitize stale '### Phase Checkpoint' walls from loaded history so old threads stop biasing new turns - Next.js dev command updated to --no-turbopack for container stability (per-route lazy compile caused cold-start 503s) - New public page prompt: agent checks middleware allowlist in the same turn - Scope discipline and QA-tool gating carried forward from prior session Code cleanup: - Remove duplicate AgentPhase declaration (TS2440) - Remove dead checkpoint emit branch and orphan 'checkpoint' phase value - Remove unused MAX_TOOL_ROUNDS constant Preview pane (build status): - 4-state machine: initial-load / building (with elapsed timer) / build-failed / not-running - pollMs 0 → 5 000ms so dev-server recovery and build completion auto-update without refresh - anatomy route + use-anatomy type: inFlightBuild gains createdAt for elapsed timer
192 lines
4.5 KiB
TypeScript
192 lines
4.5 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;
|
|
createdAt?: string;
|
|
finishedAt?: string;
|
|
commit?: string;
|
|
};
|
|
inFlightBuild?: {
|
|
status: string;
|
|
createdAt?: 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,
|
|
};
|
|
}
|