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