fix: netavark GLIBC mismatch in ISO, container adopt, app updates
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 13m24s
All checks were successful
Build Archipelago ISO (dev) / build-iso (push) Successful in 13m24s
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>
This commit is contained in:
@@ -545,6 +545,14 @@ class RPCClient {
|
||||
})
|
||||
}
|
||||
|
||||
async updatePackage(id: string): Promise<{ status: string }> {
|
||||
return this.call({
|
||||
method: 'package.update',
|
||||
params: { id },
|
||||
timeout: 660000, // Bitcoin Knots needs up to 600s for graceful shutdown
|
||||
})
|
||||
}
|
||||
|
||||
async getMarketplace(url: string): Promise<Record<string, unknown>> {
|
||||
return this.call({
|
||||
method: 'marketplace.get',
|
||||
|
||||
@@ -59,6 +59,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
startPackage: server.startPackage,
|
||||
stopPackage: server.stopPackage,
|
||||
restartPackage: server.restartPackage,
|
||||
updatePackage: server.updatePackage,
|
||||
updateServer: server.updateServer,
|
||||
restartServer: server.restartServer,
|
||||
shutdownServer: server.shutdownServer,
|
||||
|
||||
@@ -17,7 +17,7 @@ export const useServerStore = defineStore('server', () => {
|
||||
watch(() => sync.packages, (packages) => {
|
||||
if (!packages) return
|
||||
for (const [appId, pkg] of Object.entries(packages)) {
|
||||
if ((pkg.state as string) === 'installing') {
|
||||
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, {
|
||||
@@ -121,6 +121,10 @@ export const useServerStore = defineStore('server', () => {
|
||||
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)
|
||||
@@ -169,6 +173,7 @@ export const useServerStore = defineStore('server', () => {
|
||||
startPackage,
|
||||
stopPackage,
|
||||
restartPackage,
|
||||
updatePackage,
|
||||
updateServer,
|
||||
restartServer,
|
||||
shutdownServer,
|
||||
|
||||
@@ -74,6 +74,7 @@ export const PackageState = {
|
||||
Restoring: 'restoring-backup',
|
||||
Removing: 'removing',
|
||||
BackingUp: 'backing-up',
|
||||
Updating: 'updating',
|
||||
} as const
|
||||
|
||||
export type PackageState = typeof PackageState[keyof typeof PackageState]
|
||||
@@ -90,6 +91,7 @@ export interface PackageDataEntry {
|
||||
manifest: Manifest
|
||||
installed?: InstalledPackageDataEntry
|
||||
'install-progress'?: InstallProgress
|
||||
'available-update'?: string | null
|
||||
}
|
||||
|
||||
export interface Manifest {
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
@stop="stopApp"
|
||||
@restart="restartApp"
|
||||
@uninstall="uninstallApp"
|
||||
@update="updateApp"
|
||||
@channels="router.push('/dashboard/apps/lnd/channels')"
|
||||
/>
|
||||
|
||||
@@ -333,6 +334,14 @@ async function restartApp() {
|
||||
}
|
||||
}
|
||||
|
||||
async function updateApp() {
|
||||
try {
|
||||
await store.updatePackage(appId.value)
|
||||
} catch (err) {
|
||||
showActionError(`Failed to update: ${err instanceof Error ? err.message : 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
|
||||
function showUninstallModal() {
|
||||
if (!pkg.value) return
|
||||
uninstallModal.value = { show: true, appTitle: pkg.value.manifest.title }
|
||||
|
||||
@@ -26,6 +26,28 @@
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0">
|
||||
<!-- Update available -->
|
||||
<button
|
||||
v-if="pkg['available-update'] && pkg.state !== 'updating'"
|
||||
@click="$emit('update')"
|
||||
class="px-4 py-2.5 bg-orange-500/20 border border-orange-500/40 rounded-lg text-orange-200 text-sm font-medium hover:bg-orange-500/30 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Update to v{{ pkg['available-update'] }}
|
||||
</button>
|
||||
<!-- Updating in progress -->
|
||||
<span
|
||||
v-if="pkg.state === 'updating'"
|
||||
class="px-4 py-2.5 bg-orange-500/20 border border-orange-500/40 rounded-lg text-orange-200 text-sm font-medium flex items-center gap-2"
|
||||
>
|
||||
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Updating...
|
||||
</span>
|
||||
<button
|
||||
v-if="packageKey === 'lnd'"
|
||||
@click="$emit('channels')"
|
||||
@@ -130,6 +152,28 @@
|
||||
|
||||
<!-- Action Buttons (Auto Grid) -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<!-- Update available (mobile) -->
|
||||
<button
|
||||
v-if="pkg['available-update'] && pkg.state !== 'updating'"
|
||||
@click="$emit('update')"
|
||||
class="col-span-2 px-4 py-2.5 bg-orange-500/20 border border-orange-500/40 rounded-lg text-orange-200 text-sm font-medium hover:bg-orange-500/30 transition-colors flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Update to v{{ pkg['available-update'] }}
|
||||
</button>
|
||||
<!-- Updating in progress (mobile) -->
|
||||
<span
|
||||
v-if="pkg.state === 'updating'"
|
||||
class="col-span-2 px-4 py-2.5 bg-orange-500/20 border border-orange-500/40 rounded-lg text-orange-200 text-sm font-medium flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Updating...
|
||||
</span>
|
||||
<button
|
||||
v-if="canLaunch"
|
||||
@click="$emit('launch')"
|
||||
@@ -200,6 +244,7 @@ defineEmits<{
|
||||
stop: []
|
||||
restart: []
|
||||
uninstall: []
|
||||
update: []
|
||||
channels: []
|
||||
}>()
|
||||
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between py-2 border-b border-white/10">
|
||||
<span class="text-white/60 text-sm">{{ t('common.version') }}</span>
|
||||
<span class="text-white font-medium">{{ pkg.manifest.version }}</span>
|
||||
<div class="text-right">
|
||||
<span class="text-white font-medium">v{{ pkg.manifest.version }}</span>
|
||||
<span v-if="pkg['available-update']" class="text-orange-300 text-xs ml-2">
|
||||
v{{ pkg['available-update'] }} available
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="pkg.manifest.author" class="flex items-center justify-between py-2 border-b border-white/10">
|
||||
<span class="text-white/60 text-sm">{{ t('common.developer') }}</span>
|
||||
|
||||
@@ -125,6 +125,8 @@ export function getStatusClass(state: PackageState, health?: string | null, exit
|
||||
return 'bg-yellow-500/20 text-yellow-200 border border-yellow-500/30'
|
||||
case PackageState.Installing:
|
||||
return 'bg-blue-500/20 text-blue-200 border border-blue-500/30'
|
||||
case PackageState.Updating:
|
||||
return 'bg-orange-500/20 text-orange-200 border border-orange-500/30'
|
||||
default:
|
||||
return 'bg-gray-500/20 text-gray-200 border border-gray-500/30'
|
||||
}
|
||||
@@ -148,12 +150,15 @@ export function getStatusDotClass(state: PackageState, health?: string | null, e
|
||||
return 'bg-yellow-400 animate-pulse'
|
||||
case PackageState.Installing:
|
||||
return 'bg-blue-400 animate-pulse'
|
||||
case PackageState.Updating:
|
||||
return 'bg-orange-400 animate-pulse'
|
||||
default:
|
||||
return 'bg-gray-400'
|
||||
}
|
||||
}
|
||||
|
||||
export function getStatusLabel(state: PackageState, health?: string | null, exitCode?: number | null): string {
|
||||
if (state === PackageState.Updating) return 'updating...'
|
||||
if (state === PackageState.Running && health === 'starting') return 'starting up'
|
||||
if (state === PackageState.Running && health === 'unhealthy') return 'unhealthy'
|
||||
if (state === PackageState.Running && health === 'healthy') return 'healthy'
|
||||
|
||||
@@ -44,6 +44,10 @@
|
||||
class="tier-badge"
|
||||
:class="tier === 'core' ? 'tier-badge-core' : 'tier-badge-recommended'"
|
||||
>{{ tier }}</span>
|
||||
<span
|
||||
v-if="pkg['available-update']"
|
||||
class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-semibold bg-orange-500/20 text-orange-300 border border-orange-500/30"
|
||||
>Update</span>
|
||||
</div>
|
||||
<p class="text-sm text-white/50">{{ version ? `v${version}` : '' }}</p>
|
||||
<p v-if="author" class="text-xs text-white/40 mt-0.5">{{ author }}</p>
|
||||
@@ -250,6 +254,6 @@ const tier = computed(() => {
|
||||
const isTransitioning = computed(() => {
|
||||
const s = props.pkg.state
|
||||
const h = props.pkg.health
|
||||
return s === 'starting' || s === 'installing' || s === 'stopping' || s === 'restarting' || (s === 'running' && h === 'starting')
|
||||
return s === 'starting' || s === 'installing' || s === 'stopping' || s === 'restarting' || s === 'updating' || (s === 'running' && h === 'starting')
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -138,6 +138,8 @@ export function getStatusClass(state: PackageState, health?: string | null, exit
|
||||
return 'bg-yellow-500/20 text-yellow-200'
|
||||
case PackageState.Installing:
|
||||
return 'bg-blue-500/20 text-blue-200'
|
||||
case PackageState.Updating:
|
||||
return 'bg-orange-500/20 text-orange-200'
|
||||
default:
|
||||
return 'bg-gray-500/20 text-gray-200'
|
||||
}
|
||||
@@ -147,6 +149,7 @@ export function getStatusLabel(state: PackageState, health?: string | null, exit
|
||||
if (state === PackageState.Running && health === 'starting') return 'starting up'
|
||||
if (state === PackageState.Running && health === 'unhealthy') return 'unhealthy'
|
||||
if (state === PackageState.Running && health === 'healthy') return 'healthy'
|
||||
if (state === PackageState.Updating) return 'updating...'
|
||||
if (state === PackageState.Running) return 'running'
|
||||
if (state === PackageState.Exited || state === PackageState.Stopped) {
|
||||
if (exitCode === 137) return 'killed (OOM)'
|
||||
|
||||
Reference in New Issue
Block a user