Container recovery: - Health monitor: MAX_RESTART_ATTEMPTS 3→10, interval 60s→120s - Dependency-aware restarts: won't restart services before their deps - Reset dependent counters when a dependency recovers - Handle "created" state containers (were invisible to health monitor) - Added IndeedHub, mempool-api, mysql to tier system - Crash recovery: podman start timeout 30s→120s with retry - Podman client: socket timeout 5s→30s, added restart policy UI state representation: - Exit code 0 shows "stopped" (gray), not "crashed" (red) - Exit code 137 shows "killed (OOM)" - Non-zero exit shows "crashed" (red) - Added exit_code field to PackageDataEntry Install/uninstall fixes: - Install returns error when container doesn't start (was silent success) - Post-install hooks awaited instead of fire-and-forget tokio::spawn - Uninstall: graceful rm before force, volume prune, network cleanup - Uninstall returns error on partial failure (was 200 OK) Config consistency: - DB passwords read from /var/lib/archipelago/secrets/ (was hardcoded) - Bitcoin: added ZMQ ports 28332/28333 for LND block notifications - IndeedHub port 7777→8190 (was conflicting with strfry) - Marketplace versions: LND 0.17.4→0.18.4, Mempool 2.5.0→3.0.0 Performance: - Metrics collector interval 60s→300s (was duplicating health monitor) - Podman client: proper error propagation instead of unwrap_or_default Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
213 lines
10 KiB
Vue
213 lines
10 KiB
Vue
<template>
|
|
<div class="glass-card p-6 mb-6">
|
|
<!-- Desktop: Single Row Layout -->
|
|
<div class="hidden md:flex items-center gap-6">
|
|
<img
|
|
:src="pkg['static-files']?.icon || `/assets/img/app-icons/${pkg.manifest?.id || appId}.png`"
|
|
:alt="pkg.manifest.title"
|
|
class="w-20 h-20 rounded-xl shadow-xl flex-shrink-0"
|
|
@error="handleImageError"
|
|
/>
|
|
|
|
<div class="flex-1 min-w-0">
|
|
<h1 class="text-2xl font-bold text-white mb-1">{{ pkg.manifest.title }}</h1>
|
|
<p class="text-white/70 text-sm mb-2">{{ pkg.manifest.description.short }}</p>
|
|
<div class="flex items-center gap-2">
|
|
<span
|
|
class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-medium"
|
|
:class="getStatusClass(pkg.state, pkg.health, pkg['exit-code'])"
|
|
>
|
|
<span class="w-1.5 h-1.5 rounded-full mr-1.5" :class="getStatusDotClass(pkg.state, pkg.health, pkg['exit-code'])"></span>
|
|
{{ getStatusLabel(pkg.state, pkg.health, pkg['exit-code']) }}
|
|
</span>
|
|
<span class="text-white/50 text-xs">v{{ pkg.manifest.version }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex items-center gap-2 flex-shrink-0">
|
|
<button
|
|
v-if="packageKey === 'lnd'"
|
|
@click="$emit('channels')"
|
|
class="glass-button glass-button-sm px-4 py-2.5 rounded-lg text-sm font-medium 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="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
{{ t('appDetails.channels') }}
|
|
</button>
|
|
<button
|
|
v-if="canLaunch"
|
|
@click="$emit('launch')"
|
|
class="glass-button glass-button-sm px-6 py-2.5 rounded-lg text-sm font-semibold 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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
</svg>
|
|
{{ t('common.launch') }}
|
|
</button>
|
|
<template v-if="!isWebOnly">
|
|
<button
|
|
v-if="pkg.state === 'stopped' || pkg.state === 'exited'"
|
|
@click="$emit('start')"
|
|
class="px-4 py-2.5 glass-button rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
|
|
:class="pkg.state === 'exited' ? 'glass-button-danger' : 'glass-button-success'"
|
|
>
|
|
<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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
</svg>
|
|
{{ pkg.state === 'exited' ? 'Restart' : t('common.start') }}
|
|
</button>
|
|
<button
|
|
@click="$emit('restart')"
|
|
class="px-4 py-2.5 glass-button rounded-lg text-sm font-medium hover:bg-white/15 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>
|
|
{{ t('common.restart') }}
|
|
</button>
|
|
<button
|
|
v-if="pkg.state === 'running'"
|
|
@click="$emit('stop')"
|
|
class="px-4 py-2.5 bg-yellow-500/20 border border-yellow-500/40 rounded-lg text-yellow-200 text-sm font-medium hover:bg-yellow-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="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
|
</svg>
|
|
{{ t('common.stop') }}
|
|
</button>
|
|
<button
|
|
@click="$emit('uninstall')"
|
|
class="px-4 py-2.5 glass-button glass-button-danger rounded-lg text-sm font-medium 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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
{{ t('common.uninstall') }}
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mobile: Two Column Grid Layout -->
|
|
<div class="md:hidden">
|
|
<div class="flex items-start gap-4 mb-4">
|
|
<img
|
|
:src="pkg['static-files']?.icon || `/assets/img/app-icons/${pkg.manifest?.id || appId}.png`"
|
|
:alt="pkg.manifest.title"
|
|
class="w-20 h-20 rounded-xl shadow-xl flex-shrink-0"
|
|
@error="handleImageError"
|
|
/>
|
|
|
|
<div class="flex-1 min-w-0">
|
|
<h1 class="text-xl font-bold text-white mb-1">{{ pkg.manifest.title }}</h1>
|
|
<p class="text-white/70 text-xs mb-2 line-clamp-2">{{ pkg.manifest.description.short }}</p>
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<span
|
|
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium"
|
|
:class="getStatusClass(pkg.state, pkg.health, pkg['exit-code'])"
|
|
>
|
|
<span class="w-1.5 h-1.5 rounded-full mr-1" :class="getStatusDotClass(pkg.state, pkg.health, pkg['exit-code'])"></span>
|
|
{{ getStatusLabel(pkg.state, pkg.health, pkg['exit-code']) }}
|
|
</span>
|
|
<span class="text-white/50 text-xs">v{{ pkg.manifest.version }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
v-if="!isWebOnly"
|
|
@click="$emit('uninstall')"
|
|
class="flex-shrink-0 w-10 h-10 rounded-lg glass-button glass-button-danger transition-colors flex items-center justify-center"
|
|
:title="t('common.uninstall')"
|
|
>
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Action Buttons (Auto Grid) -->
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<button
|
|
v-if="canLaunch"
|
|
@click="$emit('launch')"
|
|
:class="isWebOnly ? 'col-span-2' : ''"
|
|
class="glass-button glass-button-sm px-4 py-2.5 rounded-lg text-sm font-semibold 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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
|
</svg>
|
|
{{ t('common.launch') }}
|
|
</button>
|
|
<template v-if="!isWebOnly">
|
|
<button
|
|
v-if="pkg.state === 'stopped' || pkg.state === 'exited'"
|
|
@click="$emit('start')"
|
|
class="px-4 py-2.5 glass-button rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2"
|
|
:class="pkg.state === 'exited' ? 'glass-button-danger' : 'glass-button-success'"
|
|
>
|
|
<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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
</svg>
|
|
{{ pkg.state === 'exited' ? 'Restart' : t('common.start') }}
|
|
</button>
|
|
<button
|
|
v-if="pkg.state === 'running'"
|
|
@click="$emit('stop')"
|
|
class="px-4 py-2.5 bg-yellow-500/20 border border-yellow-500/40 rounded-lg text-yellow-200 text-sm font-medium hover:bg-yellow-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="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
|
</svg>
|
|
{{ t('common.stop') }}
|
|
</button>
|
|
<button
|
|
@click="$emit('restart')"
|
|
:class="[canLaunch && (pkg.state === 'stopped' || pkg.state === 'exited' || pkg.state === 'running') ? 'col-span-2' : '']"
|
|
class="px-4 py-2.5 glass-button rounded-lg text-sm font-medium hover:bg-white/15 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>
|
|
{{ t('common.restart') }}
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useI18n } from 'vue-i18n'
|
|
import { getStatusClass, getStatusDotClass, getStatusLabel } from './appDetailsData'
|
|
|
|
const { t } = useI18n()
|
|
|
|
defineProps<{
|
|
pkg: Record<string, any>
|
|
appId: string
|
|
packageKey: string
|
|
canLaunch: boolean
|
|
isWebOnly: boolean
|
|
}>()
|
|
|
|
defineEmits<{
|
|
launch: []
|
|
start: []
|
|
stop: []
|
|
restart: []
|
|
uninstall: []
|
|
channels: []
|
|
}>()
|
|
|
|
function handleImageError(e: Event) {
|
|
const target = e.target as HTMLImageElement
|
|
if (!target.src.includes('data:image') && !target.src.includes('logo-archipelago')) {
|
|
target.src = '/assets/img/logo-archipelago.svg'
|
|
}
|
|
}
|
|
</script>
|