Browse Source

● Compact SpoolBuddy updates tab layout to fit without scrolling

  Merged version, status, and update check into a single card. Buttons
  are side by side when up to date. Reduced padding and font sizes.
  Removed separate "complete" banner since status is now cleared on
  re-registration. SSH Setup section is smaller and more compact.
maziggy 2 months ago
parent
commit
0e44e149bd

+ 1 - 1
backend/app/core/config.py

@@ -5,7 +5,7 @@ from pathlib import Path
 from pydantic_settings import BaseSettings
 
 # Application version - single source of truth
-APP_VERSION = "0.2.3"
+APP_VERSION = "0.2.3.b1"
 GITHUB_REPO = "maziggy/bambuddy"
 BUG_REPORT_RELAY_URL = os.environ.get("BUG_REPORT_RELAY_URL", "https://bambuddy.cool/api/bug-report")
 

+ 93 - 131
frontend/src/pages/spoolbuddy/SpoolBuddySettingsPage.tsx

@@ -546,153 +546,115 @@ function UpdatesTab({ device }: { device: SpoolBuddyDevice }) {
     || (updateResult?.current_version && updateResult.current_version !== '0.0.0' ? updateResult.current_version : null);
 
   return (
-    <div className="space-y-4">
-      {/* Current version */}
-      <div className="bg-zinc-800 rounded-lg p-4">
-        <h3 className="text-sm font-semibold text-zinc-300 mb-3">
-          {t('spoolbuddy.settings.daemonVersion', 'Daemon Version')}
-        </h3>
+    <div className="space-y-3">
+      {/* Version + Update status + Check — single card */}
+      <div className="bg-zinc-800 rounded-lg p-3 space-y-3">
+        {/* Version row */}
         <div className="flex justify-between items-center text-sm">
-          <span className="text-zinc-500">{t('spoolbuddy.settings.currentVersion', 'Current')}</span>
+          <span className="text-zinc-500">{t('spoolbuddy.settings.currentVersion', 'Current Version')}</span>
           <span className="text-zinc-200 font-mono">
             {displayVersion || (
-              <span className="text-zinc-500 italic">{t('spoolbuddy.settings.versionPending', 'Waiting for daemon...')}</span>
+              <span className="text-zinc-500 italic text-xs">{t('spoolbuddy.settings.versionPending', 'Waiting for daemon...')}</span>
             )}
           </span>
         </div>
-      </div>
 
-      {/* Update progress (shown when update is in progress) */}
-      {isUpdating && (
-        <div className="bg-zinc-800 rounded-lg p-4">
-          <div className="flex items-center gap-3">
-            <svg className="w-5 h-5 animate-spin text-green-400 flex-shrink-0" viewBox="0 0 24 24" fill="none">
+        {/* Update progress */}
+        {isUpdating && (
+          <div className="flex items-center gap-2 text-sm">
+            <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>
-            <div>
-              <p className="text-sm font-medium text-green-300">
-                {t('spoolbuddy.settings.updating', 'Updating...')}
-              </p>
-              <p className="text-xs text-zinc-400 mt-0.5">
-                {device.update_message || t('spoolbuddy.settings.updateWaiting', 'Waiting for device...')}
-              </p>
-            </div>
-          </div>
-        </div>
-      )}
-
-      {/* Update complete */}
-      {device.update_status === 'complete' && (
-        <div className="rounded-lg p-3 text-sm bg-green-900/30 border border-green-800">
-          <div className="flex items-center gap-2">
-            <svg className="w-4 h-4 text-green-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
-              <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
-            </svg>
-            <p className="text-green-300">{device.update_message || t('spoolbuddy.settings.updateComplete', 'Update complete!')}</p>
+            <span className="text-green-300 text-xs">
+              {device.update_message || t('spoolbuddy.settings.updateWaiting', 'Updating...')}
+            </span>
           </div>
-        </div>
-      )}
+        )}
 
-      {/* Update error */}
-      {device.update_status === 'error' && (
-        <div className="rounded-lg p-3 text-sm bg-red-900/30 border border-red-800">
-          <p className="text-red-300">{device.update_message || t('spoolbuddy.settings.updateFailed', 'Update failed')}</p>
-        </div>
-      )}
+        {/* Update error */}
+        {device.update_status === 'error' && (
+          <p className="text-xs text-red-300">{device.update_message || t('spoolbuddy.settings.updateFailed', 'Update failed')}</p>
+        )}
 
-      {/* Check for updates */}
-      <div className="bg-zinc-800 rounded-lg p-4 space-y-3">
-        <button
-          onClick={() => refetch()}
-          disabled={checking || isUpdating}
-          className="w-full px-4 py-2.5 rounded-lg text-sm font-medium bg-zinc-700 text-zinc-200 hover:bg-zinc-600 disabled:opacity-40 transition-colors min-h-[44px] flex items-center justify-center gap-2"
-        >
-          {checking && (
-            <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>
-          )}
-          {checking ? t('spoolbuddy.settings.checking', 'Checking...') : t('spoolbuddy.settings.checkUpdates', 'Check for Updates')}
-        </button>
+        {/* Error from trigger */}
+        {error && <p className="text-xs text-red-300">{error}</p>}
 
-        {/* Error feedback */}
-        {error && (
-          <div className="rounded-lg p-3 text-sm bg-red-900/30 border border-red-800">
-            <p className="text-red-300">{error}</p>
+        {/* 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>
+            <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"
+            >
+              {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={() => refetch()}
+              disabled={checking}
+              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 && (
+                <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"
+            >
+              {t('spoolbuddy.settings.forceUpdate', 'Force Update')}
+            </button>
           </div>
-        )}
-
-        {/* Result feedback */}
-        {updateResult && (
-          <div className={`rounded-lg p-3 text-sm ${
-            updateResult.update_available
-              ? 'bg-green-900/30 border border-green-800'
-              : 'bg-zinc-700/50'
-          }`}>
-            {updateResult.update_available ? (
-              <div className="space-y-3">
-                <div className="space-y-1">
-                  <p className="text-green-300 font-medium">
-                    {t('spoolbuddy.settings.updateAvailable', 'Update available')}: v{updateResult.latest_version}
-                  </p>
-                  <p className="text-xs text-zinc-400">
-                    {displayVersion ? `${displayVersion} → ${updateResult.latest_version}` : ''}
-                  </p>
-                </div>
-                <button
-                  onClick={applyUpdate}
-                  disabled={applying || isUpdating || !device.online}
-                  className="w-full px-4 py-2.5 rounded-lg text-sm font-medium bg-green-600 text-white hover:bg-green-700 disabled:opacity-40 transition-colors min-h-[44px] flex items-center justify-center gap-2"
-                >
-                  {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>
-              </div>
-            ) : (
-              <div className="space-y-3">
-                <div className="flex items-center gap-2">
-                  <svg className="w-4 h-4 text-green-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
-                    <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
-                  </svg>
-                  <p className="text-zinc-300">{t('spoolbuddy.settings.upToDate', 'Up to date')}</p>
-                </div>
-                {/* Force update button — re-deploy even when versions match */}
-                <button
-                  onClick={applyUpdate}
-                  disabled={applying || isUpdating || !device.online}
-                  className="w-full px-4 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 min-h-[36px]"
-                >
-                  {!device.online
-                    ? t('spoolbuddy.settings.deviceOffline', 'Device Offline')
-                    : t('spoolbuddy.settings.forceUpdate', 'Force Update')}
-                </button>
-              </div>
+        ) : !isUpdating ? (
+          /* No result yet → Check button */
+          <button
+            onClick={() => refetch()}
+            disabled={checking}
+            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 && (
+              <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>
             )}
-          </div>
-        )}
-
+            {t('spoolbuddy.settings.checkUpdates', 'Check for Updates')}
+          </button>
+        ) : null}
       </div>
 
-      {/* SSH Setup */}
-      <div className="bg-zinc-800 rounded-lg p-4">
+      {/* SSH Setup — collapsible */}
+      <div className="bg-zinc-800 rounded-lg p-3">
         <button
           onClick={() => setSSHExpanded(!sshExpanded)}
-          className="w-full flex justify-between items-center text-sm"
+          className="w-full flex justify-between items-center text-xs"
         >
-          <span className="font-semibold text-zinc-300">
+          <span className="font-medium text-zinc-400">
             {t('spoolbuddy.settings.sshSetup', 'SSH Setup')}
           </span>
           <svg
-            className={`w-4 h-4 text-zinc-400 transition-transform ${sshExpanded ? 'rotate-180' : ''}`}
+            className={`w-3 h-3 text-zinc-500 transition-transform ${sshExpanded ? 'rotate-180' : ''}`}
             fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}
           >
             <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
@@ -700,26 +662,26 @@ function UpdatesTab({ device }: { device: SpoolBuddyDevice }) {
         </button>
 
         {sshExpanded && (
-          <div className="mt-3 space-y-3">
-            <p className="text-xs text-zinc-400">
-              {t('spoolbuddy.settings.sshDescription', 'Add this public key to your SpoolBuddy device to enable remote updates. Run the install script with --ssh-pubkey or add it to ~/.ssh/authorized_keys on the device.')}
+          <div className="mt-2 space-y-2">
+            <p className="text-xs text-zinc-500">
+              {t('spoolbuddy.settings.sshDescription', 'SSH key is deployed automatically. For manual setup, add this key to ~/.ssh/authorized_keys on the device.')}
             </p>
             {sshKeyData?.public_key ? (
               <div className="relative">
-                <pre className="bg-zinc-900 rounded p-3 text-xs text-zinc-300 font-mono break-all whitespace-pre-wrap">
+                <pre className="bg-zinc-900 rounded p-2 text-[10px] text-zinc-400 font-mono break-all whitespace-pre-wrap">
                   {sshKeyData.public_key}
                 </pre>
                 <button
                   onClick={copyKey}
-                  className="absolute top-2 right-2 px-2 py-1 rounded text-xs bg-zinc-700 text-zinc-300 hover:bg-zinc-600 transition-colors"
+                  className="absolute top-1 right-1 px-1.5 py-0.5 rounded text-[10px] bg-zinc-700 text-zinc-300 hover:bg-zinc-600 transition-colors"
                 >
                   {copied ? t('common.copied', 'Copied!') : t('common.copy', 'Copy')}
                 </button>
               </div>
             ) : (
-              <div className="text-xs text-zinc-500 italic">
-                {t('spoolbuddy.settings.sshKeyLoading', 'Loading SSH key...')}
-              </div>
+              <span className="text-[10px] text-zinc-500 italic">
+                {t('spoolbuddy.settings.sshKeyLoading', 'Loading...')}
+              </span>
             )}
           </div>
         )}

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-CJ-drcFM.css


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


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


+ 2 - 2
static/index.html

@@ -23,8 +23,8 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-BSgCAUZw.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-CJ-drcFM.css">
+    <script type="module" crossorigin src="/assets/index-DrqpB7p-.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-Dzv7xI7m.css">
   </head>
   <body>
     <div id="root"></div>

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