- Updated BUILD-GUIDE.md to clarify instructions for building the Archipelago Auto-Installer ISO, emphasizing the recommended method of building directly on the target server. - Added auto-installation of missing dependencies (xorriso, podman) when running the build script with sudo. - Enhanced the build-auto-installer-iso.sh script to capture container images from the live server, ensuring the ISO includes the same set of applications as the dev server. - Revised deployment documentation to stress the importance of building the Rust backend on the Linux dev server and included new instructions for capturing system-level changes for ISO builds. - Improved UI components and added new bundled applications (BTCPay Server, Mempool Explorer, Nostr Relay, Strfry Relay, Tailscale) to enhance user experience.
261 lines
8.0 KiB
Vue
261 lines
8.0 KiB
Vue
<template>
|
|
<div class="p-6">
|
|
<div class="mb-6">
|
|
<button
|
|
@click="$router.back()"
|
|
class="mb-4 flex items-center gap-2 text-white/70 hover:text-white transition-colors"
|
|
>
|
|
<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="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
Back
|
|
</button>
|
|
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-white mb-2">{{ appName }}</h1>
|
|
<p class="text-white/70">Container details and management</p>
|
|
</div>
|
|
<ContainerStatus
|
|
v-if="container"
|
|
:state="container.state as any"
|
|
:health="healthStatus as any"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="loading" class="flex items-center justify-center py-12">
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-white/60"></div>
|
|
</div>
|
|
|
|
<div v-else-if="error" class="glass-card p-6">
|
|
<div class="flex items-center gap-3 text-red-400">
|
|
<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="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span>{{ error }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="container" class="space-y-6">
|
|
<!-- Container Info Card -->
|
|
<div class="glass-card p-6">
|
|
<h2 class="text-xl font-semibold text-white mb-4">Container Information</h2>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<span class="text-sm text-white/60">Container ID</span>
|
|
<p class="text-white/90 font-mono text-sm mt-1">{{ container.id }}</p>
|
|
</div>
|
|
<div>
|
|
<span class="text-sm text-white/60">Image</span>
|
|
<p class="text-white/90 text-sm mt-1">{{ container.image }}</p>
|
|
</div>
|
|
<div>
|
|
<span class="text-sm text-white/60">State</span>
|
|
<p class="text-white/90 text-sm mt-1 capitalize">{{ container.state }}</p>
|
|
</div>
|
|
<div>
|
|
<span class="text-sm text-white/60">Created</span>
|
|
<p class="text-white/90 text-sm mt-1">{{ formatDate(container.created) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions Card -->
|
|
<div class="glass-card p-6">
|
|
<h2 class="text-xl font-semibold text-white mb-4">Actions</h2>
|
|
<div class="flex gap-4">
|
|
<button
|
|
v-if="container.state !== 'running'"
|
|
@click="handleStart"
|
|
:disabled="actionLoading"
|
|
class="px-6 py-3 glass-button rounded-lg font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
|
|
>
|
|
Start Container
|
|
</button>
|
|
<button
|
|
v-else
|
|
@click="handleStop"
|
|
:disabled="actionLoading"
|
|
class="px-6 py-3 glass-button rounded-lg font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
|
|
>
|
|
Stop Container
|
|
</button>
|
|
<button
|
|
@click="handleRestart"
|
|
:disabled="actionLoading || container.state !== 'running'"
|
|
class="px-6 py-3 glass-button rounded-lg font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
|
|
>
|
|
Restart
|
|
</button>
|
|
<button
|
|
@click="handleRemove"
|
|
:disabled="actionLoading"
|
|
class="px-6 py-3 glass-button rounded-lg font-medium text-red-400/90 hover:text-red-400 transition-colors disabled:opacity-50"
|
|
>
|
|
Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logs Card -->
|
|
<div class="glass-card p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h2 class="text-xl font-semibold text-white">Logs</h2>
|
|
<button
|
|
@click="refreshLogs"
|
|
:disabled="logsLoading"
|
|
class="px-4 py-2 glass-button rounded text-sm font-medium text-white/90 hover:text-white transition-colors disabled:opacity-50"
|
|
>
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
<div class="bg-black/40 rounded-lg p-4 font-mono text-sm text-white/80 max-h-96 overflow-y-auto">
|
|
<div v-if="logsLoading" class="text-center py-4 text-white/60">
|
|
Loading logs...
|
|
</div>
|
|
<div v-else-if="logs.length === 0" class="text-center py-4 text-white/60">
|
|
No logs available
|
|
</div>
|
|
<div v-else>
|
|
<div v-for="(log, index) in logs" :key="index" class="mb-1">
|
|
{{ log }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useContainerStore } from '@/stores/container'
|
|
import ContainerStatus from '@/components/ContainerStatus.vue'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const store = useContainerStore()
|
|
|
|
const appId = computed(() => route.params.id as string)
|
|
const appName = computed(() => {
|
|
return appId.value
|
|
.split('-')
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ')
|
|
})
|
|
|
|
const container = ref<any>(null)
|
|
const logs = ref<string[]>([])
|
|
const loading = ref(false)
|
|
const logsLoading = ref(false)
|
|
const actionLoading = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const healthStatus = ref<string>('unknown')
|
|
|
|
onMounted(async () => {
|
|
await loadContainer()
|
|
await loadLogs()
|
|
await loadHealthStatus()
|
|
})
|
|
|
|
async function loadContainer() {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
const status = await store.getContainerStatus(appId.value)
|
|
container.value = status
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : 'Failed to load container'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadLogs() {
|
|
logsLoading.value = true
|
|
try {
|
|
logs.value = await store.getContainerLogs(appId.value, 100)
|
|
} catch (e) {
|
|
console.error('Failed to load logs:', e)
|
|
} finally {
|
|
logsLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function loadHealthStatus() {
|
|
await store.fetchHealthStatus()
|
|
healthStatus.value = store.getHealthStatus(appId.value)
|
|
}
|
|
|
|
async function refreshLogs() {
|
|
await loadLogs()
|
|
}
|
|
|
|
async function handleStart() {
|
|
actionLoading.value = true
|
|
try {
|
|
await store.startContainer(appId.value)
|
|
await loadContainer()
|
|
await loadHealthStatus()
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : 'Failed to start container'
|
|
} finally {
|
|
actionLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function handleStop() {
|
|
actionLoading.value = true
|
|
try {
|
|
await store.stopContainer(appId.value)
|
|
await loadContainer()
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : 'Failed to stop container'
|
|
} finally {
|
|
actionLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function handleRestart() {
|
|
actionLoading.value = true
|
|
try {
|
|
await store.stopContainer(appId.value)
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
await store.startContainer(appId.value)
|
|
await loadContainer()
|
|
await loadHealthStatus()
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : 'Failed to restart container'
|
|
} finally {
|
|
actionLoading.value = false
|
|
}
|
|
}
|
|
|
|
async function handleRemove() {
|
|
if (!confirm(`Are you sure you want to remove ${appName.value}? This will delete the container and all its data.`)) {
|
|
return
|
|
}
|
|
|
|
actionLoading.value = true
|
|
try {
|
|
await store.removeContainer(appId.value)
|
|
router.push('/dashboard/apps')
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : 'Failed to remove container'
|
|
} finally {
|
|
actionLoading.value = false
|
|
}
|
|
}
|
|
|
|
function formatDate(dateString: string): string {
|
|
try {
|
|
const date = new Date(dateString)
|
|
return date.toLocaleString()
|
|
} catch {
|
|
return dateString
|
|
}
|
|
}
|
|
</script>
|