Files
pn-new-crm/next.config.ts
Matt 449b9497ab fix(uat): batch — timeline overshoot, name-sync, reset-password, dashboard cleanup, queue/seed hygiene + alpha UAT findings doc
UAT findings landed across the last few Playwright + React Grab passes;
single grouped commit so the index doesn't fragment into 30 one-liners.

User & auth:
- `user-settings`: name now updates the avatar + topbar menu after save
  (was reading stale session).
- `me/password-reset`: 3 bugs (token validation, error response shape,
  redirect chain).
- Admin user permission-overrides route honours the same envelope as
  the rest of the admin surface.

Dashboard:
- Removed obsolete `revenue-breakdown-chart` + `dashboard-widgets-card`
  (replaced by the customisable widget grid).
- Strip `revenue_breakdown` from analytics route + use-analytics +
  service + integration test so nothing renders an empty card.
- Activity log timeline overshoot fix (`interest-timeline` +
  `entity-activity-feed`).
- Tightened tiles: active-deals, berth-heat-widget, pipeline-value, kpi-tile.
- `dev-mode-banner`: derive dismissed state synchronously instead of
  via an effect (set-state-in-effect lint rule).

Forms & lists (assorted polish):
- client / company / yacht / interest / reminder forms — validation +
  empty-state copy + tab transitions.
- companies/yachts list tweaks; berth recommender panel; qualification
  checklist; supplemental info request button.

Infra & misc:
- Queue workers (ai / email / notifications) — log shape +
  per-job timeout consistency.
- Auth / brochures / users schema small adjustments; seeds reflect
  permissions matrix changes.
- Scan shell + scanner manifest + AI admin page small fixes.
- `next.config.transpilePackages` adds `echarts`/`zrender`/`echarts-for-react`
  (recommended config from echarts-for-react inside Next).

Docs:
- `docs/superpowers/audits/alpha-uat-master.md` — single rolling
  cross-cutting UAT findings doc (per CLAUDE.md convention).
- `docs/BACKLOG.md`: dashboard stats cards (§I) + activity-log
  normalization (§J).
- 2026-05-18 audit log updated with this batch.
- `CLAUDE.md` — small manual UAT scaffold notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:56:11 +02:00

173 lines
6.7 KiB
TypeScript

import type { NextConfig } from 'next';
import bundleAnalyzer from '@next/bundle-analyzer';
import createNextIntlPlugin from 'next-intl/plugin';
import { withSentryConfig } from '@sentry/nextjs';
// next-intl plugin — points at our request-config entrypoint. Even
// though we ship only English today, the plugin is wired so future
// locale additions are a config-only change, not a code rewrite.
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
const isProd = process.env.NODE_ENV === 'production';
// Wrap the config with the bundle analyzer. Run `ANALYZE=true pnpm build`
// to get treemaps of the client + server bundles after the build
// completes. Pairs with the recharts dynamic-import work the audit
// flagged — gives us the tool to verify chart bundles only ship on the
// dashboard surface and not on routes that don't render them.
const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
/**
* Security headers applied to every response. Per audit-pass-#3 finding:
* the previous config emitted no CSP, X-Frame-Options, HSTS, or
* X-Content-Type-Options — the app was open to clickjacking + MIME
* sniffing.
*
* CSP notes:
* - 'unsafe-inline' on style-src is required by Tailwind's runtime
* style injection and Radix; revisit when Tailwind v4 ships a
* nonce story.
* - 'unsafe-eval' on script-src is dev-only — Next dev uses eval for
* HMR. Production drops it.
* - connect-src allows ws/wss for Socket.IO and https: for outgoing
* fetches; tighten in prod via per-port branding URLs once we move
* the s3 image references into a known allowlist.
* - img-src https: is wide because port branding pulls from
* s3.portnimara.com plus per-port image URLs configured at runtime.
*/
// Dev-only allow-list: react-grab (the in-page click-to-source devtool)
// is fetched from unpkg, so script/style/connect must allow it. Strip
// these entries in prod via the conditional below.
const devScriptHosts = isProd ? '' : ' http://unpkg.com https://unpkg.com';
const devConnectHosts = isProd ? '' : ' http://unpkg.com https://unpkg.com';
// Fallback CSP for paths the proxy doesn't run on (static assets,
// API JSON responses where script-src is moot). Production HTML
// responses get a stricter per-request nonce-based CSP set in
// `src/proxy.ts:applyCsp`; this header just provides a sane default
// so a misconfigured static-only route still has a CSP.
//
// Dev keeps 'unsafe-inline' + 'unsafe-eval' on script-src because
// Next's HMR runtime evaluates code dynamically and the nonce
// machinery doesn't reach it.
const csp = [
"default-src 'self'",
`script-src 'self' 'unsafe-inline'${isProd ? '' : " 'unsafe-eval'"}${devScriptHosts}`,
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https:",
"font-src 'self' data:",
`connect-src 'self' ws: wss: https:${devConnectHosts}`,
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'",
].join('; ');
const securityHeaders = [
{ key: 'Content-Security-Policy', value: csp },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(self), microphone=(), geolocation=()' },
...(isProd
? [{ key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' }]
: []),
];
const nextConfig: NextConfig = {
output: 'standalone',
// Hide the floating dev indicator (the little circle/N badge in the
// corner). Compile errors still surface via the full overlay; this
// only removes the idle "everything is fine" indicator that's been
// visible in every screenshot from the iPhone testing pass.
devIndicators: false,
// LAN access from a real iPhone hits the dev server via the Mac's
// local IP (e.g. 192.168.x.x), not localhost. Next 15 surfaces a
// warning for cross-origin /_next/* fetches unless we allow-list the
// origins explicitly. Wildcard the 192.168/0.0.0.0 ranges in dev so
// any LAN device works without a config edit per network.
...(isProd ? {} : { allowedDevOrigins: ['192.168.1.42'] }),
// Native/CJS-leaning server-only packages — list here so Next doesn't
// bundle them into the route trace (slower cold start + risk that
// native bindings fail at runtime). Build-auditor C3+M3: socket.io
// is only imported by the custom server entry point, so the Next
// tracer has no reason to include it; listing here makes the
// dependency visible to the build system.
serverExternalPackages: [
'pino',
'pino-pretty',
'bullmq',
'ioredis',
'minio',
'postgres',
'better-auth',
'nodemailer',
'socket.io',
'@socket.io/redis-adapter',
'imapflow',
'mailparser',
'pdf-lib',
'sharp',
'tesseract.js',
'@react-pdf/renderer',
'unpdf',
],
images: {
remotePatterns: [{ protocol: 'https', hostname: '*.portnimara.com' }],
},
typedRoutes: true,
// ECharts ships ES modules that older Next/webpack versions can't parse
// without a transpile-pass. Listing here is the official recommendation
// from echarts-for-react when used inside Next.
transpilePackages: ['echarts', 'zrender', 'echarts-for-react'],
outputFileTracingIncludes: {
// Bundle the EOI source PDF so the in-app EOI pathway can read it at
// runtime in the standalone build. Reading via fs.readFile from
// process.cwd() requires the file to be traced explicitly.
'/api/v1/document-templates/**': ['./assets/eoi-template.pdf'],
},
async redirects() {
return [
{
source: '/:portSlug/documents/files',
destination: '/:portSlug/documents',
permanent: true,
},
{
source: '/:portSlug/documents/files/:path*',
destination: '/:portSlug/documents',
permanent: true,
},
];
},
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders,
},
];
},
};
// Sentry wrapper is conditional: if NEXT_PUBLIC_SENTRY_DSN isn't set we
// skip its build-time source-map upload + middleware injection so dev
// builds stay fast and CI doesn't need credentials. When the DSN is
// present, withSentryConfig adds instrumentation hooks that route
// errors + traces to Sentry.
const withSentry = process.env.NEXT_PUBLIC_SENTRY_DSN
? (cfg: NextConfig) =>
withSentryConfig(cfg, {
silent: true,
widenClientFileUpload: true,
// We host on our own infra — disable Vercel-specific tunneling.
tunnelRoute: undefined,
// Strip Sentry debug logger from prod bundle.
disableLogger: true,
})
: (cfg: NextConfig) => cfg;
export default withSentry(withBundleAnalyzer(withNextIntl(nextConfig)));