fix: polish UX error handling across views (FINAL-01)

- AppDetails: replace alert() with dismissible toast, add error feedback
  for start/stop/restart/uninstall actions
- GoalDetail: add error toast for install failures instead of silent catch
- Apps: add loading skeleton when WebSocket data hasn't arrived yet
- Add appDetails.noLaunchUrl i18n key

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-11 17:33:42 +00:00
co-authored by Claude Opus 4.6
parent 2d0ac12a6a
commit 6cea156df6
5 changed files with 86 additions and 12 deletions
+34 -7
View File
@@ -448,6 +448,16 @@
</div>
</div>
</Transition>
<!-- Action error toast -->
<Transition name="fade">
<div v-if="actionError" class="fixed bottom-20 left-1/2 -translate-x-1/2 z-50 max-w-md w-full px-4" role="alert" aria-live="assertive">
<div class="bg-red-500/20 border border-red-500/40 backdrop-blur-sm rounded-lg px-4 py-3 text-red-200 text-sm flex items-center justify-between gap-3">
<span>{{ actionError }}</span>
<button @click="actionError = ''" :aria-label="t('apps.dismissError')" class="text-red-300 hover:text-white shrink-0">&times;</button>
</div>
</div>
</Transition>
</div>
</template>
@@ -574,6 +584,16 @@ const gatewayState = computed(() => {
return gw ? gw.state : 'not installed'
})
// Action error toast
const actionError = ref('')
let errorTimer: ReturnType<typeof setTimeout> | undefined
function showActionError(msg: string) {
actionError.value = msg
if (errorTimer) clearTimeout(errorTimer)
errorTimer = setTimeout(() => { actionError.value = '' }, 5000)
}
const uninstallModal = ref({
show: false,
appTitle: ''
@@ -783,8 +803,7 @@ function launchApp() {
const lanConfig = pkg.value.manifest.interfaces?.main?.['lan-config']
if (torAddress || lanConfig) {
// In development, just alert - in production would open the actual interface
alert(`Would launch ${pkg.value.manifest.title} interface`)
showActionError(t('appDetails.noLaunchUrl'))
}
}
@@ -792,7 +811,7 @@ async function startApp() {
try {
await store.startPackage(appId.value)
} catch (err) {
if (import.meta.env.DEV) console.error('Failed to start app:', err)
showActionError(`Failed to start: ${err instanceof Error ? err.message : 'Unknown error'}`)
}
}
@@ -800,7 +819,7 @@ async function stopApp() {
try {
await store.stopPackage(appId.value)
} catch (err) {
if (import.meta.env.DEV) console.error('Failed to stop app:', err)
showActionError(`Failed to stop: ${err instanceof Error ? err.message : 'Unknown error'}`)
}
}
@@ -808,7 +827,7 @@ async function restartApp() {
try {
await store.restartPackage(appId.value)
} catch (err) {
if (import.meta.env.DEV) console.error('Failed to restart app:', err)
showActionError(`Failed to restart: ${err instanceof Error ? err.message : 'Unknown error'}`)
}
}
@@ -827,8 +846,7 @@ async function confirmUninstall() {
await store.uninstallPackage(appId.value)
router.push('/dashboard/apps').catch(() => {})
} catch (err) {
if (import.meta.env.DEV) console.error('Failed to uninstall app:', err)
alert(t('common.error'))
showActionError(`Failed to uninstall: ${err instanceof Error ? err.message : 'Unknown error'}`)
}
}
@@ -893,4 +911,13 @@ function getStatusDotClass(state: PackageState): string {
transform: scale(0.95);
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>