diff --git a/loop/plan.md b/loop/plan.md index f3fcd91b..67163f46 100644 --- a/loop/plan.md +++ b/loop/plan.md @@ -390,13 +390,13 @@ - [x] **FINALDOC-03** — Finalize all Architecture Decision Records. Review and complete all ADRs. Add new ones for Year 3 decisions. Ensure every significant technical decision is documented. -- [ ] **FINALDOC-04** — Publish v0.95.0-rc2 release candidate. Tag, build ISOs, distribute for wider testing. **Acceptance**: RC2 published and distributed. +- [ ] **FINALDOC-04** — (BLOCKED: requires ISO build on server and distribution infrastructure — cannot complete from code alone) Publish v0.95.0-rc2 release candidate. Tag, build ISOs, distribute for wider testing. **Acceptance**: RC2 published and distributed. ### Q3 2028 (September -- November): v1.0 Release Preparation #### Sprint 33: Final Polish (Week 1-4) -- [ ] **FINAL-01** — Run final UX audit on every page. Complete UX review of all 20+ pages/views. Fix any remaining inconsistencies. Ensure loading states, error states, and empty states are all polished. **Acceptance**: UX audit passes with no critical issues. +- [x] **FINAL-01** — Run final UX audit on every page. Complete UX review of all 20+ pages/views. Fix any remaining inconsistencies. Ensure loading states, error states, and empty states are all polished. **Acceptance**: UX audit passes with no critical issues. - [ ] **FINAL-02** — Run final security audit. Complete security review of: all 80+ RPC endpoints, nginx configuration, container isolation, secrets management, session handling. Fix any findings. **Acceptance**: Zero critical/high findings. diff --git a/neode-ui/src/locales/en.json b/neode-ui/src/locales/en.json index 8ee9753b..50bee153 100644 --- a/neode-ui/src/locales/en.json +++ b/neode-ui/src/locales/en.json @@ -554,7 +554,8 @@ "notFoundTitle": "App Not Found", "notFoundMessage": "The requested application could not be found", "installed": "Installed", - "channels": "Channels" + "channels": "Channels", + "noLaunchUrl": "No launch URL available for this app yet" }, "containerDetails": { "back": "Back", diff --git a/neode-ui/src/views/AppDetails.vue b/neode-ui/src/views/AppDetails.vue index 70540812..b7686b2c 100644 --- a/neode-ui/src/views/AppDetails.vue +++ b/neode-ui/src/views/AppDetails.vue @@ -448,6 +448,16 @@ + + + + + @@ -574,6 +584,16 @@ const gatewayState = computed(() => { return gw ? gw.state : 'not installed' }) +// Action error toast +const actionError = ref('') +let errorTimer: ReturnType | 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; +} diff --git a/neode-ui/src/views/Apps.vue b/neode-ui/src/views/Apps.vue index 6f352206..e3ce06ce 100644 --- a/neode-ui/src/views/Apps.vue +++ b/neode-ui/src/views/Apps.vue @@ -16,8 +16,25 @@ /> + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
diff --git a/neode-ui/src/views/GoalDetail.vue b/neode-ui/src/views/GoalDetail.vue index 2331f52e..e73ecc9c 100644 --- a/neode-ui/src/views/GoalDetail.vue +++ b/neode-ui/src/views/GoalDetail.vue @@ -137,6 +137,16 @@
+ + + + +
@@ -159,6 +169,14 @@ const goalId = computed(() => route.params.goalId as string) const goal = computed(() => getGoalById(goalId.value)) const isInstalling = ref(false) +const actionError = ref('') +let errorTimer: ReturnType | undefined + +function showActionError(msg: string) { + actionError.value = msg + if (errorTimer) clearTimeout(errorTimer) + errorTimer = setTimeout(() => { actionError.value = '' }, 5000) +} const overallStatus = computed(() => goalStore.getGoalStatus(goalId.value)) @@ -252,7 +270,7 @@ async function installApp(step: GoalStep) { await appStore.installPackage(step.appId, '', 'latest') goalStore.completeStep(goalId.value, step.id) } catch (err) { - if (import.meta.env.DEV) console.error('[GoalDetail] Install failed:', err) + showActionError(`Install failed: ${err instanceof Error ? err.message : 'Unknown error'}`) } finally { isInstalling.value = false } @@ -278,3 +296,14 @@ function goBack() { router.push('/dashboard') } + +