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>
185 lines
6.1 KiB
TypeScript
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,
|
|
}
|
|
})
|