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
+38
View File
@@ -0,0 +1,38 @@
"use client";
import { useEffect } from "react";
export default function RootError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
useEffect(() => {
console.error(error);
}, [error]);
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.4rem", fontWeight: 700, color: "var(--color-text)", marginBottom: 12 }}>
Something went wrong
</h1>
<p style={{ color: "var(--color-text-muted)", lineHeight: 1.6, fontSize: "0.9rem", marginBottom: 20 }}>
An unexpected error broke this page. Try again in a moment.
</p>
<button
type="button"
onClick={reset}
style={{
padding: "8px 18px",
background: "var(--color-text)",
color: "var(--color-bg)",
border: "none",
borderRadius: 6,
fontSize: "0.85rem",
fontWeight: 600,
cursor: "pointer",
}}
>
Try again
</button>
</div>
</main>
);
}
+10 -5
View File
@@ -2,6 +2,9 @@ import type { Metadata } from "next";
import Script from "next/script";
import "./globals.css";
const PLAUSIBLE_SRC = process.env.NEXT_PUBLIC_PLAUSIBLE_SRC;
const PLAUSIBLE_WEBSITE_ID = process.env.NEXT_PUBLIC_PLAUSIBLE_WEBSITE_ID;
export const metadata: Metadata = {
title: "Thoosie Calendar",
description: "Theme park operating hours and live ride status at a glance",
@@ -12,11 +15,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<html lang="en">
<body>
{children}
<Script
src="https://tracking.thewrightserver.net/script.js"
data-website-id="a0d0582a-9bd0-4c0d-8e3c-3e6fcc99ec9a"
strategy="afterInteractive"
/>
{PLAUSIBLE_SRC && PLAUSIBLE_WEBSITE_ID ? (
<Script
src={PLAUSIBLE_SRC}
data-website-id={PLAUSIBLE_WEBSITE_ID}
strategy="afterInteractive"
/>
) : null}
</body>
</html>
);
+31
View File
@@ -0,0 +1,31 @@
import Link from "next/link";
export default function NotFound() {
return (
<main style={{ minHeight: "100vh", display: "grid", placeItems: "center", padding: 24, background: "var(--color-bg)" }}>
<div style={{ maxWidth: 480, textAlign: "center" }}>
<div style={{ fontSize: "2.5rem", fontWeight: 700, color: "var(--color-text)", marginBottom: 8 }}>404</div>
<h1 style={{ fontSize: "1.1rem", fontWeight: 600, color: "var(--color-text)", marginBottom: 12 }}>
Page not found
</h1>
<p style={{ color: "var(--color-text-muted)", lineHeight: 1.6, fontSize: "0.9rem", marginBottom: 20 }}>
We couldn&apos;t find what you were looking for.
</p>
<Link
href="/"
style={{
padding: "8px 18px",
background: "var(--color-text)",
color: "var(--color-bg)",
borderRadius: 6,
fontSize: "0.85rem",
fontWeight: 600,
textDecoration: "none",
}}
>
Back to the calendar
</Link>
</div>
</main>
);
}
+22 -5
View File
@@ -1,8 +1,10 @@
import type { ComponentProps } from "react";
import { cookies } from "next/headers";
import { HomePageClient } from "@/components/HomePageClient";
import { getTodayLocal, formatDateLocal } from "@/lib/env";
import { apiFetch } from "@/lib/api";
const BACKEND_URL = process.env.BACKEND_URL ?? "http://localhost:3001";
type WeekData = ComponentProps<typeof HomePageClient>;
const WEEK_COOKIE = "tcWeek";
@@ -24,10 +26,25 @@ export default async function HomePage() {
const saved = (await cookies()).get(WEEK_COOKIE)?.value;
const weekStart = getWeekStart(saved);
const data = await fetch(
`${BACKEND_URL}/api/calendar/week?start=${weekStart}`,
{ next: { revalidate: 120 } },
).then((r) => r.json());
const data = await apiFetch<WeekData>(
`/api/calendar/week?start=${weekStart}`,
{ revalidate: 120 },
);
if (!data) {
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 }}>
Calendar 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>
);
}
return <HomePageClient {...data} />;
}
+56
View File
@@ -0,0 +1,56 @@
"use client";
import { useEffect } from "react";
import Link from "next/link";
export default function ParkError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
useEffect(() => {
console.error(error);
}, [error]);
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 }}>
Could not load this park
</h1>
<p style={{ color: "var(--color-text-muted)", lineHeight: 1.6, fontSize: "0.9rem", marginBottom: 20 }}>
Try again in a moment, or head back to the calendar.
</p>
<div style={{ display: "flex", justifyContent: "center", gap: 10 }}>
<button
type="button"
onClick={reset}
style={{
padding: "8px 18px",
background: "var(--color-text)",
color: "var(--color-bg)",
border: "none",
borderRadius: 6,
fontSize: "0.85rem",
fontWeight: 600,
cursor: "pointer",
}}
>
Try again
</button>
<Link
href="/"
style={{
padding: "8px 18px",
background: "var(--color-surface)",
color: "var(--color-text)",
border: "1px solid var(--color-border)",
borderRadius: 6,
fontSize: "0.85rem",
fontWeight: 600,
textDecoration: "none",
}}
>
Back to calendar
</Link>
</div>
</div>
</main>
);
}
+35
View File
@@ -0,0 +1,35 @@
export default function ParkLoading() {
return (
<div style={{ minHeight: "100vh", background: "var(--color-bg)" }}>
<header style={{
position: "sticky",
top: 0,
zIndex: 20,
background: "var(--color-bg)",
borderBottom: "1px solid var(--color-border)",
padding: "12px 24px",
display: "flex",
alignItems: "center",
gap: 16,
}}>
<div className="skeleton" style={{ width: 120, height: 18, borderRadius: 4 }} />
<div style={{ width: 1, height: 16, background: "var(--color-border)" }} />
<div className="skeleton" style={{ width: 180, height: 16, borderRadius: 4 }} />
</header>
<main style={{ padding: "24px 32px", maxWidth: 1280, margin: "0 auto", display: "flex", flexDirection: "column", gap: 40 }}>
<section>
<div className="skeleton" style={{ width: "100%", height: 320, borderRadius: 8 }} />
</section>
<section>
<div className="skeleton" style={{ width: 180, height: 16, borderRadius: 4, marginBottom: 16 }} />
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: 6 }}>
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="skeleton" style={{ height: 36, borderRadius: 8 }} />
))}
</div>
</section>
</main>
</div>
);
}
+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>
);
}
+38
View File
@@ -0,0 +1,38 @@
"use client";
import { useEffect } from "react";
export default function RideError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
useEffect(() => {
console.error(error);
}, [error]);
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 }}>
Could not load ride history
</h1>
<p style={{ color: "var(--color-text-muted)", lineHeight: 1.6, fontSize: "0.9rem", marginBottom: 20 }}>
The backend is unreachable. Try again in a moment.
</p>
<button
type="button"
onClick={reset}
style={{
padding: "8px 18px",
background: "var(--color-text)",
color: "var(--color-bg)",
border: "none",
borderRadius: 6,
fontSize: "0.85rem",
fontWeight: 600,
cursor: "pointer",
}}
>
Try again
</button>
</div>
</main>
);
}
+17
View File
@@ -0,0 +1,17 @@
export default function RideLoading() {
return (
<div style={{ minHeight: "100vh", background: "var(--color-bg)", padding: "32px 24px" }}>
<div style={{ maxWidth: 960, margin: "0 auto" }}>
<div className="skeleton" style={{ width: 160, height: 14, borderRadius: 4, marginBottom: 16 }} />
<div className="skeleton" style={{ width: 320, height: 28, borderRadius: 6, marginBottom: 24 }} />
<div className="skeleton" style={{ width: 100, height: 24, borderRadius: 999, marginBottom: 24 }} />
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="skeleton" style={{ width: 80, height: 32, borderRadius: 6 }} />
))}
</div>
<div className="skeleton" style={{ width: "100%", height: 280, borderRadius: 8 }} />
</div>
</div>
);
}
+30 -6
View File
@@ -1,11 +1,11 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { PARK_MAP } from "@/lib/parks";
import UptimePill from "@/components/charts/UptimePill";
import WaitTimeTodayChart from "@/components/charts/WaitTimeTodayChart";
import WeeklyStatsChart from "@/components/charts/WeeklyStatsChart";
const BACKEND_URL = process.env.BACKEND_URL ?? "http://localhost:3001";
import { getBackendUrl } from "@/lib/api";
type Tab = "today" | "7d" | "30d";
@@ -62,6 +62,20 @@ function parseTab(raw: string | undefined): Tab {
return "today";
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { id, slug } = await params;
const park = PARK_MAP.get(id);
if (!park) return { title: "Ride not found | Thoosie Calendar" };
const rideName = decodeURIComponent(slug).replace(/-/g, " ");
const title = `${rideName}${park.shortName} | Thoosie Calendar`;
const description = `Live wait time and uptime history for ${rideName} at ${park.name}.`;
return {
title,
description,
openGraph: { title, description },
};
}
export default async function RideDetailPage({ params, searchParams }: PageProps) {
const { id, slug } = await params;
const { tab: tabParam } = await searchParams;
@@ -71,9 +85,14 @@ export default async function RideDetailPage({ params, searchParams }: PageProps
const tab = parseTab(tabParam);
const res = await fetch(`${BACKEND_URL}/api/parks/${id}/rides/${slug}`, {
next: { revalidate: 60 },
});
let res: Response;
try {
res = await fetch(`${getBackendUrl()}/api/parks/${id}/rides/${slug}`, {
next: { revalidate: 60 },
});
} catch {
return <ErrorState parkId={id} parkName={park.name} />;
}
if (res.status === 404) {
return <NoHistoryYet parkId={id} parkName={park.name} slug={slug} />;
@@ -83,7 +102,12 @@ export default async function RideDetailPage({ params, searchParams }: PageProps
return <ErrorState parkId={id} parkName={park.name} />;
}
const data: ApiResponse = await res.json();
let data: ApiResponse;
try {
data = (await res.json()) as ApiResponse;
} catch {
return <ErrorState parkId={id} parkName={park.name} />;
}
const { ride, live, today, last7d, last30d, coverage } = data;
const last30dUptime = last30d.length