feat(web): дашборд инстанса в настройках с живыми метриками
Вкладка «Инстанс» сверху показывает нагрузку сервера, ниже — диск и базу, внизу healthcheck с пингом (AGENT.md 7.19). - `Gauge` — полукруглый индикатор: заполняется по значению, меняет цвет на порогах 70 % и 90 %, доступен как `role="meter"`; - процессор и память обновляются раз в секунду (`useInstanceMetrics`), для простоя вкладки опрос останавливается; - два блока диска: «занято / всего» и вес базы (файл + WAL); видимых подписей нет — они раскрываются в тултипе при наведении на значение; - healthcheck: состояние каждой проверки, общий статус, техническая деталь и живой пинг — время ответа инстанса на запрос дашборда в миллисекундах; - переводы ru/en, тесты на индикаторы, формат «занято / всего», тултипы, healthcheck и отсутствие раздела у обычного пользователя.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { fetchInstanceMetrics, metricsQueryKey, type InstanceMetrics } from '@/api/metrics';
|
||||
|
||||
/** Как часто дашборд перезапрашивает метрики (AGENT.md 7.19: раз в секунду). */
|
||||
export const METRICS_INTERVAL_MS = 1_000;
|
||||
|
||||
export interface InstanceMetricsState {
|
||||
metrics: InstanceMetrics | undefined;
|
||||
/** Время ответа инстанса на запрос дашборда, мс (оценка пинга). */
|
||||
pingMs: number | null;
|
||||
pending: boolean;
|
||||
failed: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* useInstanceMetrics опрашивает метрики инстанса раз в секунду и измеряет
|
||||
* время ответа — оно же показывается как пинг пользователя до инстанса.
|
||||
* Оба блока дашборда используют один ключ запроса: React Query выполняет
|
||||
* единственный опрос независимо от числа подписчиков.
|
||||
*/
|
||||
export function useInstanceMetrics(enabled: boolean): InstanceMetricsState {
|
||||
const [pingMs, setPingMs] = useState<number | null>(null);
|
||||
|
||||
const measure = useCallback((started: number) => {
|
||||
if (typeof performance === 'undefined') {
|
||||
return;
|
||||
}
|
||||
const elapsed = performance.now() - started;
|
||||
// Округляем до миллисекунды и не показываем 0 мс: запрос всегда занимает время.
|
||||
setPingMs(Math.max(1, Math.round(elapsed)));
|
||||
}, []);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: metricsQueryKey,
|
||||
queryFn: async ({ signal }) => {
|
||||
const started = typeof performance === 'undefined' ? 0 : performance.now();
|
||||
const metrics = await fetchInstanceMetrics(signal);
|
||||
measure(started);
|
||||
return metrics;
|
||||
},
|
||||
enabled,
|
||||
refetchInterval: METRICS_INTERVAL_MS,
|
||||
// Опрос идёт только когда вкладка видима: незачем нагружать инстанс в фоне.
|
||||
refetchIntervalInBackground: false,
|
||||
retry: 1,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
return {
|
||||
metrics: query.data,
|
||||
pingMs,
|
||||
pending: query.isPending,
|
||||
failed: query.isError,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user