diff --git a/frontend/src/UptimeMonitorPage.jsx b/frontend/src/UptimeMonitorPage.jsx index 28a3f8a..6482281 100644 --- a/frontend/src/UptimeMonitorPage.jsx +++ b/frontend/src/UptimeMonitorPage.jsx @@ -8,19 +8,87 @@ import { IconCircleX, IconChartLine, } from '@tabler/icons-react'; +import { + AreaChart, + Area, + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from 'recharts'; import PageHeader from './components/PageHeader.jsx'; import { formatRelative } from './lib/datetime.js'; const CHECK_INTERVAL_MS = 2 * 60 * 1000; // 2 минуты -const DELAY_BETWEEN_CHECKS_MS = 1500; // пауза между проверками, чтобы не получить 429 +const DELAY_BETWEEN_CHECKS_MS = 1500; +const HISTORY_MAX = 500; +const STORAGE_KEY = 'uptime-monitor-history'; + +function loadHistoryFromStorage() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return {}; + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object') return parsed; + } catch (_) {} + return {}; +} + +function saveHistoryToStorage(historyMap) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(historyMap)); + } catch (_) {} +} + +/** Группировка истории по дням: массив { day, dayLabel, incidents } */ +function historyToIncidentsByDay(history) { + if (!Array.isArray(history) || history.length === 0) return []; + const byDay = new Map(); // dayKey -> count of down checks + history.forEach(({ ts, ok }) => { + if (ok) return; + const d = new Date(ts); + const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + byDay.set(key, (byDay.get(key) || 0) + 1); + }); + return Array.from(byDay.entries()) + .map(([day, incidents]) => ({ + day, + dayLabel: new Date(day + 'T12:00:00').toLocaleDateString('ru-RU', { + day: 'numeric', + month: 'short', + }), + incidents, + })) + .sort((a, b) => a.day.localeCompare(b.day)); +} + +/** История в точки для графика задержек: { time, ts, ms } */ +function historyToResponseTimeData(history) { + if (!Array.isArray(history) || history.length === 0) return []; + return history.map(({ ts, ok, ms }) => ({ + ts, + time: new Date(ts).toLocaleString('ru-RU', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }), + ms: ok && ms != null ? Math.round(ms) : null, + })); +} export default function UptimeMonitorPage() { const [servers, setServers] = useState([]); const [loading, setLoading] = useState(true); const [checking, setChecking] = useState(false); const [error, setError] = useState(null); - /** По serverId: { ok, lastCheckTs } */ const [statusMap, setStatusMap] = useState({}); + /** По serverId: массив { ts, ok, ms } */ + const [historyMap, setHistoryMap] = useState(loadHistoryFromStorage); + const [selectedServerId, setSelectedServerId] = useState(null); const intervalRef = useRef(null); const checkingRef = useRef(false); @@ -56,15 +124,19 @@ export default function UptimeMonitorPage() { checkingRef.current = true; setChecking(true); const results = {}; + const newHistory = {}; for (const server of jumphosts) { const serverId = server.id || server.dns || server.ip; if (!serverId) continue; + const t0 = Date.now(); + let ok = false; try { await api.post('/mikrotik/test-connection', { serverId }); - results[serverId] = { ok: true, lastCheckTs: Date.now() }; - } catch { - results[serverId] = { ok: false, lastCheckTs: Date.now() }; - } + ok = true; + } catch (_) {} + const ms = Date.now() - t0; + results[serverId] = { ok, lastCheckTs: Date.now() }; + newHistory[serverId] = [{ ts: Date.now(), ok, ms }]; await new Promise((r) => setTimeout(r, DELAY_BETWEEN_CHECKS_MS)); } setStatusMap((prev) => { @@ -72,6 +144,15 @@ export default function UptimeMonitorPage() { for (const [id, v] of Object.entries(results)) next[id] = v; return next; }); + setHistoryMap((prev) => { + const next = { ...prev }; + for (const [id, one] of Object.entries(newHistory)) { + const list = Array.isArray(next[id]) ? next[id] : []; + const merged = [...list, ...one].slice(-HISTORY_MAX); + next[id] = merged; + } + return next; + }); checkingRef.current = false; setChecking(false); }, [jumphosts]); @@ -85,6 +166,24 @@ export default function UptimeMonitorPage() { }; }, [jumphosts, runChecks]); + // Сохраняем историю в localStorage при изменении + useEffect(() => { + saveHistoryToStorage(historyMap); + }, [historyMap]); + + const selectedServer = selectedServerId + ? jumphosts.find((s) => (s.id || s.dns || s.ip) === selectedServerId) + : null; + const selectedHistory = selectedServerId ? historyMap[selectedServerId] || [] : []; + const responseTimeData = useMemo( + () => historyToResponseTimeData(selectedHistory), + [selectedHistory] + ); + const incidentsByDay = useMemo( + () => historyToIncidentsByDay(selectedHistory), + [selectedHistory] + ); + return (
| Сервер | +Статус | +Последняя проверка | +
|---|---|---|
|
+
+
+ |
+
+ {status == null && (
+ —
+ )}
+ {status?.ok === true && (
+
+ |
+ + {status?.lastCheckTs + ? formatRelative(status.lastCheckTs) + : '—'} + | +
| Сервер | -Статус | -Последняя проверка | -
|---|---|---|
|
-
-
- |
-
- {status == null && (
- —
- )}
- {status?.ok === true && (
-
- |
- - {status?.lastCheckTs - ? formatRelative(status.lastCheckTs) - : '—'} - | -
+ Нет данных. Проверки накапливаются со временем (хранятся в браузере). +
+ ) : ( ++ Нет инцидентов за сохранённый период или нет данных. +
+ ) : ( +