diff --git a/neode-ui/src/App.vue b/neode-ui/src/App.vue index ba28cc80..db124e89 100644 --- a/neode-ui/src/App.vue +++ b/neode-ui/src/App.vue @@ -411,7 +411,13 @@ onMounted(async () => { // the initial navigation resolves asynchronously — so a deep-route load // (e.g. a direct /login visit) would masquerade as a root boot. The URL // bar is the only truthful source this early. - const bootPath = window.location.pathname + // + // The kiosk chromium opens /kiosk — a marker route whose beforeEnter + // stamps localStorage.kiosk and immediately redirects to '/'. That is a + // root boot, not a deep route: without normalizing it, the fresh-install + // kiosk never plays the typing intro (deep-route suppression regression). + const rawBootPath = window.location.pathname + const bootPath = rawBootPath === '/kiosk' ? '/' : rawBootPath // Public demo: every fresh boot at the root (first visit or a browser // refresh) starts with the typing splash for the full effect. In-session SPA // navigation never remounts App, and deep-route refreshes keep their place. @@ -424,7 +430,17 @@ onMounted(async () => { if (splashCandidate && onboardingComplete !== true) { try { const { checkOnboardingStatus } = await import('@/composables/useOnboarding') - onboardingComplete = await checkOnboardingStatus() + // Bound the pre-splash status check: its retry ladder can spend ~30s + // against a still-booting backend, and this await holds the black + // "!isReady" screen the whole time — exactly where the typing intro + // should be playing on a fresh kiosk boot. Unknown within 2.5s → let + // the splash play (a fresh install IS the slow-backend case; onboarded + // nodes answer in milliseconds, so their suppression path is intact). + // handleSplashComplete re-checks with full retries after the intro. + onboardingComplete = await Promise.race([ + checkOnboardingStatus(), + new Promise((resolve) => setTimeout(() => resolve(null), 2500)), + ]) } catch { onboardingComplete = localStorage.getItem('neode_onboarding_complete') === '1' ? true : null } @@ -524,14 +540,24 @@ function onShareToMeshMessage(ev: MessageEvent) { * Routes user directly to appropriate screen based on onboarding status (from backend) */ async function handleSplashComplete() { - showSplash.value = false - document.body.classList.add('splash-complete') - isReady.value = true sessionStorage.setItem('archipelago_from_splash', '1') + // Commit the destination route BEFORE revealing RouterView. Revealing + // first rendered whatever route was current — '/' → RootRedirect's + // spinner — for a beat between the intro's final frame and the + // onboarding/login screen actually mounting: a visible flash on every + // fresh install. Await the push so the first RouterView render is + // already the destination. + const reveal = () => { + showSplash.value = false + document.body.classList.add('splash-complete') + isReady.value = true + } + const devMode = import.meta.env.VITE_DEV_MODE if (devMode === 'setup' || devMode === 'existing') { - router.push('/login').catch(() => {}) + await router.push('/login').catch(() => {}) + reveal() return } @@ -541,7 +567,8 @@ async function handleSplashComplete() { { const { IS_DEMO } = await import('@/composables/useDemoIntro') if (IS_DEMO) { - router.push('/onboarding/intro').catch(() => {}) + await router.push('/onboarding/intro').catch(() => {}) + reveal() return } } @@ -550,27 +577,24 @@ async function handleSplashComplete() { const { checkOnboardingStatus } = await import('@/composables/useOnboarding') const seenOnboarding = await checkOnboardingStatus() if (seenOnboarding === true) { - router.push('/login').catch(() => {}) - return - } - if (seenOnboarding === false) { - router.push('/onboarding/intro').catch(() => {}) - return - } - // Backend unreachable after retries. Prefer the localStorage - // cache on THIS browser (if a prior successful check set it) — - // otherwise defer to RootRedirect which polls + retries rather - // than forcing an already-onboarded user through the wizard. - if (localStorage.getItem('neode_onboarding_complete') === '1') { - router.push('/login').catch(() => {}) + await router.push('/login').catch(() => {}) + } else if (seenOnboarding === false) { + await router.push('/onboarding/intro').catch(() => {}) + } else if (localStorage.getItem('neode_onboarding_complete') === '1') { + // Backend unreachable after retries. Prefer the localStorage + // cache on THIS browser (if a prior successful check set it) — + // otherwise defer to RootRedirect which polls + retries rather + // than forcing an already-onboarded user through the wizard. + await router.push('/login').catch(() => {}) } else { - router.push('/').catch(() => {}) + await router.push('/').catch(() => {}) } } catch { // Do NOT default to /onboarding/intro here. RootRedirect has retry // + polling + boot-screen handling; let it decide. - router.push('/').catch(() => {}) + await router.push('/').catch(() => {}) } + reveal() }