// 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(null) const isAuthenticated = ref(localStorage.getItem('neode-auth') === 'true') const isConnected = ref(false) const isReconnecting = ref(false) const isLoading = ref(false) const error = ref(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 { 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 { 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 { 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 { // 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 { 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 { return rpcClient.installPackage(id, marketplaceUrl, version) } async function uninstallPackage(id: string): Promise { return rpcClient.uninstallPackage(id) } async function startPackage(id: string): Promise { return rpcClient.startPackage(id) } async function stopPackage(id: string): Promise { return rpcClient.stopPackage(id) } async function restartPackage(id: string): Promise { return rpcClient.restartPackage(id) } // Server actions async function updateServer(marketplaceUrl: string): Promise<'updating' | 'no-updates'> { return rpcClient.updateServer(marketplaceUrl) } async function restartServer(): Promise { return rpcClient.restartServer() } async function shutdownServer(): Promise { return rpcClient.shutdownServer() } async function getMetrics(): Promise { return rpcClient.getMetrics() } // Marketplace actions async function getMarketplace(url: string): Promise { return rpcClient.getMarketplace(url) } async function sideloadPackage(manifest: any, icon: string): Promise { 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, } })