feat(admin): состояние инфраструктуры на странице отладки
Сегодняшняя авария (упал гипервизор — вместе с ним брокер, Ollama, прокси) проявлялась в отладке косвенно: пустой список воркеров и ошибка очередей. Прямого ответа «что именно недоступно» страница не давала, хотя эндпоинт /admin/health его уже знает. Блок статусов рисуется и тогда, когда сам срез не собрался — именно в аварии он нужен больше всего, а собирается в этот момент дольше обычного. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -221,7 +221,10 @@ Identity (Universal Auth) и генерирует `.env` заново (`infisica
|
||||
Кнопки «Запустить всё» / «Остановить всё» — массовый старт и кооперативная отмена
|
||||
(флаг `cancel_requested`, воркер останавливается сам на ближайшем тике; уже
|
||||
начатый прогон не рвём посреди записи в базу).
|
||||
- **Панель отладки** (админка → «Отладка», `GET /api/admin/debug`): один срез —
|
||||
- **Панель отладки** (админка → «Отладка», `GET /api/admin/debug` + `/health`):
|
||||
один срез — доступность инфраструктуры (PostgreSQL/Redis/MinIO/ES/Ollama/
|
||||
брокер: этот блок показывается даже когда сам срез не собирается, потому что
|
||||
при аварии первый вопрос — что именно отвалилось),
|
||||
живые воркеры Celery и что именно они крутят, глубина очередей RabbitMQ
|
||||
(в т.ч. `unacked` и число потребителей), покрытие корпуса эмбеддингами,
|
||||
активные и проблемные прогоны, зависшие прогоны (нет heartbeat >10 мин),
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import {
|
||||
Activity, AlertTriangle, Cpu, Database, Layers, RefreshCw, Settings2, ListTree,
|
||||
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 {
|
||||
@@ -56,12 +58,23 @@ export function 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) {
|
||||
return <div className="text-red-600 text-sm">Не удалось получить срез состояния: {String(error)}</div>;
|
||||
}
|
||||
if (!data) {
|
||||
return <div className="text-gray-400 text-sm">Сбор данных…</div>;
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{error
|
||||
? <div className="text-red-600 text-sm">Не удалось получить срез состояния: {String(error)}</div>
|
||||
: <div className="text-gray-400 text-sm">Сбор данных…</div>}
|
||||
<HealthCard health={health} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const queuesErr = 'error' in data.queues ? (data.queues as { error: string }).error : null;
|
||||
@@ -80,6 +93,8 @@ export function Debug() {
|
||||
<span className="text-xs text-gray-400">срез от {new Date(data.generated_at).toLocaleTimeString('ru-RU')}</span>
|
||||
</div>
|
||||
|
||||
<HealthCard health={health} />
|
||||
|
||||
{/* Активные прогоны заливки */}
|
||||
<Card icon={Layers} title={`Заливка источников — активных прогонов: ${data.sources.active_runs.length}`}>
|
||||
{data.sources.stale_run_ids.length > 0 && (
|
||||
@@ -244,6 +259,31 @@ export function Debug() {
|
||||
);
|
||||
}
|
||||
|
||||
/** Что из инфраструктуры доступно прямо сейчас — первый вопрос при разборе аварии. */
|
||||
function HealthCard({ health }: { health?: Health[] }) {
|
||||
return (
|
||||
<Card icon={Server} title="Инфраструктура">
|
||||
{health?.length ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-1">
|
||||
{health.map((h) => (
|
||||
<div key={h.name} className="flex items-center justify-between gap-3 py-1 border-b border-gray-50">
|
||||
<span className="flex items-center gap-2 text-sm text-gray-700">
|
||||
{h.ok
|
||||
? <CheckCircle2 className="w-4 h-4 text-emerald-500 shrink-0" />
|
||||
: <XCircle className="w-4 h-4 text-red-500 shrink-0" />}
|
||||
{h.name}
|
||||
</span>
|
||||
<span className={`text-xs truncate ${h.ok ? 'text-gray-400' : 'text-red-500'}`} title={h.detail}>
|
||||
{h.detail || (h.ok ? 'OK' : 'недоступен')}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : <p className="text-sm text-gray-400">Опрос сервисов…</p>}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function Card({ icon: Icon, title, children }: { icon: React.ElementType; title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-gray-100 p-5">
|
||||
|
||||
Reference in New Issue
Block a user