mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
Next reads the body of every non-GET request the proxy matches into memory before the route runs. The ceiling was 512 MB so the Apple Health importer could receive whole archives, which let any anonymous POST to any API path make the server hold that much. The ceiling is now 1 MB (proxyClientMaxBodySize). The fifteen routes whose real bodies are larger are left out of the proxy matcher and read their own body after authenticating; apiHandler gives them the proxy's refusals (worker-only, demo mode), security headers and a CSP. safeJson now counts bytes while it reads and stops at its cap instead of reading everything first, the CSV and dose-history imports do the same, and a multipart backup upload is counted while it is read. A test compiles the matcher the way Next does and checks it skips exactly the listed routes and every route with a larger body is listed. Local lab OCR runs its WebAssembly engine in a worker served from /tesseract/. Those responses, and the /labs and /documents pages that start the OCR, now carry 'wasm-unsafe-eval' in script-src; no other route does.
327 lines
15 KiB
TypeScript
327 lines
15 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",
|
|
// `next build` type-checks the whole tsconfig program since 16.3, which
|
|
// reaches the unit tests under `src/**/__tests__` and their fixtures under
|
|
// `tests/`. The image build context leaves `tests/` and `e2e/` out on
|
|
// purpose, so the build reads a tsconfig without them; `pnpm typecheck`
|
|
// still covers the full program.
|
|
typescript: { tsconfigPath: "tsconfig.build.json" },
|
|
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 reads the body of every non-GET request the proxy (`src/proxy.ts`)
|
|
// matches into memory, so both the proxy and the route can read it, and it
|
|
// does so BEFORE the route runs — before any authentication. Everything past
|
|
// this ceiling is dropped, not refused. It used to be 512 MB so the Apple
|
|
// Health importer could receive whole archives (GitHub #281), which let any
|
|
// anonymous POST make the server hold that much. It is 1 MB now: the routes
|
|
// whose real bodies are larger are left out of the proxy matcher and read
|
|
// their own body after authenticating (`src/lib/http/proxy-bypass-routes.ts`),
|
|
// and every other route's own cap is at or below this. Large exports still
|
|
// want a matching reverse-proxy body limit (docs/self-hosting/reverse-proxy.md).
|
|
experimental: {
|
|
optimizePackageImports: ["recharts", "lucide-react"],
|
|
proxyClientMaxBodySize: "1mb",
|
|
},
|
|
};
|
|
|
|
/**
|
|
* 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);
|