Browse Source

Fix SpoolBuddy update UI: loading states, page reload, stale data

  Check button had no visual feedback because isFetching doesn't trigger
  reliably with cached data — replaced with manual loading state. Removed
  kiosk restart via getty; the frontend now detects daemon re-registration
  via WebSocket and calls window.location.reload(), keeping the user on
  the same page and fetching all fresh data.
maziggy 2 months ago
parent
commit
559d41cc5e

+ 1 - 2
CHANGELOG.md

@@ -7,9 +7,8 @@ All notable changes to Bambuddy will be documented in this file.
 ### 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 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 Kiosk Shows Stale Frontend After Update** — After an update, the kiosk browser displayed the old frontend because Chromium served cached assets even after restarting. Added `--disk-cache-size=0` to the Chromium launch flags so it always loads fresh content from the Bambuddy backend.
 - **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 Status Stale After Restart** — After a SpoolBuddy update completed, the UI permanently showed "Update complete, daemon restarting..." and a stale "update available" banner. Root cause: the SSH update set status to `"complete"` after the daemon had already restarted and re-registered, overwriting the cleared state. Fixed by letting daemon re-registration be the completion signal — it now clears any update status (`pending`/`updating`/`complete`/`error`), and the final `"complete"` write was removed. Also added WebSocket handlers for `spoolbuddy_update` and `spoolbuddy_online` events so the frontend refreshes immediately.
+- **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.
 - **Spool Assignment on Empty AMS Slots** ([#784](https://github.com/maziggy/bambuddy/issues/784)) — Empty AMS slots (no physical spool detected) showed "Assign Spool" and "Configure" buttons in the hover popup. Assigning a spool to an empty slot created a stuck state because no "Unassign" button is available for empty slots. Removed both buttons from empty AMS and HT AMS slot popups. External spool holders are unaffected. Reported by @RosdasHH.
 

+ 2 - 9
backend/app/services/spoolbuddy_ssh.py

@@ -225,15 +225,8 @@ async def perform_ssh_update(device_id: str, ip_address: str, install_path: str
             await _update_progress("error", f"Service restart failed: {stderr[:200]}")
             return
 
-        # Step 6: Restart kiosk browser to load updated frontend
-        rc, _, stderr = await _run_ssh_command(
-            ip_address,
-            "sudo /usr/bin/systemctl restart getty@tty1.service",
-            private_key,
-        )
-        if rc != 0:
-            # Non-fatal — kiosk may not be set up on all devices
-            logger.warning("SpoolBuddy %s: kiosk restart failed (non-fatal): %s", device_id, stderr[:200])
+        # No explicit kiosk restart — the frontend detects daemon re-registration
+        # via WebSocket and reloads itself automatically.
         logger.info("SpoolBuddy %s: SSH update complete (branch=%s)", device_id, branch)
 
     except Exception as e:

+ 29 - 7
frontend/src/pages/spoolbuddy/SpoolBuddySettingsPage.tsx

@@ -506,10 +506,23 @@ function UpdatesTab({ device }: { device: SpoolBuddyDevice }) {
   const [error, setError] = useState<string | null>(null);
   const [sshExpanded, setSSHExpanded] = useState(false);
   const [copied, setCopied] = useState(false);
+  const [manualChecking, setManualChecking] = useState(false);
+  const wasUpdatingRef = useRef(false);
 
   const isUpdating = device.update_status === 'pending' || device.update_status === 'updating';
 
-  const { data: updateResult, isFetching: checking, refetch } = useQuery({
+  // Track when an update was in progress so we can reload when device comes back
+  useEffect(() => {
+    if (isUpdating) {
+      wasUpdatingRef.current = true;
+    } else if (wasUpdatingRef.current && device.update_status == null) {
+      // Status cleared = daemon re-registered after update. Reload for fresh state.
+      wasUpdatingRef.current = false;
+      window.location.reload();
+    }
+  }, [isUpdating, device.update_status]);
+
+  const { data: updateResult, refetch } = useQuery({
     queryKey: ['spoolbuddy-update-check', device.device_id],
     queryFn: () => spoolbuddyApi.checkDaemonUpdate(device.device_id),
     staleTime: 30 * 1000,
@@ -522,6 +535,15 @@ function UpdatesTab({ device }: { device: SpoolBuddyDevice }) {
     staleTime: Infinity,
   });
 
+  const checkForUpdates = async () => {
+    setManualChecking(true);
+    try {
+      await refetch();
+    } finally {
+      setManualChecking(false);
+    }
+  };
+
   const applyUpdate = async () => {
     setApplying(true);
     setError(null);
@@ -606,11 +628,11 @@ function UpdatesTab({ device }: { device: SpoolBuddyDevice }) {
           /* Up to date → Check + Force buttons side by side */
           <div className="flex gap-2">
             <button
-              onClick={() => refetch()}
-              disabled={checking || applying}
+              onClick={checkForUpdates}
+              disabled={manualChecking || applying}
               className="flex-1 px-3 py-2 rounded-lg text-xs font-medium bg-zinc-700 text-zinc-300 hover:bg-zinc-600 disabled:opacity-40 transition-colors flex items-center justify-center gap-1"
             >
-              {checking && (
+              {manualChecking && (
                 <svg className="w-3 h-3 animate-spin" viewBox="0 0 24 24" fill="none">
                   <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
                   <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
@@ -635,11 +657,11 @@ function UpdatesTab({ device }: { device: SpoolBuddyDevice }) {
         ) : !isUpdating ? (
           /* No result yet → Check button */
           <button
-            onClick={() => refetch()}
-            disabled={checking}
+            onClick={checkForUpdates}
+            disabled={manualChecking}
             className="w-full px-3 py-2 rounded-lg text-sm font-medium bg-zinc-700 text-zinc-200 hover:bg-zinc-600 disabled:opacity-40 transition-colors flex items-center justify-center gap-2"
           >
-            {checking && (
+            {manualChecking && (
               <svg className="w-4 h-4 animate-spin" viewBox="0 0 24 24" fill="none">
                 <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
                 <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-B8Ecu_qy.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-Cj4TkOE2.js"></script>
+    <script type="module" crossorigin src="/assets/index-B8Ecu_qy.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-Dzv7xI7m.css">
   </head>
   <body>

Some files were not shown because too many files changed in this diff