release(v1.7.38-alpha): onboarding auto-heal + silent returning logins + app-store trim
- auth.rs now infers onboarding-complete from setup_complete + password_hash so nodes stop bouncing users through the intro wizard after browser clear / update / reboot; the flag self-heals to disk on next check - frontend: "backend uncertain" no longer defaults to /onboarding/intro — useOnboarding returns null + callers poll / retry instead of flashing the wizard - login sounds (synthwave, welcome voice, pop, whoosh, oomph) gated by isFirstInstallPhase(); typing sounds unaffected - removed FIPS app, Nostr Relay, Nostr VPN, Routstr, Penpot from catalog, frontend config, Rust AppMetadata + install dispatch + install_penpot_stack; docker/fips-ui + docker/nostr-vpn-ui + apps/penpot dirs and 5 icons deleted; 15 image versions deleted from tx1138, .168, gitea-local registries (.160 Gitea was 502 at release time — follow-up) - AIUI baked into frontend release tarball via demo/aiui/; deploy-to-target falls back to demo/aiui/ when the AIUI sibling checkout is missing - prebuild hook syncs app-catalog/catalog.json → public/catalog.json so the two copies can no longer drift (was the source of the "apps still visible" bug — public/ had stale data) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
cfc98c600e
commit
36a6101026
@@ -1,7 +1,28 @@
|
||||
/**
|
||||
* Login screen audio: intro loop (MP3) + transition sounds.
|
||||
*
|
||||
* First-install vs returning-user gate: the synthwave loop, welcome
|
||||
* voice, pop/whoosh/oomph transitions exist for the first-boot cinematic
|
||||
* moment. After the user has completed onboarding we silence all of
|
||||
* them — every subsequent login should be quiet. Typing sounds are
|
||||
* exempt and continue to play regardless.
|
||||
*/
|
||||
|
||||
/** True when the node has not yet completed onboarding — i.e. we're
|
||||
* still in the first-install cinematic. Reads the localStorage cache
|
||||
* set by useOnboarding (which is re-seeded from the backend on each
|
||||
* successful check), so this stays correct after a browser clear
|
||||
* once the onboarding-complete probe runs. Sound calls that fire
|
||||
* before that probe completes will fall through silent on an already-
|
||||
* onboarded node — which is exactly what we want. */
|
||||
function isFirstInstallPhase(): boolean {
|
||||
try {
|
||||
return localStorage.getItem('neode_onboarding_complete') !== '1'
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
let audioContext: AudioContext | null = null
|
||||
let introAudio: HTMLAudioElement | null = null
|
||||
let introGain: GainNode | null = null
|
||||
@@ -30,6 +51,7 @@ const LOOP_START_URL = '/assets/audio/loop-start.mp3'
|
||||
/** Play loop-start when transitioning from typing intro to Welcome Noderunner, as the intro music comes in.
|
||||
* Uses Web Audio API so it plays after context is resumed (user gesture). */
|
||||
export function playLoopStart() {
|
||||
if (!isFirstInstallPhase()) return
|
||||
const ctx = getContext()
|
||||
if (!ctx) return
|
||||
try {
|
||||
@@ -62,6 +84,7 @@ export function resumeAudioContext() {
|
||||
|
||||
/** Start intro loop - Cosmic Updrift. Only works after resumeAudioContext() (user gesture). */
|
||||
export function startSynthwave() {
|
||||
if (!isFirstInstallPhase()) return
|
||||
const ctxOrNull = getContext()
|
||||
if (!ctxOrNull) return
|
||||
|
||||
@@ -123,6 +146,7 @@ export function stopAllAudio() {
|
||||
|
||||
/** Pop sound - plays when intro initiator (tap to start) is pressed */
|
||||
export function playPop() {
|
||||
if (!isFirstInstallPhase()) return
|
||||
const audio = new Audio('/assets/audio/pop.mp3')
|
||||
audio.volume = 0.6
|
||||
audio.play().catch(() => {})
|
||||
@@ -130,6 +154,7 @@ export function playPop() {
|
||||
|
||||
/** Whoosh transition on successful login */
|
||||
export function playLoginSuccessWhoosh() {
|
||||
if (!isFirstInstallPhase()) return
|
||||
const woosh = new Audio('/assets/audio/woosh.mp3')
|
||||
woosh.volume = 0.5
|
||||
woosh.play().catch(() => {})
|
||||
@@ -169,6 +194,7 @@ const WELCOME_SPEECH_URL = '/assets/audio/welcome-noderunner.mp3'
|
||||
* ELEVENLABS_API_KEY=your_key node neode-ui/scripts/generate-welcome-speech.js
|
||||
* Browse sci-fi voices at elevenlabs.io/voice-library and set ELEVENLABS_VOICE_ID for custom voice. */
|
||||
export function playWelcomeNoderunnerSpeech() {
|
||||
if (!isFirstInstallPhase()) return
|
||||
const audio = new Audio(WELCOME_SPEECH_URL)
|
||||
audio.volume = 0.9
|
||||
audio.play().catch(() => {})
|
||||
@@ -226,6 +252,7 @@ export function playKeyboardTypingSound() {
|
||||
|
||||
/** Gaming-style boot thud - soft impact when dashboard loads */
|
||||
export function playDashboardLoadOomph() {
|
||||
if (!isFirstInstallPhase()) return
|
||||
const ctx = getContext()
|
||||
if (!ctx) return
|
||||
|
||||
|
||||
@@ -1,41 +1,63 @@
|
||||
/**
|
||||
* Onboarding state - prefers backend, falls back to localStorage for mock/offline.
|
||||
* Hardened: retries on 502/503, never blocks completion.
|
||||
* 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 = 3): Promise<T | null> {
|
||||
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|timeout|fetch|network/i.test(msg)
|
||||
const isRetryable = /502|503|504|timeout|fetch|network|abort/i.test(msg)
|
||||
if (!isRetryable || i === maxRetries - 1) return null
|
||||
await new Promise((r) => setTimeout(r, 800 * (i + 1)))
|
||||
// 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
|
||||
}
|
||||
|
||||
export async function isOnboardingComplete(): Promise<boolean> {
|
||||
// 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)
|
||||
/**
|
||||
* 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) {
|
||||
// Re-seed the localStorage cache so non-async consumers
|
||||
// (OnboardingWrapper's useVideoBackground computed, etc.) see the
|
||||
// right answer after the user clears site data on an already-
|
||||
// onboarded node.
|
||||
if (result) {
|
||||
try { localStorage.setItem('neode_onboarding_complete', '1') } catch {}
|
||||
} else {
|
||||
try { localStorage.removeItem('neode_onboarding_complete') } catch {}
|
||||
}
|
||||
return result
|
||||
}
|
||||
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.
|
||||
return localStorage.getItem('neode_onboarding_complete') === '1'
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user