// Server store — computed server state and RPC action proxies import { defineStore } from 'pinia' import { computed, ref, watch } from 'vue' import { rpcClient } from '../api/rpc-client' import { useSyncStore } from './sync' import type { InstallProgress } from '../views/marketplace/marketplaceData' export const useServerStore = defineStore('server', () => { const sync = useSyncStore() // Global install/uninstall tracking — persists across navigation const installingApps = ref>(new Map()) const uninstallingApps = ref>(new Set()) // Watch WebSocket data for real install progress — runs globally, not just on Marketplace page watch(() => sync.packages, (packages) => { if (!packages) return for (const [appId, pkg] of Object.entries(packages)) { if ((pkg.state as string) === 'installing' || (pkg.state as string) === 'updating') { // Backend confirms it's installing — update or create tracking entry if (!installingApps.value.has(appId)) { installingApps.value.set(appId, { id: appId, title: pkg.manifest?.title || appId, status: 'downloading', progress: 0, message: 'Installing...', attempt: 0, }) } const progress = pkg['install-progress'] if (progress) { const current = installingApps.value.get(appId)! const pct = progress.size > 0 ? Math.round((progress.downloaded / progress.size) * 100) : 0 const downloadedMB = (progress.downloaded / (1024 * 1024)).toFixed(1) const totalMB = (progress.size / (1024 * 1024)).toFixed(1) let message = 'Downloading...' if (progress.size > 1024 && pct < 100) { message = `Downloading: ${downloadedMB} / ${totalMB} MB (${pct}%)` } else if (pct >= 100 || (progress.size > 0 && progress.downloaded >= progress.size)) { message = 'Installing package...' } installingApps.value.set(appId, { ...current, status: pct >= 100 ? 'installing' : 'downloading', progress: Math.min(pct, 95), message, }) } } else if (installingApps.value.has(appId)) { const state = pkg.state as string // Only clear when app is fully running or definitively stopped — not during 'starting' transition if (state === 'running' || state === 'stopped' || state === 'exited') { installingApps.value.delete(appId) } } } // Clear installingApps entries for apps that vanished from backend data // Only clean up entries that have errored — active installs may take minutes to pull images for (const [appId] of installingApps.value) { if (packages && !(appId in packages)) { const entry = installingApps.value.get(appId) if (entry && entry.status === 'error') { installingApps.value.delete(appId) } } } // Clear uninstallingApps when the container disappears from backend data for (const appId of uninstallingApps.value) { if (packages && !(appId in packages)) { uninstallingApps.value.delete(appId) } } }, { deep: true }) function setInstallProgress(appId: string, progress: Partial & { id: string; title: string }) { const existing = installingApps.value.get(appId) installingApps.value.set(appId, { status: 'downloading', progress: 0, message: 'Preparing...', attempt: 0, ...existing, ...progress, }) } function clearInstallProgress(appId: string) { installingApps.value.delete(appId) } function isInstalling(appId: string): boolean { return installingApps.value.has(appId) } // Computed — derived from sync store's data const serverName = computed(() => sync.serverInfo?.name || 'Archipelago') const isRestarting = computed(() => sync.serverInfo?.['status-info']?.restarting || false) const isShuttingDown = computed(() => sync.serverInfo?.['status-info']?.['shutting-down'] || false) const isOffline = computed(() => !sync.isConnected || isRestarting.value || isShuttingDown.value) // Package actions. install/uninstall/update are async on the backend: // the RPC returns immediately with { status: 'installing'|'removing'|'updating', // package_id } after flipping state, and the real work runs in a spawn. // Progress is streamed via the WebSocket state push, not the RPC response. async function installPackage( id: string, marketplaceUrl: string, version: string, ): Promise<{ status: string; package_id: string }> { return rpcClient.installPackage(id, marketplaceUrl, version) } async function uninstallPackage(id: string): Promise<{ status: string; package_id: string }> { 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) } async function updatePackage(id: string): Promise<{ status: string; package_id: string }> { return rpcClient.updatePackage(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) } function updateServerName(name: string) { if (sync.data?.['server-info']) { sync.data['server-info'].name = name } } return { // Computed serverName, isRestarting, isShuttingDown, isOffline, // Install/uninstall tracking (global, persists across navigation) installingApps, setInstallProgress, clearInstallProgress, isInstalling, uninstallingApps, // Actions installPackage, uninstallPackage, startPackage, stopPackage, restartPackage, updatePackage, updateServer, restartServer, shutdownServer, getMetrics, getMarketplace, updateServerName, } })