fix: install/uninstall UI state, progress bar, auto-Tor hidden services
- Install progress bar replaces action buttons (no overlay) - Hide status badge during install/uninstall - Uninstall keeps progress state until container disappears from WebSocket - Uninstall RPC timeout increased to 660s (Bitcoin UTXO flush) - Installing apps appear in My Apps immediately as placeholders - Auto-configure Tor hidden service for every app on install - Widen Tor module visibility for install hooks - Only clear stale install entries on error status Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -517,7 +517,7 @@ class RPCClient {
|
||||
return this.call({
|
||||
method: 'package.uninstall',
|
||||
params: { id },
|
||||
timeout: 120000,
|
||||
timeout: 660000, // Bitcoin Knots needs up to 600s for UTXO flush
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -57,16 +57,21 @@ export const useServerStore = defineStore('server', () => {
|
||||
}
|
||||
}
|
||||
// Clear installingApps entries for apps that vanished from backend data
|
||||
// (container was removed, install failed and was cleaned up, etc.)
|
||||
// Only clean up entries that have errored — active installs may take minutes to pull images
|
||||
for (const [appId] of installingApps.value) {
|
||||
if (packages && !(appId in packages)) {
|
||||
const entry = installingApps.value.get(appId)
|
||||
if (entry && entry.attempt > 30) {
|
||||
// App has been "installing" for 30+ seconds but backend doesn't know about it — failed
|
||||
if (entry && entry.status === 'error') {
|
||||
installingApps.value.delete(appId)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear uninstallingApps when the container disappears from backend data
|
||||
for (const appId of uninstallingApps.value) {
|
||||
if (packages && !(appId in packages)) {
|
||||
uninstallingApps.value.delete(appId)
|
||||
}
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
function setInstallProgress(appId: string, progress: Partial<InstallProgress> & { id: string; title: string }) {
|
||||
|
||||
@@ -207,7 +207,7 @@ const packages = computed(() => {
|
||||
id: appId,
|
||||
title: progress.title,
|
||||
version: '',
|
||||
description: { short: progress.message, long: '' },
|
||||
description: { short: '', long: '' },
|
||||
'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '',
|
||||
'support-site': '', 'marketing-site': '', 'donation-url': null,
|
||||
},
|
||||
|
||||
@@ -12,19 +12,7 @@
|
||||
>
|
||||
<!-- Installing indicator — no overlay, just replaces action buttons at bottom -->
|
||||
|
||||
<!-- Uninstalling overlay -->
|
||||
<div
|
||||
v-if="isUninstalling"
|
||||
class="absolute inset-0 z-20 flex items-center justify-center bg-black/70 backdrop-blur-sm rounded-xl"
|
||||
>
|
||||
<div class="flex items-center gap-3 text-white/90">
|
||||
<svg class="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span class="text-sm font-medium">{{ t('common.uninstalling') }}...</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Uninstalling — handled in button area below, no overlay -->
|
||||
|
||||
<!-- Uninstall Icon (not for web-only apps) -->
|
||||
<button
|
||||
@@ -66,7 +54,7 @@
|
||||
{{ description }}
|
||||
</p>
|
||||
|
||||
<div v-if="!isInstalling && pkg.state !== 'installing'" class="flex items-center gap-2">
|
||||
<div v-if="!isInstalling && !isUninstalling && pkg.state !== 'installing'" class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium"
|
||||
:class="getStatusClass(pkg.state, pkg.health, pkg['exit-code'])"
|
||||
@@ -107,7 +95,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isUninstalling" class="mt-4 flex gap-2">
|
||||
<!-- Uninstalling progress — replaces action buttons -->
|
||||
<div v-else-if="isUninstalling" class="mt-4">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<svg class="animate-spin h-3 w-3 text-red-400" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span class="text-xs text-red-300">{{ t('common.uninstalling') }}...</span>
|
||||
</div>
|
||||
<div class="mt-1.5 w-full h-1.5 bg-white/10 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-red-400/60 rounded-full animate-pulse w-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="mt-4 flex gap-2">
|
||||
<!-- Launch -->
|
||||
<button
|
||||
v-if="canLaunch(pkg)"
|
||||
|
||||
@@ -75,12 +75,13 @@ export function useAppsActions() {
|
||||
try {
|
||||
uninstallingApps.add(appId)
|
||||
await store.uninstallPackage(appId)
|
||||
// State update comes via WebSocket — no manual deletion needed
|
||||
// Don't clear uninstallingApps here — let the WebSocket watcher clear it
|
||||
// when the container actually disappears from backend data
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('Failed to uninstall app:', err)
|
||||
showActionError(`Failed to uninstall: ${err instanceof Error ? err.message : 'Unknown error'}`)
|
||||
} finally {
|
||||
uninstallingApps.delete(appId)
|
||||
} finally {
|
||||
uninstalling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user