From 06186ab30c471482d8df0209cc805018eac99dab Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 16 Jul 2026 15:35:47 -0400 Subject: [PATCH] fix(intro): restore the typing intro on fresh kiosk installs + seamless splash-to-onboarding handoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three regressions of the fresh-install cinematic, all in App.vue: 1. The kiosk chromium opens /kiosk (a marker route that stamps localStorage.kiosk and redirects to /). The deep-route splash suppression added in PR #91 reads window.location.pathname, so the kiosk boot was classified as a deep route and the typing intro → "Welcome Noderunner" → onboarding sequence never played. Normalize /kiosk to / for the splash decision. 2. The pre-splash onboarding-status check runs a ~30s retry ladder; against a still-booting backend it held the black !isReady screen the whole time. Bound it with a 2.5s race — unknown means "play the intro" (a fresh install IS the slow-backend case; onboarded nodes answer in milliseconds so their suppression is unaffected). 3. handleSplashComplete revealed RouterView before routing, flashing RootRedirect's spinner at '/' between the intro's final frame and the onboarding screen. Commit the destination route first, then reveal. Co-Authored-By: Claude Fable 5 --- neode-ui/src/App.vue | 68 ++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 22 deletions(-) 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() }