fix(ui): play the intro on backend-confirmed fresh nodes despite stale browser flags
neode_intro_seen / neode_onboarding_complete are per-origin browser state: after a reinstall (or another node on a DHCP-recycled IP) they describe the previous node and muted a genuinely fresh install's intro. Root boots now always ask the backend; a confirmed-fresh answer plays the intro and clears the stale flag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b6468ebf3c
commit
20edd31abb
@ -424,10 +424,14 @@ onMounted(async () => {
|
|||||||
const { IS_DEMO } = await import('@/composables/useDemoIntro')
|
const { IS_DEMO } = await import('@/composables/useDemoIntro')
|
||||||
if (IS_DEMO && bootPath === '/') replayRequested = true
|
if (IS_DEMO && bootPath === '/') replayRequested = true
|
||||||
let onboardingComplete: boolean | null = localStorage.getItem('neode_onboarding_complete') === '1' ? true : null
|
let onboardingComplete: boolean | null = localStorage.getItem('neode_onboarding_complete') === '1' ? true : null
|
||||||
const splashCandidate = !seenIntro
|
// Root boots always ask the backend — even when this browser thinks it has
|
||||||
&& (fromBoot || (bootPath === '/' && import.meta.env.VITE_DEV_MODE !== 'boot'))
|
// seen the intro. Both `neode_intro_seen` and `neode_onboarding_complete`
|
||||||
|
// are per-origin browser state: after a reinstall (or another node coming
|
||||||
|
// up on a DHCP-recycled IP) they describe the PREVIOUS node and would mute
|
||||||
|
// a fresh install's intro / misroute it to login.
|
||||||
|
const splashCandidate = fromBoot || (bootPath === '/' && import.meta.env.VITE_DEV_MODE !== 'boot')
|
||||||
|
|
||||||
if (splashCandidate && onboardingComplete !== true) {
|
if (splashCandidate) {
|
||||||
try {
|
try {
|
||||||
const { checkOnboardingStatus } = await import('@/composables/useOnboarding')
|
const { checkOnboardingStatus } = await import('@/composables/useOnboarding')
|
||||||
// Bound the pre-splash status check: its retry ladder can spend ~30s
|
// Bound the pre-splash status check: its retry ladder can spend ~30s
|
||||||
@ -437,10 +441,17 @@ onMounted(async () => {
|
|||||||
// the splash play (a fresh install IS the slow-backend case; onboarded
|
// the splash play (a fresh install IS the slow-backend case; onboarded
|
||||||
// nodes answer in milliseconds, so their suppression path is intact).
|
// nodes answer in milliseconds, so their suppression path is intact).
|
||||||
// handleSplashComplete re-checks with full retries after the intro.
|
// handleSplashComplete re-checks with full retries after the intro.
|
||||||
onboardingComplete = await Promise.race([
|
const live = await Promise.race([
|
||||||
checkOnboardingStatus(),
|
checkOnboardingStatus(),
|
||||||
new Promise<null>((resolve) => setTimeout(() => resolve(null), 2500)),
|
new Promise<null>((resolve) => setTimeout(() => resolve(null), 2500)),
|
||||||
])
|
])
|
||||||
|
if (live !== null) onboardingComplete = live
|
||||||
|
if (live === false && seenIntro) {
|
||||||
|
// Backend-confirmed fresh node behind a browser with a stale flag —
|
||||||
|
// drop it so this boot (and every later one) plays the intro.
|
||||||
|
try { localStorage.removeItem('neode_intro_seen') } catch { /* noop */ }
|
||||||
|
seenIntro = false
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
onboardingComplete = localStorage.getItem('neode_onboarding_complete') === '1' ? true : null
|
onboardingComplete = localStorage.getItem('neode_onboarding_complete') === '1' ? true : null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,4 +38,50 @@ describe('shouldShowIntroSplash', () => {
|
|||||||
replayRequested: true,
|
replayRequested: true,
|
||||||
})).toBe(true)
|
})).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('a confirmed-fresh node plays the intro despite a stale per-origin seenIntro flag (reinstall / DHCP-recycled IP)', () => {
|
||||||
|
expect(shouldShowIntroSplash({
|
||||||
|
seenIntro: true,
|
||||||
|
routePath: '/',
|
||||||
|
fromBoot: false,
|
||||||
|
onboardingComplete: false,
|
||||||
|
})).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('a confirmed-fresh node plays the intro on the boot-screen handoff too', () => {
|
||||||
|
expect(shouldShowIntroSplash({
|
||||||
|
seenIntro: true,
|
||||||
|
routePath: '/login',
|
||||||
|
fromBoot: true,
|
||||||
|
onboardingComplete: false,
|
||||||
|
})).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('stale seenIntro still suppresses when the backend answer is unknown', () => {
|
||||||
|
expect(shouldShowIntroSplash({
|
||||||
|
seenIntro: true,
|
||||||
|
routePath: '/',
|
||||||
|
fromBoot: false,
|
||||||
|
onboardingComplete: null,
|
||||||
|
})).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('fresh node on a deep route without boot handoff stays suppressed', () => {
|
||||||
|
expect(shouldShowIntroSplash({
|
||||||
|
seenIntro: false,
|
||||||
|
routePath: '/onboarding/seed',
|
||||||
|
fromBoot: false,
|
||||||
|
onboardingComplete: false,
|
||||||
|
})).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('boot dev mode never root-boots into the intro', () => {
|
||||||
|
expect(shouldShowIntroSplash({
|
||||||
|
seenIntro: false,
|
||||||
|
routePath: '/',
|
||||||
|
fromBoot: false,
|
||||||
|
devMode: 'boot',
|
||||||
|
onboardingComplete: false,
|
||||||
|
})).toBe(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -10,10 +10,19 @@ export interface IntroSplashDecisionInput {
|
|||||||
|
|
||||||
export function shouldShowIntroSplash(input: IntroSplashDecisionInput): boolean {
|
export function shouldShowIntroSplash(input: IntroSplashDecisionInput): boolean {
|
||||||
if (input.replayRequested) return true
|
if (input.replayRequested) return true
|
||||||
|
|
||||||
|
const isDirectRoute = input.routePath !== '/'
|
||||||
|
// A node the backend CONFIRMS has never completed onboarding always gets
|
||||||
|
// the full intro on a root boot. `seenIntro` is per-origin browser state —
|
||||||
|
// after a reinstall (or a DHCP-recycled IP), the browser still carries the
|
||||||
|
// previous node's flag at the same origin, which silently muted the intro
|
||||||
|
// on genuinely fresh installs.
|
||||||
|
if (input.onboardingComplete === false && (input.fromBoot || (!isDirectRoute && input.devMode !== 'boot'))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if (input.seenIntro) return false
|
if (input.seenIntro) return false
|
||||||
if (input.onboardingComplete === true) return false
|
if (input.onboardingComplete === true) return false
|
||||||
|
|
||||||
const isDirectRoute = input.routePath !== '/'
|
|
||||||
if (input.fromBoot) return true
|
if (input.fromBoot) return true
|
||||||
if (input.devMode === 'boot') return false
|
if (input.devMode === 'boot') return false
|
||||||
return !isDirectRoute
|
return !isDirectRoute
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user