archy/neode-ui/src/composables/useOnboarding.ts

79 lines
3.1 KiB
TypeScript
Raw Normal View History

/**
* Onboarding state - backend is authoritative.
* "Unknown" (backend unreachable) must NEVER default to false
* that would falsely send an already-onboarded user back through
* the intro after a browser clear / update / reboot.
*/
import { rpcClient } from '@/api/rpc-client'
async function callWithRetry<T>(fn: () => Promise<T>, maxRetries = 5): Promise<T | null> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn()
} catch (e) {
const msg = e instanceof Error ? e.message : ''
const isRetryable = /502|503|504|timeout|fetch|network|abort/i.test(msg)
if (!isRetryable || i === maxRetries - 1) return null
// Exponential-ish backoff: 500, 1000, 2000, 4000, 8000 (capped)
const delay = Math.min(500 * Math.pow(2, i), 8000)
await new Promise((r) => setTimeout(r, delay))
}
}
return null
}
/**
* Returns true/false if the backend gave a definitive answer, null if
* the backend is unreachable. Callers MUST handle null explicitly
* do not coerce to boolean without thinking about the consequences.
*/
export async function checkOnboardingStatus(): Promise<boolean | null> {
const result = await callWithRetry(() => rpcClient.isOnboardingComplete(), 5)
if (result !== null) {
if (result) {
try { localStorage.setItem('neode_onboarding_complete', '1') } catch {}
} else {
try { localStorage.removeItem('neode_onboarding_complete') } catch {}
}
}
return result
}
/**
* Boolean-only variant for places that genuinely cannot wait.
* Backend answer wins; on backend-unreachable, trusts a prior
* localStorage cache (set by a past successful check on THIS node).
* Returns false only when both the backend and the cache agree
* or when the cache is empty on a genuinely fresh install.
*
* Prefer checkOnboardingStatus() where possible so the caller can
* distinguish "confirmed fresh install" from "can't reach backend".
*/
export async function isOnboardingComplete(): Promise<boolean> {
const result = await checkOnboardingStatus()
if (result !== null) return result
// Backend unreachable — trust the local cache. If the cache says
// we're onboarded, we almost certainly are (this browser saw a
// prior backend 'true' and re-seeded the flag). If the cache is
// empty, we genuinely don't know; returning false here is the
// last-resort fallback, and the calling views should additionally
// keep polling the backend instead of treating this as gospel.
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>
2026-04-22 04:45:33 -04:00
return localStorage.getItem('neode_onboarding_complete') === '1'
}
export async function completeOnboarding(): Promise<void> {
await callWithRetry(() => rpcClient.completeOnboarding(), 3)
localStorage.setItem('neode_onboarding_complete', '1')
localStorage.removeItem('neode_onboarding_step')
}
/** Save current onboarding step so refresh resumes where user left off */
export function saveOnboardingStep(step: string): void {
localStorage.setItem('neode_onboarding_step', step)
}
/** Get the last saved onboarding step, or 'intro' if none */
export function getSavedOnboardingStep(): string {
return localStorage.getItem('neode_onboarding_step') || 'intro'
}