The credential issuance and verification handlers used Handle::block_on() directly inside the tokio runtime, causing a deadlock. Wrapped with block_in_place() to properly yield the runtime thread. Also completed full feature verification across all 25 test groups (~175 checks) on live server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
263 lines
8.1 KiB
Vue
263 lines
8.1 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>
|
|
|
|
<Transition name="content-fade" mode="out-in">
|
|
<div v-if="loading" key="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" key="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" key="content" 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>
|
|
</Transition>
|
|
</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>
|