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:
+18
-19
@@ -15,6 +15,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
let isWsSubscribed = false
|
||||
let sessionValidated = false
|
||||
|
||||
// Computed
|
||||
const serverInfo = computed(() => data.value?.['server-info'])
|
||||
@@ -33,13 +34,16 @@ export const useAppStore = defineStore('app', () => {
|
||||
try {
|
||||
await rpcClient.login(password)
|
||||
isAuthenticated.value = true
|
||||
sessionValidated = true
|
||||
localStorage.setItem('neode-auth', 'true')
|
||||
|
||||
// Connect WebSocket after successful login
|
||||
await connectWebSocket()
|
||||
|
||||
// Initialize data
|
||||
// Initialize data structure immediately so dashboard can render
|
||||
await initializeData()
|
||||
|
||||
// Connect WebSocket in background - don't block login flow
|
||||
connectWebSocket().catch((err) => {
|
||||
console.warn('[Store] WebSocket connection failed after login, will retry:', err)
|
||||
})
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Login failed'
|
||||
throw err
|
||||
@@ -55,10 +59,10 @@ export const useAppStore = defineStore('app', () => {
|
||||
console.error('Logout error:', err)
|
||||
} finally {
|
||||
isAuthenticated.value = false
|
||||
sessionValidated = false
|
||||
localStorage.removeItem('neode-auth')
|
||||
data.value = null
|
||||
isWsSubscribed = false
|
||||
// Disconnect WebSocket on logout (this prevents reconnection)
|
||||
wsClient.disconnect()
|
||||
isConnected.value = false
|
||||
isReconnecting.value = false
|
||||
@@ -182,48 +186,42 @@ export const useAppStore = defineStore('app', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Check session validity on app load
|
||||
// Check session validity on app load or stale auth
|
||||
async function checkSession(): Promise<boolean> {
|
||||
console.log('[Store] Checking session...')
|
||||
|
||||
if (!localStorage.getItem('neode-auth')) {
|
||||
console.log('[Store] No auth token found')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to make an authenticated request to verify session
|
||||
console.log('[Store] Validating session with backend...')
|
||||
await rpcClient.call({ method: 'server.echo', params: { message: 'ping' } })
|
||||
isAuthenticated.value = true
|
||||
console.log('[Store] Session valid, reconnecting WebSocket...')
|
||||
sessionValidated = true
|
||||
|
||||
// Initialize data structure first
|
||||
await initializeData()
|
||||
|
||||
// Connect WebSocket - don't wait for it, let it reconnect in background
|
||||
// This ensures the page loads quickly even if WebSocket is slow
|
||||
connectWebSocket().catch((err) => {
|
||||
console.warn('[Store] WebSocket reconnection failed, will retry automatically:', err)
|
||||
// The WebSocket client will handle retries automatically
|
||||
console.warn('[Store] WebSocket reconnection failed, will retry:', err)
|
||||
isReconnecting.value = true
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('[Store] Session check failed:', err)
|
||||
// Session invalid, clear auth
|
||||
localStorage.removeItem('neode-auth')
|
||||
isAuthenticated.value = false
|
||||
sessionValidated = false
|
||||
isWsSubscribed = false
|
||||
isConnected.value = false
|
||||
isReconnecting.value = false
|
||||
// Disconnect WebSocket if session is invalid
|
||||
wsClient.disconnect()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function needsSessionValidation(): boolean {
|
||||
return isAuthenticated.value && !sessionValidated
|
||||
}
|
||||
|
||||
// Package actions
|
||||
async function installPackage(id: string, marketplaceUrl: string, version: string): Promise<string> {
|
||||
return rpcClient.installPackage(id, marketplaceUrl, version)
|
||||
@@ -293,6 +291,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
login,
|
||||
logout,
|
||||
checkSession,
|
||||
needsSessionValidation,
|
||||
connectWebSocket,
|
||||
installPackage,
|
||||
uninstallPackage,
|
||||
|
||||
@@ -16,14 +16,45 @@ function mustOpenInNewTab(url: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** Rewrite to same-origin proxy so iframe can embed (nginx strips X-Frame-Options) */
|
||||
/** Port → proxy path for apps (nginx strips X-Frame-Options) */
|
||||
const PORT_TO_PROXY: Record<string, string> = {
|
||||
'81': '/app/nginx-proxy-manager/',
|
||||
'3000': '/app/grafana/',
|
||||
'3001': '/app/uptime-kuma/',
|
||||
'8080': '/app/endurain/',
|
||||
'8081': '/app/lnd/',
|
||||
'8082': '/app/vaultwarden/',
|
||||
'8083': '/app/filebrowser/',
|
||||
'8085': '/app/nextcloud/',
|
||||
'8096': '/app/jellyfin/',
|
||||
'8123': '/app/homeassistant/',
|
||||
'8240': '/app/tailscale/',
|
||||
'8334': '/app/bitcoin-ui/',
|
||||
'8888': '/app/searxng/',
|
||||
'9000': '/app/portainer/',
|
||||
'9001': '/app/penpot/',
|
||||
'9980': '/app/onlyoffice/',
|
||||
'11434': '/app/ollama/',
|
||||
'2283': '/app/immich/',
|
||||
'23000': '/app/btcpay/',
|
||||
'2342': '/app/photoprism/',
|
||||
'4080': '/app/mempool/',
|
||||
'50002': '/app/electrs/',
|
||||
'8175': '/app/fedimint/',
|
||||
}
|
||||
|
||||
/** Rewrite to same-origin proxy so iframe can embed (avoids mixed content on HTTPS) */
|
||||
function toEmbeddableUrl(url: string): string {
|
||||
try {
|
||||
const u = new URL(url)
|
||||
const origin = window.location.origin
|
||||
// Only Vaultwarden and Penpot support subpath proxy; Nextcloud/Immich open in new tab
|
||||
if (u.port === '8082') return `${origin}/app/vaultwarden/`
|
||||
if (u.port === '9001') return `${origin}/app/penpot/`
|
||||
const proxyPath = PORT_TO_PROXY[u.port]
|
||||
const sameHost = u.hostname === window.location.hostname
|
||||
const needsProxy = window.location.protocol === 'https:' && u.protocol === 'http:'
|
||||
// Use proxy when: (a) mixed content, or (b) vaultwarden/penpot always (subpath required)
|
||||
if (proxyPath && sameHost && (needsProxy || u.port === '8082' || u.port === '9001')) {
|
||||
return `${origin}${proxyPath}`
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
@@ -37,12 +68,13 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
|
||||
let previousActiveElement: HTMLElement | null = null
|
||||
|
||||
function open(payload: { url: string; title: string; openInNewTab?: boolean }) {
|
||||
const embeddableUrl = toEmbeddableUrl(payload.url)
|
||||
if (payload.openInNewTab || mustOpenInNewTab(payload.url)) {
|
||||
window.open(payload.url, '_blank', 'noopener,noreferrer')
|
||||
window.open(embeddableUrl, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
previousActiveElement = (document.activeElement as HTMLElement) || null
|
||||
url.value = toEmbeddableUrl(payload.url)
|
||||
url.value = embeddableUrl
|
||||
title.value = payload.title
|
||||
isOpen.value = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user