archy/neode-ui/src/stores/server.ts
Dorian a8c6a36cd1 fix: netavark GLIBC mismatch in ISO, container adopt, app updates
ISO build no longer copies netavark from build host (Debian 13/GLIBC 2.41)
which broke container networking on Debian 12 targets. Rootfs already
installs netavark from Debian 12 repos — just configure the backend.

Install RPC now adopts existing containers (from first-boot) instead of
erroring on duplicates. Container scanner extracts real versions from
image tags and detects available updates against pinned versions.

Frontend shows update button with version info when updates are available.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 11:47:35 +02:00

185 lines
6.1 KiB
TypeScript

// 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<Map<string, InstallProgress>>(new Map())
const uninstallingApps = ref<Set<string>>(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<InstallProgress> & { 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
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)
}
async function updatePackage(id: string): Promise<{ status: string }> {
return rpcClient.updatePackage(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<Record<string, unknown>> {
return rpcClient.getMetrics()
}
// Marketplace actions
async function getMarketplace(url: string): Promise<Record<string, unknown>> {
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,
}
})