Просмотр исходного кода

fix(frontend): revert base: '' so deep SPA routes load their assets on initial navigation (issue #1221)

  PR #1195 (d6a31393) set Vite's base: '' to emit relative asset URLs
  in the built index.html, intended as a partial improvement for
  path-prefixed reverse proxies. The relative URLs broke every deep
  SPA route on initial load: popup windows, direct URL paste, page
  refresh on /camera/<id>, /projects/<id>, /groups/<id>/edit,
  /external/<id>, /files/trash, and the SpoolBuddy kiosk paths.

  Browser resolves ./assets/index-XXX.js against the document URL.
  For /camera/<id>, that gives /camera/assets/index-XXX.js — the SPA
  catch-all returns index.html (text/html) for that path, and modern
  browsers refuse to execute HTML as a JS module under
  X-Content-Type-Options: nosniff. Hence the empty-popup symptom
  reported on #1221 across P1S / P2S / X1 / Docker / git / Chrome /
  Firefox / Brave / Safari, plus the quieter "blank page on refresh"
  on every other deep route.

  Reverts the two PR #1195 lines: removes base: '' from
  vite.config.ts (Vite default '/' restored, emitting absolute asset
  URLs /assets/..., /manifest.json, /sw-register.js) and reverts
  register('sw.js') to register('/sw.js') in public/sw-register.js.

  PR #1195's class of bug — path-prefixed reverse proxy users serving
  Bambuddy at a subpath — was already explicitly closed as wontfix in
  that thread because supporting it requires subpath-aware
  bootstrapping (API_BASE, React Router basename, PWA manifest scope,
  SW scope) for every user forever. The supported alternative for that
  audience stays as documented in the #1195 closing comment: NPM
  (Nginx Proxy Manager) addon + Cloudflare Tunnel at a real domain
  with HTTPS, then HA Webpage panel embedding via
  TRUSTED_FRAME_ORIGINS — that path doesn't depend on base: '' at all.

  The trade-off is intentional: revert reaches every user impacted by
  deep-route initial-load bugs (much larger population than
  path-prefixed proxy users), in exchange for an already-wontfixed
  subpath-proxy regression that has a working alternative.
maziggy 2 недель назад
Родитель
Сommit
bb03a2b373
6 измененных файлов с 34 добавлено и 14 удалено
  1. 0 0
      CHANGELOG.md
  2. 1 1
      frontend/public/sw-register.js
  3. 9 4
      frontend/vite.config.ts
  4. 8 8
      static/index.html
  5. 1 1
      static/sw-register.js
  6. 15 0
      test.sh

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
CHANGELOG.md


+ 1 - 1
frontend/public/sw-register.js

@@ -10,7 +10,7 @@ if ('serviceWorker' in navigator) {
     });
   } else {
     window.addEventListener('load', () => {
-      navigator.serviceWorker.register('sw.js')
+      navigator.serviceWorker.register('/sw.js')
         .then((registration) => {
           console.log('SW registered:', registration.scope);
         })

+ 9 - 4
frontend/vite.config.ts

@@ -74,10 +74,15 @@ function serveGcodeViewer() {
 }
 
 export default defineConfig({
-  // Empty base emits relative asset URLs (./assets/... instead of /assets/...)
-  // so the built SPA loads correctly when served at any subpath — HA Ingress,
-  // nginx/Traefik path prefix, Cloudflare Tunnel path routing, etc. (#1195).
-  base: '',
+  // Default base ('/') emits absolute asset URLs (/assets/...). Required so
+  // deep SPA routes (camera popup at /camera/<id>, /projects/<id>, kiosk
+  // /spoolbuddy/ams, refresh on any nested route) resolve their <script>
+  // and <link> tags to /assets/... instead of /<route-prefix>/assets/...,
+  // which the SPA fallback would otherwise return as text/html and the
+  // browser would refuse to execute (#1221). The earlier `base: ''` partial
+  // fix for subpath reverse proxies (#1195, wontfix) is reverted — that
+  // audience uses NPM + Cloudflare Tunnel at a real domain per the
+  // documented workaround, which doesn't depend on this setting.
   plugins: [react(), serveGcodeViewer()],
   build: {
     outDir: '../static',

+ 8 - 8
static/index.html

@@ -17,17 +17,17 @@
     <meta name="apple-mobile-web-app-title" content="Bambuddy" />
 
     <!-- Manifest -->
-    <link rel="manifest" href="./manifest.json" />
+    <link rel="manifest" href="/manifest.json" />
 
     <!-- Favicons -->
-    <link rel="icon" type="image/png" sizes="32x32" href="./img/favicon-32x32.png" />
-    <link rel="icon" type="image/png" sizes="16x16" href="./img/favicon-16x16.png" />
-    <link rel="apple-touch-icon" sizes="180x180" href="./img/apple-touch-icon.png" />
+    <link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
+    <link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
+    <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
 
     <!-- Splash screens for iOS -->
-    <link rel="apple-touch-startup-image" href="./img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="./assets/index-BofTUuqf.js"></script>
-    <link rel="stylesheet" crossorigin href="./assets/index-CacBES2t.css">
+    <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
+    <script type="module" crossorigin src="/assets/index-BofTUuqf.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-CacBES2t.css">
   </head>
   <body>
     <div id="root"></div>
@@ -35,6 +35,6 @@
     <!-- Service Worker Registration (skip on SpoolBuddy kiosk).
          Kept as an external file so the CSP `script-src 'self'` covers it
          without needing 'unsafe-inline' or per-build hashes. -->
-    <script src="./sw-register.js"></script>
+    <script src="/sw-register.js"></script>
   </body>
 </html>

+ 1 - 1
static/sw-register.js

@@ -10,7 +10,7 @@ if ('serviceWorker' in navigator) {
     });
   } else {
     window.addEventListener('load', () => {
-      navigator.serviceWorker.register('sw.js')
+      navigator.serviceWorker.register('/sw.js')
         .then((registration) => {
           console.log('SW registered:', registration.scope);
         })

+ 15 - 0
test.sh

@@ -0,0 +1,15 @@
+docker exec bambuddy python3 -c '
+import socket
+host = "192.168.255.133"
+ports = [80, 322, 990, 2024, 2025, 2026, 3000, 3002, 6000, 8883]
+for p in ports:
+	s = socket.socket()
+	s.settimeout(3)
+	try:
+		s.connect((host, p))
+		print(f"{p} open")
+	except OSError as e:
+		print(f"{p} closed ({e.__class__.__name__}: {e})")
+	finally:
+		s.close()
+'

Некоторые файлы не были показаны из-за большого количества измененных файлов