Files
Profeto/frontend/src/admin/pages/Monitoring.tsx
T
shangfangjian 786f10aa11 feat: 核心模块增强 — 加密 + 运行时配置 + 日志缓冲
- crypto.py: API Key 加密/解密工具
- runtime_config.py: 运行时动态配置管理
- log_buffer.py: 内存日志缓冲区
- config.py: 新增加密配置项
- http_client.py: 增强重试和错误处理
2026-09-19 11:58:03 +08:00

153 lines
5.7 KiB
TypeScript

/**
* Admin 后台 - 监控面板(报刊风)
*
* 功能:
* - /health 存活检查(自动:30 秒一轮;可手动刷新)
* - /health/ready 数据库就绪检查
* - 服务名 / 版本 / 运行时间 / 检查项(后端返回什么就展示什么)
*/
import { useCallback, useEffect, useState } from 'react'
import { fetchHealth } from '../dal'
import { api } from '../api'
import { Card, CardBody, CardHeader, SectionHeader, Alert, Spinner, Badge } from '../components'
export default function MonitoringPage() {
const [health, setHealth] = useState<any>(null)
const [ready, setReady] = useState<'ready' | 'not_ready' | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [lastCheck, setLastCheck] = useState<string>('')
const refresh = useCallback(async () => {
setLoading(true)
setError(null)
try {
const [h, r] = await Promise.allSettled([
fetchHealth(),
api.get<{ status: string }>('/health/ready'),
])
setHealth(h.status === 'fulfilled' ? h.value : null)
setReady(r.status === 'fulfilled' ? (r.value?.status as 'ready' | 'not_ready') : null)
if (h.status === 'rejected') {
setError(h.reason instanceof Error ? h.reason.message : '无法连接到后端')
}
setLastCheck(new Date().toLocaleTimeString('zh-CN', { hour12: false }))
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
refresh()
const t = setInterval(refresh, 30_000)
return () => clearInterval(t)
}, [refresh])
const alive = health?.status === 'healthy' || health?.status === 'ok'
return (
<div className="space-y-6">
<SectionHeader
title="系统监控"
description="存活与数据库就绪检查,每 30 秒自动巡检一次。"
/>
<div className="flex items-center justify-between">
<span className="text-2xs text-ink-400">
{lastCheck && `最近巡检 ${lastCheck}`}
</span>
<button onClick={refresh} disabled={loading} className="btn btn-sm">
{loading ? (<><Spinner /> 检查中</>) : '立即巡检'}
</button>
</div>
{error && (
<Alert
kind="error"
title="无法连接到后端"
message={`${error}\n请确认服务是否正常运行,以及登录会话是否已过期。`}
/>
)}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{/* 存活状态 */}
<div className="border border-ink-900 bg-paper-50 p-5">
<div className="text-2xs tracking-[0.2em] text-ink-400">存活状态</div>
<div className="mt-2 flex items-center gap-2">
<span
className={`inline-block h-2 w-2 ${alive ? 'bg-ink-900' : 'bg-press'}`}
aria-hidden="true"
/>
<span className={`font-serif text-xl font-bold ${alive ? 'text-ink-900' : 'text-press'}`}>
{health ? (alive ? '正常' : String(health.status)) : '—'}
</span>
</div>
</div>
{/* 数据库就绪 */}
<div className="border border-ink-900 bg-paper-50 p-5">
<div className="text-2xs tracking-[0.2em] text-ink-400">数据库就绪</div>
<div className="mt-2 flex items-center gap-2">
<span
className={`inline-block h-2 w-2 ${ready === 'ready' ? 'bg-ink-900' : ready === null ? 'bg-ink-300' : 'bg-press'}`}
aria-hidden="true"
/>
<span
className={`font-serif text-xl font-bold ${ready === 'not_ready' ? 'text-press' : 'text-ink-900'}`}
>
{ready === 'ready' ? '就绪' : ready === 'not_ready' ? '未就绪' : '—'}
</span>
</div>
</div>
{/* 服务名 */}
<div className="border border-ink-900 bg-paper-50 p-5">
<div className="text-2xs tracking-[0.2em] text-ink-400">服务</div>
<div className="mt-2 font-serif text-xl font-bold text-ink-900">
{health?.service || 'profeto'}
</div>
</div>
{/* 版本 */}
{health?.version && (
<div className="border border-ink-900 bg-paper-50 p-5">
<div className="text-2xs tracking-[0.2em] text-ink-400">版本</div>
<div className="mt-2 font-serif text-xl font-bold tabular-nums text-ink-900">
{health.version}
</div>
</div>
)}
{/* 运行时间 */}
{health?.uptime_seconds != null && (
<div className="border border-ink-900 bg-paper-50 p-5">
<div className="text-2xs tracking-[0.2em] text-ink-400">运行时间</div>
<div className="mt-2 font-serif text-xl font-bold tabular-nums text-ink-900">
{Math.floor(health.uptime_seconds / 3600)}h{' '}
{Math.floor((health.uptime_seconds % 3600) / 60)}m
</div>
</div>
)}
{/* 检查项 */}
{health?.checks && Object.keys(health.checks).length > 0 && (
<div className="border border-ink-900 bg-paper-50 p-5 sm:col-span-2 lg:col-span-1">
<div className="text-2xs tracking-[0.2em] text-ink-400">健康检查</div>
<div className="mt-2 space-y-1">
{Object.entries(health.checks).map(([key, val]) => (
<div key={key} className="flex items-center justify-between text-sm">
<span className="text-2xs text-ink-500">{key}</span>
<Badge status={String(val) === 'pass' ? 'success' : 'error'}>
{String(val)}
</Badge>
</div>
))}
</div>
</div>
)}
</div>
</div>
)
}