/** * Onboarding state - prefers backend, falls back to localStorage for mock/offline. * Hardened: retries on 502/503, never blocks completion. */ import { rpcClient } from '@/api/rpc-client' async function callWithRetry(fn: () => Promise, maxRetries = 3): Promise { for (let i = 0; i < maxRetries; i++) { try { return await fn() } catch (e) { const msg = e instanceof Error ? e.message : '' const isRetryable = /502|503|timeout|fetch|network/i.test(msg) if (!isRetryable || i === maxRetries - 1) return null await new Promise((r) => setTimeout(r, 800 * (i + 1))) } } return null } export async function isOnboardingComplete(): Promise { // 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 localStorage.getItem('neode_onboarding_complete') === '1' } export async function completeOnboarding(): Promise { 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' }