Browse Source

Fix stale frontend after deploy due to service worker caching

  The SW used stale-while-revalidate for JS/CSS, serving old cached
  bundles even after a new build. Changed to network-first (Vite already
  content-hashes filenames), bumped cache version v24→v25, and added
  Cache-Control: no-cache to the sw.js endpoint so browsers always fetch
  the latest service worker.
maziggy 2 months ago
parent
commit
7f1ced60fd
4 changed files with 26 additions and 17 deletions
  1. 1 0
      CHANGELOG.md
  2. 5 1
      backend/app/main.py
  3. 10 8
      frontend/public/sw.js
  4. 10 8
      static/sw.js

+ 1 - 0
CHANGELOG.md

@@ -7,6 +7,7 @@ All notable changes to Bambuddy will be documented in this file.
 ### Fixed
 ### Fixed
 - **SpoolBuddy Update Check Always Shows "Up to Date"** — The SpoolBuddy daemon update check compared the device's firmware version against GitHub releases instead of the running Bambuddy backend version. This meant the check could incorrectly report "up to date" even when the daemon was behind. Fixed by comparing directly against `APP_VERSION` from the backend config.
 - **SpoolBuddy Update Check Always Shows "Up to Date"** — The SpoolBuddy daemon update check compared the device's firmware version against GitHub releases instead of the running Bambuddy backend version. This meant the check could incorrectly report "up to date" even when the daemon was behind. Fixed by comparing directly against `APP_VERSION` from the backend config.
 - **SpoolBuddy Updates Now Use SSH** — Replaced the fragile self-update mechanism (daemon pulls its own code via git, permission errors on `.git/`, hardcoded `main` branch) with SSH-based updates driven by the Bambuddy backend. Bambuddy now SSHes into the SpoolBuddy Pi and runs git fetch/checkout, pip install, systemctl restart, and kiosk browser restart remotely. Updates automatically use the same branch as Bambuddy. SSH key pairing is fully automatic — Bambuddy generates an ED25519 keypair and includes the public key in the device registration response; the daemon deploys it to `authorized_keys` on first connect. The install script creates the `spoolbuddy` user with a bash shell and sudoers entries for daemon and kiosk restart. A "Force Update" button allows re-deploying even when versions match. The SSH public key is also shown in SpoolBuddy Settings → Updates → SSH Setup for manual pairing if needed.
 - **SpoolBuddy Updates Now Use SSH** — Replaced the fragile self-update mechanism (daemon pulls its own code via git, permission errors on `.git/`, hardcoded `main` branch) with SSH-based updates driven by the Bambuddy backend. Bambuddy now SSHes into the SpoolBuddy Pi and runs git fetch/checkout, pip install, systemctl restart, and kiosk browser restart remotely. Updates automatically use the same branch as Bambuddy. SSH key pairing is fully automatic — Bambuddy generates an ED25519 keypair and includes the public key in the device registration response; the daemon deploys it to `authorized_keys` on first connect. The install script creates the `spoolbuddy` user with a bash shell and sudoers entries for daemon and kiosk restart. A "Force Update" button allows re-deploying even when versions match. The SSH public key is also shown in SpoolBuddy Settings → Updates → SSH Setup for manual pairing if needed.
+- **Frontend Not Updating After Deploy** — The service worker used stale-while-revalidate for JS/CSS assets, serving the old cached bundle even after a new build was deployed. Changed to network-first for JS/CSS (Vite content-hashes filenames so cache-busting is built in), bumped SW cache version, and added `Cache-Control: no-cache` to the `sw.js` endpoint so browsers always pick up new service worker versions immediately.
 - **SpoolBuddy Kiosk Starts Before Network Is Ready** — On fresh installs, the kiosk browser launched before the network was fully up, showing a connection error for 10-15 seconds until connectivity was restored. The getty@tty1 autologin override now waits for `network-online.target` so Chromium has connectivity when it starts.
 - **SpoolBuddy Kiosk Starts Before Network Is Ready** — On fresh installs, the kiosk browser launched before the network was fully up, showing a connection error for 10-15 seconds until connectivity was restored. The getty@tty1 autologin override now waits for `network-online.target` so Chromium has connectivity when it starts.
 - **SpoolBuddy Update UI Stale After Restart** — After a SpoolBuddy update, the UI permanently showed the old version and "update available" because: (1) the SSH update set status to `"complete"` after the daemon had already re-registered, overwriting the cleared state; (2) the kiosk restart navigated away from the updates page; (3) query cache served stale data. Fixed by letting daemon re-registration clear all update status, removing the kiosk restart in favor of a frontend-driven `window.location.reload()` triggered via WebSocket when the daemon comes back online, and adding proper loading states to Check/Force Update buttons.
 - **SpoolBuddy Update UI Stale After Restart** — After a SpoolBuddy update, the UI permanently showed the old version and "update available" because: (1) the SSH update set status to `"complete"` after the daemon had already re-registered, overwriting the cleared state; (2) the kiosk restart navigated away from the updates page; (3) query cache served stale data. Fixed by letting daemon re-registration clear all update status, removing the kiosk restart in favor of a frontend-driven `window.location.reload()` triggered via WebSocket when the daemon comes back online, and adding proper loading states to Check/Force Update buttons.
 - **Virtual Printer Proxy A1 Printing Fails** ([#757](https://github.com/maziggy/bambuddy/issues/757)) — BambuStudio could not send prints to A1 (and potentially P1S) virtual printers in proxy mode. The slicer connects to undocumented proprietary ports 2024-2026 on these models, which the proxy was not forwarding, causing BambuStudio to show an access code dialog instead of printing. Added transparent TCP pass-through proxying for ports 2024-2026. These ports are silently ignored on models that don't use them (X1C, H2C, P2S). Reported by @Utility9298.
 - **Virtual Printer Proxy A1 Printing Fails** ([#757](https://github.com/maziggy/bambuddy/issues/757)) — BambuStudio could not send prints to A1 (and potentially P1S) virtual printers in proxy mode. The slicer connects to undocumented proprietary ports 2024-2026 on these models, which the proxy was not forwarding, causing BambuStudio to show an access code dialog instead of printing. Added transparent TCP pass-through proxying for ports 2024-2026. These ports are silently ignored on models that don't use them (X1C, H2C, P2S). Reported by @Utility9298.

+ 5 - 1
backend/app/main.py

@@ -4113,7 +4113,11 @@ async def serve_service_worker():
     """Serve service worker."""
     """Serve service worker."""
     sw_file = app_settings.static_dir / "sw.js"
     sw_file = app_settings.static_dir / "sw.js"
     if sw_file.exists():
     if sw_file.exists():
-        return FileResponse(sw_file, media_type="application/javascript")
+        return FileResponse(
+            sw_file,
+            media_type="application/javascript",
+            headers={"Cache-Control": "no-cache, no-store, must-revalidate"},
+        )
     return {"error": "Service worker not found"}
     return {"error": "Service worker not found"}
 
 
 
 

+ 10 - 8
frontend/public/sw.js

@@ -1,6 +1,6 @@
 // Bambuddy Service Worker
 // Bambuddy Service Worker
-const CACHE_NAME = 'bambuddy-v24';
-const STATIC_CACHE = 'bambuddy-static-v24';
+const CACHE_NAME = 'bambuddy-v25';
+const STATIC_CACHE = 'bambuddy-static-v25';
 
 
 // Static assets to cache on install
 // Static assets to cache on install
 const STATIC_ASSETS = [
 const STATIC_ASSETS = [
@@ -113,15 +113,16 @@ self.addEventListener('fetch', (event) => {
     return;
     return;
   }
   }
 
 
-  // JS/CSS assets - stale-while-revalidate
+  // JS/CSS assets - network first (Vite content-hashes filenames, so
+  // cache-busting is built in; network-first ensures new builds load immediately)
   if (
   if (
     url.pathname.startsWith('/assets/') ||
     url.pathname.startsWith('/assets/') ||
     url.pathname.endsWith('.js') ||
     url.pathname.endsWith('.js') ||
     url.pathname.endsWith('.css')
     url.pathname.endsWith('.css')
   ) {
   ) {
     event.respondWith(
     event.respondWith(
-      caches.match(request).then((cached) => {
-        const fetchPromise = fetch(request).then((response) => {
+      fetch(request)
+        .then((response) => {
           if (response.ok) {
           if (response.ok) {
             const clone = response.clone();
             const clone = response.clone();
             caches.open(CACHE_NAME).then((cache) => {
             caches.open(CACHE_NAME).then((cache) => {
@@ -129,9 +130,10 @@ self.addEventListener('fetch', (event) => {
             });
             });
           }
           }
           return response;
           return response;
-        });
-        return cached || fetchPromise;
-      })
+        })
+        .catch(() => {
+          return caches.match(request);
+        })
     );
     );
     return;
     return;
   }
   }

+ 10 - 8
static/sw.js

@@ -1,6 +1,6 @@
 // Bambuddy Service Worker
 // Bambuddy Service Worker
-const CACHE_NAME = 'bambuddy-v24';
-const STATIC_CACHE = 'bambuddy-static-v24';
+const CACHE_NAME = 'bambuddy-v25';
+const STATIC_CACHE = 'bambuddy-static-v25';
 
 
 // Static assets to cache on install
 // Static assets to cache on install
 const STATIC_ASSETS = [
 const STATIC_ASSETS = [
@@ -113,15 +113,16 @@ self.addEventListener('fetch', (event) => {
     return;
     return;
   }
   }
 
 
-  // JS/CSS assets - stale-while-revalidate
+  // JS/CSS assets - network first (Vite content-hashes filenames, so
+  // cache-busting is built in; network-first ensures new builds load immediately)
   if (
   if (
     url.pathname.startsWith('/assets/') ||
     url.pathname.startsWith('/assets/') ||
     url.pathname.endsWith('.js') ||
     url.pathname.endsWith('.js') ||
     url.pathname.endsWith('.css')
     url.pathname.endsWith('.css')
   ) {
   ) {
     event.respondWith(
     event.respondWith(
-      caches.match(request).then((cached) => {
-        const fetchPromise = fetch(request).then((response) => {
+      fetch(request)
+        .then((response) => {
           if (response.ok) {
           if (response.ok) {
             const clone = response.clone();
             const clone = response.clone();
             caches.open(CACHE_NAME).then((cache) => {
             caches.open(CACHE_NAME).then((cache) => {
@@ -129,9 +130,10 @@ self.addEventListener('fetch', (event) => {
             });
             });
           }
           }
           return response;
           return response;
-        });
-        return cached || fetchPromise;
-      })
+        })
+        .catch(() => {
+          return caches.match(request);
+        })
     );
     );
     return;
     return;
   }
   }