91 lines
2.3 KiB
TypeScript
91 lines
2.3 KiB
TypeScript
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<string, ProjectHealthCheck>;
|
|
}
|
|
|
|
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<HealthResponse>;
|
|
|
|
export async function createProjectHealthSnapshot(
|
|
projects: ProjectDemo[],
|
|
fetcher: HealthFetcher = (url, init) => fetch(url, init),
|
|
now = new Date(),
|
|
): Promise<ProjectHealthSnapshot> {
|
|
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) };
|
|
}
|