Repository Public
1import { access, constants } from "node:fs/promises";2import { sql } from "@/lib/db";3import { env } from "@/lib/env";4import { redis } from "@/lib/queue";56export const dynamic = "force-dynamic";7/** GET /api/health — liveness of each dependency. 200 when the site can serve, 503 otherwise. */8export async function GET() {9 const checks: Record<string, { ok: boolean; detail?: string }> = {};10 const run = async (name: string, fn: () => Promise<string | void>) => { try { checks[name] = { ok: true, detail: (await fn()) || undefined }; } catch (e) { checks[name] = { ok: false, detail: (e as Error).message.slice(0, 120) }; } };11 await run("database", async () => { await sql`SELECT 1`; });12 await run("redis", async () => { await Promise.race([redis().ping(), new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 2000))]); });13 await run("assets", async () => { await access(env.assetRoot, constants.R_OK | constants.W_OK); });14 await run("worker", async () => {15 const [r] = await sql<{ at: string }[]>`SELECT value->>'at' AS at FROM settings WHERE key = 'worker_heartbeat'`;16 const age = r ? (Date.now() - new Date(r.at).getTime()) / 1000 : Infinity;17 if (age > 150) throw new Error(r ? `last heartbeat ${Math.round(age)} s ago` : "no heartbeat");18 return `heartbeat ${Math.round(age)} s ago`;19 });20 const serving = checks.database.ok && checks.assets.ok;21 return Response.json({ status: serving ? (Object.values(checks).every((c) => c.ok) ? "ok" : "degraded") : "down", checks, time: new Date().toISOString() }, { status: serving ? 200 : 503, headers: { "Cache-Control": "no-store" } });22}23