import { memo, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import '@/i18n/domains' import { Activity, Database, Gauge, Loader2, RefreshCw } from 'lucide-react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { PageContainer } from '@/components/PageContainer' import { EmptyState } from '@/components/EmptyState' import { ErrorState } from '@/components/ErrorState' import { LoadingSpinner } from '@/components/LoadingSpinner' import { StatCard } from '@/components/StatCard' import { http } from '@/api/http' import { getUser } from '@/lib/auth-storage' interface PathCount { path: string; count: number } interface Summary { http: { total: number; byStatus: Record; inFlight: number; topPaths: PathCount[] } db: { sizeBytes: number; saves: number; saveFailures: number; records: Record } alerts: { evaluations: Record } loki: { queries: Record } runtime: { goroutines: number; goVersion: string; version: string } } const fmtBytes = (n: number) => { if (n >= 1 << 30) return `${(n / (1 << 30)).toFixed(2)} GB` if (n >= 1 << 20) return `${(n / (1 << 20)).toFixed(1)} MB` if (n >= 1 << 10) return `${(n / (1 << 10)).toFixed(1)} KB` return `${n} B` } /** 平台自监控(admin):聚合 /metrics 为可读快照 — 请求/DB/告警引擎/运行时 */ const MonitorPage = memo(function MonitorPage() { const { t } = useTranslation('monitor') const isAdmin = getUser()?.role === 'admin' const [data, setData] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const versionRef = useRef(0) const load = useCallback(() => { const v = ++versionRef.current setLoading(true) setError('') http.get('/system/metrics-summary') .then(result => { if (v === versionRef.current) setData(result) }) .catch(err => { if (v === versionRef.current) setError(err instanceof Error ? err.message : t('loadFailed')) }) .finally(() => { if (v === versionRef.current) setLoading(false) }) }, [t]) useEffect(() => { if (isAdmin) load() }, [isAdmin, load]) if (!isAdmin) { return ( ) } const statusEntries = data ? Object.entries(data.http.byStatus).sort(([a], [b]) => a.localeCompare(b)) : [] const err5xx = statusEntries.filter(([code]) => code.startsWith('5')).reduce((acc, [, v]) => acc + v, 0) return ( {loading ? : } {t('refresh')} } > {error ? ( ) : loading && !data ? ( ) : !data ? null : ( <>
0 ? 'text-destructive' : 'text-muted-foreground'} iconBg={err5xx > 0 ? 'bg-destructive/10' : 'bg-muted'} />
{t('topPaths.title')}

{t('topPaths.subtitle')}

{data.http.topPaths.length === 0 ? (

{t('empty')}

) : ( {data.http.topPaths.map(item => ( ))}
{t('topPaths.path')}{t('topPaths.count')}
{item.path} {item.count.toLocaleString()}
)}
{t('breakdown.title')}

{t('breakdown.subtitle')}

{t('breakdown.byStatus')}

{statusEntries.length === 0 ?

{t('empty')}

: statusEntries.map(([code, count]) => (
{code} {count.toLocaleString()}
))}

{t('breakdown.alerts')}

{Object.entries(data.alerts.evaluations).map(([result, count]) => (
{t(`result.${result}`, result)} {count.toLocaleString()}
))}

{t('breakdown.loki')}

{Object.entries(data.loki.queries).map(([result, count]) => (
{t(`result.${result}`, result)} {count.toLocaleString()}
))}

{t('breakdown.db')}

{t('breakdown.dbSaves')}{data.db.saves.toLocaleString()}
0 ? 'text-destructive' : 'text-muted-foreground'}>{t('breakdown.dbFailures')}{data.db.saveFailures.toLocaleString()}

{t('buildInfo', { version: data.runtime.version || '-', go: data.runtime.goVersion || '-' })}

)}
) }) export default MonitorPage