feat: add deployment monitor page

- Add /deploy_status API endpoint for system status check
- Create Monitor.tsx component with real-time status display
- Support CUDA, FFmpeg, Whisper model status monitoring
- Auto-refresh every 30 seconds with manual refresh option
This commit is contained in:
sibuchen
2026-02-06 16:15:11 +08:00
parent 7b45db2f59
commit 8cd8c6f7b4
5 changed files with 309 additions and 9 deletions

View File

@@ -7,12 +7,9 @@ import { Route } from 'react-router-dom'
import Index from '@/pages/Index.tsx'
import NotFoundPage from '@/pages/NotFoundPage'
import Model from '@/pages/SettingPage/Model.tsx'
import Transcriber from '@/pages/SettingPage/transcriber.tsx'
import ProviderForm from '@/components/Form/modelForm/Form.tsx'
import StepBar from '@/pages/HomePage/components/StepBar.tsx'
import Downloading from '@/components/Lottie/download.tsx'
import Prompt from '@/pages/SettingPage/Prompt.tsx'
import AboutPage from '@/pages/SettingPage/about.tsx'
import Monitor from '@/pages/SettingPage/Monitor.tsx'
import Downloader from '@/pages/SettingPage/Downloader.tsx'
import DownloaderForm from '@/components/Form/DownloaderForm/Form.tsx'
import { useEffect } from 'react'
@@ -56,6 +53,7 @@ function App() {
<Route path="download" element={<Downloader />}>
<Route path=":id" element={<DownloaderForm />} />
</Route>
<Route path="monitor" element={<Monitor />}></Route>
<Route path="about" element={<AboutPage />}></Route>
<Route path="*" element={<NotFoundPage />} />
</Route>

View File

@@ -1,10 +1,8 @@
import {
BotMessageSquare,
SquareChevronRight,
Captions,
HardDriveDownload,
Wrench,
Info,
Activity,
} from 'lucide-react'
import MenuBar, { IMenuProps } from '@/pages/SettingPage/components/menuBar.tsx'
@@ -37,6 +35,12 @@ const Menu = () => {
// icon: <SquareChevronRight />,
// path: '/settings/prompt',
// },
{
id: 'monitor',
name: '部署监控',
icon: <Activity />,
path: '/settings/monitor',
},
{
id: 'about',
name: '关于',

View File

@@ -0,0 +1,241 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { ScrollArea } from '@/components/ui/scroll-area'
import {
Server,
Cpu,
AudioLines,
Film,
RefreshCw,
CheckCircle2,
XCircle,
Loader2
} from 'lucide-react'
import { useState, useEffect, useCallback } from 'react'
import { getDeployStatus, DeployStatus } from '@/services/system'
export default function Monitor() {
const [status, setStatus] = useState<DeployStatus | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
const fetchStatus = useCallback(async () => {
try {
setLoading(true)
setError(null)
const data = await getDeployStatus()
setStatus(data)
setLastUpdated(new Date())
} catch (err) {
setError('无法连接到后端服务')
setStatus(null)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchStatus()
// 自动刷新(每 30 秒)
const interval = setInterval(fetchStatus, 30000)
return () => clearInterval(interval)
}, [fetchStatus])
const StatusBadge = ({ ok, label }: { ok: boolean; label?: string }) => (
<Badge
variant={ok ? 'default' : 'destructive'}
className={ok ? 'bg-green-500 hover:bg-green-600' : ''}
>
{ok ? (
<><CheckCircle2 className="mr-1 h-3 w-3" />{label || '正常'}</>
) : (
<><XCircle className="mr-1 h-3 w-3" />{label || '异常'}</>
)}
</Badge>
)
return (
<ScrollArea className="h-full overflow-y-auto bg-white">
<div className="container mx-auto px-4 py-8">
{/* Header */}
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold"></h1>
<p className="text-muted-foreground text-sm">
</p>
</div>
<div className="flex items-center gap-4">
{lastUpdated && (
<span className="text-muted-foreground text-xs">
: {lastUpdated.toLocaleTimeString()}
</span>
)}
<Button
variant="outline"
size="sm"
onClick={fetchStatus}
disabled={loading}
>
{loading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<RefreshCw className="mr-2 h-4 w-4" />
)}
</Button>
</div>
</div>
{error && (
<div className="mb-6 rounded-lg border border-red-200 bg-red-50 p-4 text-red-700">
{error}
</div>
)}
{/* Status Cards */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{/* Backend FastAPI */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-lg font-medium">
<Server className="mr-2 inline h-5 w-5 text-blue-500" />
FastAPI
</CardTitle>
{status && <StatusBadge ok={status.backend.status === 'running'} label="运行中" />}
</CardHeader>
<CardContent>
{loading && !status ? (
<div className="flex items-center gap-2 text-gray-500">
<Loader2 className="h-4 w-4 animate-spin" />
...
</div>
) : status ? (
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span className={status.backend.status === 'running' ? 'font-medium text-green-600' : 'font-medium text-red-600'}>
{status.backend.status === 'running' ? '运行中' : status.backend.status}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-mono">{status.backend.port}</span>
</div>
</div>
) : null}
</CardContent>
</Card>
{/* CUDA GPU */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-lg font-medium">
<Cpu className="mr-2 inline h-5 w-5 text-green-500" />
CUDA GPU
</CardTitle>
{status && <StatusBadge ok={status.cuda.available} label={status.cuda.available ? '已启用' : '未启用'} />}
</CardHeader>
<CardContent>
{loading && !status ? (
<div className="flex items-center gap-2 text-gray-500">
<Loader2 className="h-4 w-4 animate-spin" />
...
</div>
) : status ? (
<div className="space-y-2 text-sm">
{status.cuda.available ? (
<>
<div className="flex justify-between">
<span className="text-muted-foreground">GPU:</span>
<span className="font-medium">{status.cuda.gpu_name}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">CUDA :</span>
<span className="font-mono">{status.cuda.version}</span>
</div>
</>
) : (
<div className="text-muted-foreground">
CUDA 使 CPU
</div>
)}
</div>
) : null}
</CardContent>
</Card>
{/* Whisper Model */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-lg font-medium">
<AudioLines className="mr-2 inline h-5 w-5 text-purple-500" />
Whisper
</CardTitle>
{status && <StatusBadge ok={true} label="已配置" />}
</CardHeader>
<CardContent>
{loading && !status ? (
<div className="flex items-center gap-2 text-gray-500">
<Loader2 className="h-4 w-4 animate-spin" />
...
</div>
) : status ? (
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-medium">{status.whisper.model_size}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span className="font-mono">{status.whisper.transcriber_type}</span>
</div>
</div>
) : null}
</CardContent>
</Card>
{/* FFmpeg */}
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-lg font-medium">
<Film className="mr-2 inline h-5 w-5 text-orange-500" />
FFmpeg
</CardTitle>
{status && <StatusBadge ok={status.ffmpeg.available} label={status.ffmpeg.available ? '可用' : '不可用'} />}
</CardHeader>
<CardContent>
{loading && !status ? (
<div className="flex items-center gap-2 text-gray-500">
<Loader2 className="h-4 w-4 animate-spin" />
...
</div>
) : status ? (
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">:</span>
<span className={status.ffmpeg.available ? 'font-medium text-green-600' : 'font-medium text-red-600'}>
{status.ffmpeg.available ? '已安装' : '未安装'}
</span>
</div>
{!status.ffmpeg.available && (
<div className="text-xs text-red-500">
FFmpeg PATH
</div>
)}
</div>
) : null}
</CardContent>
</Card>
</div>
{/* Footer Info */}
<div className="mt-8 text-center text-xs text-gray-400">
30
</div>
</div>
</ScrollArea>
)
}

View File

@@ -1,5 +1,29 @@
import request from '@/utils/request'
export const systemCheck=async()=>{
export const systemCheck = async () => {
return await request.get('/sys_health')
}
export interface DeployStatus {
backend: {
status: string
port: number
}
cuda: {
available: boolean
version: string | null
gpu_name: string | null
}
whisper: {
model_size: string
transcriber_type: string
}
ffmpeg: {
available: boolean
}
}
export const getDeployStatus = async (): Promise<DeployStatus> => {
return await request.get('/deploy_status')
}

View File

@@ -42,4 +42,37 @@ async def sys_health():
@router.get("/sys_check")
async def sys_check():
return R.success()
return R.success()
@router.get("/deploy_status")
async def deploy_status():
"""返回部署监控所需的所有状态信息"""
import torch
import os
# CUDA 状态
cuda_available = torch.cuda.is_available()
cuda_info = {
"available": cuda_available,
"version": torch.version.cuda if cuda_available else None,
"gpu_name": torch.cuda.get_device_name(0) if cuda_available else None,
}
# Whisper 模型状态
model_size = os.getenv("WHISPER_MODEL_SIZE", "base")
transcriber_type = os.getenv("TRANSCRIBER_TYPE", "fast-whisper")
# FFmpeg 状态
try:
ensure_ffmpeg_or_raise()
ffmpeg_ok = True
except:
ffmpeg_ok = False
return R.success(data={
"backend": {"status": "running", "port": int(os.getenv("BACKEND_PORT", 8483))},
"cuda": cuda_info,
"whisper": {"model_size": model_size, "transcriber_type": transcriber_type},
"ffmpeg": {"available": ffmpeg_ok},
})