feat(ops): web + worker container healthchecks (#20)

docker-compose only health-checked db. Add:
- web: an unauthenticated GET /api/health (excluded from the auth middleware) +
  a compose healthcheck that hits it (node global fetch — no curl/wget in image).
- worker: a background daemon thread touches /tmp/worker.alive every ~15s
  (independent of the main loop, so it stays fresh through a long download) +
  a compose healthcheck on the file's mtime (unhealthy if >90s stale).

Now `docker compose ps` reports real health for all services.
worker 223 tests, web 154, tsc + build + compose config clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-14 13:28:26 +02:00
parent 7e5c083cef
commit eb51489ba9
5 changed files with 55 additions and 1 deletions
+10
View File
@@ -0,0 +1,10 @@
import { describe, it, expect } from "vitest";
import { GET } from "./route";
describe("GET /api/health", () => {
it("returns 200 ok", async () => {
const res = GET();
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ ok: true });
});
});
+5
View File
@@ -0,0 +1,5 @@
// Unauthenticated liveness endpoint for the docker-compose healthcheck. Excluded from the
// auth gate in middleware.ts.
export function GET() {
return Response.json({ ok: true });
}
+4 -1
View File
@@ -8,7 +8,10 @@ export async function middleware(req: NextRequest) {
if (!token) return NextResponse.next(); // auth disabled (no LYRA_PASSWORD)
const { pathname } = req.nextUrl;
if (pathname === "/login" || pathname === "/api/auth") return NextResponse.next();
// /api/health is the unauthenticated liveness probe for the compose healthcheck.
if (pathname === "/login" || pathname === "/api/auth" || pathname === "/api/health") {
return NextResponse.next();
}
const cookie = req.cookies.get(AUTH_COOKIE)?.value ?? "";
if (safeEqual(cookie, token)) return NextResponse.next();