mission-control/lib/data/gantt-api.ts
OpenClaw Bot 54038373fc updated docker
Signed-off-by: OpenClaw Bot <ai-agent@topdoglabs.com>
2026-02-27 17:29:57 -06:00

138 lines
3.9 KiB
TypeScript

const DEFAULT_GANTT_API_BASE_URL = "http://localhost:3000/api";
const DEFAULT_REVALIDATE_SECONDS = 15;
let hasWarnedDefaultBaseUrl = false;
type CacheEntry = {
expiresAt: number;
value: unknown;
promise?: Promise<unknown>;
};
const responseCache = new Map<string, CacheEntry>();
function normalizeBaseUrl(url: string): string {
return url.replace(/\/+$/, "");
}
function deriveApiBaseUrlFromGanttBoardUrl(): string | null {
const ganttBoardUrl = process.env.NEXT_PUBLIC_GANTT_BOARD_URL;
if (!ganttBoardUrl) return null;
try {
const parsed = new URL(ganttBoardUrl);
const pathname = parsed.pathname.replace(/\/+$/, "");
parsed.pathname = `${pathname}/api`;
parsed.search = "";
parsed.hash = "";
return normalizeBaseUrl(parsed.toString());
} catch {
return null;
}
}
export function getGanttApiBaseUrl(): string {
const derivedFromGanttBoardUrl = deriveApiBaseUrlFromGanttBoardUrl();
const configured =
process.env.GANTT_API_BASE_URL ||
process.env.NEXT_PUBLIC_GANTT_API_BASE_URL ||
derivedFromGanttBoardUrl ||
DEFAULT_GANTT_API_BASE_URL;
if (
configured === DEFAULT_GANTT_API_BASE_URL &&
process.env.NODE_ENV === "production" &&
!hasWarnedDefaultBaseUrl
) {
hasWarnedDefaultBaseUrl = true;
console.warn(
"[gantt-api] Using default base URL http://localhost:3000/api. Set GANTT_API_BASE_URL or NEXT_PUBLIC_GANTT_API_BASE_URL for deployed environments.",
);
}
return normalizeBaseUrl(configured);
}
function getRevalidateSeconds(): number {
const raw = Number(process.env.GANTT_API_REVALIDATE_SECONDS || String(DEFAULT_REVALIDATE_SECONDS));
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_REVALIDATE_SECONDS;
}
function buildAuthHeaders(): HeadersInit {
const headers: Record<string, string> = {};
if (process.env.GANTT_API_BEARER_TOKEN) {
headers.Authorization = `Bearer ${process.env.GANTT_API_BEARER_TOKEN}`;
}
if (process.env.GANTT_API_COOKIE) {
headers.Cookie = process.env.GANTT_API_COOKIE;
}
return headers;
}
export async function fetchGanttApi<T>(endpoint: string): Promise<T> {
const baseUrl = getGanttApiBaseUrl();
const revalidateSeconds = getRevalidateSeconds();
const cacheKey = `${baseUrl}${endpoint}`;
const now = Date.now();
const cached = responseCache.get(cacheKey);
if (cached && cached.expiresAt > now) {
if (cached.promise) {
return (await cached.promise) as T;
}
return cached.value as T;
}
const requestPromise = (async () => {
const response = await fetch(cacheKey, {
method: "GET",
cache: "no-store",
headers: {
Accept: "application/json",
...buildAuthHeaders(),
},
});
const contentType = response.headers.get("content-type") || "";
const isJson = contentType.includes("application/json");
const payload = (isJson ? await response.json().catch(() => null) : null) as
| { error?: string; message?: string }
| null;
const nonJsonBody = !isJson ? await response.text().catch(() => "") : "";
if (!response.ok) {
const details =
payload?.error ||
payload?.message ||
(nonJsonBody ? nonJsonBody.replace(/\s+/g, " ").slice(0, 200) : response.statusText);
throw new Error(`gantt-board API request failed (${response.status} ${endpoint}): ${details}`);
}
if (!isJson) {
throw new Error(`gantt-board API request failed (${response.status} ${endpoint}): expected JSON response`);
}
return payload as T;
})();
responseCache.set(cacheKey, {
expiresAt: now + revalidateSeconds * 1000,
value: null,
promise: requestPromise as Promise<unknown>,
});
try {
const result = await requestPromise;
responseCache.set(cacheKey, {
expiresAt: Date.now() + revalidateSeconds * 1000,
value: result as unknown,
});
return result;
} catch (error) {
responseCache.delete(cacheKey);
throw error;
}
}