-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathnext.config.ts
More file actions
319 lines (315 loc) · 14.3 KB
/
Copy pathnext.config.ts
File metadata and controls
319 lines (315 loc) · 14.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
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);