refactor: production-essentials hardening pass
Build and Deploy / Lint, typecheck, test (push) Successful in 30s
Build and Deploy / Build & Push (push) Successful in 1m39s

Backend: structured logger, env-validated config, graceful SIGTERM/SIGINT
shutdown, per-IP rate limiter, per-tier scheduler concurrency latch, error
context on previously-silent catches, compiled-JS Dockerfile stage.

Frontend: lib/api.ts consolidates BACKEND_URL with lazy production-required
check, root + per-segment error.tsx / not-found.tsx / loading.tsx,
generateMetadata on park and ride pages, graceful fallback when backend is
unreachable, Plausible script gated on env vars.

Infra: CI runs lint + typecheck + tests on both packages before docker build,
compose adds healthchecks, log rotation, and memory limits; .env.example
documents every variable.

Cleanup: removed empty app/api/parks/ dir and 0-byte root parks.db, moved
wait-times-urls.txt into docs/, dropped an `as any` cast.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 10:17:52 -04:00
parent 6447db3008
commit 5d9daee627
30 changed files with 860 additions and 126 deletions
+46
View File
@@ -0,0 +1,46 @@
/**
* Single source of truth for the backend URL used by Next.js server
* components. Defaults to localhost in development; throws at *request time*
* in production if BACKEND_URL isn't set so a misdeployed container fails
* with a clear message instead of silently pointing at localhost. The check
* is lazy so Next.js build-time page-data collection doesn't trip it.
*/
let warned = false;
export function getBackendUrl(): string {
const explicit = process.env.BACKEND_URL;
if (explicit) return explicit;
if (process.env.NODE_ENV === "production") {
throw new Error(
"BACKEND_URL env var is required in production. " +
"Set it to the backend service URL (e.g. http://backend:3001).",
);
}
if (!warned) {
warned = true;
console.warn("[lib/api] BACKEND_URL unset — defaulting to http://localhost:3001 (dev only)");
}
return "http://localhost:3001";
}
/**
* Fetch JSON from the backend with a default revalidate window. Returns
* `null` on network failure or non-2xx status — callers handle the null
* to render a graceful fallback instead of crashing the server render.
*/
export async function apiFetch<T>(
path: string,
options: { revalidate?: number } = {},
): Promise<T | null> {
const { revalidate = 60 } = options;
try {
const res = await fetch(`${getBackendUrl()}${path}`, {
next: { revalidate },
});
if (!res.ok) return null;
return (await res.json()) as T;
} catch {
return null;
}
}