diff --git a/frontend/src/UptimeMonitorPage.jsx b/frontend/src/UptimeMonitorPage.jsx index 0377a81..28a3f8a 100644 --- a/frontend/src/UptimeMonitorPage.jsx +++ b/frontend/src/UptimeMonitorPage.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; +import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import api from './lib/api.js'; import { IconClock, @@ -7,109 +7,30 @@ import { IconCircleCheck, IconCircleX, IconChartLine, - IconPlayerPlay, - IconPlayerPause, } from '@tabler/icons-react'; import PageHeader from './components/PageHeader.jsx'; -import ServerAutocompleteInput from './components/ServerAutocompleteInput.jsx'; import { formatRelative } from './lib/datetime.js'; -const CHECK_INTERVAL_MS = 60 * 1000; // 1 минута -const HISTORY_MAX = 300; // храним последние 300 проверок для расчёта доступности - -/** Форматирование длительности (секунды → "X days Y hours Z mins") */ -function formatDuration(seconds) { - if (seconds == null || seconds < 0 || !Number.isFinite(seconds)) return '—'; - const d = Math.floor(seconds / 86400); - const h = Math.floor((seconds % 86400) / 3600); - const m = Math.floor((seconds % 3600) / 60); - const s = Math.floor(seconds % 60); - const parts = []; - if (d > 0) parts.push(`${d} дн`); - if (h > 0) parts.push(`${h} ч`); - if (m > 0) parts.push(`${m} мин`); - if (s > 0 || parts.length === 0) parts.push(`${s} сек`); - return parts.join(' '); -} - -/** Считает доступность и инциденты по истории проверок */ -function computeUptimeStats(history) { - if (!Array.isArray(history) || history.length === 0) { - return { availability: null, incidents: 0, totalDowntimeSec: 0, longestDowntimeSec: 0, avgIncidentSec: 0, upSinceSec: null }; - } - const now = Date.now() / 1000; - let upCount = 0; - let incidentCount = 0; - let totalDowntimeSec = 0; - let longestDowntimeSec = 0; - let currentDowntimeSec = 0; - let lastUpTs = null; - let upSinceSec = null; - - for (let i = 0; i < history.length; i++) { - const { ts, up } = history[i]; - const tsSec = ts / 1000; - if (up) { - upCount++; - if (lastUpTs != null && i > 0) { - const gap = tsSec - lastUpTs; - if (gap > 60) { - incidentCount++; - totalDowntimeSec += currentDowntimeSec; - if (currentDowntimeSec > longestDowntimeSec) longestDowntimeSec = currentDowntimeSec; - } - } - lastUpTs = tsSec; - currentDowntimeSec = 0; - if (upSinceSec == null) upSinceSec = now - tsSec; - } else { - if (i > 0) { - const prev = history[i - 1]; - const gap = (ts - prev.ts) / 1000; - currentDowntimeSec += gap; - } - } - } - if (currentDowntimeSec > 0) { - incidentCount++; - totalDowntimeSec += currentDowntimeSec; - if (currentDowntimeSec > longestDowntimeSec) longestDowntimeSec = currentDowntimeSec; - } - const total = history.length; - const availability = total > 0 ? (upCount / total) * 100 : null; - const avgIncidentSec = incidentCount > 0 ? totalDowntimeSec / incidentCount : 0; - return { - availability, - incidents: incidentCount, - totalDowntimeSec, - longestDowntimeSec, - avgIncidentSec, - upSinceSec: lastUpTs != null ? upSinceSec : null, - }; -} +const CHECK_INTERVAL_MS = 2 * 60 * 1000; // 2 минуты +const DELAY_BETWEEN_CHECKS_MS = 1500; // пауза между проверками, чтобы не получить 429 export default function UptimeMonitorPage() { const [servers, setServers] = useState([]); - const [routerServerId, setRouterServerId] = useState(''); const [loading, setLoading] = useState(true); - const [pinging, setPinging] = useState(false); + const [checking, setChecking] = useState(false); const [error, setError] = useState(null); - /** По каждому target (ip или id): массив { ts, up, ms } */ - const [historyMap, setHistoryMap] = useState({}); - /** Выбранная цель для детальной карточки (ip или id) */ - const [selectedTargetKey, setSelectedTargetKey] = useState(null); + /** По serverId: { ok, lastCheckTs } */ + const [statusMap, setStatusMap] = useState({}); const intervalRef = useRef(null); - const pausedRef = useRef(false); + const checkingRef = useRef(false); - const routerServers = servers.filter( - (s) => ['jumphost', 'home'].includes(String(s.type || '').toLowerCase()) + const jumphosts = useMemo( + () => + servers.filter((s) => + ['jumphost', 'home'].includes(String(s.type || '').toLowerCase()) + ), + [servers] ); - const targets = servers.filter((s) => { - const ip = s.ip || s.extIp; - if (!ip) return false; - const id = s.id || s.dns || s.ip; - return id !== routerServerId && ip !== routerServerId; - }); const fetchServers = useCallback(async () => { setLoading(true); @@ -129,70 +50,40 @@ export default function UptimeMonitorPage() { fetchServers(); }, [fetchServers]); - const runPings = useCallback(async () => { - if (!routerServerId || targets.length === 0) return; - setPinging(true); - const serverId = routerServerId; + const runChecks = useCallback(async () => { + if (jumphosts.length === 0) return; + if (checkingRef.current) return; + checkingRef.current = true; + setChecking(true); const results = {}; - for (const target of targets) { - const ip = target.ip || target.extIp; - if (!ip) continue; - const key = target.id || target.ip || target.dns || ip; + for (const server of jumphosts) { + const serverId = server.id || server.dns || server.ip; + if (!serverId) continue; try { - const { data } = await api.post('/mikrotik/ping', { - serverId, - target: ip, - gatewayIp: null, - count: 3, - }); - const up = data && typeof data.avgMs === 'number'; - const ms = up ? data.avgMs : null; - results[key] = { ts: Date.now(), up, ms }; + await api.post('/mikrotik/test-connection', { serverId }); + results[serverId] = { ok: true, lastCheckTs: Date.now() }; } catch { - results[key] = { ts: Date.now(), up: false, ms: null }; + results[serverId] = { ok: false, lastCheckTs: Date.now() }; } + await new Promise((r) => setTimeout(r, DELAY_BETWEEN_CHECKS_MS)); } - setHistoryMap((prev) => { + setStatusMap((prev) => { const next = { ...prev }; - for (const [key, entry] of Object.entries(results)) { - const list = Array.isArray(next[key]) ? next[key] : []; - const newList = [...list, entry].slice(-HISTORY_MAX); - next[key] = newList; - } + for (const [id, v] of Object.entries(results)) next[id] = v; return next; }); - setPinging(false); - }, [routerServerId, targets]); + checkingRef.current = false; + setChecking(false); + }, [jumphosts]); - /** Автообновление по интервалу */ useEffect(() => { - if (!routerServerId || pausedRef.current) return; - runPings(); - intervalRef.current = setInterval(runPings, CHECK_INTERVAL_MS); + if (jumphosts.length === 0) return; + runChecks(); + intervalRef.current = setInterval(runChecks, CHECK_INTERVAL_MS); return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; - }, [routerServerId, runPings]); - - const handlePause = () => { - pausedRef.current = !pausedRef.current; - if (pausedRef.current && intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } else if (!pausedRef.current && routerServerId) { - runPings(); - intervalRef.current = setInterval(runPings, CHECK_INTERVAL_MS); - } - setPinging((p) => p); - }; - - const selectedTarget = selectedTargetKey - ? targets.find((t) => (t.id || t.ip || t.dns) === selectedTargetKey) - : targets[0]; - const selectedHistory = selectedTargetKey && historyMap[selectedTargetKey]; - const selectedStats = selectedHistory ? computeUptimeStats(selectedHistory) : null; - const lastEntry = selectedHistory && selectedHistory.length > 0 ? selectedHistory[selectedHistory.length - 1] : null; - const isUp = lastEntry?.up ?? null; + }, [jumphosts, runChecks]); return (
| Период | -Доступность | -Простой | -Инциденты | -Самый долгий | -Средний инцидент | -
|---|---|---|---|---|---|
| По сохранённой истории | -- {selectedStats.availability != null - ? `${selectedStats.availability.toFixed(2)}%` - : '—'} - | -{formatDuration(selectedStats.totalDowntimeSec)} | -{selectedStats.incidents} | -{formatDuration(selectedStats.longestDowntimeSec)} | -{formatDuration(selectedStats.avgIncidentSec)} | -
| Сервер / IP | -Статус | -Последняя проверка | -RTT | -Доступность | -Инциденты | -
|---|---|---|---|---|---|
|
-
-
- |
-
- {last == null && (
- —
- )}
- {last?.up === true && (
-
- |
- {last ? formatRelative(last.ts) : '—'} | -{last?.ms != null ? `${Math.round(last.ms)} мс` : '—'} | -- {stats.availability != null ? `${stats.availability.toFixed(1)}%` : '—'} - | -{stats.incidents} | -
Выберите роутер
+Нет Jumphost
- Укажите jumphost или домашний роутер с MikroTik API — с него будет выполняться ping по целям. + Добавьте серверы типа «Jumphost» или «Home» с настроенным MikroTik API — здесь будет отображаться их доступность.
| Сервер | +Статус | +Последняя проверка | +
|---|---|---|
|
+
+
+ |
+
+ {status == null && (
+ —
+ )}
+ {status?.ok === true && (
+
+ |
+ + {status?.lastCheckTs + ? formatRelative(status.lastCheckTs) + : '—'} + | +