frieren
a self-hosted git server in one binary — everyone reads, only the owner writes
feat(web): health endpoint reporting backend reachability
6277ae45c4d43aa302ccd42c07ee6963c19fe9b3
justin06lee · Aug 19, 2026, 01:20 AM (2h ago)
README.md | 2 +- web/app/api/health/route.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-)
| @@ -107,7 +107,7 @@ It deploys anywhere Next.js runs; on Vercel, set two things on the project: | |||
| 107 | 107 | - `FRIEREN_API_URL` — the public URL of your frieren backend (e.g. `https://git.example.com`) | |
| 108 | 108 | - `FRIEREN_CLONE_URL` — optional; shown in clone commands when it differs from the API URL | |
| 109 | 109 | ||
| 110 | − | Until the backend is reachable, the site renders a graceful "archive unreachable" state with setup instructions, and recovers on its own once the server answers. | |
| 110 | + | Until the backend is reachable, the site renders a graceful "archive unreachable" state with setup instructions, and recovers on its own once the server answers. `/api/health` on the deployed site reports whether it can reach the backend and how fast — the first place to look if the offline card ever shows. | |
| 111 | 111 | ||
| 112 | 112 | ## What it deliberately isn't | |
| 113 | 113 |
| @@ -0,0 +1,31 @@ | |||
| 1 | + | import { apiBase } from "@/lib/api"; | |
| 2 | + | ||
| 3 | + | // Reports whether this deployment can reach its backend — the first place to | |
| 4 | + | // look when the site shows the offline card. | |
| 5 | + | export const dynamic = "force-dynamic"; | |
| 6 | + | ||
| 7 | + | export async function GET() { | |
| 8 | + | const base = apiBase(); | |
| 9 | + | if (!base) { | |
| 10 | + | return Response.json({ configured: false, ok: false }); | |
| 11 | + | } | |
| 12 | + | const started = Date.now(); | |
| 13 | + | try { | |
| 14 | + | const res = await fetch(`${base}/api/repos`, { cache: "no-store" }); | |
| 15 | + | return Response.json({ | |
| 16 | + | configured: true, | |
| 17 | + | ok: res.ok, | |
| 18 | + | status: res.status, | |
| 19 | + | ms: Date.now() - started, | |
| 20 | + | }); | |
| 21 | + | } catch (e) { | |
| 22 | + | const cause = e instanceof Error ? (e.cause as { code?: string } | undefined) : undefined; | |
| 23 | + | return Response.json({ | |
| 24 | + | configured: true, | |
| 25 | + | ok: false, | |
| 26 | + | error: e instanceof Error ? e.message : String(e), | |
| 27 | + | code: cause?.code ?? null, | |
| 28 | + | ms: Date.now() - started, | |
| 29 | + | }); | |
| 30 | + | } | |
| 31 | + | } | |
| 0 | 32 |