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() {