Refactor Indeehub integration and enhance deployment documentation

- Updated Indeehub references throughout the codebase, changing the name from "IndeedHub" to "Indeehub" for consistency.
- Implemented a virtual app structure for Indeehub, allowing it to open an external URL without requiring a container.
- Enhanced deployment scripts and documentation to clarify SSH access and password management for Indeehub.
- Improved error handling and retry logic in various components to ensure better user experience during onboarding and app interactions.
- Updated CSS for visual enhancements and added new buttons for improved navigation in the AppLauncherOverlay.
This commit is contained in:
Dorian
2026-03-01 17:53:18 +00:00
parent 2c15311ab6
commit 7a05e11834
34 changed files with 877 additions and 163 deletions
+13 -5
View File
@@ -6,10 +6,18 @@ let audioContext: AudioContext | null = null
let introAudio: HTMLAudioElement | null = null
let introGain: GainNode | null = null
/** Get AudioContext - only returns existing. Create via resumeAudioContext() after user gesture. */
function getContext(): AudioContext | null {
return audioContext
}
/** Create AudioContext if needed (call only from user gesture - click/tap/key) */
function ensureContext(): AudioContext | null {
if (audioContext) return audioContext
try {
audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
const Ctx = window.AudioContext || (window as any).webkitAudioContext
if (!Ctx) return null
audioContext = new Ctx()
return audioContext
} catch {
return null
@@ -44,21 +52,21 @@ export function playLoopStart() {
.catch(() => {})
}
/** Resume audio context (call on first user interaction to unlock autoplay) */
/** Resume audio context - MUST be called from user gesture (click/tap/key). Creates context if needed. */
export function resumeAudioContext() {
const ctx = getContext()
const ctx = ensureContext()
if (ctx?.state === 'suspended') {
ctx.resume().catch(() => {})
}
}
/** Start intro loop - Cosmic Updrift (free royalty) */
/** Start intro loop - Cosmic Updrift. Only works after resumeAudioContext() (user gesture). */
export function startSynthwave() {
const ctxOrNull = getContext()
if (!ctxOrNull) return
try {
if (ctxOrNull.state === 'suspended') ctxOrNull.resume()
if (ctxOrNull.state === 'suspended') ctxOrNull.resume().catch(() => {})
} catch {
return
}
+20 -10
View File
@@ -1,20 +1,30 @@
/**
* 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'
export async function isOnboardingComplete(): Promise<boolean> {
try {
return await rpcClient.isOnboardingComplete()
} catch {
return localStorage.getItem('neode_onboarding_complete') === '1'
async function callWithRetry<T>(fn: () => Promise<T>, maxRetries = 3): 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)
if (!isRetryable || i === maxRetries - 1) return null
await new Promise((r) => setTimeout(r, 800 * (i + 1)))
}
}
return null
}
export async function isOnboardingComplete(): Promise<boolean> {
const result = await callWithRetry(() => rpcClient.isOnboardingComplete(), 2)
if (result !== null) return result
return localStorage.getItem('neode_onboarding_complete') === '1'
}
export async function completeOnboarding(): Promise<void> {
try {
await rpcClient.completeOnboarding()
} finally {
localStorage.setItem('neode_onboarding_complete', '1')
}
await callWithRetry(() => rpcClient.completeOnboarding(), 3)
localStorage.setItem('neode_onboarding_complete', '1')
}