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
+2 -2
View File
@@ -643,8 +643,8 @@ function launchApp() {
prod: 'http://localhost:8103' // Self-hosted splash screen
},
'indeedhub': {
dev: 'http://localhost:7777',
prod: 'http://localhost:7777' // Containerized indeehub prototype
dev: 'https://archipelago.indeehub.studio',
prod: 'https://archipelago.indeehub.studio'
},
// Dummy apps - replace with real URLs when packaged
'bitcoin': {
+2 -2
View File
@@ -245,8 +245,8 @@ function launchApp(id: string) {
prod: 'http://localhost:8103' // Self-hosted splash screen
},
'indeedhub': {
dev: 'http://localhost:7777',
prod: 'http://localhost:7777' // Containerized indeehub prototype
dev: 'https://archipelago.indeehub.studio',
prod: 'https://archipelago.indeehub.studio'
}
}
+8 -2
View File
@@ -701,8 +701,14 @@ function getIconPath(iconName: string): string[] {
}
async function handleLogout() {
await store.logout()
router.push('/login')
try {
await store.logout()
} catch {
/* proceed to login regardless */
}
router.push('/login').catch(() => {
window.location.href = '/login'
})
}
// Track previous route for transition logic
+34 -12
View File
@@ -136,7 +136,7 @@ import AnimatedLogo from '@/components/AnimatedLogo.vue'
import { useAppStore } from '../stores/app'
import { useLoginTransitionStore } from '../stores/loginTransition'
import { rpcClient } from '../api/rpc-client'
import { startSynthwave, stopSynthwave, playLoginSuccessWhoosh, playPop } from '@/composables/useLoginSounds'
import { resumeAudioContext, startSynthwave, stopSynthwave, playLoginSuccessWhoosh, playPop } from '@/composables/useLoginSounds'
const router = useRouter()
const store = useAppStore()
@@ -155,17 +155,25 @@ const isSetupMode = computed(() => {
})
onMounted(async () => {
if (sessionStorage.getItem('archipelago_from_splash') !== '1') {
startSynthwave()
} else {
sessionStorage.removeItem('archipelago_from_splash')
const fromSplash = sessionStorage.getItem('archipelago_from_splash') === '1'
if (fromSplash) sessionStorage.removeItem('archipelago_from_splash')
const unlock = () => {
if (!fromSplash) {
resumeAudioContext()
startSynthwave()
}
document.removeEventListener('click', unlock)
document.removeEventListener('touchstart', unlock)
document.removeEventListener('keydown', unlock)
}
document.addEventListener('click', unlock, { once: true })
document.addEventListener('touchstart', unlock, { once: true })
document.addEventListener('keydown', unlock, { once: true })
if (isSetupMode.value) {
try {
const result = await rpcClient.call<boolean>({ method: 'auth.isSetup', params: {} })
const result = await rpcClient.call<boolean>({ method: 'auth.isSetup', params: {}, timeout: 8000 })
isSetup.value = Boolean(result)
} catch (err) {
console.error('Failed to check setup status:', err)
} catch {
isSetup.value = false
}
} else {
@@ -207,10 +215,17 @@ async function handleSetup() {
loginTransition.setJustLoggedIn(true)
await store.login(password.value)
await new Promise(r => setTimeout(r, 520))
router.replace({ name: 'home' })
await router.replace({ name: 'home' }).catch(() => {
window.location.href = '/dashboard'
})
} catch (err) {
whooshAway.value = false
error.value = err instanceof Error ? err.message : 'Setup failed. Please try again.'
const msg = err instanceof Error ? err.message : ''
if (/502|503|Bad Gateway|timeout|fetch|network/i.test(msg)) {
error.value = 'Server is starting up. Please try again in a moment.'
} else {
error.value = msg || 'Setup failed. Please try again.'
}
startSynthwave()
} finally {
loading.value = false
@@ -237,10 +252,17 @@ async function handleLogin() {
playLoginSuccessWhoosh()
loginTransition.setJustLoggedIn(true)
await new Promise(r => setTimeout(r, 520))
router.replace({ name: 'home' })
await router.replace({ name: 'home' }).catch(() => {
window.location.href = '/dashboard'
})
} catch (err) {
whooshAway.value = false
error.value = err instanceof Error ? err.message : 'Login failed. Please check your password.'
const msg = err instanceof Error ? err.message : ''
if (/502|503|Bad Gateway|timeout|fetch|network/i.test(msg)) {
error.value = 'Server is starting up. Please try again in a moment.'
} else {
error.value = msg || 'Login failed. Please check your password.'
}
startSynthwave()
} finally {
loading.value = false
+11
View File
@@ -851,6 +851,17 @@ function getCuratedAppList() {
dockerImage: 'docker.io/fedimint/fedimintd:v0.10.0',
manifestUrl: null,
repoUrl: 'https://github.com/fedimint/fedimint'
},
{
id: 'indeedhub',
title: 'Indeehub',
version: '0.1.0',
description: 'Bitcoin documentary streaming platform. Stream God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology.',
icon: 'https://indeehub.studio/favicon.ico',
author: 'Indeehub Team',
dockerImage: 'localhost/indeedhub:latest',
manifestUrl: null,
repoUrl: 'https://github.com/indeedhub/indeedhub'
}
]
}
+2 -2
View File
@@ -139,11 +139,11 @@ async function downloadBackup() {
}
function proceed() {
router.push('/onboarding/verify')
router.push('/onboarding/verify').catch(() => {})
}
function skipForNow() {
router.push('/onboarding/verify')
router.push('/onboarding/verify').catch(() => {})
}
</script>
+17 -14
View File
@@ -101,30 +101,33 @@ async function generateDid() {
isGenerating.value = true
errorMessage.value = ''
try {
const { did, pubkey } = await rpcClient.getNodeDid()
generatedDid.value = did
localStorage.setItem('neode_did', did)
localStorage.setItem('neode_did_state', JSON.stringify({ did, kid: pubkey }))
} catch (err) {
errorMessage.value = err instanceof Error ? err.message : 'Failed to load node identity'
// Fallback: show placeholder if backend unavailable (e.g. mock mode)
if (!generatedDid.value) {
generatedDid.value = 'did:key:z6Mk... (connect to server)'
for (let attempt = 0; attempt < 3; attempt++) {
try {
const { did, pubkey } = await rpcClient.getNodeDid()
generatedDid.value = did
localStorage.setItem('neode_did', did)
localStorage.setItem('neode_did_state', JSON.stringify({ did, kid: pubkey }))
break
} catch (err) {
errorMessage.value = err instanceof Error ? err.message : 'Server unavailable. Retrying...'
if (attempt < 2) {
await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)))
} else {
generatedDid.value = 'did:key:z6Mk... (connect to server)'
}
}
} finally {
isGenerating.value = false
}
isGenerating.value = false
}
function proceed() {
if (generatedDid.value && !generatedDid.value.includes('...')) {
router.push('/onboarding/backup')
router.push('/onboarding/backup').catch(() => {})
}
}
function skipForNow() {
router.push('/onboarding/backup')
router.push('/onboarding/backup').catch(() => {})
}
</script>
+1 -1
View File
@@ -60,7 +60,7 @@ import { useRouter } from 'vue-router'
const router = useRouter()
function goToLogin() {
router.push('/login')
router.push('/login').catch(() => {})
}
</script>
+1 -1
View File
@@ -38,7 +38,7 @@ import { useRouter } from 'vue-router'
const router = useRouter()
function goToOptions() {
router.push('/onboarding/path')
router.push('/onboarding/path').catch(() => {})
}
</script>
+6 -2
View File
@@ -99,8 +99,12 @@ function selectOption(option: string) {
async function proceed() {
if (selected.value) {
await completeOnboarding()
router.push('/login')
try {
await completeOnboarding()
} catch {
/* localStorage fallback ensures onboarding is marked complete */
}
router.push('/login').catch(() => {})
}
}
</script>
+2 -5
View File
@@ -139,15 +139,12 @@ function toggleOption(option: string) {
}
function proceed() {
// Save selected options to localStorage
localStorage.setItem('neode_selected_paths', JSON.stringify(selectedOptions.value))
// Don't mark onboarding complete yet - continue to DID creation
router.push('/onboarding/did')
router.push('/onboarding/did').catch(() => {})
}
function skipForNow() {
// Skip to DID creation
router.push('/onboarding/did')
router.push('/onboarding/did').catch(() => {})
}
</script>
+12 -4
View File
@@ -113,13 +113,21 @@ function generateMockSignature(): string {
}
async function proceed() {
await completeOnboarding()
router.push('/login')
try {
await completeOnboarding()
} catch {
/* localStorage fallback ensures we can proceed */
}
router.push('/login').catch(() => {})
}
async function skipForNow() {
await completeOnboarding()
router.push('/login')
try {
await completeOnboarding()
} catch {
/* localStorage fallback ensures we can proceed */
}
router.push('/login').catch(() => {})
}
</script>
+5 -3
View File
@@ -322,16 +322,18 @@ onMounted(() => {
isTransitioning.value = false
isGlitching.value = false
document.body.classList.add('video-background-active')
if (sessionStorage.getItem('archipelago_from_splash') !== '1') {
startSynthwave()
}
const unlock = () => {
resumeAudioContext()
if (sessionStorage.getItem('archipelago_from_splash') !== '1') {
startSynthwave()
}
document.removeEventListener('click', unlock)
document.removeEventListener('touchstart', unlock)
document.removeEventListener('keydown', unlock)
}
document.addEventListener('click', unlock, { once: true })
document.addEventListener('touchstart', unlock, { once: true })
document.addEventListener('keydown', unlock, { once: true })
}
})
</script>
+12 -3
View File
@@ -20,11 +20,20 @@ const router = useRouter()
onMounted(async () => {
const devMode = import.meta.env.VITE_DEV_MODE
if (devMode === 'setup' || devMode === 'existing') {
router.replace('/login')
router.replace('/login').catch(() => {})
return
}
const seenOnboarding = await isOnboardingComplete()
router.replace(seenOnboarding ? '/login' : '/onboarding/intro')
let seenOnboarding = false
try {
const result = await Promise.race([
isOnboardingComplete(),
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 5000)),
])
seenOnboarding = result
} catch {
seenOnboarding = localStorage.getItem('neode_onboarding_complete') === '1'
}
router.replace(seenOnboarding ? '/login' : '/onboarding/intro').catch(() => {})
})
</script>