feat(UptimeMonitor): implement uptime cache retrieval and update logic for improved monitoring performance
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m36s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m36s
This commit is contained in:
@@ -22,6 +22,7 @@ const NETWORK_CONFIG_KEY = 'network-config.json';
|
|||||||
const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json';
|
const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json';
|
||||||
const PING_CACHE_PREFIX = 'ping-cache/';
|
const PING_CACHE_PREFIX = 'ping-cache/';
|
||||||
const SPEEDTEST_CACHE_PREFIX = 'speed-test-cache/';
|
const SPEEDTEST_CACHE_PREFIX = 'speed-test-cache/';
|
||||||
|
const UPTIME_CACHE_KEY = 'uptime-monitor-cache/latest.json';
|
||||||
|
|
||||||
/** Загрузить UI-настройки из S3 (для pingDomain, pingCacheMinutes и др.) */
|
/** Загрузить UI-настройки из S3 (для pingDomain, pingCacheMinutes и др.) */
|
||||||
async function loadUiSettings() {
|
async function loadUiSettings() {
|
||||||
@@ -244,11 +245,64 @@ async function testMikrotikConnection(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Записать результат проверки в кеш Uptime Monitor (S3). Не блокирует ответ. */
|
||||||
|
function updateUptimeCache(serverId, entry) {
|
||||||
|
const { ok, lastCheckTs, ms } = entry;
|
||||||
|
return readS3TextObject(UPTIME_CACHE_KEY)
|
||||||
|
.catch(() => ({ body: '{}' }))
|
||||||
|
.then((data) => {
|
||||||
|
let cache = { results: {}, updatedAt: null };
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data?.body || '{}');
|
||||||
|
if (parsed && typeof parsed === 'object' && parsed.results) cache = parsed;
|
||||||
|
} catch (_) {}
|
||||||
|
cache.results[serverId] = { ok, lastCheckTs, ms };
|
||||||
|
cache.updatedAt = Date.now();
|
||||||
|
return writeS3JsonObject(UPTIME_CACHE_KEY, cache);
|
||||||
|
})
|
||||||
|
.catch((e) => console.warn('[uptime] cache write failed:', e?.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/uptime/cache
|
||||||
|
* Возвращает закешированные результаты проверок, если кеш младше TTL из настроек.
|
||||||
|
*/
|
||||||
|
async function getUptimeCache(req, res) {
|
||||||
|
try {
|
||||||
|
const uiSettings = await loadUiSettings();
|
||||||
|
const ttlSec = Math.max(0, parseInt(uiSettings.uptimeMonitorCacheSeconds, 10) || 120);
|
||||||
|
if (ttlSec === 0) {
|
||||||
|
return res.json({ results: {}, updatedAt: null });
|
||||||
|
}
|
||||||
|
const data = await readS3TextObject(UPTIME_CACHE_KEY).catch(() => null);
|
||||||
|
if (!data?.body) {
|
||||||
|
return res.json({ results: {}, updatedAt: null });
|
||||||
|
}
|
||||||
|
let cache = { results: {}, updatedAt: null };
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data.body);
|
||||||
|
if (parsed && typeof parsed === 'object') cache = parsed;
|
||||||
|
} catch (_) {}
|
||||||
|
const now = Date.now();
|
||||||
|
if (!cache.updatedAt || now - cache.updatedAt > ttlSec * 1000) {
|
||||||
|
return res.json({ results: {}, updatedAt: null });
|
||||||
|
}
|
||||||
|
return res.json({
|
||||||
|
results: cache.results && typeof cache.results === 'object' ? cache.results : {},
|
||||||
|
updatedAt: cache.updatedAt,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[uptime] getUptimeCache', e);
|
||||||
|
return res.json({ results: {}, updatedAt: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/uptime/check
|
* POST /api/uptime/check
|
||||||
* Проверка доступности одного сервера (jumphost/home). Тип проверки берётся из ui-settings.uptimeMonitorCheckType.
|
* Проверка доступности одного сервера (jumphost/home). Тип проверки берётся из ui-settings.uptimeMonitorCheckType.
|
||||||
* Body: { serverId }
|
* Body: { serverId }
|
||||||
* Returns: { ok: boolean, ms?: number }
|
* Returns: { ok: boolean, ms?: number }
|
||||||
|
* Результат пишется в кеш (S3) для отображения при заходе на страницу.
|
||||||
*/
|
*/
|
||||||
async function uptimeCheck(req, res) {
|
async function uptimeCheck(req, res) {
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
@@ -280,6 +334,8 @@ async function uptimeCheck(req, res) {
|
|||||||
await client.print('system/resource');
|
await client.print('system/resource');
|
||||||
ok = true;
|
ok = true;
|
||||||
ms = Date.now() - t0;
|
ms = Date.now() - t0;
|
||||||
|
const lastCheckTs = Date.now();
|
||||||
|
updateUptimeCache(serverId, { ok, lastCheckTs, ms });
|
||||||
return res.json({ ok, ms });
|
return res.json({ ok, ms });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,6 +349,8 @@ async function uptimeCheck(req, res) {
|
|||||||
);
|
);
|
||||||
if (!iface || (!iface.remoteIp && !iface.localIp)) {
|
if (!iface || (!iface.remoteIp && !iface.localIp)) {
|
||||||
ms = Date.now() - t0;
|
ms = Date.now() - t0;
|
||||||
|
const lastCheckTs = Date.now();
|
||||||
|
updateUptimeCache(serverId, { ok: false, lastCheckTs, ms });
|
||||||
return res.json({ ok: false, ms, error: 'Нет туннеля с внутренним адресом для этого сервера' });
|
return res.json({ ok: false, ms, error: 'Нет туннеля с внутренним адресом для этого сервера' });
|
||||||
}
|
}
|
||||||
const target = iface.serverId === serverId || iface.serverId === server.ip || iface.serverId === server.dns
|
const target = iface.serverId === serverId || iface.serverId === server.ip || iface.serverId === server.dns
|
||||||
@@ -301,15 +359,21 @@ async function uptimeCheck(req, res) {
|
|||||||
const gatewayIp = target;
|
const gatewayIp = target;
|
||||||
if (!target) {
|
if (!target) {
|
||||||
ms = Date.now() - t0;
|
ms = Date.now() - t0;
|
||||||
|
const lastCheckTs = Date.now();
|
||||||
|
updateUptimeCache(serverId, { ok: false, lastCheckTs, ms });
|
||||||
return res.json({ ok: false, ms, error: 'Нет целевого адреса для пинга' });
|
return res.json({ ok: false, ms, error: 'Нет целевого адреса для пинга' });
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await runPingViaRouter(serverId, gatewayIp, target, 3);
|
const result = await runPingViaRouter(serverId, gatewayIp, target, 3);
|
||||||
ms = Date.now() - t0;
|
ms = Date.now() - t0;
|
||||||
ok = typeof result.avgMs === 'number';
|
ok = typeof result.avgMs === 'number';
|
||||||
|
const lastCheckTs = Date.now();
|
||||||
|
updateUptimeCache(serverId, { ok, lastCheckTs, ms: ok ? result.avgMs : ms });
|
||||||
return res.json({ ok, ms: ok ? result.avgMs : ms });
|
return res.json({ ok, ms: ok ? result.avgMs : ms });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ms = Date.now() - t0;
|
ms = Date.now() - t0;
|
||||||
|
const lastCheckTs = Date.now();
|
||||||
|
updateUptimeCache(serverId, { ok: false, lastCheckTs, ms });
|
||||||
return res.json({ ok: false, ms, error: err?.message || String(err) });
|
return res.json({ ok: false, ms, error: err?.message || String(err) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,11 +388,15 @@ async function uptimeCheck(req, res) {
|
|||||||
const sourceServer = others[0];
|
const sourceServer = others[0];
|
||||||
if (!sourceServer) {
|
if (!sourceServer) {
|
||||||
ms = Date.now() - t0;
|
ms = Date.now() - t0;
|
||||||
|
const lastCheckTs = Date.now();
|
||||||
|
updateUptimeCache(serverId, { ok: false, lastCheckTs, ms });
|
||||||
return res.json({ ok: false, ms, error: 'Нет другого jumphost для внешнего пинга' });
|
return res.json({ ok: false, ms, error: 'Нет другого jumphost для внешнего пинга' });
|
||||||
}
|
}
|
||||||
const targetIp = server.ip || server.extIp;
|
const targetIp = server.ip || server.extIp;
|
||||||
if (!targetIp) {
|
if (!targetIp) {
|
||||||
ms = Date.now() - t0;
|
ms = Date.now() - t0;
|
||||||
|
const lastCheckTs = Date.now();
|
||||||
|
updateUptimeCache(serverId, { ok: false, lastCheckTs, ms });
|
||||||
return res.json({ ok: false, ms, error: 'У сервера нет внешнего IP' });
|
return res.json({ ok: false, ms, error: 'У сервера нет внешнего IP' });
|
||||||
}
|
}
|
||||||
const sourceId = sourceServer.id || sourceServer.dns || sourceServer.ip;
|
const sourceId = sourceServer.id || sourceServer.dns || sourceServer.ip;
|
||||||
@@ -336,14 +404,20 @@ async function uptimeCheck(req, res) {
|
|||||||
const result = await runPingViaRouter(sourceId, null, targetIp, 3);
|
const result = await runPingViaRouter(sourceId, null, targetIp, 3);
|
||||||
ms = Date.now() - t0;
|
ms = Date.now() - t0;
|
||||||
ok = typeof result.avgMs === 'number';
|
ok = typeof result.avgMs === 'number';
|
||||||
|
const lastCheckTs = Date.now();
|
||||||
|
updateUptimeCache(serverId, { ok, lastCheckTs, ms: ok ? result.avgMs : ms });
|
||||||
return res.json({ ok, ms: ok ? result.avgMs : ms });
|
return res.json({ ok, ms: ok ? result.avgMs : ms });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ms = Date.now() - t0;
|
ms = Date.now() - t0;
|
||||||
|
const lastCheckTs = Date.now();
|
||||||
|
updateUptimeCache(serverId, { ok: false, lastCheckTs, ms });
|
||||||
return res.json({ ok: false, ms, error: err?.message || String(err) });
|
return res.json({ ok: false, ms, error: err?.message || String(err) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ms = Date.now() - t0;
|
ms = Date.now() - t0;
|
||||||
|
const lastCheckTs = Date.now();
|
||||||
|
updateUptimeCache(serverId, { ok: false, lastCheckTs, ms });
|
||||||
return res.json({ ok: false, ms });
|
return res.json({ ok: false, ms });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const ms = Date.now() - t0;
|
const ms = Date.now() - t0;
|
||||||
@@ -1462,4 +1536,5 @@ module.exports = {
|
|||||||
getAddressLists,
|
getAddressLists,
|
||||||
applyAddressListSummary,
|
applyAddressListSummary,
|
||||||
uptimeCheck,
|
uptimeCheck,
|
||||||
|
getUptimeCache,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -471,6 +471,7 @@ app.get('/api/mikrotik/address-lists', mikrotikConfigRoutes.getAddressLists);
|
|||||||
app.post('/api/mikrotik/address-lists/apply-summary', writeLimiter, mikrotikConfigRoutes.applyAddressListSummary);
|
app.post('/api/mikrotik/address-lists/apply-summary', writeLimiter, mikrotikConfigRoutes.applyAddressListSummary);
|
||||||
|
|
||||||
// === UPTIME MONITOR (проверка доступности: http / internal-ping / external-ping из настроек) ===
|
// === UPTIME MONITOR (проверка доступности: http / internal-ping / external-ping из настроек) ===
|
||||||
|
app.get('/api/uptime/cache', mikrotikConfigRoutes.getUptimeCache);
|
||||||
app.post('/api/uptime/check', writeLimiter, mikrotikConfigRoutes.uptimeCheck);
|
app.post('/api/uptime/check', writeLimiter, mikrotikConfigRoutes.uptimeCheck);
|
||||||
|
|
||||||
// === TRAFFIC STATS (MikroTik interfaces by jumphost) ===
|
// === TRAFFIC STATS (MikroTik interfaces by jumphost) ===
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export default function SettingsPage() {
|
|||||||
const [trafficInterfacesError, setTrafficInterfacesError] = useState('');
|
const [trafficInterfacesError, setTrafficInterfacesError] = useState('');
|
||||||
const [uptimeMonitorIntervalSeconds, setUptimeMonitorIntervalSeconds] = useState('120');
|
const [uptimeMonitorIntervalSeconds, setUptimeMonitorIntervalSeconds] = useState('120');
|
||||||
const [uptimeMonitorCheckType, setUptimeMonitorCheckType] = useState('http');
|
const [uptimeMonitorCheckType, setUptimeMonitorCheckType] = useState('http');
|
||||||
|
const [uptimeMonitorCacheSeconds, setUptimeMonitorCacheSeconds] = useState('120');
|
||||||
const [serversList, setServersList] = useState([]);
|
const [serversList, setServersList] = useState([]);
|
||||||
const [sidebarSearch, setSidebarSearch] = useState('');
|
const [sidebarSearch, setSidebarSearch] = useState('');
|
||||||
const [activeSection, setActiveSection] = useState(() => {
|
const [activeSection, setActiveSection] = useState(() => {
|
||||||
@@ -260,6 +261,9 @@ export default function SettingsPage() {
|
|||||||
setUptimeMonitorCheckType(
|
setUptimeMonitorCheckType(
|
||||||
checkType === 'internal-ping' || checkType === 'external-ping' ? checkType : 'http'
|
checkType === 'internal-ping' || checkType === 'external-ping' ? checkType : 'http'
|
||||||
);
|
);
|
||||||
|
setUptimeMonitorCacheSeconds(
|
||||||
|
data?.uptimeMonitorCacheSeconds != null ? String(data.uptimeMonitorCacheSeconds) : '120'
|
||||||
|
);
|
||||||
const raw = data?.trafficInterfaces;
|
const raw = data?.trafficInterfaces;
|
||||||
setTrafficInterfacesSelected(
|
setTrafficInterfacesSelected(
|
||||||
Array.isArray(raw)
|
Array.isArray(raw)
|
||||||
@@ -370,6 +374,10 @@ export default function SettingsPage() {
|
|||||||
uptimeMonitorCheckType === 'internal-ping' || uptimeMonitorCheckType === 'external-ping'
|
uptimeMonitorCheckType === 'internal-ping' || uptimeMonitorCheckType === 'external-ping'
|
||||||
? uptimeMonitorCheckType
|
? uptimeMonitorCheckType
|
||||||
: 'http',
|
: 'http',
|
||||||
|
uptimeMonitorCacheSeconds: Math.max(
|
||||||
|
0,
|
||||||
|
parseInt(uptimeMonitorCacheSeconds, 10) || 120
|
||||||
|
),
|
||||||
trafficInterfaces: Array.isArray(trafficInterfacesSelected)
|
trafficInterfaces: Array.isArray(trafficInterfacesSelected)
|
||||||
? trafficInterfacesSelected.map((p) => ({
|
? trafficInterfacesSelected.map((p) => ({
|
||||||
serverKey: p.serverKey,
|
serverKey: p.serverKey,
|
||||||
@@ -762,6 +770,20 @@ export default function SettingsPage() {
|
|||||||
HTTP: подключение к RouterOS API. Внутренний: пинг через туннель (как на карте сети). Внешний: пинг внешнего IP с другого jumphost.
|
HTTP: подключение к RouterOS API. Внутренний: пинг через туннель (как на карте сети). Внешний: пинг внешнего IP с другого jumphost.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<FormField
|
||||||
|
label="Кеш результатов (сек)"
|
||||||
|
name="uptimeMonitorCacheSeconds"
|
||||||
|
type="number"
|
||||||
|
value={uptimeMonitorCacheSeconds}
|
||||||
|
onChange={setUptimeMonitorCacheSeconds}
|
||||||
|
placeholder="120"
|
||||||
|
helpText="При заходе на страницу показываются последние проверки, если кеш младше этого срока (0 — не показывать кеш)."
|
||||||
|
disabled={saving}
|
||||||
|
min={0}
|
||||||
|
max={86400}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import PageHeader from './components/PageHeader.jsx';
|
|||||||
import { formatRelative } from './lib/datetime.js';
|
import { formatRelative } from './lib/datetime.js';
|
||||||
|
|
||||||
const DEFAULT_CHECK_INTERVAL_SEC = 120;
|
const DEFAULT_CHECK_INTERVAL_SEC = 120;
|
||||||
const DELAY_BETWEEN_CHECKS_MS = 1500;
|
const PARALLEL_CHECKS = 4; // количество одновременных проверок
|
||||||
const HISTORY_MAX = 500;
|
const HISTORY_MAX = 500;
|
||||||
const STORAGE_KEY = 'uptime-monitor-history';
|
const STORAGE_KEY = 'uptime-monitor-history';
|
||||||
|
|
||||||
@@ -90,8 +90,8 @@ export default function UptimeMonitorPage() {
|
|||||||
const [historyMap, setHistoryMap] = useState(loadHistoryFromStorage);
|
const [historyMap, setHistoryMap] = useState(loadHistoryFromStorage);
|
||||||
const [selectedServerId, setSelectedServerId] = useState(null);
|
const [selectedServerId, setSelectedServerId] = useState(null);
|
||||||
const [checkIntervalSeconds, setCheckIntervalSeconds] = useState(DEFAULT_CHECK_INTERVAL_SEC);
|
const [checkIntervalSeconds, setCheckIntervalSeconds] = useState(DEFAULT_CHECK_INTERVAL_SEC);
|
||||||
/** serverId, для которого сейчас идёт проверка — показываем «Проверка…» в строке */
|
/** serverId, по которым сейчас идёт проверка — показываем «Проверка…» в строке (до PARALLEL_CHECKS одновременно) */
|
||||||
const [currentCheckingServerId, setCurrentCheckingServerId] = useState(null);
|
const [currentCheckingServerIds, setCurrentCheckingServerIds] = useState([]);
|
||||||
const intervalRef = useRef(null);
|
const intervalRef = useRef(null);
|
||||||
const checkingRef = useRef(false);
|
const checkingRef = useRef(false);
|
||||||
|
|
||||||
@@ -128,44 +128,66 @@ export default function UptimeMonitorPage() {
|
|||||||
fetchServers();
|
fetchServers();
|
||||||
}, [fetchServers]);
|
}, [fetchServers]);
|
||||||
|
|
||||||
|
// При заходе на страницу подставляем кеш — сразу видны последние проверки
|
||||||
|
useEffect(() => {
|
||||||
|
if (jumphosts.length === 0) return;
|
||||||
|
api.get('/uptime/cache')
|
||||||
|
.then(({ data }) => {
|
||||||
|
if (data?.results && typeof data.results === 'object' && Object.keys(data.results).length > 0) {
|
||||||
|
setStatusMap((prev) => ({ ...prev, ...data.results }));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
}, [jumphosts]);
|
||||||
|
|
||||||
const checkIntervalMs = checkIntervalSeconds * 1000;
|
const checkIntervalMs = checkIntervalSeconds * 1000;
|
||||||
|
|
||||||
|
/** Параллельный запуск проверок (до PARALLEL_CHECKS одновременно), результат по мере готовности */
|
||||||
const runChecks = useCallback(async () => {
|
const runChecks = useCallback(async () => {
|
||||||
if (jumphosts.length === 0) return;
|
if (jumphosts.length === 0) return;
|
||||||
if (checkingRef.current) return;
|
if (checkingRef.current) return;
|
||||||
checkingRef.current = true;
|
checkingRef.current = true;
|
||||||
setChecking(true);
|
setChecking(true);
|
||||||
for (const server of jumphosts) {
|
setCurrentCheckingServerIds([]);
|
||||||
const serverId = server.id || server.dns || server.ip;
|
let index = 0;
|
||||||
if (!serverId) continue;
|
const getNext = () => {
|
||||||
setCurrentCheckingServerId(serverId);
|
const i = index++;
|
||||||
const t0 = Date.now();
|
return i < jumphosts.length ? jumphosts[i] : null;
|
||||||
let ok = false;
|
};
|
||||||
let ms = null;
|
const checkOne = async () => {
|
||||||
try {
|
let server;
|
||||||
const { data: checkData } = await api.post('/uptime/check', { serverId });
|
while ((server = getNext()) != null) {
|
||||||
ok = checkData?.ok === true;
|
const serverId = server.id || server.dns || server.ip;
|
||||||
ms = typeof checkData?.ms === 'number' ? checkData.ms : Date.now() - t0;
|
if (!serverId) continue;
|
||||||
} catch (_) {
|
setCurrentCheckingServerIds((prev) => [...prev, serverId]);
|
||||||
ms = Date.now() - t0;
|
const t0 = Date.now();
|
||||||
|
let ok = false;
|
||||||
|
let ms = null;
|
||||||
|
try {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
const lastCheckTs = Date.now();
|
||||||
|
const entry = { ts: lastCheckTs, ok, ms };
|
||||||
|
setStatusMap((prev) => ({ ...prev, [serverId]: { ok, lastCheckTs } }));
|
||||||
|
setHistoryMap((prev) => {
|
||||||
|
const list = Array.isArray(prev[serverId]) ? prev[serverId] : [];
|
||||||
|
const merged = [...list, entry].slice(-HISTORY_MAX);
|
||||||
|
return { ...prev, [serverId]: merged };
|
||||||
|
});
|
||||||
|
setCurrentCheckingServerIds((prev) => prev.filter((id) => id !== serverId));
|
||||||
}
|
}
|
||||||
const lastCheckTs = Date.now();
|
};
|
||||||
const entry = { ts: lastCheckTs, ok, ms };
|
await Promise.all(Array.from({ length: PARALLEL_CHECKS }, checkOne));
|
||||||
// Обновляем UI сразу после каждой проверки — результат виден по мере прохождения тестов
|
setCurrentCheckingServerIds([]);
|
||||||
setStatusMap((prev) => ({ ...prev, [serverId]: { ok, lastCheckTs } }));
|
|
||||||
setHistoryMap((prev) => {
|
|
||||||
const list = Array.isArray(prev[serverId]) ? prev[serverId] : [];
|
|
||||||
const merged = [...list, entry].slice(-HISTORY_MAX);
|
|
||||||
return { ...prev, [serverId]: merged };
|
|
||||||
});
|
|
||||||
setCurrentCheckingServerId(null);
|
|
||||||
await new Promise((r) => setTimeout(r, DELAY_BETWEEN_CHECKS_MS));
|
|
||||||
}
|
|
||||||
setCurrentCheckingServerId(null);
|
|
||||||
checkingRef.current = false;
|
checkingRef.current = false;
|
||||||
setChecking(false);
|
setChecking(false);
|
||||||
}, [jumphosts]);
|
}, [jumphosts]);
|
||||||
|
|
||||||
|
// Автопроверка по интервалу в фоне (параллельно с другими проверками)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (jumphosts.length === 0) return;
|
if (jumphosts.length === 0) return;
|
||||||
runChecks();
|
runChecks();
|
||||||
@@ -278,28 +300,28 @@ export default function UptimeMonitorPage() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{currentCheckingServerId === id && (
|
{currentCheckingServerIds.includes(id) && (
|
||||||
<span className="badge bg-blue-lt text-blue">
|
<span className="badge bg-blue-lt text-blue">
|
||||||
<span className="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true" />
|
<span className="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true" />
|
||||||
Проверка…
|
Проверка…
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{currentCheckingServerId !== id && status == null && (
|
{!currentCheckingServerIds.includes(id) && status == null && (
|
||||||
<span className="badge bg-secondary">—</span>
|
<span className="badge bg-secondary">—</span>
|
||||||
)}
|
)}
|
||||||
{currentCheckingServerId !== id && status?.ok === true && (
|
{!currentCheckingServerIds.includes(id) && status?.ok === true && (
|
||||||
<span className="badge bg-success-lt text-success">
|
<span className="badge bg-success-lt text-success">
|
||||||
<IconCircleCheck size={14} /> Доступен
|
<IconCircleCheck size={14} /> Доступен
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{currentCheckingServerId !== id && status?.ok === false && (
|
{!currentCheckingServerIds.includes(id) && status?.ok === false && (
|
||||||
<span className="badge bg-danger-lt text-danger">
|
<span className="badge bg-danger-lt text-danger">
|
||||||
<IconCircleX size={14} /> Недоступен
|
<IconCircleX size={14} /> Недоступен
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-muted">
|
<td className="text-muted">
|
||||||
{currentCheckingServerId === id
|
{currentCheckingServerIds.includes(id)
|
||||||
? '…'
|
? '…'
|
||||||
: status?.lastCheckTs
|
: status?.lastCheckTs
|
||||||
? formatRelative(status.lastCheckTs)
|
? formatRelative(status.lastCheckTs)
|
||||||
|
|||||||
Reference in New Issue
Block a user