// Copyright (c) 2026 Ivan Vasilev // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree. import { useState, useEffect, useCallback, useRef } from 'react'; import { apiFetch, copyText, getServerId, type NodeInfo, type PageProps } from '../../lib/shared'; import { IcoCopy, IcoTrash } from '../../components/icons'; import LineChart from '../../components/LineChart'; import './monitor.css'; // Вкладка «Монитор»: встроенный Prometheus и основные графики. // Кнопка «Подключить Prometheus» просит awg-ctrl сервера поставить его (`awg-ctrl monitor install`, root; // сама панель пакетов не ставит). У панели без VPN он ставится на ноду, и панель читает его через её агента. // Графики читают Prometheus через фиксированные запросы панели (internal/ui/monitor.go). interface Status { ready: boolean; can_install: boolean; command: string; host: { id: number; name: string } | null; // Prometheus на ноде (или будет поставлен туда); null — на этом сервере job: { action: 'install' | 'remove'; running: boolean; exit: number | null; log: string } | null; // последняя операция } type Pts = [number, number][]; interface Overview { range: string; step: number; from: number; to: number; servers: { id: number; name: string; ok: boolean; down: number | null; up: number | null; online: number | null; heavy: number | null; congested: boolean; cpu: number | null; mem: number | null; cores: number | null; mem_total: number | null; used_down: number | null; used_up: number | null; // байт за весь период }[]; total: Series; } interface Series { down: Pts; up: Pts; online: Pts; heavy: Pts; drops: Pts; cpu: Pts; mem: Pts } const RANGES: [string, string][] = [['1h', '1 час'], ['6h', '6 часов'], ['24h', '24 часа'], ['7d', '7 дней'], ['30d', '30 дней']]; const nf = (v: number, d = 1) => v.toLocaleString('ru-RU', { maximumFractionDigits: d }); // Скорость в битах/с → «12,4 Мбит/с» function rate(v: number, d = 1): string { if (v >= 1e9) return `${nf(v / 1e9, d)} Гбит/с`; if (v >= 1e6) return `${nf(v / 1e6, d)} Мбит/с`; if (v >= 1e3) return `${nf(v / 1e3, d)} Кбит/с`; return `${nf(v, 0)} бит/с`; } const lastOf = (p: Pts) => (p.length ? p[p.length - 1][1] : null); // Полоска нагрузки: цвет заливки — серьёзность (норма → внимание → опасно), дорожка — светлая ступень той же шкалы. // Цифра рядом всегда есть: цвет не единственный носитель смысла. const level = (v: number) => (v >= 90 ? 'mon-bar--bad' : v >= 70 ? 'mon-bar--warn' : ''); function Bar({ v, label }: { v: number | null | undefined; label: string }) { const pct = v == null ? 0 : Math.min(100, Math.max(0, v)); return ( ); } // Прогресс операции из строк «N/M шаг», которые печатает сам установщик (cli/src/monitor.ts). Шаг N считаем // выполненным наполовину: так полоска не стоит на нуле, пока идёт долгий apt, и не обещает больше, чем известно. const STEP = /^(\d+)\/(\d+)\s+(.+)$/; function JobProgress({ job, starting }: { job: Status['job']; starting: boolean }) { let step: [number, number, string] | null = null; for (const l of job?.log.split('\n') ?? []) { const m = STEP.exec(l.trim()); if (m) step = [Number(m[1]), Number(m[2]), m[3]]; } const failed = !!job && job.exit !== null && job.exit !== 0; const done = !!job && job.exit === 0; const pct = done ? 100 : step ? ((step[0] - 0.5) / step[1]) * 100 : 0; const label = failed ? `Не удалось (код ${job!.exit})` : done ? (job!.action === 'install' ? 'Готово' : 'Удалено') : step ? `Шаг ${step[0]} из ${step[1]}: ${step[2]}` : 'Запуск…'; return (
{j.log.trim()}
{j.action === 'install' ? 'Установка' : 'Удаление'} не удалась. Если дело в правах или пакетах, выполните вручную: {`awg-ctrl monitor ${j.action}`}
> ); }; // ───────── установка ───────── const setup = status && !status.ready && (Панель поставит Prometheus {status.host ? <>на сервер {status.host.name}> : 'на этот сервер'} и сама подключит его к своим метрикам: основные графики — скорость, устройства онлайн, нагрузка на канал, — появятся здесь через минуту-две. Prometheus слушает только 127.0.0.1, наружу он не открывается.
{status.can_install ? (У этой панели нет своего VPN-сервера: выберите ноду в сервер-баре, чтобы поставить Prometheus на неё кнопкой, или выполните на ней от root:
{status.command}
Стоит {status.host ? <>на сервере {status.host.name} (панель читает его через агента ноды)> : 'на этом сервере'} и слушает только 127.0.0.1:9090, графики хранит 31 день. Удаление остановит его, снимет пакет и сотрёт накопленные данные.
{status.can_install ? (Выполните на сервере с Prometheus от root:
awg-ctrl monitor remove
>
)}
{status.job?.action === 'remove' && failureLog()}
{ovErr}
} {ov && !one?.ok && (Этот сервер сейчас не отдаёт метрики: графики могут быть неполными или пустыми.
)}Сервер {server.health?.server || server.name} {server.online ? 'на связи' : 'нет связи'}
)} {!status &&Загрузка…
} {setup} {dashboard} {manage} > ); }