feat(UptimeMonitor): add uptime check endpoint and frontend settings for monitoring server availability
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m34s

This commit is contained in:
2026-02-22 12:57:53 +07:00
parent afa093781e
commit f80e962767
4 changed files with 197 additions and 9 deletions
+62
View File
@@ -13,6 +13,7 @@ import {
IconChartBar,
IconRefresh,
IconServer,
IconClock,
} from '@tabler/icons-react';
import FormField from './components/FormField';
import ErrorAlert from './components/ErrorAlert';
@@ -37,6 +38,7 @@ const SIDEBAR_GROUPS = [
items: [
{ id: 'ping', title: 'Пинг через MikroTik', icon: IconCloud },
{ id: 'ping-services', title: 'Пинг на главной', icon: IconChartPie },
{ id: 'uptime-monitor', title: 'Uptime Monitor', icon: IconClock },
],
},
{
@@ -82,6 +84,8 @@ export default function SettingsPage() {
const [trafficJumphosts, setTrafficJumphosts] = useState([]);
const [trafficInterfacesLoading, setTrafficInterfacesLoading] = useState(false);
const [trafficInterfacesError, setTrafficInterfacesError] = useState('');
const [uptimeMonitorIntervalSeconds, setUptimeMonitorIntervalSeconds] = useState('120');
const [uptimeMonitorCheckType, setUptimeMonitorCheckType] = useState('http');
const [serversList, setServersList] = useState([]);
const [sidebarSearch, setSidebarSearch] = useState('');
const [activeSection, setActiveSection] = useState(() => {
@@ -247,6 +251,15 @@ export default function SettingsPage() {
? String(data.interfaceSpeedTestCacheMinutes)
: ''
);
setUptimeMonitorIntervalSeconds(
data?.uptimeMonitorIntervalSeconds != null
? String(data.uptimeMonitorIntervalSeconds)
: '120'
);
const checkType = String(data?.uptimeMonitorCheckType || 'http').toLowerCase();
setUptimeMonitorCheckType(
checkType === 'internal-ping' || checkType === 'external-ping' ? checkType : 'http'
);
const raw = data?.trafficInterfaces;
setTrafficInterfacesSelected(
Array.isArray(raw)
@@ -349,6 +362,14 @@ export default function SettingsPage() {
0,
parseInt(interfaceSpeedTestCacheMinutes, 10) || 0
),
uptimeMonitorIntervalSeconds: Math.max(
30,
parseInt(uptimeMonitorIntervalSeconds, 10) || 120
),
uptimeMonitorCheckType:
uptimeMonitorCheckType === 'internal-ping' || uptimeMonitorCheckType === 'external-ping'
? uptimeMonitorCheckType
: 'http',
trafficInterfaces: Array.isArray(trafficInterfacesSelected)
? trafficInterfacesSelected.map((p) => ({
serverKey: p.serverKey,
@@ -704,6 +725,47 @@ export default function SettingsPage() {
</>
)}
{activeSection === 'uptime-monitor' && (
<>
<SectionHeading title="Uptime Monitor" icon={IconClock} />
<p className="text-muted mb-3">
Настройки страницы «Uptime Monitor»: интервал проверки доступности Jumphost/Home и способ проверки.
</p>
<div className="row g-2">
<div className="col-md-6">
<FormField
label="Интервал проверки (сек)"
name="uptimeMonitorIntervalSeconds"
type="number"
value={uptimeMonitorIntervalSeconds}
onChange={setUptimeMonitorIntervalSeconds}
placeholder="120"
helpText="Как часто проверять доступность (по умолчанию 120 сек)."
disabled={saving}
min={30}
max={86400}
/>
</div>
<div className="col-md-6">
<label className="form-label">Тип проверки</label>
<select
className="form-select"
value={uptimeMonitorCheckType}
onChange={(e) => setUptimeMonitorCheckType(e.target.value)}
disabled={saving}
>
<option value="http">HTTP доступность MikroTik REST API</option>
<option value="internal-ping">Внутренний пинг по внутренним адресам туннелей</option>
<option value="external-ping">Внешний пинг по внешним IP серверов</option>
</select>
<div className="form-text">
HTTP: подключение к RouterOS API. Внутренний: пинг через туннель (как на карте сети). Внешний: пинг внешнего IP с другого jumphost.
</div>
</div>
</div>
</>
)}
{activeSection === 'ptr-zone' && (
<>
<SectionHeading title="Настройка PTR зоны" icon={IconWorld} />
+22 -9
View File
@@ -22,7 +22,7 @@ import {
import PageHeader from './components/PageHeader.jsx';
import { formatRelative } from './lib/datetime.js';
const CHECK_INTERVAL_MS = 2 * 60 * 1000; // 2 минуты
const DEFAULT_CHECK_INTERVAL_SEC = 120;
const DELAY_BETWEEN_CHECKS_MS = 1500;
const HISTORY_MAX = 500;
const STORAGE_KEY = 'uptime-monitor-history';
@@ -89,6 +89,7 @@ export default function UptimeMonitorPage() {
/** По serverId: массив { ts, ok, ms } */
const [historyMap, setHistoryMap] = useState(loadHistoryFromStorage);
const [selectedServerId, setSelectedServerId] = useState(null);
const [checkIntervalSeconds, setCheckIntervalSeconds] = useState(DEFAULT_CHECK_INTERVAL_SEC);
const intervalRef = useRef(null);
const checkingRef = useRef(false);
@@ -104,8 +105,15 @@ export default function UptimeMonitorPage() {
setLoading(true);
setError(null);
try {
const { data } = await api.get('/servers');
const [serversRes, settingsRes] = await Promise.all([
api.get('/servers'),
api.get('/ui-settings').catch(() => ({ data: {} })),
]);
const data = serversRes?.data;
setServers(Array.isArray(data) ? data : []);
const ui = settingsRes?.data || {};
const interval = Math.max(30, Math.min(86400, parseInt(ui.uptimeMonitorIntervalSeconds, 10) || DEFAULT_CHECK_INTERVAL_SEC));
setCheckIntervalSeconds(interval);
} catch (e) {
setError(e?.response?.data?.message || e?.message || 'Не удалось загрузить серверы');
setServers([]);
@@ -118,6 +126,8 @@ export default function UptimeMonitorPage() {
fetchServers();
}, [fetchServers]);
const checkIntervalMs = checkIntervalSeconds * 1000;
const runChecks = useCallback(async () => {
if (jumphosts.length === 0) return;
if (checkingRef.current) return;
@@ -130,11 +140,14 @@ export default function UptimeMonitorPage() {
if (!serverId) continue;
const t0 = Date.now();
let ok = false;
let ms = null;
try {
await api.post('/mikrotik/test-connection', { serverId });
ok = true;
} catch (_) {}
const ms = Date.now() - t0;
const { data: checkData } = await api.post('/uptime/check', { serverId });
ok = checkData?.ok === true;
ms = typeof checkData?.ms === 'number' ? checkData.ms : Date.now() - t0;
} catch (_) {
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));
@@ -160,11 +173,11 @@ export default function UptimeMonitorPage() {
useEffect(() => {
if (jumphosts.length === 0) return;
runChecks();
intervalRef.current = setInterval(runChecks, CHECK_INTERVAL_MS);
intervalRef.current = setInterval(runChecks, checkIntervalMs);
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [jumphosts, runChecks]);
}, [jumphosts, runChecks, checkIntervalMs]);
// Сохраняем историю в localStorage при изменении
useEffect(() => {
@@ -229,7 +242,7 @@ export default function UptimeMonitorPage() {
<div className="card-header">
<h3 className="card-title">Доступность Jumphost и домашних роутеров</h3>
<div className="card-actions text-muted small">
Проверка каждые {CHECK_INTERVAL_MS / 60000} мин · Выберите сервер для графиков
Проверка каждые {checkIntervalSeconds} сек · Выберите сервер для графиков
</div>
</div>
<div className="table-responsive">