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
+65 -19
View File
@@ -1,3 +1,4 @@
import type { Metadata } from "next";
import { BackToCalendarLink } from "@/components/BackToCalendarLink";
import { notFound } from "next/navigation";
import { PARK_MAP } from "@/lib/parks";
@@ -6,14 +7,45 @@ import { LiveRidePanel } from "@/components/LiveRidePanel";
import type { RideStatus, RidesFetchResult } from "@/lib/scrapers/sixflags";
import type { LiveRidesResult } from "@/lib/scrapers/queuetimes";
import { getTodayLocal } from "@/lib/env";
const BACKEND_URL = process.env.BACKEND_URL ?? "http://localhost:3001";
import { apiFetch } from "@/lib/api";
import type { DayData } from "@/lib/types";
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ month?: string }>;
}
interface CalendarMonthResponse {
parkId: string;
year: number;
month: number;
monthData: Record<string, DayData>;
today: string;
}
interface RidesResponse {
parkId: string;
today: string;
parkOpenToday: boolean;
withinWindow: boolean;
isWeatherDelay: boolean;
liveRides: LiveRidesResult | null;
scheduleFallback: RidesFetchResult | null;
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { id } = await params;
const park = PARK_MAP.get(id);
if (!park) return { title: "Park not found | Thoosie Calendar" };
const title = `${park.name} | Thoosie Calendar`;
const description = `Operating hours and live ride status for ${park.name} (${park.location.city}, ${park.location.state}).`;
return {
title,
description,
openGraph: { title, description },
};
}
function parseMonthParam(param: string | undefined): string {
if (param && /^\d{4}-\d{2}$/.test(param)) {
const [y, m] = param.split("-").map(Number);
@@ -35,26 +67,25 @@ export default async function ParkPage({ params, searchParams }: PageProps) {
const [year, month] = monthStr.split("-").map(Number);
const [calendarData, ridesData] = await Promise.all([
fetch(`${BACKEND_URL}/api/calendar/${id}/month?month=${monthStr}`, {
next: { revalidate: 300 },
}).then((r) => r.json()),
fetch(`${BACKEND_URL}/api/parks/${id}/rides`, {
next: { revalidate: 60 },
}).then((r) => r.json()),
apiFetch<CalendarMonthResponse>(
`/api/calendar/${id}/month?month=${monthStr}`,
{ revalidate: 300 },
),
apiFetch<RidesResponse>(
`/api/parks/${id}/rides`,
{ revalidate: 60 },
),
]);
if (!calendarData) {
return <DataUnavailable parkName={park.name} />;
}
const { monthData, today } = calendarData;
const {
parkOpenToday,
isWeatherDelay,
liveRides,
scheduleFallback: ridesResult,
}: {
parkOpenToday: boolean;
isWeatherDelay: boolean;
liveRides: LiveRidesResult | null;
scheduleFallback: RidesFetchResult | null;
} = ridesData;
const parkOpenToday = ridesData?.parkOpenToday ?? false;
const isWeatherDelay = ridesData?.isWeatherDelay ?? false;
const liveRides = ridesData?.liveRides ?? null;
const ridesResult = ridesData?.scheduleFallback ?? null;
return (
<div style={{ minHeight: "100vh", background: "var(--color-bg)" }}>
@@ -355,3 +386,18 @@ function Callout({ children }: { children: React.ReactNode }) {
</div>
);
}
function DataUnavailable({ parkName }: { parkName: string }) {
return (
<main style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24, background: "var(--color-bg)" }}>
<div style={{ maxWidth: 480, textAlign: "center" }}>
<h1 style={{ fontSize: "1.2rem", fontWeight: 700, color: "var(--color-text)", marginBottom: 12 }}>
{parkName} data is unavailable
</h1>
<p style={{ color: "var(--color-text-muted)", lineHeight: 1.6, fontSize: "0.9rem" }}>
We could not reach the backend. Refresh in a moment.
</p>
</div>
</main>
);
}