feat: add migrations, test scripts, and utility tools
- Add database migrations (006-028) for face recognition, identity, file_uuid - Add test scripts for ASR, face, search, processing - Add portal frontend (Tauri) - Add config, benchmark, and monitoring utilities - Add model checkpoints and pretrained model references
This commit is contained in:
550
portal/src/views/HomeView.vue
Normal file
550
portal/src/views/HomeView.vue
Normal file
@@ -0,0 +1,550 @@
|
||||
<template>
|
||||
<div class="space-y-8">
|
||||
<!-- Hero Section -->
|
||||
<div class="bg-gradient-to-r from-blue-900 to-purple-900 rounded-lg p-8">
|
||||
<h2 class="text-3xl font-bold mb-4">歡迎使用 Momentry Portal</h2>
|
||||
<p class="text-gray-300 mb-6">
|
||||
影片內容搜尋與人物管理平台
|
||||
</p>
|
||||
<div class="flex space-x-4">
|
||||
<router-link
|
||||
to="/search"
|
||||
class="bg-blue-600 hover:bg-blue-700 px-6 py-3 rounded-lg font-semibold transition"
|
||||
>
|
||||
開始搜尋
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/persons"
|
||||
class="bg-gray-700 hover:bg-gray-600 px-6 py-3 rounded-lg font-semibold transition"
|
||||
>
|
||||
人物管理
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ingest Stats Section -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h3 class="text-xl font-semibold text-green-400 mb-4">入庫統計</h3>
|
||||
|
||||
<div v-if="ingestStats" class="space-y-4">
|
||||
<!-- Row 1: Videos + Total Chunks -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Total Videos -->
|
||||
<div class="bg-gray-900 p-4 rounded border border-gray-600 text-center">
|
||||
<div class="text-3xl font-bold text-blue-400">{{ ingestStats.total_videos }}</div>
|
||||
<div class="text-sm text-gray-400 mt-1">影片總數</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Chunks -->
|
||||
<div class="bg-gray-900 p-4 rounded border border-gray-600 text-center">
|
||||
<div class="text-3xl font-bold text-purple-400">{{ ingestStats.total_chunks }}</div>
|
||||
<div class="text-sm text-gray-400 mt-1">片段總數</div>
|
||||
</div>
|
||||
|
||||
<!-- Searchable Chunks -->
|
||||
<div class="bg-gray-900 p-4 rounded border border-green-700 text-center">
|
||||
<div class="text-3xl font-bold text-green-400">{{ ingestStats.searchable_chunks }}</div>
|
||||
<div class="text-sm text-gray-400 mt-1">可搜尋</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: Chunk Types Breakdown -->
|
||||
<div class="bg-gray-900 p-4 rounded border border-gray-600">
|
||||
<div class="text-sm text-gray-500 mb-3">片段類型分類</div>
|
||||
<div class="grid grid-cols-3 gap-4 text-center">
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-pink-400">{{ ingestStats.sentence_chunks }}</div>
|
||||
<div class="text-xs text-gray-400">Sentence (句子)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-orange-400">{{ ingestStats.cut_chunks }}</div>
|
||||
<div class="text-xs text-gray-400">Cut (剪輯點)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-2xl font-bold text-indigo-400">{{ ingestStats.time_chunks }}</div>
|
||||
<div class="text-xs text-gray-400">Time (時間段)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chunk Type Definitions -->
|
||||
<div class="mt-4 pt-3 border-t border-gray-700 space-y-2 text-xs text-gray-500">
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-pink-400 font-semibold">Sentence</span>
|
||||
<span>基於語音辨識的自然語句分割,每個片段代表一句完整的對話或敘述,適合語意搜尋與內容理解。</span>
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-orange-400 font-semibold">Cut</span>
|
||||
<span>基於影片場景切換的分割點,偵測畫面變化(如鏡頭切換、場景轉換)作為片段邊界,適合視覺內容分析。</span>
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
<span class="text-indigo-400 font-semibold">Time</span>
|
||||
<span>基於固定時間間隔的分割(如每 60 秒),確保片段長度一致,適合時間序列分析與段落瀏覽。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 3: Processing Status -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Chunks with Summary -->
|
||||
<div class="bg-gray-900 p-4 rounded border border-emerald-700 text-center">
|
||||
<div class="text-3xl font-bold text-emerald-400">{{ ingestStats.chunks_with_summary }}</div>
|
||||
<div class="text-sm text-gray-400 mt-1">已生成摘要</div>
|
||||
</div>
|
||||
|
||||
<!-- Chunks with Visual -->
|
||||
<div class="bg-gray-900 p-4 rounded border border-cyan-700 text-center">
|
||||
<div class="text-3xl font-bold text-cyan-400">{{ ingestStats.chunks_with_visual }}</div>
|
||||
<div class="text-sm text-gray-400 mt-1">有視覺分析</div>
|
||||
</div>
|
||||
|
||||
<!-- Pending Videos -->
|
||||
<div class="bg-gray-900 p-4 rounded border border-yellow-700 text-center">
|
||||
<div class="text-3xl font-bold text-yellow-400">{{ ingestStats.pending_videos }}</div>
|
||||
<div class="text-sm text-gray-400 mt-1">待處理</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-gray-400">載入中...</div>
|
||||
</div>
|
||||
|
||||
<!-- SFTPGo Status Section -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h3 class="text-xl font-semibold text-orange-400 mb-4">SFTPGo 狀態</h3>
|
||||
|
||||
<div v-if="sftpgoStatus" class="space-y-4">
|
||||
<!-- Basic Info -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||
<div class="bg-gray-900 p-4 rounded border border-gray-600">
|
||||
<span class="text-xs text-gray-500 uppercase tracking-wider">Username</span>
|
||||
<p class="text-white mt-1 text-lg font-semibold">{{ sftpgoStatus.username }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-900 p-4 rounded border border-gray-600 lg:col-span-2">
|
||||
<span class="text-xs text-gray-500 uppercase tracking-wider">Home Path</span>
|
||||
<p class="text-gray-300 mt-1 text-sm font-mono break-all">{{ sftpgoStatus.home_dir }}</p>
|
||||
</div>
|
||||
<div class="bg-gray-900 p-4 rounded border border-gray-600">
|
||||
<span class="text-xs text-gray-500 uppercase tracking-wider">Files Count</span>
|
||||
<button
|
||||
@click="openSftpgoFiles"
|
||||
class="text-orange-400 mt-1 text-lg font-semibold hover:text-orange-300 cursor-pointer flex items-center space-x-1"
|
||||
>
|
||||
<span>{{ sftpgoStatus.files_count }}</span>
|
||||
<span>🔗</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="bg-gray-900 p-4 rounded border border-gray-600">
|
||||
<span class="text-xs text-gray-500 uppercase tracking-wider">Registered Videos</span>
|
||||
<p class="text-green-400 mt-1 text-lg font-semibold">{{ sftpgoStatus.registered_videos.length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SFTPGo URL -->
|
||||
<div class="mt-4 bg-gray-900 p-4 rounded border border-gray-600">
|
||||
<span class="text-xs text-gray-500 uppercase tracking-wider">SFTPGo 檔案管理</span>
|
||||
<div class="mt-2 space-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
@click="openSftpgoFiles"
|
||||
class="flex-1 px-4 py-3 bg-orange-700 hover:bg-orange-600 text-white text-center rounded font-medium no-underline"
|
||||
>
|
||||
📂 點擊開啟 SFTPGo 檔案管理
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<span class="text-gray-500">或複製網址:</span>
|
||||
<input
|
||||
:value="sftpgoUrl"
|
||||
readonly
|
||||
class="flex-1 px-3 py-1 bg-gray-800 border border-gray-600 rounded text-gray-300 text-xs font-mono"
|
||||
/>
|
||||
<button
|
||||
@click="copySftpgoUrl"
|
||||
class="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-white text-xs rounded"
|
||||
>
|
||||
複製
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Registered Videos List -->
|
||||
<div v-if="sftpgoStatus.registered_videos.length > 0" class="bg-gray-900 rounded border border-gray-600">
|
||||
<div class="p-3 border-b border-gray-700">
|
||||
<span class="text-sm font-semibold text-gray-300">已註冊影片</span>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-700">
|
||||
<div v-for="video in sftpgoStatus.registered_videos" :key="video.uuid" class="p-3 flex justify-between items-center">
|
||||
<div>
|
||||
<p class="text-white text-sm">{{ video.file_name }}</p>
|
||||
<p class="text-gray-500 text-xs">{{ video.uuid }}</p>
|
||||
</div>
|
||||
<span :class="video.status === 'completed' ? 'bg-green-900 text-green-300' : 'bg-yellow-900 text-yellow-300'" class="px-2 py-1 rounded text-xs">
|
||||
{{ video.status }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="bg-gray-900 p-4 rounded border border-gray-600 text-center text-gray-500 text-sm">
|
||||
尚未註冊任何影片
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-gray-400">載入中...</div>
|
||||
</div>
|
||||
|
||||
<!-- Inference Engines Section -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h3 class="text-xl font-semibold text-pink-400 mb-4">推理引擎狀態</h3>
|
||||
|
||||
<div v-if="inferenceHealth" class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Ollama (Embedding) -->
|
||||
<div class="bg-gray-900 p-4 rounded border border-gray-600">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-pink-400">🧠</span>
|
||||
<span class="font-semibold">{{ inferenceHealth.ollama.engine }}</span>
|
||||
</div>
|
||||
<span :class="inferenceHealth.ollama.status === 'ok' ? 'text-green-400' : 'text-red-400'">
|
||||
{{ inferenceHealth.ollama.status === 'ok' ? '●' : '○' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">模型</span>
|
||||
<span class="text-white">{{ inferenceHealth.ollama.model }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">用途</span>
|
||||
<span class="text-purple-400">Embedding</span>
|
||||
</div>
|
||||
<div v-if="inferenceHealth.ollama.latency_ms" class="flex justify-between">
|
||||
<span class="text-gray-500">延遲</span>
|
||||
<span class="text-white">{{ inferenceHealth.ollama.latency_ms }}ms</span>
|
||||
</div>
|
||||
<div v-if="inferenceHealth.ollama.error" class="text-red-400 text-xs mt-2">
|
||||
{{ inferenceHealth.ollama.error }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- llama-server (LLM) -->
|
||||
<div class="bg-gray-900 p-4 rounded border border-gray-600">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-cyan-400">💬</span>
|
||||
<span class="font-semibold">{{ inferenceHealth.llama_server.engine }}</span>
|
||||
</div>
|
||||
<span :class="inferenceHealth.llama_server.status === 'ok' ? 'text-green-400' : 'text-red-400'">
|
||||
{{ inferenceHealth.llama_server.status === 'ok' ? '●' : '○' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-1 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">模型</span>
|
||||
<span class="text-white">{{ inferenceHealth.llama_server.model }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500">用途</span>
|
||||
<span class="text-cyan-400">LLM (5W1H, Summary)</span>
|
||||
</div>
|
||||
<div v-if="inferenceHealth.llama_server.latency_ms" class="flex justify-between">
|
||||
<span class="text-gray-500">延遲</span>
|
||||
<span class="text-white">{{ inferenceHealth.llama_server.latency_ms }}ms</span>
|
||||
</div>
|
||||
<div v-if="inferenceHealth.llama_server.error" class="text-red-400 text-xs mt-2">
|
||||
{{ inferenceHealth.llama_server.error }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-gray-400">載入中...</div>
|
||||
</div>
|
||||
|
||||
<!-- Health Check Section -->
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-xl font-semibold text-blue-400">服務狀態</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-400">API: {{ apiBaseUrl }}</span>
|
||||
<button
|
||||
@click="refreshHealth"
|
||||
class="text-blue-400 hover:text-blue-300 text-sm"
|
||||
:disabled="loading"
|
||||
>
|
||||
{{ loading ? '檢查中...' : '重新檢查' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overall Status -->
|
||||
<div v-if="healthError" class="bg-red-900/30 rounded-lg p-4 border border-red-700">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-red-400">⚠</span>
|
||||
<span class="text-red-300">{{ healthError }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="health" class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<!-- PostgreSQL -->
|
||||
<ServiceStatusCard
|
||||
name="PostgreSQL"
|
||||
:status="health.services.postgres.status"
|
||||
:latency="health.services.postgres.latency_ms"
|
||||
:error="health.services.postgres.error"
|
||||
/>
|
||||
<!-- Redis -->
|
||||
<ServiceStatusCard
|
||||
name="Redis"
|
||||
:status="health.services.redis.status"
|
||||
:latency="health.services.redis.latency_ms"
|
||||
:error="health.services.redis.error"
|
||||
/>
|
||||
<!-- Qdrant -->
|
||||
<ServiceStatusCard
|
||||
name="Qdrant"
|
||||
:status="health.services.qdrant.status"
|
||||
:latency="health.services.qdrant.latency_ms"
|
||||
:error="health.services.qdrant.error"
|
||||
/>
|
||||
<!-- MongoDB -->
|
||||
<ServiceStatusCard
|
||||
name="MongoDB"
|
||||
:status="health.services.mongodb.status"
|
||||
:latency="health.services.mongodb.latency_ms"
|
||||
:error="health.services.mongodb.error"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="text-gray-400 text-sm">載入中...</div>
|
||||
|
||||
<!-- Version Info -->
|
||||
<div v-if="health" class="mt-4 pt-4 border-t border-gray-700 flex justify-between text-sm text-gray-400">
|
||||
<span>版本: {{ health.version }}</span>
|
||||
<span>運行時間: {{ formatUptime(health.uptime_ms) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h3 class="text-xl font-semibold text-blue-400 mb-2">搜尋功能</h3>
|
||||
<p class="text-gray-400">智能搜尋影片內容,支援語意向量與關鍵字檢索</p>
|
||||
</div>
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h3 class="text-xl font-semibold text-green-400 mb-2">人物管理</h3>
|
||||
<p class="text-gray-400">管理全域身份、區域人物與臉部特徵</p>
|
||||
</div>
|
||||
<div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
|
||||
<h3 class="text-xl font-semibold text-purple-400 mb-2">臉部擷取</h3>
|
||||
<p class="text-gray-400">擷取並管理人物臉部截圖</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getHealth, getIngestStats, getSftpgoStatus, getInferenceHealth } from '@/api/client'
|
||||
|
||||
const isTauri = () => {
|
||||
return (window as any).__TAURI__ !== undefined
|
||||
}
|
||||
|
||||
interface ServiceStatus {
|
||||
status: string
|
||||
latency_ms: number | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
interface ServiceHealth {
|
||||
postgres: ServiceStatus
|
||||
redis: ServiceStatus
|
||||
qdrant: ServiceStatus
|
||||
mongodb: ServiceStatus
|
||||
}
|
||||
|
||||
interface DetailedHealthResponse {
|
||||
status: string
|
||||
version: string
|
||||
uptime_ms: number
|
||||
services: ServiceHealth
|
||||
}
|
||||
|
||||
interface IngestStats {
|
||||
total_videos: number
|
||||
total_chunks: number
|
||||
sentence_chunks: number
|
||||
cut_chunks: number
|
||||
time_chunks: number
|
||||
searchable_chunks: number
|
||||
chunks_with_visual: number
|
||||
chunks_with_summary: number
|
||||
pending_videos: number
|
||||
}
|
||||
|
||||
interface RegisteredVideo {
|
||||
uuid: string
|
||||
file_name: string
|
||||
status: string
|
||||
}
|
||||
|
||||
interface SftpgoStatus {
|
||||
username: string
|
||||
home_dir: string
|
||||
files_count: number
|
||||
registered_videos: RegisteredVideo[]
|
||||
last_login: string | null
|
||||
}
|
||||
|
||||
interface InferenceEngineStatus {
|
||||
engine: string
|
||||
model: string
|
||||
status: string
|
||||
latency_ms: number | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
interface InferenceHealthResponse {
|
||||
ollama: InferenceEngineStatus
|
||||
llama_server: InferenceEngineStatus
|
||||
}
|
||||
|
||||
const health = ref<DetailedHealthResponse | null>(null)
|
||||
const healthError = ref<string | null>(null)
|
||||
const ingestStats = ref<IngestStats | null>(null)
|
||||
const sftpgoStatus = ref<SftpgoStatus | null>(null)
|
||||
const inferenceHealth = ref<InferenceHealthResponse | null>(null)
|
||||
const loading = ref(false)
|
||||
const apiBaseUrl = ref('http://127.0.0.1:3003 (dev)')
|
||||
const sftpgoUrl = ref('https://sftpgo.momentry.ddns.net/web/client')
|
||||
|
||||
async function fetchHealth() {
|
||||
loading.value = true
|
||||
healthError.value = null
|
||||
try {
|
||||
health.value = await getHealth()
|
||||
} catch (e) {
|
||||
healthError.value = String(e)
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
async function fetchIngestStats() {
|
||||
try {
|
||||
ingestStats.value = await getIngestStats()
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch ingest stats:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSftpgoStatus() {
|
||||
try {
|
||||
sftpgoStatus.value = await getSftpgoStatus()
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch sftpgo status:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchInferenceHealth() {
|
||||
try {
|
||||
inferenceHealth.value = await getInferenceHealth()
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch inference health:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function openSftpgoFiles() {
|
||||
const url = sftpgoUrl.value
|
||||
console.log('Momentry: Opening URL:', url, 'isTauri:', isTauri())
|
||||
alert('即將開啟:' + url)
|
||||
|
||||
if (isTauri()) {
|
||||
// Use Tauri invoke in app mode
|
||||
try {
|
||||
import('@tauri-apps/api/core').then(({ invoke }) => {
|
||||
invoke('plugin:shell|open', { path: url }).then(() => {
|
||||
console.log('Momentry: Opened with shell')
|
||||
alert('已開啟')
|
||||
}).catch((e) => {
|
||||
console.error('Momentry: Shell error:', e)
|
||||
alert('開啟失敗:' + e)
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Momentry: Import error:', e)
|
||||
alert('導入失敗:' + e)
|
||||
}
|
||||
return
|
||||
}
|
||||
// Use browser open in web mode
|
||||
window.open(url, '_blank')?.focus()
|
||||
}
|
||||
|
||||
function copySftpgoUrl() {
|
||||
navigator.clipboard.writeText(sftpgoUrl.value)
|
||||
alert('已複製網址:' + sftpgoUrl.value)
|
||||
}
|
||||
|
||||
async function refreshHealth() {
|
||||
await fetchHealth()
|
||||
}
|
||||
|
||||
function formatUptime(ms: number): string {
|
||||
const seconds = Math.floor(ms / 1000)
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const days = Math.floor(hours / 24)
|
||||
|
||||
if (days > 0) return `${days}d ${hours % 24}h`
|
||||
if (hours > 0) return `${hours}h ${minutes % 60}m`
|
||||
if (minutes > 0) return `${minutes}m ${seconds % 60}s`
|
||||
return `${seconds}s`
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchHealth()
|
||||
fetchIngestStats()
|
||||
fetchSftpgoStatus()
|
||||
fetchInferenceHealth()
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, h } from 'vue'
|
||||
|
||||
const ServiceStatusCard = defineComponent({
|
||||
props: {
|
||||
name: String,
|
||||
status: String,
|
||||
latency: [Number, null],
|
||||
error: [String, null]
|
||||
},
|
||||
setup(props) {
|
||||
const statusColor = () => {
|
||||
if (props.status === 'ok') return 'text-green-400'
|
||||
if (props.status === 'degraded') return 'text-yellow-400'
|
||||
return 'text-red-400'
|
||||
}
|
||||
|
||||
const bgColor = () => {
|
||||
if (props.status === 'ok') return 'bg-green-900/20 border-green-700'
|
||||
if (props.status === 'degraded') return 'bg-yellow-900/20 border-yellow-700'
|
||||
return 'bg-red-900/20 border-red-700'
|
||||
}
|
||||
|
||||
return () => h('div', {
|
||||
class: `rounded-lg p-3 border ${bgColor()}`
|
||||
}, [
|
||||
h('div', { class: 'flex items-center justify-between' }, [
|
||||
h('span', { class: 'font-semibold' }, props.name),
|
||||
h('span', { class: statusColor() }, props.status === 'ok' ? '●' : '○')
|
||||
]),
|
||||
props.latency ? h('div', { class: 'text-xs text-gray-400 mt-1' }, `${props.latency}ms`) : null,
|
||||
props.error ? h('div', { class: 'text-xs text-red-400 mt-1 truncate' }, props.error) : null
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
export default {
|
||||
components: { ServiceStatusCard }
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user