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 (
@@ -201,37 +92,17 @@ export default function UptimeMonitorPage() { title="Uptime Monitor" icon={} pretitle="Extra" - meta="Мониторинг доступности по ping с выбранного роутера (как на карте сети)" + meta="Проверка доступности всех Jumphost и домашних роутеров (MikroTik API)" actions={ -
- - setRouterServerId(String(v || '').trim())} - servers={routerServers} - placeholder="Jumphost или домашний роутер" - className="form-control form-control-flush" - maxSuggestions={10} - /> - - -
+ } /> @@ -241,202 +112,76 @@ export default function UptimeMonitorPage() {
)} - {!loading && routerServerId && targets.length === 0 && ( -
- Нет целей для мониторинга. Добавьте серверы с IP (кроме выбранного роутера). -
- )} - - {!loading && routerServerId && targets.length > 0 && ( - <> - {/* Детальная карточка выбранной цели (в стиле Tabler Uptime) */} - {selectedTarget && ( -
-
-
-
-

- {selectedTarget.dns || selectedTarget.ip || selectedTarget.name || selectedTargetKey} -

-
- {isUp === true && ( - Up - )} - {isUp === false && ( - Down - )} - {isUp === null && ( - - )} -
-
-
-
-
-
-
-
Доступен уже
-
- {isUp && lastEntry - ? formatDuration((Date.now() - lastEntry.ts) / 1000) - : isUp ? '—' : '0 сек'} -
-
-
-
-
-
-
-
Проверка каждые {CHECK_INTERVAL_MS / 60000} мин
-
Последняя: {lastEntry ? formatRelative(lastEntry.ts) : '—'}
-
-
-
-
-
-
-
RTT (средн.)
-
- {lastEntry?.ms != null ? `${Math.round(lastEntry.ms)} мс` : '—'} -
-
-
-
-
-
-
-
Инциденты (по истории)
-
{selectedStats?.incidents ?? '—'}
-
-
-
-
-
-
-
-
- )} - - {/* Таблица периодов (как на Tabler) — по выбранной цели */} - {selectedTarget && selectedStats != null && ( -
-
-
-
-

Доступность по периодам

-
-
- - - - - - - - - - - - - - - - - - - - - -
ПериодДоступностьПростойИнцидентыСамый долгийСредний инцидент
По сохранённой истории - {selectedStats.availability != null - ? `${selectedStats.availability.toFixed(2)}%` - : '—'} - {formatDuration(selectedStats.totalDowntimeSec)}{selectedStats.incidents}{formatDuration(selectedStats.longestDowntimeSec)}{formatDuration(selectedStats.avgIncidentSec)}
-
-
-
-
- )} - - {/* Таблица всех целей */} -
-
-

Цели мониторинга

-
-
- - - - - - - - - - - - - {targets.map((t) => { - const key = t.id || t.ip || t.dns || t.ip; - const hist = historyMap[key] || []; - const last = hist.length > 0 ? hist[hist.length - 1] : null; - const stats = computeUptimeStats(hist); - const isSelected = (selectedTargetKey || (targets[0] && (targets[0].id || targets[0].ip))) === key; - return ( - setSelectedTargetKey(key)} - > - - - - - - - - ); - })} - -
Сервер / IPСтатусПоследняя проверкаRTTДоступностьИнциденты
-
- - {t.dns || t.name || t.ip || t.extIp || key} -
-
- {last == null && ( - - )} - {last?.up === true && ( - - Up - - )} - {last?.up === false && ( - - Down - - )} - {last ? formatRelative(last.ts) : '—'}{last?.ms != null ? `${Math.round(last.ms)} мс` : '—'} - {stats.availability != null ? `${stats.availability.toFixed(1)}%` : '—'} - {stats.incidents}
-
-
- - )} - - {!loading && !routerServerId && ( + {!loading && jumphosts.length === 0 && (
-

Выберите роутер

+

Нет Jumphost

- Укажите jumphost или домашний роутер с MikroTik API — с него будет выполняться ping по целям. + Добавьте серверы типа «Jumphost» или «Home» с настроенным MikroTik API — здесь будет отображаться их доступность.

)} + + {!loading && jumphosts.length > 0 && ( +
+
+

Доступность Jumphost и домашних роутеров

+
+ Проверка каждые {CHECK_INTERVAL_MS / 60000} мин +
+
+
+ + + + + + + + + + {jumphosts.map((s) => { + const id = s.id || s.dns || s.ip; + const status = statusMap[id]; + const label = s.dns || s.name || s.ip || id; + return ( + + + + + + ); + })} + +
СерверСтатусПоследняя проверка
+
+ + {label} +
+
+ {status == null && ( + + )} + {status?.ok === true && ( + + Доступен + + )} + {status?.ok === false && ( + + Недоступен + + )} + + {status?.lastCheckTs + ? formatRelative(status.lastCheckTs) + : '—'} +
+
+
+ )} );