From d2f5d42737c877e639d831ff52596bf75d616a54 Mon Sep 17 00:00:00 2001 From: Beniamin Malinski Date: Thu, 2 Apr 2026 22:13:09 +0200 Subject: [PATCH 1/7] feat: add Netlify deployment config for Console frontend Add netlify.toml with: - Build config (bun install + rsbuild build) - CORS headers for cross-origin Module Federation loading - Cache-busting for embedded.js and mf-manifest.json - Immutable caching for hashed static assets - SPA fallback redirect for standalone mode This enables Console frontend to be deployed as a static Netlify site, serving the same assets to all clusters. The per-cluster API URL is provided by Cloud UI via the gatewayApiUrl prop at runtime. Companion PRs: - redpanda-data/cloudv2#25386 (Console Netlify support in Cloud UI) - redpanda-data/console#2361 (MF v2 search params fix) Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/netlify.toml | 32 ++++++++++++++++++++++++++++++++ frontend/public/_redirects | 3 +++ 2 files changed, 35 insertions(+) create mode 100644 frontend/netlify.toml create mode 100644 frontend/public/_redirects diff --git a/frontend/netlify.toml b/frontend/netlify.toml new file mode 100644 index 0000000000..e1354d4e6d --- /dev/null +++ b/frontend/netlify.toml @@ -0,0 +1,32 @@ +[build] + base = "frontend" + command = "bun install && NODE_OPTIONS=--max_old_space_size=16384 bun run build" + publish = "build" + +[build.environment] + NODE_VERSION = "22" + +[[headers]] + for = "/*" + [headers.values] + Access-Control-Allow-Origin = "*" + Access-Control-Allow-Methods = "GET, OPTIONS" + Access-Control-Allow-Headers = "*" + X-Content-Type-Options = "nosniff" + +# MF remote entry and manifest must never be cached +[[headers]] + for = "/embedded.js" + [headers.values] + Cache-Control = "no-cache, no-store, must-revalidate" + +[[headers]] + for = "/mf-manifest.json" + [headers.values] + Cache-Control = "no-cache, no-store, must-revalidate" + +# Hashed static assets are immutable +[[headers]] + for = "/static/*" + [headers.values] + Cache-Control = "public, max-age=31536000, immutable" diff --git a/frontend/public/_redirects b/frontend/public/_redirects new file mode 100644 index 0000000000..fdc5dfe6f1 --- /dev/null +++ b/frontend/public/_redirects @@ -0,0 +1,3 @@ +# SPA fallback for standalone mode. In embedded (MF) mode, only +# remoteEntry.js/embedded.js and chunks are fetched. +/* /index.html 200 From ed9d2c58bed696122f0a5dd3108286323ecc59c5 Mon Sep 17 00:00:00 2001 From: Beniamin Malinski Date: Fri, 3 Apr 2026 12:00:59 +0200 Subject: [PATCH 2/7] fix: narrow CORS scope and add preflight cache in netlify.toml - Move CORS headers from /* to specific MF entry points only (embedded.js, mf-manifest.json, static/*) - Add Access-Control-Max-Age for preflight caching - Add comment explaining NODE_OPTIONS heap ceiling Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/netlify.toml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/frontend/netlify.toml b/frontend/netlify.toml index e1354d4e6d..dced40e585 100644 --- a/frontend/netlify.toml +++ b/frontend/netlify.toml @@ -1,5 +1,7 @@ [build] base = "frontend" + # NODE_OPTIONS: 16 GB is a V8 heap ceiling guard, not actual memory usage. + # It prevents OOM kills during large builds; real RSS stays well below this. command = "bun install && NODE_OPTIONS=--max_old_space_size=16384 bun run build" publish = "build" @@ -9,24 +11,34 @@ [[headers]] for = "/*" [headers.values] - Access-Control-Allow-Origin = "*" - Access-Control-Allow-Methods = "GET, OPTIONS" - Access-Control-Allow-Headers = "*" X-Content-Type-Options = "nosniff" -# MF remote entry and manifest must never be cached +# MF remote entry — never cached, CORS required for cross-origin import [[headers]] for = "/embedded.js" [headers.values] Cache-Control = "no-cache, no-store, must-revalidate" + Access-Control-Allow-Origin = "*" + Access-Control-Allow-Methods = "GET, OPTIONS" + Access-Control-Allow-Headers = "*" + Access-Control-Max-Age = "86400" +# MF manifest — never cached, CORS required for cross-origin fetch [[headers]] for = "/mf-manifest.json" [headers.values] Cache-Control = "no-cache, no-store, must-revalidate" + Access-Control-Allow-Origin = "*" + Access-Control-Allow-Methods = "GET, OPTIONS" + Access-Control-Allow-Headers = "*" + Access-Control-Max-Age = "86400" -# Hashed static assets are immutable +# Hashed static assets are immutable, CORS required for cross-origin import [[headers]] for = "/static/*" [headers.values] Cache-Control = "public, max-age=31536000, immutable" + Access-Control-Allow-Origin = "*" + Access-Control-Allow-Methods = "GET, OPTIONS" + Access-Control-Allow-Headers = "*" + Access-Control-Max-Age = "86400" From d588f9128c0f1ac5afaab988ceaa329b34a96051 Mon Sep 17 00:00:00 2001 From: Beniamin Malinski Date: Fri, 3 Apr 2026 12:13:10 +0200 Subject: [PATCH 3/7] fix: pin BUN_VERSION in netlify.toml for reproducible builds Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/netlify.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/netlify.toml b/frontend/netlify.toml index dced40e585..08bdae93f4 100644 --- a/frontend/netlify.toml +++ b/frontend/netlify.toml @@ -7,6 +7,7 @@ [build.environment] NODE_VERSION = "22" + BUN_VERSION = "1.3" [[headers]] for = "/*" From 0a01be8296d3fd1eb9df43f8f7f3d45dc3ff5b88 Mon Sep 17 00:00:00 2001 From: Beniamin Malinski Date: Fri, 3 Apr 2026 12:23:01 +0200 Subject: [PATCH 4/7] fix: pin exact BUN_VERSION for reproducible builds Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/netlify.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/netlify.toml b/frontend/netlify.toml index 08bdae93f4..736c61caa7 100644 --- a/frontend/netlify.toml +++ b/frontend/netlify.toml @@ -7,7 +7,7 @@ [build.environment] NODE_VERSION = "22" - BUN_VERSION = "1.3" + BUN_VERSION = "1.3.11" [[headers]] for = "/*" From 44d329fc73fdf05689ff2750f4c306607b6f0cc7 Mon Sep 17 00:00:00 2001 From: Beniamin Malinski Date: Fri, 3 Apr 2026 12:23:12 +0200 Subject: [PATCH 5/7] fix: split navigateTo into pathname + search for idiomatic router.navigate TanStack Router expects search params via the search option, not embedded in the to string. Split on ? and pass separately for forward compatibility with future TanStack Router versions. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/federation/console-app.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/federation/console-app.tsx b/frontend/src/federation/console-app.tsx index 5b09ae8b7b..b3a7d76f01 100644 --- a/frontend/src/federation/console-app.tsx +++ b/frontend/src/federation/console-app.tsx @@ -289,7 +289,11 @@ function ConsoleAppInner({ if (navigateTo !== currentPath) { // Update ref to prevent echo back to host lastNotifiedPathRef.current = navigateTo; - router.navigate({ to: navigateTo }); + const [toPath, toSearch] = navigateTo.split('?'); + router.navigate({ + to: toPath, + search: toSearch ? Object.fromEntries(new URLSearchParams(toSearch)) : undefined, + }); } }, [navigateTo, isInitialized, router]); From df13b7e9e51799f57a4292586f27d1207a322138 Mon Sep 17 00:00:00 2001 From: Beniamin Malinski Date: Fri, 3 Apr 2026 13:17:33 +0200 Subject: [PATCH 6/7] fix: use indexOf for navigateTo split instead of split('?') split('?') would silently discard params after a second '?' in malformed paths. indexOf correctly handles the first '?' only. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/federation/console-app.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/federation/console-app.tsx b/frontend/src/federation/console-app.tsx index b3a7d76f01..8072ee6477 100644 --- a/frontend/src/federation/console-app.tsx +++ b/frontend/src/federation/console-app.tsx @@ -289,7 +289,9 @@ function ConsoleAppInner({ if (navigateTo !== currentPath) { // Update ref to prevent echo back to host lastNotifiedPathRef.current = navigateTo; - const [toPath, toSearch] = navigateTo.split('?'); + const qIdx = navigateTo.indexOf('?'); + const toPath = qIdx >= 0 ? navigateTo.slice(0, qIdx) : navigateTo; + const toSearch = qIdx >= 0 ? navigateTo.slice(qIdx + 1) : undefined; router.navigate({ to: toPath, search: toSearch ? Object.fromEntries(new URLSearchParams(toSearch)) : undefined, From 286d8043e3cb2a2fd3acaad3a394442a55b2c528 Mon Sep 17 00:00:00 2001 From: Beniamin Malinski Date: Tue, 7 Apr 2026 10:05:42 +0200 Subject: [PATCH 7/7] fix: lower NODE_OPTIONS heap ceiling to 4096 MB Align with Cloud UI's netlify.toml. 16 GB was unnecessarily high and could mask bundler memory leaks. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/netlify.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/netlify.toml b/frontend/netlify.toml index 736c61caa7..989ba55fe8 100644 --- a/frontend/netlify.toml +++ b/frontend/netlify.toml @@ -1,8 +1,8 @@ [build] base = "frontend" - # NODE_OPTIONS: 16 GB is a V8 heap ceiling guard, not actual memory usage. + # NODE_OPTIONS: 4 GB is a V8 heap ceiling guard, not actual memory usage. # It prevents OOM kills during large builds; real RSS stays well below this. - command = "bun install && NODE_OPTIONS=--max_old_space_size=16384 bun run build" + command = "bun install && NODE_OPTIONS=--max_old_space_size=4096 bun run build" publish = "build" [build.environment]