From cc8d11cfb13b1f2f74ad95ba4d3fca9c9016da55 Mon Sep 17 00:00:00 2001 From: Denis Shatskiy Date: Mon, 11 Aug 2025 00:00:07 +0700 Subject: [PATCH] feat: Add server availability check API and integrate it into the Dashboard and ServerManager components for enhanced server monitoring and user experience --- backend/server.js | 50 +++++++++++++++++++++++++ frontend/src/Dashboard.jsx | 6 ++- frontend/src/ServerManager.jsx | 67 ++++++++++++++++++++++++++++++++-- 3 files changed, 118 insertions(+), 5 deletions(-) diff --git a/backend/server.js b/backend/server.js index e19ddb0..d64024c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -5,6 +5,7 @@ const cors = require('cors'); const path = require('path'); const compression = require('compression'); const Ajv = require('ajv'); +const net = require('net'); const app = express(); const port = 3001; @@ -1395,6 +1396,55 @@ app.get('/api/auto-urls', async (req, res) => { } }); +// --- Servers availability check --- +// Simple TCP connect check to 80 then 443 with short timeouts +function tcpCheck(host, port, timeoutMs) { + return new Promise((resolve) => { + const socket = new net.Socket(); + let settled = false; + const onOk = () => { if (!settled) { settled = true; try{ socket.destroy(); }catch{} resolve(true); } }; + const onFail = () => { if (!settled) { settled = true; try{ socket.destroy(); }catch{} resolve(false); } }; + socket.setTimeout(timeoutMs); + socket.once('error', onFail); + socket.once('timeout', onFail); + socket.connect(port, host, onOk); + }); +} + +async function checkOneServer(srv, timeoutMs = 1500) { + const hostCandidates = []; + if (srv.ip) hostCandidates.push(srv.ip); + if (srv.dns) hostCandidates.push(srv.dns); + for (const host of hostCandidates) { + // try 80 then 443 + const ok80 = await tcpCheck(host, 80, timeoutMs); + if (ok80) return true; + const ok443 = await tcpCheck(host, 443, timeoutMs); + if (ok443) return true; + } + return false; +} + +app.get('/api/servers/availability', async (req, res) => { + try { + const data = await s3.getObject({ Bucket: BUCKET_NAME, Key: 'servers.json' }).promise(); + let servers = []; + try { + servers = JSON.parse(data.Body.toString('utf-8')); + if (!Array.isArray(servers)) servers = []; + } catch { + servers = []; + } + const checks = await Promise.allSettled(servers.map(s => checkOneServer(s))); + const statuses = servers.map((s, i) => ({ ip: s.ip, dns: s.dns, online: checks[i].status === 'fulfilled' ? Boolean(checks[i].value) : false })); + const online = statuses.filter(x => x.online).length; + res.json({ online, total: servers.length, statuses }); + } catch (e) { + console.error('availability error', e); + res.status(500).json({ online: 0, total: 0, statuses: [] }); + } +}); + // Update auto URLs in S3 app.post('/api/auto-urls', async (req, res) => { const { urls } = req.body; diff --git a/frontend/src/Dashboard.jsx b/frontend/src/Dashboard.jsx index 14ffd64..aa9a707 100644 --- a/frontend/src/Dashboard.jsx +++ b/frontend/src/Dashboard.jsx @@ -84,6 +84,7 @@ function Dashboard() { axios.get('/api/ip-ranges'), axios.get('/api/asns'), axios.get('/api/servers'), + axios.get('/api/servers/availability'), axios.get('/api/s3/last-modified') ]); @@ -92,7 +93,8 @@ function Dashboard() { const ipRangesRes = results[1]; const asnsRes = results[2]; const serversRes = results[3]; - const s3Res = results[4]; + const availabilityRes = results[4]; + const s3Res = results[5]; // Получаем данные серверов для подсчета дополнительной статистики const domains = domainsRes.status === 'fulfilled' ? domainsRes.value.data : []; @@ -102,7 +104,7 @@ function Dashboard() { setRaw({ domains, ipRanges, asns, servers }); const countries = new Set(servers.map(server => server.country).filter(Boolean)); const providers = new Set(servers.map(server => server.provider).filter(Boolean)); - const onlineServers = servers.filter(server => server.status === 'Онлайн').length; + const onlineServers = availabilityRes.status === 'fulfilled' ? (availabilityRes.value.data?.online || 0) : (servers.filter(server => server.status === 'Онлайн').length); // Получаем дату последнего обновления // Новый формат: объект с ключами { domainsNew, asns, servers, filters, ipRanges } diff --git a/frontend/src/ServerManager.jsx b/frontend/src/ServerManager.jsx index 92efb80..066b9bf 100644 --- a/frontend/src/ServerManager.jsx +++ b/frontend/src/ServerManager.jsx @@ -15,7 +15,9 @@ import { IconAlertTriangle, IconServer, IconChevronDown, - IconLink + IconLink, + IconFilter, + IconMapPin } from '@tabler/icons-react'; import GraphView from './GraphView'; @@ -66,6 +68,11 @@ function ServerManager() { type: 'routes', version: 'v4.rsc' }); + const presets = { + default: { baseUrl: 'https://functions.yandexcloud.net/d4eno3im0qgsr4tj5hdo', cloudflare_gateway: 'SWE-IHOR', bunny_gateway: 'SWE-IHOR', fastly_gateway: 'SWE-IHOR', telegram_gateway: '94.142.140.1', hetzner_gateway: '94.142.140.1', type: 'routes', version: 'v4.rsc' }, + cloudflareSWE: { baseUrl: 'https://cf.example/api', cloudflare_gateway: 'SWE-IHOR', bunny_gateway: '', fastly_gateway: '', telegram_gateway: '', hetzner_gateway: '', type: 'routes', version: 'v4.rsc' }, + hetznerDE: { baseUrl: 'https://hetzner.example/api', cloudflare_gateway: '', bunny_gateway: '', fastly_gateway: '', telegram_gateway: '', hetzner_gateway: '94.142.140.1', type: 'routes', version: 'v4.rsc' } + }; // Состояние для мониторинга сети const [networkStatus, setNetworkStatus] = useState({}); @@ -81,6 +88,11 @@ function ServerManager() { ]); const [newConnection, setNewConnection] = useState({ from: '', to: '', tunnelType: 'GRE', ipA: '', ipB: '' }); + // Фильтры графа + const [graphTunnelFilters, setGraphTunnelFilters] = useState({ GRE: true, IPSec: true, WireGuard: true, OpenVPN: true }); + const [graphCountryFilter, setGraphCountryFilter] = useState(''); + const [graphProviderFilter, setGraphProviderFilter] = useState(''); + useEffect(() => { fetchServers(); }, []); @@ -207,6 +219,15 @@ function ServerManager() { return `${urlSettings.baseUrl}?${params.toString()}`; }; + const applyPreset = (name) => { + const p = presets[name]; + if (!p) return; + setUrlSettings(prev => ({ ...prev, ...p })); + localStorage.setItem('urlSettings', JSON.stringify({ ...urlSettings, ...p })); + setSuccess('Профиль применён'); + setTimeout(() => setSuccess(''), 2000); + }; + // Функция резервного копирования для http const fallbackCopyToClipboard = (text) => { const textArea = document.createElement('textarea'); @@ -244,9 +265,13 @@ function ServerManager() { }; const exportAllLinks = () => { + const filtered = servers + .filter(s => (!filterTunnel || s.tunnel === filterTunnel)) + .filter(s => (!filterCountry || s.country === filterCountry)) + .filter(s => (!filterProvider || s.provider === filterProvider)); const csvContent = [ ['IP', 'DNS', 'Country', 'Provider', 'Tunnel', 'Generated URL'], - ...servers.map(server => [ + ...filtered.map(server => [ server.ip, server.dns, server.country, @@ -456,6 +481,17 @@ function ServerManager() { Добавить + {/* Пресеты генератора ссылок */} +
+ +
+ {Object.keys(presets).map(key => ( + + ))} +
+
+ ))} + +
+ + +
+
+ + +
+ + (!graphCountryFilter || s.country === graphCountryFilter) && (!graphProviderFilter || s.provider === graphProviderFilter))} + connections={connections.filter(c => graphTunnelFilters[c.tunnelType])} + />
GRE