|
|
@@ -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>
|
|
|
)}
|