import { useState, useEffect } from 'react'; import { Topbar } from '../../components/layout/Topbar'; import { Search, Upload, Download, RefreshCw, CheckCircle, XCircle, AlertTriangle, Info } from 'lucide-react'; import { UploadModal } from '../Docs/UploadModal'; import { useLanguage } from '../../contexts/LanguageContext'; import { getModelUsage, pingModelConnections } from '../../api/status'; import type { ModelUsageEntry } from '../../api/index'; const TOKEN_KEY = 'auth_token'; function authHeader(): Record { const t = localStorage.getItem(TOKEN_KEY); return t ? { Authorization: `Bearer ${t}` } : {}; } // ── API types ────────────────────────────────────────────────────────────── interface Stats { documents_total: number; documents_indexed: number; documents_failed: number; chunks_total: number; } interface Health { milvus: { status: string; connected?: boolean; collection_name?: string; num_entities?: number; error?: string }; minio: { status: string; connected: boolean }; bm25: { available: boolean }; reranker: { enabled: boolean; model: string | null }; sessions: { active: number; max: number }; } interface Config { embedding_model: string; embedding_dim: number; embedding_base_url: string; milvus_collection: string; parser_backend: string; chunk_backend: string; llm_provider: string; llm_model: string; parser_failure_mode: string; artifact_prefix?: string; document_metadata_path?: string; } // ── Small helpers ────────────────────────────────────────────────────────── function StatusIcon({ status }: { status: 'ok' | 'error' | 'warn' | 'info' }) { if (status === 'ok') return ; if (status === 'error') return ; if (status === 'warn') return ; return ; } function ServiceRow({ name, status, detail }: { name: string; status: 'ok' | 'error' | 'warn' | 'info'; detail?: string }) { const { t } = useLanguage(); return (
{name} {detail && {detail}} {status === 'ok' ? t.status.badgeOnline : status === 'error' ? t.status.badgeError : status === 'warn' ? t.status.badgeDegraded : t.status.badgeUnknown}
); } function ConfigRow({ label, value }: { label: string; value: string | number | null | undefined }) { return (
{label} {value ?? '—'}
); } // ── Main component ───────────────────────────────────────────────────────── export function StatusPage() { const { t } = useLanguage(); const [stats, setStats] = useState(null); const [health, setHealth] = useState(null); const [config, setConfig] = useState(null); const [loading, setLoading] = useState(true); const [healthLoading, setHealthLoading] = useState(true); const [modelsLoading, setModelsLoading] = useState(true); const [configOpen, setConfigOpen] = useState(false); const [refreshKey, setRefreshKey] = useState(0); const [showUpload, setShowUpload] = useState(false); const [lastRefresh, setLastRefresh] = useState(null); const [modelUsage, setModelUsage] = useState(null); const [pinging, setPinging] = useState(false); useEffect(() => { setLoading(true); setHealthLoading(true); setModelsLoading(true); // Fetch all endpoints in parallel. The first three use raw fetch() (legacy // pattern already established in this file); model usage uses the typed // fetchAPI-based client from api/status.ts — new code should prefer that. Promise.allSettled([ fetch('/api/v1/status/stats', { headers: authHeader() }).then(r => r.json()), fetch('/api/v1/status/health', { headers: authHeader() }).then(r => r.json()), fetch('/api/v1/status/config', { headers: authHeader() }).then(r => r.json()), getModelUsage(), ]).then(([statsRes, healthRes, configRes, modelsRes]) => { if (statsRes.status === 'fulfilled') setStats(statsRes.value); else setStats({ documents_total: 0, documents_indexed: 0, documents_failed: 0, chunks_total: 0 }); if (healthRes.status === 'fulfilled') setHealth(healthRes.value); if (configRes.status === 'fulfilled') setConfig(configRes.value); if (modelsRes.status === 'fulfilled') setModelUsage(modelsRes.value.models); else setModelUsage(null); setLoading(false); setHealthLoading(false); setModelsLoading(false); setLastRefresh(new Date()); }); }, [refreshKey]); // ── Derived values ─────────────────────────────────────────────────────── const indexedPct = stats && stats.documents_total > 0 ? Math.round((stats.documents_indexed / stats.documents_total) * 100) : 0; function milvusStatus(): 'ok' | 'error' | 'warn' | 'info' { if (!health) return 'info'; return health.milvus.status === 'ok' ? 'ok' : 'error'; } function milvusDetail() { if (!health) return undefined; if (health.milvus.error) return health.milvus.error.slice(0, 60); const parts: string[] = []; if (health.milvus.collection_name) parts.push(health.milvus.collection_name); if (health.milvus.num_entities !== undefined) parts.push(`${health.milvus.num_entities.toLocaleString()} entities`); return parts.join(' · ') || undefined; } // ── Export ─────────────────────────────────────────────────────────────── function handleExport() { const data = { stats, health, config, exportedAt: new Date().toISOString() }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `regulation-hub-status-${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); } async function handleTestConnections() { setPinging(true); try { const res = await pingModelConnections(); setModelUsage(res.models); } catch { // Leave modelUsage as-is; the card below already shows a muted // "never_called"/error state per row when data can't be refreshed. } finally { setPinging(false); } } function modelBadgeStatus(status: ModelUsageEntry['status']): 'ok' | 'error' | 'warn' | 'info' { if (status === 'ok') return 'ok'; if (status === 'error') return 'error'; if (status === 'disabled') return 'info'; return 'info'; // never_called } function modelStatusLabel(entry: ModelUsageEntry): string { if (entry.status === 'never_called') return t.status.modelStatusNeverCalled; if (entry.status === 'disabled') return t.status.modelStatusDisabled; return entry.status === 'ok' ? t.status.badgeOnline : t.status.badgeError; } /** Small relative-ish hint shown next to provider/model — "Never" or a local time string. */ function modelLastCalledLabel(entry: ModelUsageEntry): string { if (!entry.last_called_at) return t.status.lastCalledNever; return new Date(entry.last_called_at).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }); } return (
} />
{/* ── Stats grid ────────────────────────────────────────────────── */}
{loading ? :
{stats?.documents_total ?? '—'}
}
{t.status.statTotal}
{loading ? :
{stats?.documents_indexed ?? '—'}
}
{t.status.statIndexed}
{loading ? :
{stats?.documents_failed ?? '—'}
}
{t.status.statFailed}
{loading ? :
{stats?.chunks_total?.toLocaleString() ?? '—'}
}
{t.status.statChunks}
{/* Indexed progress bar */} {!loading && stats && stats.documents_total > 0 && (
{t.status.statCoverage}
60 ? 'var(--accent)' : 'var(--warn)', transition: 'width 0.6s ease', }} />
{indexedPct}% ({stats.documents_indexed}/{stats.documents_total})
)} {/* ── Main panel grid ───────────────────────────────────────────── */}
{/* System health */}
{t.status.cardHealth} {lastRefresh && ( Updated {lastRefresh.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' })} )}
{healthLoading ? (
{[1, 2, 3, 4, 5].map(i => (
))}
) : health ? ( <> ) : (
{t.status.healthEndpointError}
)}
{/* AI Models — connection status + cumulative token usage */}
{t.status.cardModels}
{modelsLoading ? (
{[1, 2, 3, 4].map(i => (
))}
) : modelUsage ? ( modelUsage.map(entry => { const roleLabel = entry.role === 'main_llm' ? t.status.roleMainLlm : entry.role === 'hyde_llm' ? t.status.roleHydeLlm : entry.role === 'embedding' ? t.status.roleEmbedding : t.status.roleReranker; return (
{roleLabel} {entry.provider}/{entry.model} {entry.shares_usage_with && ` · ${t.status.sharesUsageWithMain}`} {` · ${modelLastCalledLabel(entry)}`} {entry.total_tokens > 0 || entry.status === 'ok' || entry.status === 'error' ? entry.total_tokens.toLocaleString() : '—'} {modelStatusLabel(entry)}
); }) ) : (
{t.status.modelsLoadError}
)}
{/* System config (collapsible) */}
{configOpen && (
{config ? ( <> ) : (
{t.status.configLoadError}
)}
)}
{/* Document breakdown */}
{t.status.cardBreakdown}
{loading ? (
{[1, 2, 3].map(i =>
)}
) : stats ? ( <> {[ { label: t.status.breakdownIndexed, value: stats.documents_indexed, total: stats.documents_total, color: 'var(--ok)' }, { label: t.status.breakdownProcessing, value: stats.documents_total - stats.documents_indexed - stats.documents_failed, total: stats.documents_total, color: 'var(--warn)' }, { label: t.status.breakdownFailed, value: stats.documents_failed, total: stats.documents_total, color: 'var(--danger)' }, ].map(row => { const pct = stats.documents_total > 0 ? Math.round((Math.max(0, row.value) / stats.documents_total) * 100) : 0; return (
{row.label} {Math.max(0, row.value)} ({pct}%)
); })}
{t.status.totalChunks} {stats.chunks_total.toLocaleString()}
) : null}
{/* Sessions & reranker quick facts */} {health && (
{t.status.cardRuntime}
{t.status.labelActiveSessions} {health.sessions.active}
{t.status.labelSessionCapacity} {health.sessions.max}
{t.status.labelBM25} {health.bm25.available ? t.status.statusActive : t.status.statusUnavailable}
)}
Regulation Hub · T-Systems AI · {health ? (health.milvus.status === 'ok' && health.minio.connected ? t.status.footerAllOk : t.status.footerDegraded) : t.status.footerChecking}
{showUpload && setShowUpload(false)} />}
); }