feat(admin): состояние инфраструктуры на странице отладки
Сегодняшняя авария (упал гипервизор — вместе с ним брокер, Ollama, прокси) проявлялась в отладке косвенно: пустой список воркеров и ошибка очередей. Прямого ответа «что именно недоступно» страница не давала, хотя эндпоинт /admin/health его уже знает. Блок статусов рисуется и тогда, когда сам срез не собрался — именно в аварии он нужен больше всего, а собирается в этот момент дольше обычного. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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