release(v1.7.33-alpha): onboarding/login UX fixes + PWA cache bust

- useOnboarding.ts: prefer the backend over localStorage when checking
  onboarding completion. The old order (localStorage first) meant any
  browser that had ever onboarded a node would treat every new fresh
  node as already-onboarded and skip the wizard, dumping the user
  straight at the inline set-password form. Backend is now authoritative;
  localStorage stays as the offline fallback.
- OnboardingWrapper.vue: skip the intro video on `/login` once
  `neode_onboarding_complete` is set. Returning logged-out users now
  get the static lock-screen background + glitch overlay instead of
  replaying the full intro on every logout.
- RootRedirect.vue: when the health check fails, only show the full
  BootScreen if the node was never onboarded. For already-onboarded
  nodes (i.e. an OTA-update blip), keep the spinner and poll the
  health endpoint every 2s for up to 60s before falling back to the
  boot screen. Fixes the "fake boot loader" / "server starting up"
  screens flashing on every successful update.
- loginTransition store: new `justCompletedOnboarding` flag distinct
  from `justLoggedIn`. Set true only by the inline setup-password
  flow (handleSetup). Dashboard.vue branches on it: full glitch+zoom
  reveal for the post-onboarding entry, quick zoom + welcome typing
  on every other login (no triple glitch flashes, ~1.2s vs 8s).
- vite.config.ts: bump assets cache from `assets-cache-v2` to
  `assets-cache-v3` so service workers running the previous bundle
  invalidate their cache and pick up the new UI cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-04-22 04:45:33 -04:00
parent 974fce5870
commit aa0677be57
10 changed files with 87 additions and 9 deletions

2
core/Cargo.lock generated
View File

@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "archipelago"
version = "1.7.32-alpha"
version = "1.7.33-alpha"
dependencies = [
"anyhow",
"archipelago-container",

View File

@ -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"]

View File

@ -19,11 +19,13 @@ async function callWithRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T
}
export async function isOnboardingComplete(): Promise<boolean> {
// 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<void> {

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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(() => {

View File

@ -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

View File

@ -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
})
</script>

View File

@ -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