import { useEffect, useState, useCallback } from 'react'; import { Loader2, RefreshCw, Activity, Database, Wifi } from 'lucide-react'; import { Card, CardContent } from '@shared/components/ui/card'; import { Badge } from '@shared/components/ui/badge'; import type { BadgeIntent } from '@lib/theme/variants'; import { fetchVesselAnalysis, type VesselAnalysisStats } from '@/services/vesselAnalysisApi'; const API_BASE = import.meta.env.VITE_API_URL ?? '/api'; interface PredictionHealth { status?: string; message?: string; snpdb?: boolean; kcgdb?: boolean; store?: { vessels?: number; points?: number; memory_mb?: number; targets?: number; permitted?: number }; } interface AnalysisStatus { timestamp?: string; duration_sec?: number; vessel_count?: number; upserted?: number; error?: string | null; status?: string; } /** * 시스템 상태 대시보드 (관제 모니터 카드). * * 표시: * 1. 우리 백엔드 (kcg-ai-backend) 상태 * 2. iran 백엔드 + Prediction (분석 사이클) * 3. 분석 결과 통계 (현재 시점) */ export function SystemStatusPanel() { const [stats, setStats] = useState(null); const [health, setHealth] = useState(null); const [analysis, setAnalysis] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const load = useCallback(async () => { setLoading(true); setError(''); try { const [vaRes, healthRes, statusRes] = await Promise.all([ fetchVesselAnalysis().catch(() => null), fetch(`${API_BASE}/prediction/health`, { credentials: 'include' }).then((r) => r.json()).catch(() => null), fetch(`${API_BASE}/prediction/status`, { credentials: 'include' }).then((r) => r.json()).catch(() => null), ]); if (vaRes) setStats(vaRes.stats); if (healthRes) setHealth(healthRes); if (statusRes) setAnalysis(statusRes); } catch (e: unknown) { setError(e instanceof Error ? e.message : 'unknown'); } finally { setLoading(false); } }, []); useEffect(() => { load(); // 30초마다 자동 새로고침 const timer = setInterval(load, 30000); return () => clearInterval(timer); }, [load]); return (
시스템 상태 (30초 자동 갱신)
{error &&
에러: {error}
}
{/* KCG 백엔드 */} } title="KCG AI Backend" status="UP" statusIntent="success" details={[ ['포트', ':8080'], ['프로파일', 'local'], ['DB', 'kcgaidb'], ]} /> {/* iran 백엔드 */} } title="iran 백엔드 (분석)" status={stats ? 'CONNECTED' : 'DISCONNECTED'} statusIntent={stats ? 'success' : 'critical'} details={[ ['선박 분석', stats ? `${stats.total.toLocaleString()}건` : '-'], ['클러스터', stats ? `${stats.clusterCount}` : '-'], ['어구 그룹', stats ? `${stats.gearGroups}` : '-'], ]} /> {/* Prediction */} } title="Prediction Service" status={health?.status || 'UNKNOWN'} statusIntent={health?.status === 'ok' ? 'success' : 'warning'} details={[ ['SNPDB', health?.snpdb === true ? 'OK' : '-'], ['KCGDB', health?.kcgdb === true ? 'OK' : '-'], ['최근 분석', analysis?.duration_sec ? `${analysis.duration_sec}초` : '-'], ]} />
{/* 위험도 분포 */} {stats && (
)}
); } function ServiceCard({ icon, title, status, statusIntent, details }: { icon: React.ReactNode; title: string; status: string; statusIntent: BadgeIntent; details: [string, string][]; }) { return (
{icon} {title}
{status}
{details.map(([k, v]) => (
{k} {v}
))}
); } function RiskBox({ label, value, color }: { label: string; value: number; color: string }) { return (
{label}
{value.toLocaleString()}
); }