/** * Error + status pages. Every page here is a self-contained HTML document — * inline CSS only, no external stylesheet, no JavaScript (so it renders under the * strict CSP, even when the app's assets are what failed). Theme-aware via * `prefers-color-scheme`, styled in the WrNexus design language (ink-navy, * azure, a faint blueprint grid + glow). Development shows the stack trace; * production never leaks internal paths. */ import { escapeHtml } from "./security.ts"; export type Mode = "development" | "production"; interface ErrorPageOptions { status: number; /** Big display code, e.g. "404" / "500". */ code: string; title: string; message: string; /** Monospace eyebrow, e.g. "ERROR 404". */ eyebrow?: string; /** Optional dev-only detail (error name + stack), rendered in a code panel. */ detail?: { heading: string; body: string }; /** Show a "Back home" action (default true). */ home?: boolean; } /** Shared, self-contained, theme-aware error document. */ function errorDocument(o: ErrorPageOptions): string { const eyebrow = escapeHtml(o.eyebrow ?? `ERROR ${o.status}`); const title = escapeHtml(o.title); const message = escapeHtml(o.message); const detail = o.detail ? `
${escapeHtml(o.detail.heading)}
${escapeHtml(o.detail.body)}
` : ""; const home = o.home === false ? "" : `Back to home`; return ` ${title}

${eyebrow}

${title}

${message}

${home}
${detail}
`; } /** Common status → friendly copy, for a generic HTML status page. */ const STATUS_COPY: Record = { 400: { title: "Bad request", message: "The request couldn't be understood. Check the URL and try again.", }, 401: { title: "Sign in required", message: "You need to be signed in to view this page." }, 403: { title: "Access denied", message: "You don't have permission to view this page." }, 404: { title: "Page not found", message: "The page you're looking for doesn't exist or has moved.", }, 413: { title: "Too large", message: "The request was larger than the server allows." }, 429: { title: "Slow down", message: "You've made too many requests. Please wait a moment and try again.", }, 500: { title: "Something went wrong", message: "The server hit an unexpected error. Please try again in a moment.", }, 502: { title: "Bad gateway", message: "We couldn't reach an upstream service. Please try again shortly.", }, 503: { title: "Temporarily unavailable", message: "The service is down for a moment. Please try again shortly.", }, }; /** A beautiful, self-contained HTML page for any 4xx/5xx status. */ export function renderStatusPage(status: number): Response { const copy = STATUS_COPY[status] ?? { title: status >= 500 ? "Something went wrong" : "Something's not right", message: "An unexpected response was returned. Please try again.", }; return new Response( errorDocument({ status, code: String(status), title: copy.title, message: copy.message }), { status, headers: { "content-type": "text/html; charset=utf-8" } }, ); } /** Readable, styled development error page — includes the stack trace. */ export function renderDevError(err: unknown, status = 500): Response { const error = err instanceof Error ? err : new Error(String(err)); const name = error.name || "Error"; const message = error.message || "Unknown error"; return new Response( errorDocument({ status, code: String(status), eyebrow: "DEVELOPMENT ERROR", title: name, message, detail: { heading: `${name}: ${message}`, body: error.stack || "(no stack available)" }, }), { status, headers: { "content-type": "text/html; charset=utf-8" } }, ); } /** Generic production error page — no stack, no file paths. */ export function renderProdError(status = 500): Response { return renderStatusPage(status); } /** Pick the right error page for the current mode. */ export function renderError(err: unknown, mode: Mode): Response { return mode === "development" ? renderDevError(err) : renderProdError(); } /** Beautiful 404 page. */ export function renderNotFound(): Response { return renderStatusPage(404); }