From aa0677be5723bb7dd81c3317d55ca1f92433dbc0 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 22 Apr 2026 04:45:33 -0400 Subject: [PATCH] release(v1.7.33-alpha): onboarding/login UX fixes + PWA cache bust - useOnboarding.ts: prefer the backend over localStorage when checking onboarding completion. The old order (localStorage first) meant any browser that had ever onboarded a node would treat every new fresh node as already-onboarded and skip the wizard, dumping the user straight at the inline set-password form. Backend is now authoritative; localStorage stays as the offline fallback. - OnboardingWrapper.vue: skip the intro video on `/login` once `neode_onboarding_complete` is set. Returning logged-out users now get the static lock-screen background + glitch overlay instead of replaying the full intro on every logout. - RootRedirect.vue: when the health check fails, only show the full BootScreen if the node was never onboarded. For already-onboarded nodes (i.e. an OTA-update blip), keep the spinner and poll the health endpoint every 2s for up to 60s before falling back to the boot screen. Fixes the "fake boot loader" / "server starting up" screens flashing on every successful update. - loginTransition store: new `justCompletedOnboarding` flag distinct from `justLoggedIn`. Set true only by the inline setup-password flow (handleSetup). Dashboard.vue branches on it: full glitch+zoom reveal for the post-onboarding entry, quick zoom + welcome typing on every other login (no triple glitch flashes, ~1.2s vs 8s). - vite.config.ts: bump assets cache from `assets-cache-v2` to `assets-cache-v3` so service workers running the previous bundle invalidate their cache and pick up the new UI cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) --- core/Cargo.lock | 2 +- core/archipelago/Cargo.toml | 2 +- neode-ui/src/composables/useOnboarding.ts | 8 +++--- .../stores/__tests__/loginTransition.test.ts | 9 +++++++ neode-ui/src/stores/loginTransition.ts | 13 ++++++++++ neode-ui/src/views/Dashboard.vue | 17 +++++++++++- neode-ui/src/views/Login.vue | 1 + neode-ui/src/views/OnboardingWrapper.vue | 16 +++++++++++- neode-ui/src/views/RootRedirect.vue | 26 ++++++++++++++++++- neode-ui/vite.config.ts | 2 +- 10 files changed, 87 insertions(+), 9 deletions(-) diff --git a/core/Cargo.lock b/core/Cargo.lock index 1c648364..1da20db5 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "archipelago" -version = "1.7.32-alpha" +version = "1.7.33-alpha" dependencies = [ "anyhow", "archipelago-container", diff --git a/core/archipelago/Cargo.toml b/core/archipelago/Cargo.toml index edc724e3..28e858db 100644 --- a/core/archipelago/Cargo.toml +++ b/core/archipelago/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "archipelago" -version = "1.7.32-alpha" +version = "1.7.33-alpha" edition = "2021" description = "Archipelago Bitcoin Node OS - Native backend" authors = ["Archipelago Team"] diff --git a/neode-ui/src/composables/useOnboarding.ts b/neode-ui/src/composables/useOnboarding.ts index b20e9156..4c70acd3 100644 --- a/neode-ui/src/composables/useOnboarding.ts +++ b/neode-ui/src/composables/useOnboarding.ts @@ -19,11 +19,13 @@ async function callWithRetry(fn: () => Promise, maxRetries = 3): Promise { - // localStorage is set on completion and survives backend restarts/resets - if (localStorage.getItem('neode_onboarding_complete') === '1') return true + // Prefer the backend — localStorage gets stale across nodes (a + // browser that onboarded node A would otherwise treat fresh node B + // as already-onboarded and skip the wizard entirely). Only fall + // back to localStorage if the backend is unreachable. const result = await callWithRetry(() => rpcClient.isOnboardingComplete(), 2) if (result !== null) return result - return false + return localStorage.getItem('neode_onboarding_complete') === '1' } export async function completeOnboarding(): Promise { diff --git a/neode-ui/src/stores/__tests__/loginTransition.test.ts b/neode-ui/src/stores/__tests__/loginTransition.test.ts index 089986e3..0eb66cc3 100644 --- a/neode-ui/src/stores/__tests__/loginTransition.test.ts +++ b/neode-ui/src/stores/__tests__/loginTransition.test.ts @@ -10,10 +10,19 @@ describe('useLoginTransitionStore', () => { it('starts with all flags false', () => { const store = useLoginTransitionStore() expect(store.justLoggedIn).toBe(false) + expect(store.justCompletedOnboarding).toBe(false) expect(store.pendingWelcomeTyping).toBe(false) expect(store.startWelcomeTyping).toBe(false) }) + it('setJustCompletedOnboarding updates justCompletedOnboarding', () => { + const store = useLoginTransitionStore() + store.setJustCompletedOnboarding(true) + expect(store.justCompletedOnboarding).toBe(true) + store.setJustCompletedOnboarding(false) + expect(store.justCompletedOnboarding).toBe(false) + }) + it('setJustLoggedIn updates justLoggedIn', () => { const store = useLoginTransitionStore() store.setJustLoggedIn(true) diff --git a/neode-ui/src/stores/loginTransition.ts b/neode-ui/src/stores/loginTransition.ts index c85d71b4..c1004000 100644 --- a/neode-ui/src/stores/loginTransition.ts +++ b/neode-ui/src/stores/loginTransition.ts @@ -4,6 +4,13 @@ import { ref } from 'vue' /** Signals that we just logged in - Dashboard uses this for zoom + oomph */ export const useLoginTransitionStore = defineStore('loginTransition', () => { const justLoggedIn = ref(false) + /** + * True only when the user just finished the onboarding wizard + * (first password setup), as distinct from a regular re-login. + * Dashboard uses this to decide whether to play the full glitchy + * reveal vs just a quick interface-draw. + */ + const justCompletedOnboarding = ref(false) /** Show empty welcome block until typing starts (hide static text) */ const pendingWelcomeTyping = ref(false) /** Trigger welcome typing on Home - set true after dashboard animation finishes */ @@ -13,6 +20,10 @@ export const useLoginTransitionStore = defineStore('loginTransition', () => { justLoggedIn.value = value } + function setJustCompletedOnboarding(value: boolean) { + justCompletedOnboarding.value = value + } + function setPendingWelcomeTyping(value: boolean) { pendingWelcomeTyping.value = value } @@ -24,6 +35,8 @@ export const useLoginTransitionStore = defineStore('loginTransition', () => { return { justLoggedIn, setJustLoggedIn, + justCompletedOnboarding, + setJustCompletedOnboarding, pendingWelcomeTyping, setPendingWelcomeTyping, startWelcomeTyping, diff --git a/neode-ui/src/views/Dashboard.vue b/neode-ui/src/views/Dashboard.vue index 35cf90a9..571d80cb 100644 --- a/neode-ui/src/views/Dashboard.vue +++ b/neode-ui/src/views/Dashboard.vue @@ -264,10 +264,13 @@ watch(() => route.path, (newPath) => { onMounted(() => { previousRoutePath = route.path document.body.classList.add('dashboard-active') - if (loginTransition.justLoggedIn) { + if (loginTransition.justCompletedOnboarding) { + // Full glitchy reveal — only on the very first dashboard entry + // right after onboarding (one-time event, persists in feel). playDashboardLoadOomph() showZoomIn.value = true loginTransition.setPendingWelcomeTyping(true) + loginTransition.setJustCompletedOnboarding(false) loginTransition.setJustLoggedIn(false) const triggerRevealGlitch = () => { isGlitching.value = true @@ -281,6 +284,18 @@ onMounted(() => { loginTransition.setStartWelcomeTyping(true) loginTransition.setPendingWelcomeTyping(false) }, 4000) + } else if (loginTransition.justLoggedIn) { + // Regular re-login — quick interface draw, no triple glitch flashes. + // Just the zoom-in for a short beat, then welcome typing fires fast. + playDashboardLoadOomph() + showZoomIn.value = true + loginTransition.setPendingWelcomeTyping(true) + loginTransition.setJustLoggedIn(false) + scheduledTimeout(() => { showZoomIn.value = false }, 1200) + scheduledTimeout(() => { + loginTransition.setStartWelcomeTyping(true) + loginTransition.setPendingWelcomeTyping(false) + }, 600) } window.addEventListener('keydown', handleKioskShortcuts) diff --git a/neode-ui/src/views/Login.vue b/neode-ui/src/views/Login.vue index ce2b17fd..12ca0062 100644 --- a/neode-ui/src/views/Login.vue +++ b/neode-ui/src/views/Login.vue @@ -408,6 +408,7 @@ async function handleSetup() { stopSynthwave() whooshAway.value = true playLoginSuccessWhoosh() + loginTransition.setJustCompletedOnboarding(true) loginTransition.setJustLoggedIn(true) await new Promise(r => setTimeout(r, 520)) await router.replace(loginRedirectTo.value).catch(() => { diff --git a/neode-ui/src/views/OnboardingWrapper.vue b/neode-ui/src/views/OnboardingWrapper.vue index 76e2546d..0b9f3da1 100644 --- a/neode-ui/src/views/OnboardingWrapper.vue +++ b/neode-ui/src/views/OnboardingWrapper.vue @@ -86,9 +86,23 @@ const videoBackgroundRoutes = ['/onboarding/intro', '/login'] // Login uses video when coming from splash, or static + glitch when direct const isLoginRoute = computed(() => route.path === '/login') +// True once onboarding is complete. Used to skip the intro video on +// the /login route so that returning (logged-out) users go straight +// to the screensaver-style static + glitch background instead of +// replaying the full intro every time. +const onboardingDone = computed(() => { + try { + return localStorage.getItem('neode_onboarding_complete') === '1' + } catch { + return false + } +}) + // Check if current route should use video background const useVideoBackground = computed(() => { - return videoBackgroundRoutes.includes(route.path) + if (!videoBackgroundRoutes.includes(route.path)) return false + if (route.path === '/login' && onboardingDone.value) return false + return true }) // Map each route to a specific background image diff --git a/neode-ui/src/views/RootRedirect.vue b/neode-ui/src/views/RootRedirect.vue index c3935b75..caac6831 100644 --- a/neode-ui/src/views/RootRedirect.vue +++ b/neode-ui/src/views/RootRedirect.vue @@ -129,7 +129,31 @@ onMounted(async () => { return } - // Server not ready — show boot screen (waiting for backend) + // Server not ready. The full BootScreen is meant for a genuine + // cold-start (fresh install), not for the brief blip during an + // OTA update where the backend restarts. If onboarding has already + // completed we just keep the spinner and retry until the server + // responds again. + const wasOnboardedBefore = localStorage.getItem('neode_onboarding_complete') === '1' + if (wasOnboardedBefore) { + log('server down + onboarded → polling without boot screen') + let retries = 0 + const maxRetries = 30 // 30 * 2s = 60s before giving up and showing boot screen + const poll = setInterval(async () => { + retries++ + if (await quickHealthCheck()) { + clearInterval(poll) + proceedToApp() + return + } + if (retries >= maxRetries) { + clearInterval(poll) + log('server still down after retries → falling back to boot screen') + showBootScreen.value = true + } + }, 2000) + return + } showBootScreen.value = true }) diff --git a/neode-ui/vite.config.ts b/neode-ui/vite.config.ts index dcd3167c..6c84b093 100644 --- a/neode-ui/vite.config.ts +++ b/neode-ui/vite.config.ts @@ -94,7 +94,7 @@ export default defineConfig({ urlPattern: /\/assets\/.*/i, handler: 'CacheFirst', options: { - cacheName: 'assets-cache-v2', + cacheName: 'assets-cache-v3', expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days