Browse Source

Fix SpoolBuddy update UI responsiveness and stale data

  Rewrote update button states: single `busy` flag stays set from click
  through to device pickup — no more gap with no feedback. Buttons hide
  while any operation is in progress. Listens for spoolbuddy-online
  WebSocket event to reload the page after daemon re-registers, ensuring
  fresh version and status. Set staleTime to 0 on update check queries.
maziggy 2 months ago
parent
commit
8be4bed384

+ 1 - 1
frontend/src/components/spoolbuddy/SpoolBuddyLayout.tsx

@@ -64,7 +64,7 @@ export function SpoolBuddyLayout() {
     queryFn: () => device ? spoolbuddyApi.checkDaemonUpdate(device.device_id) : Promise.resolve(null),
     enabled: !!device,
     refetchInterval: 5 * 60 * 1000, // re-check every 5 minutes
-    staleTime: 30 * 1000,
+    staleTime: 0,
   });
 
   // Update alert based on device state and available updates

+ 62 - 90
frontend/src/pages/spoolbuddy/SpoolBuddySettingsPage.tsx

@@ -502,30 +502,36 @@ function ScaleTab({ device, weight, weightStable, rawAdc }: {
 
 function UpdatesTab({ device }: { device: SpoolBuddyDevice }) {
   const { t } = useTranslation();
-  const [applying, setApplying] = useState(false);
+  const [busy, setBusy] = useState<'checking' | 'applying' | null>(null);
   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';
 
-  // Track when an update was in progress so we can reload when device comes back
+  // When applying succeeds and device picks up the update, keep showing busy
   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();
+    if (isUpdating && busy === 'applying') {
+      setBusy(null); // device has picked it up, isUpdating takes over the UI
     }
-  }, [isUpdating, device.update_status]);
+  }, [isUpdating, busy]);
+
+  // Reload the page when daemon comes back online after an update
+  useEffect(() => {
+    const handleOnline = () => {
+      if (isUpdating) {
+        // Daemon re-registered — reload to get fresh version + state
+        setTimeout(() => window.location.reload(), 1000);
+      }
+    };
+    window.addEventListener('spoolbuddy-online', handleOnline);
+    return () => window.removeEventListener('spoolbuddy-online', handleOnline);
+  }, [isUpdating]);
 
   const { data: updateResult, refetch } = useQuery({
     queryKey: ['spoolbuddy-update-check', device.device_id],
     queryFn: () => spoolbuddyApi.checkDaemonUpdate(device.device_id),
-    staleTime: 30 * 1000,
+    staleTime: 0,
   });
 
   const { data: sshKeyData } = useQuery({
@@ -536,26 +542,29 @@ function UpdatesTab({ device }: { device: SpoolBuddyDevice }) {
   });
 
   const checkForUpdates = async () => {
-    setManualChecking(true);
+    setBusy('checking');
+    setError(null);
     try {
       await refetch();
     } finally {
-      setManualChecking(false);
+      setBusy(null);
     }
   };
 
   const applyUpdate = async () => {
-    setApplying(true);
+    setBusy('applying');
     setError(null);
     try {
       await spoolbuddyApi.triggerUpdate(device.device_id);
+      // Don't clear busy — keep showing spinner until isUpdating takes over or timeout
     } catch (e) {
       setError(e instanceof Error ? e.message : 'Failed to trigger update');
-    } finally {
-      setApplying(false);
+      setBusy(null);
     }
   };
 
+  const showSpinner = busy != null || isUpdating;
+
   const copyKey = () => {
     if (sshKeyData?.public_key) {
       navigator.clipboard.writeText(sshKeyData.public_key);
@@ -581,95 +590,58 @@ function UpdatesTab({ device }: { device: SpoolBuddyDevice }) {
           </span>
         </div>
 
-        {/* Update progress */}
-        {isUpdating && (
-          <div className="flex items-center gap-2 text-sm">
+        {/* Status / progress row */}
+        {showSpinner ? (
+          <div className="flex items-center gap-2">
             <svg className="w-4 h-4 animate-spin text-green-400 flex-shrink-0" 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" />
             </svg>
             <span className="text-green-300 text-xs">
-              {device.update_message || t('spoolbuddy.settings.updateWaiting', 'Updating...')}
+              {busy === 'checking' ? t('spoolbuddy.settings.checking', 'Checking for updates...')
+                : device.update_message || t('spoolbuddy.settings.updateWaiting', 'Updating...')}
             </span>
           </div>
-        )}
-
-        {/* Update error */}
-        {device.update_status === 'error' && (
+        ) : device.update_status === 'error' ? (
           <p className="text-xs text-red-300">{device.update_message || t('spoolbuddy.settings.updateFailed', 'Update failed')}</p>
-        )}
-
-        {/* Error from trigger */}
-        {error && <p className="text-xs text-red-300">{error}</p>}
+        ) : error ? (
+          <p className="text-xs text-red-300">{error}</p>
+        ) : updateResult?.update_available ? (
+          <p className="text-xs text-green-300">
+            {t('spoolbuddy.settings.updateAvailable', 'Update available')}: {displayVersion} → {updateResult.latest_version}
+          </p>
+        ) : null}
 
-        {/* Update available → Apply button */}
-        {updateResult?.update_available ? (
-          <>
-            <p className="text-xs text-green-300">
-              {t('spoolbuddy.settings.updateAvailable', 'Update available')}: {displayVersion} → {updateResult.latest_version}
-            </p>
+        {/* Action buttons */}
+        {!showSpinner && (
+          updateResult?.update_available ? (
             <button
               onClick={applyUpdate}
-              disabled={applying || isUpdating || !device.online}
-              className="w-full px-3 py-2 rounded-lg text-sm font-medium bg-green-600 text-white hover:bg-green-700 disabled:opacity-40 transition-colors flex items-center justify-center gap-2"
+              disabled={!device.online}
+              className="w-full px-3 py-2 rounded-lg text-sm font-medium bg-green-600 text-white hover:bg-green-700 disabled:opacity-40 transition-colors"
             >
-              {applying && (
-                <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" />
-                </svg>
-              )}
               {!device.online
                 ? t('spoolbuddy.settings.deviceOffline', 'Device Offline')
                 : t('spoolbuddy.settings.applyUpdate', 'Apply Update')}
             </button>
-          </>
-        ) : updateResult && !isUpdating ? (
-          /* Up to date → Check + Force buttons side by side */
-          <div className="flex gap-2">
-            <button
-              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"
-            >
-              {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" />
-                </svg>
-              )}
-              {t('spoolbuddy.settings.checkUpdates', 'Check for Updates')}
-            </button>
-            <button
-              onClick={applyUpdate}
-              disabled={applying || isUpdating || !device.online}
-              className="px-3 py-2 rounded-lg text-xs font-medium bg-zinc-700 text-zinc-400 hover:bg-zinc-600 hover:text-zinc-200 disabled:opacity-40 transition-colors flex items-center justify-center gap-1"
-            >
-              {applying && (
-                <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" />
-                </svg>
-              )}
-              {t('spoolbuddy.settings.forceUpdate', 'Force Update')}
-            </button>
-          </div>
-        ) : !isUpdating ? (
-          /* No result yet → Check button */
-          <button
-            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"
-          >
-            {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" />
-              </svg>
-            )}
-            {t('spoolbuddy.settings.checkUpdates', 'Check for Updates')}
-          </button>
-        ) : null}
+          ) : (
+            <div className="flex gap-2">
+              <button
+                onClick={checkForUpdates}
+                className="flex-1 px-3 py-2 rounded-lg text-xs font-medium bg-zinc-700 text-zinc-300 hover:bg-zinc-600 transition-colors"
+              >
+                {t('spoolbuddy.settings.checkUpdates', 'Check for Updates')}
+              </button>
+              <button
+                onClick={applyUpdate}
+                disabled={!device.online}
+                className="px-3 py-2 rounded-lg text-xs font-medium bg-zinc-700 text-zinc-400 hover:bg-zinc-600 hover:text-zinc-200 disabled:opacity-40 transition-colors"
+              >
+                {t('spoolbuddy.settings.forceUpdate', 'Force Update')}
+              </button>
+            </div>
+          )
+        )}
       </div>
 
       {/* SSH Setup — collapsible */}

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-aq4iRnuK.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-B8Ecu_qy.js"></script>
+    <script type="module" crossorigin src="/assets/index-aq4iRnuK.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