export const PROJECT_HEALTH_MAX_AGE_MS = 15 * 60 * 1000; export interface ProjectHealthCheck { status: number | null; contentType: string | null; checkedAt: string; finalUrl?: string; latencyMs?: number; error?: string; } export interface ProjectHealthSnapshot { checkedAt: string; projects: Record; } export type DemoAvailability = "available" | "unavailable" | "unknown" | "none"; export function getDemoAvailability( demoUrl: string | null | undefined, check: ProjectHealthCheck | undefined, ): DemoAvailability { if (!demoUrl) return "none"; if (!check) return "unknown"; if (check.error || check.status === null) return "unknown"; return check.status === 200 && check.contentType?.split(";", 1)[0].trim().toLowerCase() === "text/html" ? "available" : "unavailable"; } interface ProjectDemo { id: string; demoUrl: string; } interface HealthResponse { status: number; url: string; headers: { get(name: string): string | null }; body?: { cancel?: () => unknown } | null; } type HealthFetcher = ( url: string, init: RequestInit, ) => Promise; export async function createProjectHealthSnapshot( projects: ProjectDemo[], fetcher: HealthFetcher = (url, init) => fetch(url, init), now = new Date(), ): Promise { const checkedAt = now.toISOString(); const checks = await Promise.all( projects.map(async (project) => { try { const response = await fetcher(project.demoUrl, { redirect: "follow", signal: AbortSignal.timeout(10_000), headers: { "user-agent": "evandro-project-health/1.0" }, }); await response.body?.cancel?.(); return [ project.id, { status: response.status, contentType: response.headers.get("content-type"), checkedAt, finalUrl: response.url, }, ] as const; } catch (error) { return [ project.id, { status: null, contentType: null, checkedAt, error: error instanceof Error ? error.message : "Health check failed", }, ] as const; } }), ); return { checkedAt, projects: Object.fromEntries(checks) }; }