release(v1.7.33-alpha): onboarding/login UX fixes + PWA cache bust
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 12m50s
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 12m50s
- 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:
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.32-alpha"
|
||||
version = "1.7.33-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
{
|
||||
"version": "1.7.32-alpha",
|
||||
"version": "1.7.33-alpha",
|
||||
"release_date": "2026-04-22",
|
||||
"changelog": [
|
||||
"Critical fix: the v1.7.31-alpha frontend package shipped with the wrong archive layout, which caused the web UI to return 403/500 after the update landed. v1.7.32-alpha ships the frontend correctly — nodes that got stuck on the 403 page will auto-recover on this update.",
|
||||
"Shutdown fix: updates no longer briefly show the archipelago service as 'Failed' in systemd. The old version was logging 'shut down cleanly' but leaving a background mDNS thread alive, so systemd would force-kill it 15 seconds later and mark the unit failed. The process now exits promptly after saving its state."
|
||||
"Onboarding fix: a fresh node would skip the full onboarding wizard and dump you straight at 'set password' if your browser had ever onboarded another node. The check now asks the actual node first instead of trusting browser memory.",
|
||||
"Lock screen fix: logging out used to replay the full intro video every single time. Now once you've onboarded, logging out drops you on the static lock-screen background — login is instant, no movie.",
|
||||
"Update fix: a brief network hiccup during an OTA update no longer triggers the full mock 'boot screen' animation on already-onboarded nodes. The page just shows a quiet spinner and reconnects when the backend is back. Boot screen is reserved for genuine fresh boots.",
|
||||
"Login animation: the glitchy zoom-and-flash reveal now only plays the very first time you reach the dashboard after onboarding. Every subsequent login gets a quick interface draw — fast, no glitches.",
|
||||
"PWA cache bumped so old browsers/devices reliably pick up new UI versions instead of serving stale cached assets after an update."
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"name": "archipelago",
|
||||
"current_version": "1.7.31-alpha",
|
||||
"new_version": "1.7.32-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.32-alpha/archipelago",
|
||||
"sha256": "f5c0d51a3235b7619ac5b71140abd07b04cc90555205a4c0416c8c8c4a9a4588",
|
||||
"size_bytes": 40791792
|
||||
"current_version": "1.7.32-alpha",
|
||||
"new_version": "1.7.33-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.33-alpha/archipelago",
|
||||
"sha256": "c75a226658cb8af7ecb4eff937cbc221bb2b1c93bf1dadd61c99b2f550376c8b",
|
||||
"size_bytes": 40793648
|
||||
},
|
||||
{
|
||||
"name": "archipelago-frontend-1.7.32-alpha.tar.gz",
|
||||
"current_version": "1.7.31-alpha",
|
||||
"new_version": "1.7.32-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.32-alpha/archipelago-frontend-1.7.32-alpha.tar.gz",
|
||||
"sha256": "1eb1deaf479538f0552f395fc1aea67b1a247ddef6bfbf436353ba1997eac1be",
|
||||
"size_bytes": 77008678
|
||||
"name": "archipelago-frontend-1.7.33-alpha.tar.gz",
|
||||
"current_version": "1.7.32-alpha",
|
||||
"new_version": "1.7.33-alpha",
|
||||
"download_url": "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.33-alpha/archipelago-frontend-1.7.33-alpha.tar.gz",
|
||||
"sha256": "d6fd4648046d4ea05d33ef56180afda80e118f6d655ba7d339a2135a0a28e838",
|
||||
"size_bytes": 77011007
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
BIN
releases/v1.7.33-alpha/archipelago
Executable file
BIN
releases/v1.7.33-alpha/archipelago
Executable file
Binary file not shown.
BIN
releases/v1.7.33-alpha/archipelago-frontend-1.7.33-alpha.tar.gz
Normal file
BIN
releases/v1.7.33-alpha/archipelago-frontend-1.7.33-alpha.tar.gz
Normal file
Binary file not shown.
Reference in New Issue
Block a user