● 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.
This commit is contained in:
maziggy
2026-03-23 14:36:58 +01:00
parent b83a99ebab
commit 0e44e149bd
6 changed files with 100 additions and 138 deletions
+1 -1
View File
@@ -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")
@@ -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>
</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>
)}
{/* 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 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>
<span className="text-green-300 text-xs">
{device.update_message || t('spoolbuddy.settings.updateWaiting', 'Updating...')}
</span>
</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>
{/* Update 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>}
{/* 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>
) : !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 one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -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>