'use client'; import { useEffect, useState } from 'react'; import { Activity, AlertTriangle, CheckCircle2, Clock3, Database, Loader2, RefreshCw, ServerCog, } from 'lucide-react'; import { RouteGuard } from '@/components/auth/Guard'; import { api } from '@/lib/api'; type JobStatus = 'queued' | 'running' | 'succeeded' | 'failed'; interface OpsRuntimeSnapshot { collectedAt: string; thresholds: { apiSlowRequestMs: number; prismaSlowQueryMs: number; }; database: { ok: boolean; error?: string; }; slowRequests: Array<{ id: string; method: string; path: string; durationMs: number; thresholdMs: number; occurredAt: string; }>; slowQueries: Array<{ id: string; queryPreview: string; durationMs: number; thresholdMs: number; occurredAt: string; }>; jobQueue: { totals: Record & { total: number }; byType: Array & { type: string; total: number; oldestQueuedAt?: string; nextLeaseExpiresAt?: string; }>; recentFailures: Array<{ id: string; type: string; attempts: number; maxAttempts: number; lastError: string; updatedAt?: string; }>; }; dirtySummaryCount: number; access: { requiredPermission: string; backendEnforced: boolean; adapter: string; }; } const STATUS_LABEL: Record = { queued: '排队', running: '运行', succeeded: '成功', failed: '失败', }; export default function OpsPage() { return ( ); } function OpsPageContent() { const [snapshot, setSnapshot] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); const fetchSnapshot = async (initial = false) => { if (initial) setLoading(true); else setRefreshing(true); try { const next = await api.get('/ops/runtime'); setSnapshot(next); setError(null); } catch (e: any) { setError(e?.message || '读取失败'); } finally { setLoading(false); setRefreshing(false); } }; useEffect(() => { void fetchSnapshot(true); const timer = window.setInterval(() => { void fetchSnapshot(); }, 30_000); return () => window.clearInterval(timer); }, []); return (

运维看板

{snapshot && ( {formatClock(snapshot.collectedAt)} )}
{loading ? (
加载中
) : error ? (
{error}
) : snapshot ? (
) : null}
); } function OverviewStrip({ snapshot }: { snapshot: OpsRuntimeSnapshot }) { const cards = [ { label: '慢请求', value: snapshot.slowRequests.length, sub: `阈值 ${snapshot.thresholds.apiSlowRequestMs}ms`, icon: Clock3, tone: 'blue', }, { label: '慢查询', value: snapshot.slowQueries.length, sub: `阈值 ${snapshot.thresholds.prismaSlowQueryMs}ms`, icon: Database, tone: 'amber', }, { label: '后台任务', value: snapshot.jobQueue.totals.total, sub: `${snapshot.jobQueue.totals.queued} 排队 / ${snapshot.jobQueue.totals.running} 运行`, icon: ServerCog, tone: 'zinc', }, { label: '脏 Summary', value: snapshot.dirtySummaryCount, sub: 'xiaobao_risk_summaries', icon: AlertTriangle, tone: snapshot.dirtySummaryCount > 0 ? 'red' : 'emerald', }, ]; return (
{cards.map((card) => { const Icon = card.icon; return (

{card.label}

{card.value}

{card.sub}

); })}
); } function SlowRequestsPanel({ snapshot }: { snapshot: OpsRuntimeSnapshot }) { return (
{snapshot.slowRequests.length === 0 ? ( ) : snapshot.slowRequests.map((item) => (
{item.method} {item.path} {item.durationMs}ms {formatClock(item.occurredAt)}
))}
); } function SlowQueriesPanel({ snapshot }: { snapshot: OpsRuntimeSnapshot }) { return (
{snapshot.slowQueries.length === 0 ? ( ) : snapshot.slowQueries.map((item) => (
{item.queryPreview} {item.durationMs}ms {formatClock(item.occurredAt)}
))}
); } function JobQueuePanel({ snapshot }: { snapshot: OpsRuntimeSnapshot }) { const rows = snapshot.jobQueue.byType; return (
{rows.length === 0 ? ( ) : rows.map((row) => (
{row.type} {row.total}
{(['queued', 'running', 'succeeded', 'failed'] as JobStatus[]).map((status) => ( ))}
{(row.oldestQueuedAt || row.nextLeaseExpiresAt) && (
{row.oldestQueuedAt && 最早排队 {formatClock(row.oldestQueuedAt)}} {row.nextLeaseExpiresAt && Lease {formatClock(row.nextLeaseExpiresAt)}}
)}
))}
); } function FailuresPanel({ snapshot }: { snapshot: OpsRuntimeSnapshot }) { const failures = snapshot.jobQueue.recentFailures; const dbOk = snapshot.database.ok; return (
{dbOk ? : } {dbOk ? '数据库可读' : snapshot.database.error || '数据库不可读'}
{failures.length === 0 ? ( ) : failures.map((item) => (
{item.type} {item.attempts}/{item.maxAttempts}

{item.lastError || '-'}

{item.updatedAt &&

{formatClock(item.updatedAt)}

}
))}
); } function StatusBars({ totals }: { totals: OpsRuntimeSnapshot['jobQueue']['totals'] }) { const statuses: JobStatus[] = ['queued', 'running', 'succeeded', 'failed']; const total = Math.max(1, totals.total); return (
{statuses.map((status) => ( ))}
{statuses.map((status) => )}
); } function StatusPill({ status, count }: { status: JobStatus; count: number }) { return (
{STATUS_LABEL[status]} {count}
); } function PanelHeader({ title, right }: { title: string; right: string }) { return (

{title}

{right}
); } function EmptyRow({ label }: { label: string }) { return
{label}
; } function formatClock(value?: string) { if (!value) return '-'; const date = new Date(value); if (!Number.isFinite(date.getTime())) return '-'; return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); } function toneClass(tone: string) { if (tone === 'blue') return 'bg-blue-50 text-blue-700'; if (tone === 'amber') return 'bg-amber-50 text-amber-700'; if (tone === 'red') return 'bg-red-50 text-red-700'; if (tone === 'emerald') return 'bg-emerald-50 text-emerald-700'; return 'bg-zinc-100 text-zinc-700'; } function statusBarClass(status: JobStatus) { if (status === 'queued') return 'bg-blue-500'; if (status === 'running') return 'bg-amber-500'; if (status === 'succeeded') return 'bg-emerald-500'; return 'bg-red-500'; } function statusPillClass(status: JobStatus) { if (status === 'queued') return 'bg-blue-50 text-blue-700'; if (status === 'running') return 'bg-amber-50 text-amber-700'; if (status === 'succeeded') return 'bg-emerald-50 text-emerald-700'; return 'bg-red-50 text-red-700'; }