import React from 'react'; import { useQuery } from '@tanstack/react-query'; import { Activity, AlertTriangle, CheckCircle2, Cpu, Database, Layers, RefreshCw, Server, Settings2, ListTree, XCircle, } from 'lucide-react'; import { adminApi } from '../../api/client'; import { ProgressBar } from '../../components/ProgressBar'; interface Health { name: string; ok: boolean; detail?: string } interface ActiveTask { name: string; id: string; args: string; started_ago_s: number | null } interface Worker { name: string; concurrency: number | null; reserved: number; active: ActiveTask[] } interface Run { id: number; source_id: number; status: string; stage: string; target: number; fetched: number; processed: number; added: number; duplicates: number; skipped: number; failed: number; error: string | null; percent: number; started_at: string; run_started_at: string | null; heartbeat_at: string | null; queued_s: number | null; duration_s: number | null; } interface DebugData { generated_at: string; celery: { workers: Worker[]; error?: string }; queues: Record | { error: string }; corpus: { documents: number; documents_marked_embedded: number; vector_index: { backend?: string; vectors?: number; dim?: number; embed_model?: string; error?: string }; fingerprints_estimate: number; elasticsearch_documents: number | null; }; sources: { by_status: Record; active_runs: Run[]; stale_run_ids: number[]; recent_problem_runs: Run[]; }; tasks_24h: Record; recent_failed_tasks: { public_id: string; type: string; error: string; created_at: string }[]; config: Record; } const STAGE_LABELS: Record = { queued: 'в очереди', fetch: 'выборка', index: 'индексация', finished: 'завершено', }; function fmtNum(n: number | null | undefined): string { return n == null ? '—' : n.toLocaleString('ru-RU'); } /** Сколько идущий прогон уже работает — по нему видно застрявший. */ function runningFor(run: Run): string { if (!run.run_started_at) return 'ещё не начат'; const sec = Math.max(0, (Date.now() - new Date(run.run_started_at + 'Z').getTime()) / 1000); return sec < 90 ? `${Math.round(sec)}с` : `${Math.round(sec / 60)} мин`; } export function Debug() { const { data, isFetching, error } = useQuery({ queryKey: ['admin-debug'], queryFn: () => adminApi.debug().then((r) => r.data as DebugData), refetchInterval: 5000, }); // Отдельным запросом: когда отваливается инфраструктура (брокер, Ollama), // отладка должна первой показывать ЧТО именно недоступно, а не только следствия const { data: health } = useQuery({ queryKey: ['admin-health'], queryFn: () => adminApi.health().then((r) => r.data as Health[]), refetchInterval: 10000, }); if (error || !data) { return (
{error ?
Не удалось получить срез состояния: {String(error)}
:
Сбор данных…
}
); } const queuesErr = 'error' in data.queues ? (data.queues as { error: string }).error : null; const queues = queuesErr ? {} : (data.queues as Record); // Покрытие L3 считаем по РЕАЛЬНОМУ содержимому индекса: пометка faiss_id в БД // остаётся и когда вектор туда не попал, и завышает картину в разы const vectors = data.corpus.vector_index?.vectors ?? null; const embedPercent = data.corpus.documents && vectors != null ? (vectors / data.corpus.documents) * 100 : 0; const staleMarks = vectors != null ? Math.max(0, data.corpus.documents_marked_embedded - vectors) : 0; return (

Отладка {isFetching && }

срез от {new Date(data.generated_at).toLocaleTimeString('ru-RU')}
{/* Активные прогоны заливки */} {data.sources.stale_run_ids.length > 0 && (
Прогоны без признаков жизни больше 10 минут: {data.sources.stale_run_ids.join(', ')}. Обычно это упавший или перезапущенный worker-indexer — проверьте его логи и очередь queue.index.
)} {data.sources.active_runs.length ? (
{data.sources.active_runs.map((r) => (
#{r.id} · источник {r.source_id} · {runningFor(r)}
))}
) : (

Сейчас ничего не заливается.

)}
{Object.entries(data.sources.by_status).map(([s, c]) => ( {s}: {c} ))}
{/* Воркеры */} {data.celery.error &&

{data.celery.error}

} {data.celery.workers.length ? (
{data.celery.workers.map((w) => (
{w.name} параллельно: {w.concurrency ?? '—'} · в работе: {w.active.length} · зарезервировано: {w.reserved}
{w.active.length ? (
{w.active.map((t) => (
{t.started_ago_s != null ? `${t.started_ago_s}с` : '—'} {t.name} {t.args}
))}
) :

простаивает

}
))}
) :

Воркеры не отвечают на ping.

}
{/* Очереди */} {queuesErr ? (

{queuesErr}

) : ( {Object.entries(queues).map(([name, q]) => ( ))}
ОчередьЖдутВ работеПотребителей
{name} {fmtNum(q.messages)} {fmtNum(q.unacked)} {q.consumers}
)}
{/* Корпус */}
{data.corpus.vector_index?.error && (

индекс недоступен: {data.corpus.vector_index.error}

)} = 99 ? 'done' : 'partial'} label={`покрытие L3 (реально в индексе ${data.corpus.vector_index?.backend || '—'}): ` + `без вектора ${fmtNum(Math.max(0, data.corpus.documents - (vectors ?? 0)))} документов`} /> {staleMarks > 0 && (
У {fmtNum(staleMarks)} документов в базе стоит отметка faiss_id, но вектора в индексе нет — обычно это след пересоздания индекса после смены модели эмбеддингов. Такие документы не участвуют в семантическом поиске и не будут пересчитаны, пока отметку не сбросить: scripts/ops/faiss_reconcile.py.
)}
{/* Проблемные прогоны */} {data.sources.recent_problem_runs.length > 0 && (
{data.sources.recent_problem_runs.map((r) => (
#{r.id} источник {r.source_id} {r.status} получено {r.fetched}/{r.target}, добавлено {r.added}, ошибок {r.failed} {r.duration_s != null && ` · работал ${Math.round(r.duration_s)}с`} {r.error && {r.error}}
))}
)} {/* Задачи пользователей */}
{Object.entries(data.tasks_24h).map(([s, c]) => ( {s}: {c} ))} {!Object.keys(data.tasks_24h).length && задач не было}
{data.recent_failed_tasks.length > 0 && (
Последние упавшие:
{data.recent_failed_tasks.map((t) => (
{new Date(t.created_at).toLocaleString('ru-RU')} {t.type} {t.error || '—'}
))}
)}
{/* Конфигурация */}
{Object.entries(data.config).map(([k, v]) => (
{k} {v}
))}
); } /** Что из инфраструктуры доступно прямо сейчас — первый вопрос при разборе аварии. */ function HealthCard({ health }: { health?: Health[] }) { return ( {health?.length ? (
{health.map((h) => (
{h.ok ? : } {h.name} {h.detail || (h.ok ? 'OK' : 'недоступен')}
))}
) :

Опрос сервисов…

}
); } function Card({ icon: Icon, title, children }: { icon: React.ElementType; title: string; children: React.ReactNode }) { return (

{title}

{children}
); } function Metric({ label, value }: { label: string; value: string }) { return (
{value}
{label}
); }