2026-01-24 22:59:20 +00:00

283 lines
8.4 KiB
TypeScript

// Main application store using Pinia
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { DataModel } from '../types/api'
import { wsClient, applyDataPatch } from '../api/websocket'
import { rpcClient } from '../api/rpc-client'
export const useAppStore = defineStore('app', () => {
// State
const data = ref<DataModel | null>(null)
const isAuthenticated = ref(localStorage.getItem('neode-auth') === 'true')
const isConnected = ref(false)
const isReconnecting = ref(false)
const isLoading = ref(false)
const error = ref<string | null>(null)
let isWsSubscribed = false
// Computed
const serverInfo = computed(() => data.value?.['server-info'])
const packages = computed(() => data.value?.['package-data'] || {})
const uiData = computed(() => data.value?.ui)
const serverName = computed(() => serverInfo.value?.name || 'Archipelago')
const isRestarting = computed(() => serverInfo.value?.['status-info']?.restarting || false)
const isShuttingDown = computed(() => serverInfo.value?.['status-info']?.['shutting-down'] || false)
const isOffline = computed(() => !isConnected.value || isRestarting.value || isShuttingDown.value)
// Actions
async function login(password: string): Promise<void> {
isLoading.value = true
error.value = null
try {
await rpcClient.login(password)
isAuthenticated.value = true
localStorage.setItem('neode-auth', 'true')
// Connect WebSocket after successful login
await connectWebSocket()
// Initialize data
await initializeData()
} catch (err) {
error.value = err instanceof Error ? err.message : 'Login failed'
throw err
} finally {
isLoading.value = false
}
}
async function logout(): Promise<void> {
try {
await rpcClient.logout()
} catch (err) {
console.error('Logout error:', err)
} finally {
isAuthenticated.value = 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
}
}
async function connectWebSocket(): Promise<void> {
try {
console.log('[Store] Connecting WebSocket...')
isReconnecting.value = true
// Don't create multiple subscriptions - check if already subscribed
if (!isWsSubscribed) {
// Subscribe to updates BEFORE connecting (so we catch initial data)
isWsSubscribed = true
wsClient.subscribe((update: any) => {
// Handle mock backend format: {type: 'initial', data: {...}}
if (update?.type === 'initial' && update?.data) {
console.log('[Store] Received initial data from mock backend')
data.value = update.data
isConnected.value = true
isReconnecting.value = false
}
// Handle real backend format: {rev: 0, data: {...}}
else if (update?.data && update?.rev !== undefined) {
console.log('[Store] Received dump from real backend at revision', update.rev)
data.value = update.data
isConnected.value = true
isReconnecting.value = false
}
// Handle patch updates (both backends)
else if (data.value && update?.patch) {
try {
console.log('[Store] Applying patch at revision', update.rev || 'unknown')
data.value = applyDataPatch(data.value, update.patch)
// Mark as connected once we receive any valid patch
if (!isConnected.value) {
isConnected.value = true
isReconnecting.value = false
}
} catch (err) {
console.error('[Store] Failed to apply WebSocket patch:', err)
}
}
})
}
// Now connect (or reconnect if already connected)
await wsClient.connect()
console.log('[Store] WebSocket connected')
} catch (err) {
console.error('[Store] WebSocket connection failed:', err)
// Don't mark as disconnected immediately - let reconnection logic handle it
// The WebSocket client will retry automatically
isReconnecting.value = true
// Don't throw - allow app to work without real-time updates
// The WebSocket will reconnect in the background
}
}
async function initializeData(): Promise<void> {
// Initialize with empty data structure
// The WebSocket will populate it with real data
data.value = {
'server-info': {
id: '',
version: '',
name: null,
pubkey: '',
'status-info': {
restarting: false,
'shutting-down': false,
updated: false,
'backup-progress': null,
'update-progress': null,
},
'lan-address': null,
unread: 0,
'wifi-ssids': [],
'zram-enabled': false,
},
'package-data': {},
ui: {
name: null,
'ack-welcome': '',
marketplace: {
'selected-hosts': [],
'known-hosts': {},
},
theme: 'dark',
},
}
}
// Check session validity on app load
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...')
// 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
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
isWsSubscribed = false
isConnected.value = false
isReconnecting.value = false
// Disconnect WebSocket if session is invalid
wsClient.disconnect()
return false
}
}
// Package actions
async function installPackage(id: string, marketplaceUrl: string, version: string): Promise<string> {
return rpcClient.installPackage(id, marketplaceUrl, version)
}
async function uninstallPackage(id: string): Promise<void> {
return rpcClient.uninstallPackage(id)
}
async function startPackage(id: string): Promise<void> {
return rpcClient.startPackage(id)
}
async function stopPackage(id: string): Promise<void> {
return rpcClient.stopPackage(id)
}
async function restartPackage(id: string): Promise<void> {
return rpcClient.restartPackage(id)
}
// Server actions
async function updateServer(marketplaceUrl: string): Promise<'updating' | 'no-updates'> {
return rpcClient.updateServer(marketplaceUrl)
}
async function restartServer(): Promise<void> {
return rpcClient.restartServer()
}
async function shutdownServer(): Promise<void> {
return rpcClient.shutdownServer()
}
async function getMetrics(): Promise<any> {
return rpcClient.getMetrics()
}
// Marketplace actions
async function getMarketplace(url: string): Promise<any> {
return rpcClient.getMarketplace(url)
}
async function sideloadPackage(manifest: any, icon: string): Promise<string> {
return rpcClient.sideloadPackage(manifest, icon)
}
return {
// State
data,
isAuthenticated,
isConnected,
isReconnecting,
isLoading,
error,
// Computed
serverInfo,
packages,
uiData,
serverName,
isRestarting,
isShuttingDown,
isOffline,
// Actions
login,
logout,
checkSession,
connectWebSocket,
installPackage,
uninstallPackage,
startPackage,
stopPackage,
restartPackage,
updateServer,
restartServer,
shutdownServer,
getMetrics,
getMarketplace,
sideloadPackage,
}
})