Files
HealthLog/next.config.ts
T

320 lines
14 KiB
TypeScript

import type { NextConfig } from "next";
import bundleAnalyzer from "@next/bundle-analyzer";
import { version as PKG_VERSION } from "./package.json";
const nextConfig: NextConfig = {
output: "standalone",
poweredByHeader: false,
// v1.4.38.4 — expose the package.json version to the client bundle
// so the `<VersionPoller>` can compare the shell-baked version
// against the live `/api/version` response and trigger a self-heal
// (SW unregister + cache wipe + hard reload) when the server moves
// ahead of the running shell after a deploy. Without this the user
// had to discover "pull-to-refresh" themselves after every release.
env: {
NEXT_PUBLIC_APP_VERSION: PKG_VERSION,
},
// v1.4.33 IW2 — strip `console.*` calls from the production bundle
// (keep `console.error` + `console.warn` so the GlitchTip reporter
// and prod-side debug rails still surface). The Lighthouse audit
// flagged ~211 KiB of bundled JS as "unminified" — Turbopack already
// mangles + minifies the chunks, but the in-tree `console.log`
// breadcrumbs from the chart wiring + Coach SSE handlers carried
// hundreds of preserved string literals through to the client. The
// SWC compiler drops the calls + their literal-only arguments
// entirely in production.
compiler: {
removeConsole:
process.env.NODE_ENV === "production"
? { exclude: ["error", "warn"] }
: false,
},
// v1.4.33 IW2 — bfcache hygiene. `Permissions-Policy: unload=()`
// tells the browser the page does not need the `unload` event,
// which Chromium uses as a hint to admit the page to the
// back/forward cache on navigation away. Pair with the absence of
// any `beforeunload` / `unload` listener in our own code so the
// bfcache restore path stays clear. The other CSP-style security
// headers already live on the response via the standalone runtime;
// we add only the bfcache hint here.
//
// v1.4.34 IW-A — second rule layers the bfcache-friendly
// `Cache-Control` directive onto every authenticated HTML page
// response (the source negative-lookahead excludes `/api/*` and
// `/_next/*` so static assets keep their immutable caching and API
// routes keep their explicit headers). The framework default for
// pages that read cookies is `no-store, must-revalidate`, which
// Chromium counts as a hard bfcache breaker. `private, max-age=0,
// must-revalidate` keeps shared caches out (proxies, CDNs cannot
// store personal data), still forces revalidation on every
// navigation so session swaps detect on the wire, and is
// bfcache-eligible — back/forward navigation restores the page
// from memory instead of paying a full reload. See
// `src/lib/http/cache-headers.ts` for the typed constant reused by
// route handlers that opt into the same posture.
// v1.8.0 — the routed Insights sub-pages migrated from German to
// English slugs (`/insights/blutdruck` → `/insights/blood-pressure`,
// …). Every legacy German URL 301-redirects to its English target so
// bookmarks, the PWA's cached navigation, and any external link keep
// resolving. The redirect set is exhaustive and matches the rename
// table in `docs/adr/0001-insights-naming-convention.md`; the slug
// registry itself lives in `src/lib/insights/sub-page-metric.ts`.
// `bmi`, `hrv`, and `workouts` were already English and need no entry.
async redirects() {
const insightsSlugRenames: Array<[string, string]> = [
["blutdruck", "blood-pressure"],
["puls", "pulse"],
["sauerstoff", "oxygen"],
["koerpertemperatur", "body-temperature"],
["atemfrequenz", "respiratory-rate"],
["gewicht", "weight"],
["koerperwasser", "body-water"],
["knochenmasse", "bone-mass"],
["fettfreie-masse", "fat-free-mass"],
["fettmasse", "fat-mass"],
["muskelmasse", "muscle-mass"],
["viszeralfett", "visceral-fat"],
["magermasse", "lean-body-mass"],
["aktive-energie", "active-energy"],
["stockwerke", "flights-climbed"],
["gehstrecke", "walking-distance"],
["gangstabilitaet", "walking-steadiness"],
["gehpuls", "walking-heart-rate"],
["gangasymmetrie", "walking-asymmetry"],
["doppelstandphase", "double-support-time"],
["schrittlaenge", "step-length"],
["gehgeschwindigkeit", "walking-speed"],
["schlaf", "sleep"],
["ruhepuls", "resting-pulse"],
["pulswellengeschwindigkeit", "pulse-wave-velocity"],
["gefaessalter", "vascular-age"],
["laermbelastung", "environmental-audio"],
["kopfhoererpegel", "headphone-audio"],
["laermereignisse", "audio-events"],
["tageslicht", "daylight"],
["blutzucker", "blood-glucose"],
["hauttemperatur", "skin-temperature"],
["stimmung", "mood"],
["medikamente", "medications"],
];
return [
...insightsSlugRenames.flatMap(([from, to]) => [
{
source: `/insights/${from}`,
destination: `/insights/${to}`,
permanent: true,
},
// Preserve any nested path (e.g. a future `/insights/sleep/2026-05-31`).
{
source: `/insights/${from}/:path*`,
destination: `/insights/${to}/:path*`,
permanent: true,
},
]),
// v1.18.0 — the Coach moved out of the Insights surface to a
// standalone top-level page. The legacy `/insights/coach` URL
// 301-redirects to `/coach` so bookmarks, the PWA's cached
// navigation, and any pre-update push deep-link keep resolving.
{
source: "/insights/coach",
destination: "/coach",
permanent: true,
},
// v1.18.1 (D4) — Sources is a standalone `/settings/sources` route again
// (split back out of the Integrations sub-tabs), so the v1.18.0 redirect
// to `/settings/integrations` was dropped.
// v1.18.0 (S4) — the standalone "Erinnerungen" hub at
// `/settings/reminders` was a link-only page that duplicated the
// canonical editors. Reminder TYPES now live in Notifications, each
// gated on its module. 301-redirect so bookmarks, the PWA's cached
// navigation, and the old cross-links keep resolving.
{
source: "/settings/reminders",
destination: "/settings/notifications",
permanent: true,
},
// v1.25.7 — delivery channels live under Settings → Integrationen (they
// are delivery providers, the same family as the connected services).
// The channels content sits under the `#channels` anchor there, so the
// old URL 301-redirects to it. The per-channel anchors (`#telegram`,
// `#ntfy`, …) inside the panel keep resolving.
{
source: "/settings/channels",
destination: "/settings/integrations#channels",
permanent: true,
},
// v1.25.11 (#148) — "Darstellung" (`/settings/layout`) is a HUB that lists
// each module's view/sort/order surface; every module now lives on its own
// subpage at `/settings/layout/<module>`. The legacy per-module routes
// 301-redirect to the matching SUBPAGE (no longer an in-page anchor) so
// bookmarks, the PWA's cached navigation, and the per-module page-header
// cogs keep resolving. `dashboard` + `insights` join the redirect set so
// all seven modules share one canonical URL shape.
{
source: "/settings/dashboard",
destination: "/settings/layout/dashboard",
permanent: true,
},
{
source: "/settings/insights",
destination: "/settings/layout/insights",
permanent: true,
},
{
source: "/settings/medications",
destination: "/settings/layout/medications",
permanent: true,
},
{
source: "/settings/mood",
destination: "/settings/layout/mood",
permanent: true,
},
{
source: "/settings/labs",
destination: "/settings/layout/labs",
permanent: true,
},
{
source: "/settings/illness",
destination: "/settings/layout/illness",
permanent: true,
},
{
source: "/settings/vorsorge",
destination: "/settings/layout/vorsorge",
permanent: true,
},
// v1.25.7 — clinician share links fold into the Gesundheitsakte section
// as a labelled "Sharing" group; the old standalone route 301-redirects
// to the anchor there.
{
source: "/settings/sharing",
destination: "/settings/gesundheitsakte#sharing",
permanent: true,
},
// v1.22.0 — the preventive-care surface moved from the German slug
// `/vorsorge` to the English `/checkups` (the feature reads "Checkups"
// in every non-German locale now). 301-redirect so bookmarks, the PWA's
// cached navigation, and any push deep-link keep resolving. The
// persisted internal id stays `vorsorge`; only the URL changes.
{
source: "/vorsorge",
destination: "/checkups",
permanent: true,
},
{
source: "/vorsorge/:path*",
destination: "/checkups/:path*",
permanent: true,
},
];
},
async headers() {
return [
{
source: "/:path*",
headers: [{ key: "Permissions-Policy", value: "unload=()" }],
},
{
source: "/((?!api|_next).*)",
headers: [
{
key: "Cache-Control",
value: "private, max-age=0, must-revalidate",
},
],
},
];
},
serverExternalPackages: [
"@prisma/client",
"pg-boss",
"pg",
// Document PDF handling MUST run the real, un-bundled modules. When
// Turbopack bundles pdfjs-dist into the server chunks, its runtime
// `require('@napi-rs/canvas')` and its NodeCanvasFactory render path break
// in the standalone image (a scanned PDF fails to rasterize with a bare
// `Error`, so the read falls back to "PDF scanning needs a Claude vision
// provider"), while the identical un-bundled module rasterizes the same
// document fine. Externalise both so the server loads the real files from
// node_modules; the Dockerfile hoists them to the top level so the runtime
// `import()` resolves in the standalone tree, and outputFileTracingIncludes
// still ships pdfjs's wasm decoders.
"pdfjs-dist",
"@napi-rs/canvas",
],
// v1.4.25 Fix-G — `src/lib/ai/prompts/safety-contracts.ts` reads its
// sibling YAML files at runtime via `__dirname + readFileSync`. The
// standalone bundler ships the JS but not the YAML, so the build
// crashed during page-data collection with ENOENT once the Turbopack
// chunking-error layer was cleared. Telling Next to trace the YAML
// files explicitly keeps them in the runtime image alongside the
// bundled module.
outputFileTracingIncludes: {
"*": [
"./src/lib/ai/prompts/safety-contracts.*.yaml",
// Document AI local text extraction runs `pdf-parse` (→ pdfjs-dist)
// server-side in the content-index job — the provider-free fallback that
// keeps content search working with no AI configured. Both are pure JS +
// wasm; there is NO native `.node` on the text-layer path (`@napi-rs/canvas`
// is only pulled for image rendering, which `getText()` never invokes).
// The standalone tracer can miss pdfjs-dist's wasm decoders (they resolve
// via `new URL(..., import.meta.url)`), so pin both packages into the
// runtime image explicitly. pnpm keeps pdfjs-dist unhoisted under `.pnpm`.
"./node_modules/.pnpm/pdf-parse@*/node_modules/pdf-parse/**",
"./node_modules/.pnpm/pdfjs-dist@*/node_modules/pdfjs-dist/**",
// Document AI rasterization (`src/lib/documents/rasterize-pdf.ts`) renders
// a scanned/image-only PDF page to a JPEG via `@napi-rs/canvas` so an
// image-only-wire provider (codex/OAuth) can read it — the ambient
// auto-read path. `@napi-rs/canvas` loads a native `.node` binary that the
// Turbopack NFT tracer resolves dynamically and therefore misses, so pin
// the JS loader AND the two musl prebuilts (the Alpine runtime image is
// amd64 + arm64) into the standalone image explicitly. pnpm keeps them
// unhoisted under `.pnpm`. No cairo/pango apk is needed — the prebuilt
// `.node` is self-contained.
"./node_modules/.pnpm/@napi-rs+canvas@*/node_modules/@napi-rs/canvas/**",
"./node_modules/.pnpm/@napi-rs+canvas-linux-x64-musl@*/node_modules/@napi-rs/canvas-linux-x64-musl/**",
"./node_modules/.pnpm/@napi-rs+canvas-linux-arm64-musl@*/node_modules/@napi-rs/canvas-linux-arm64-musl/**",
],
},
// v1.4.34 IW-A — silence the Turbopack NFT trace warnings emitted
// during `next build`. The tracer follows the `MAXMIND_LICENSE_KEY`
// env access in `src/lib/geo.ts` back into the config file, then
// emits "cannot be traced" warnings for paths it tried to walk
// (next.config.ts → mood-entries/bulk route, etc.). The standalone
// bundle is controlled by `output: "standalone"` above; this exclude
// only narrows trace reporting and has no runtime effect.
outputFileTracingExcludes: {
"*": ["./next.config.ts"],
},
// Next 16 caps the request body that passes THROUGH middleware (our
// `src/proxy.ts`) at ~10MB by default, silently truncating larger bodies
// before the route handler sees them. The Apple Health `export.zip` importer
// (`/api/import/apple-health-export`) streams multi-MB archives to disk; the
// 10MB cap truncated them so the ZIP end-of-central-directory was lost and
// the parser failed (GitHub #281). Raise the ceiling so real exports pass
// intact. The value is a ceiling, not a buffer — only the actual upload size
// is held — so this is safe; very large exports also want a matching
// reverse-proxy body limit (see docs/self-hosting/reverse-proxy.md).
experimental: {
optimizePackageImports: ["recharts", "lucide-react"],
middlewareClientMaxBodySize: "512mb",
},
};
/**
* v1.4.28 R3d — opt-in bundle analyzer behind `ANALYZE=1`.
*
* `pnpm analyze` (defined in `package.json`) sets the env var and
* runs `next build`; the analyzer writes static HTML reports to
* `.next/analyze/*.html`. The wrapper is a no-op when the env var is
* unset so the regular `pnpm build` pipeline pays nothing.
*/
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === "1",
});
export default withBundleAnalyzer(nextConfig);