frierena personal git archive

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(-)
README.md+11
@@ -107,7 +107,7 @@ It deploys anywhere Next.js runs; on Vercel, set two things on the project:
107107- `FRIEREN_API_URL` — the public URL of your frieren backend (e.g. `https://git.example.com`)
108108- `FRIEREN_CLONE_URL` — optional; shown in clone commands when it differs from the API URL
109109
110Until 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.
111111
112112## What it deliberately isn't
113113
web/app/api/health/route.ts+310
@@ -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+}
032