diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..65c1943 --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# ── Frontend (web) ────────────────────────────────────────────────────────── +# Backend API base URL. Required in production — the frontend throws at +# startup if this is unset and NODE_ENV=production. +BACKEND_URL=http://localhost:3001 + +# Optional: Plausible analytics. Both must be set for the script to render. +# NEXT_PUBLIC_PLAUSIBLE_SRC=https://plausible.example.com/script.js +# NEXT_PUBLIC_PLAUSIBLE_WEBSITE_ID=your-website-id + +# ── Backend ───────────────────────────────────────────────────────────────── +# Port the Hono server listens on (default 3001). +PORT=3001 + +# IANA timezone used by node-cron schedules and operating-hour windows. +TZ=America/New_York + +# How long a park's schedule data is considered fresh before the tiered +# scraper re-fetches it (default 72). +PARK_HOURS_STALENESS_HOURS=72 + +# Per-IP request limit for the public API, per minute (default 60). +RATE_LIMIT_PER_MIN=60 diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 5dc4f49..abea687 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -6,8 +6,44 @@ on: - main jobs: + verify: + name: Lint, typecheck, test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install frontend deps + run: npm ci + + - name: Frontend lint + run: npm run lint + + - name: Frontend typecheck + run: npm run typecheck + + - name: Frontend tests + run: npm test + + - name: Install backend deps + run: npm ci + working-directory: backend + + - name: Backend typecheck + run: npm run typecheck + working-directory: backend + + - name: Backend tests + run: npm test + working-directory: backend + build-push: name: Build & Push + needs: verify runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index d772b82..8601431 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,9 @@ yarn-error.log* /backend/data/ parks.db +# debug script artifacts +/debug/ + # env files .env* !.env.example @@ -45,3 +48,4 @@ parks.db # typescript *.tsbuildinfo next-env.d.ts +.gstack/ diff --git a/Dockerfile b/Dockerfile index 5f2ed53..75e217e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,13 +6,25 @@ RUN npm ci COPY . . RUN npm run build -# ── backend-deps: backend node_modules (better-sqlite3 needs build tools) ──── -FROM node:22-bookworm-slim AS backend-deps +# ── backend-build: compile backend TypeScript to JS (better-sqlite3 build) ─── +FROM node:22-bookworm-slim AS backend-build RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && \ rm -rf /var/lib/apt/lists/* WORKDIR /app +COPY backend/package.json backend/package-lock.json* ./backend/ +COPY backend/tsconfig.json ./backend/ +RUN cd backend && npm ci +COPY backend/src ./backend/src +COPY lib ./lib +RUN cd backend && npm run build + +# ── backend-prod-deps: production-only node_modules (omits tsc/tsx) ────────── +FROM node:22-bookworm-slim AS backend-prod-deps +RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && \ + rm -rf /var/lib/apt/lists/* +WORKDIR /app/backend COPY backend/package.json backend/package-lock.json* ./ -RUN npm ci +RUN npm ci --omit=dev # ── web ────────────────────────────────────────────────────────────────────── # Minimal Next.js standalone runner. No database, no native modules. @@ -37,6 +49,7 @@ CMD ["node", "server.js"] # ── backend ────────────────────────────────────────────────────────────────── # Hono API server + node-cron scheduler. Owns the SQLite database exclusively. +# Runs compiled JS (no tsx/tsc at runtime). FROM node:22-bookworm-slim AS backend WORKDIR /app @@ -45,11 +58,9 @@ ENV NODE_ENV=production RUN addgroup --system --gid 1001 nodejs && \ adduser --system --uid 1001 nextjs -COPY --from=backend-deps --chown=nextjs:nodejs /app/node_modules ./backend/node_modules -COPY --chown=nextjs:nodejs backend/src ./backend/src +COPY --from=backend-prod-deps --chown=nextjs:nodejs /app/backend/node_modules ./backend/node_modules +COPY --from=backend-build --chown=nextjs:nodejs /app/backend/dist ./backend/dist COPY --chown=nextjs:nodejs backend/package.json ./backend/package.json -COPY --chown=nextjs:nodejs backend/tsconfig.json ./backend/tsconfig.json -COPY --chown=nextjs:nodejs lib ./lib RUN mkdir -p /app/backend/data && chown nextjs:nodejs /app/backend/data VOLUME ["/app/backend/data"] @@ -59,4 +70,4 @@ EXPOSE 3001 ENV PORT=3001 WORKDIR /app/backend -CMD ["npx", "tsx", "src/index.ts"] +CMD ["node", "dist/backend/src/index.js"] diff --git a/README.md b/README.md index f03512f..e50075e 100644 --- a/README.md +++ b/README.md @@ -135,18 +135,24 @@ Images are built and pushed automatically by CI on every push to `main`. ### Environment variables +See [`.env.example`](.env.example) for the full list and defaults. + **web:** | Variable | Default | Description | |----------|---------|-------------| -| `BACKEND_URL` | `http://backend:3001` | Backend API base URL (Docker internal networking) | +| `BACKEND_URL` | _(required in prod)_ | Backend API base URL. Throws at startup if unset when `NODE_ENV=production`. | +| `NEXT_PUBLIC_PLAUSIBLE_SRC` | — | Plausible script URL. Analytics only render when both this and the website ID are set. | +| `NEXT_PUBLIC_PLAUSIBLE_WEBSITE_ID` | — | Plausible website ID. | **backend:** | Variable | Default | Description | |----------|---------|-------------| -| `TZ` | `UTC` | Timezone for cron schedules (e.g. `America/New_York`) | -| `PARK_HOURS_STALENESS_HOURS` | `72` | Hours before park schedule data is re-fetched | +| `PORT` | `3001` | Port the Hono server listens on. | +| `TZ` | `UTC` | Timezone for cron schedules (e.g. `America/New_York`). | +| `PARK_HOURS_STALENESS_HOURS` | `72` | Hours before park schedule data is re-fetched. | +| `RATE_LIMIT_PER_MIN` | `60` | Per-IP request limit for the public API, per minute. | ### Updating diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 0000000..9c51f6b --- /dev/null +++ b/app/error.tsx @@ -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 ( +
+
+

+ Something went wrong +

+

+ An unexpected error broke this page. Try again in a moment. +

+ +
+
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx index 123edec..472371c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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 }) {children} -