From 3d986b81a04a8d21948a9ac0de321f19345c4b7c Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 15 Jul 2026 11:34:06 +0100 Subject: [PATCH] fix(ui): login keeps the intro video until the first login; rotation after MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The login page sometimes swapped to a random bg-intro-N still on load, breaking the seamless handoff from the intro video. Now: until anyone has logged in / set a password (neode_first_login_done, stamped by every Login success path incl. TOTP and setup), /login keeps the VIDEO background — one continuous shot from the splash. From the second login onward the lock screen rotates through the static backgrounds as designed, and if the static path is ever hit pre-first-login it pins to bg-intro.jpg (the video's end frame) instead of rotating. Co-Authored-By: Claude Fable 5 --- neode-ui/src/views/Login.vue | 8 +++++- neode-ui/src/views/OnboardingWrapper.vue | 32 +++++++++++++----------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/neode-ui/src/views/Login.vue b/neode-ui/src/views/Login.vue index 013e5914..65396110 100644 --- a/neode-ui/src/views/Login.vue +++ b/neode-ui/src/views/Login.vue @@ -426,6 +426,8 @@ async function handleSetup() { playLoginSuccessWhoosh() loginTransition.setJustCompletedOnboarding(true) loginTransition.setJustLoggedIn(true) + // First password setup counts as the first login (video → static rotation). + try { localStorage.setItem('neode_first_login_done', '1') } catch { /* ignore */ } await new Promise(r => setTimeout(r, 520)) await router.replace(loginRedirectTo.value).catch(() => { window.location.href = loginRedirectTo.value @@ -538,13 +540,17 @@ async function handleTotpVerify() { /** A login right after the onboarding wizard (flag set by OnboardingDone) * gets the FULL dashboard entrance — zoom + oomph — even though it's a * regular password login (e.g. the demo). Subsequent logins stay - * deliberately low-key (justLoggedIn only). */ + * deliberately low-key (justLoggedIn only). + * Also stamps neode_first_login_done: the login page keeps the intro VIDEO + * background until someone has logged in once (OnboardingWrapper reads it + * to pick video vs the rotating static backgrounds). */ function consumeOnboardingFinale() { try { if (sessionStorage.getItem('archy_onboarding_finale') === '1') { sessionStorage.removeItem('archy_onboarding_finale') loginTransition.setJustCompletedOnboarding(true) } + localStorage.setItem('neode_first_login_done', '1') } catch { /* ignore */ } } diff --git a/neode-ui/src/views/OnboardingWrapper.vue b/neode-ui/src/views/OnboardingWrapper.vue index 0a51e7fb..e2964bf4 100644 --- a/neode-ui/src/views/OnboardingWrapper.vue +++ b/neode-ui/src/views/OnboardingWrapper.vue @@ -86,22 +86,22 @@ 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(() => { + +// Check if current route should use video background. +// The FIRST login (nobody has logged in / set a password yet) keeps the VIDEO +// running so the splash → login handoff is one continuous shot; only after a +// successful login does /login switch to the rotating static backgrounds. +// Login.vue sets neode_first_login_done on every successful login. +const hasLoggedInBefore = () => { try { - return localStorage.getItem('neode_onboarding_complete') === '1' + return localStorage.getItem('neode_first_login_done') === '1' } catch { return false } -}) - -// Check if current route should use video background +} const useVideoBackground = computed(() => { if (!videoBackgroundRoutes.includes(route.path)) return false - if (route.path === '/login' && onboardingDone.value) return false + if (route.path === '/login' && hasLoggedInBefore()) return false return true }) @@ -122,9 +122,12 @@ const routeBackgrounds: Record = { '/login': 'bg-intro.jpg' // Video loops from splash (same as intro) } -// Rotate the login background so the lock screen doesn't look -// identical on every logout. Cycles through bg-intro-1..6 using a -// counter persisted to localStorage so subsequent visits advance. +// Until the user has logged in (or set their password) once, the login +// background stays PINNED to the same still the intro video ends on — the +// video → login handoff only reads as one continuous shot when both show the +// identical frame. From the second login onward the lock screen rotates +// through bg-intro-1..6 for variety (counter persisted in localStorage). +// Login.vue sets neode_first_login_done on every successful login. const LOGIN_BACKGROUNDS = [ 'bg-intro-1.webp', 'bg-intro-2.jpg', @@ -135,13 +138,14 @@ const LOGIN_BACKGROUNDS = [ ] function pickNextLoginBackground(): string { try { + if (localStorage.getItem('neode_first_login_done') !== '1') return 'bg-intro.jpg' const raw = localStorage.getItem('neode_login_bg_idx') const prev = raw !== null ? parseInt(raw, 10) : -1 const next = (Number.isFinite(prev) ? prev + 1 : 0) % LOGIN_BACKGROUNDS.length localStorage.setItem('neode_login_bg_idx', String(next)) return LOGIN_BACKGROUNDS[next]! } catch { - return LOGIN_BACKGROUNDS[Math.floor(Math.random() * LOGIN_BACKGROUNDS.length)]! + return 'bg-intro.jpg' } } const loginBackground = ref(pickNextLoginBackground())