feat(web): дашборд инстанса в настройках с живыми метриками

Вкладка «Инстанс» сверху показывает нагрузку сервера, ниже — диск и базу, внизу
healthcheck с пингом (AGENT.md 7.19).

- `Gauge` — полукруглый индикатор: заполняется по значению, меняет цвет на
  порогах 70 % и 90 %, доступен как `role="meter"`;
- процессор и память обновляются раз в секунду (`useInstanceMetrics`), для
  простоя вкладки опрос останавливается;
- два блока диска: «занято / всего» и вес базы (файл + WAL); видимых подписей
  нет — они раскрываются в тултипе при наведении на значение;
- healthcheck: состояние каждой проверки, общий статус, техническая деталь и
  живой пинг — время ответа инстанса на запрос дашборда в миллисекундах;
- переводы ru/en, тесты на индикаторы, формат «занято / всего», тултипы,
  healthcheck и отсутствие раздела у обычного пользователя.
This commit is contained in:
2026-09-20 21:26:25 +03:00
parent 4c3736b5fb
commit e1107600ec
9 changed files with 715 additions and 2 deletions
+102
View File
@@ -0,0 +1,102 @@
interface GaugeProps {
/** Заполнение в процентах (0…100). */
value: number;
/** Подпись для скринридера и тултипа. */
label: string;
/** Крупное значение в центре (например, «23 %»). */
display: string;
/** Уточнение под значением (например, «1,2 / 3,4 ГБ»). */
caption?: string | undefined;
/** Порог, после которого индикатор считается нагруженным. */
warnAt?: number;
dangerAt?: number;
testId?: string;
}
/** Радиус и параметры полукруга: дуга 180° от левого края к правому. */
const RADIUS = 62;
const CENTER_X = 80;
const CENTER_Y = 74;
const STROKE = 12;
const LENGTH = Math.PI * RADIUS;
/** describeArc собирает путь полукруга (180°) в координатах SVG. */
function describeArc(): string {
const startX = CENTER_X - RADIUS;
const endX = CENTER_X + RADIUS;
return `M ${String(startX)} ${String(CENTER_Y)} A ${String(RADIUS)} ${String(RADIUS)} 0 0 1 ${String(endX)} ${String(CENTER_Y)}`;
}
function toneClass(value: number, warnAt: number, dangerAt: number): string {
if (value >= dangerAt) {
return 'text-danger';
}
if (value >= warnAt) {
return 'text-warning';
}
return 'text-accent';
}
/**
* Gauge — полукруглый индикатор нагрузки: заполняется по значению и меняет
* цвет при высокой загрузке. Длина дуги считается через `stroke-dasharray`,
* поэтому анимация — обычный CSS-переход (при `prefers-reduced-motion`
* анимации отключаются глобально).
*/
export function Gauge({
value,
label,
display,
caption,
warnAt = 70,
dangerAt = 90,
testId,
}: GaugeProps) {
const clamped = Math.min(100, Math.max(0, Number.isFinite(value) ? value : 0));
const filled = (clamped / 100) * LENGTH;
const tone = toneClass(clamped, warnAt, dangerAt);
return (
<figure
className="flex flex-col items-center gap-1"
data-testid={testId}
role="meter"
aria-label={label}
aria-valuenow={Math.round(clamped)}
aria-valuemin={0}
aria-valuemax={100}
title={`${label}: ${display}`}
>
<svg viewBox="0 0 160 88" className="h-[88px] w-[160px]" aria-hidden="true">
<path
d={describeArc()}
className="stroke-surface-3"
strokeWidth={STROKE}
strokeLinecap="round"
fill="none"
/>
<path
d={describeArc()}
className={`${tone} transition-[stroke-dasharray] duration-[var(--duration-base)] ease-out`}
stroke="currentColor"
strokeWidth={STROKE}
strokeLinecap="round"
fill="none"
strokeDasharray={`${String(filled)} ${String(LENGTH)}`}
data-testid={testId === undefined ? undefined : `${testId}-arc`}
/>
</svg>
<figcaption className="-mt-8 flex flex-col items-center">
<span
className={`text-xl font-semibold tabular-nums ${tone}`}
data-testid={testId === undefined ? undefined : `${testId}-value`}
>
{display}
</span>
{caption === undefined ? null : (
<span className="text-xs text-fg-muted tabular-nums">{caption}</span>
)}
</figcaption>
</figure>
);
}