diff --git a/backend/routes/miscRoutes.js b/backend/routes/miscRoutes.js index 877297a..ad5be57 100644 --- a/backend/routes/miscRoutes.js +++ b/backend/routes/miscRoutes.js @@ -309,6 +309,59 @@ function tcpCheck(host, port, timeoutMs) { }); } +/** Измерить RTT (мс) до host:port по TCP. Возвращает число мс или null при ошибке/таймауте. */ +function measureTcpRtt(host, port = 443, timeoutMs = 6000) { + return new Promise((resolve) => { + const start = Date.now(); + const socket = new net.Socket(); + let settled = false; + const settle = (ms) => { + if (!settled) { + settled = true; + try { socket.destroy(); } catch (_) {} + resolve(ms); + } + }; + socket.setTimeout(timeoutMs, () => settle(null)); + socket.once('error', () => settle(null)); + socket.connect(port, host, () => { + const ms = Math.round(Date.now() - start); + settle(ms); + }); + }); +} + +/** Список целей для пинга с главной: id, имя, хост, порт */ +const PING_SERVICES = [ + { id: 'google', name: 'Google', host: '8.8.8.8', port: 443 }, + { id: 'cloudflare', name: 'Cloudflare', host: '1.1.1.1', port: 443 }, + { id: 'yandex', name: 'Yandex', host: 'ya.ru', port: 443 }, + { id: 'instagram', name: 'Instagram', host: 'instagram.com', port: 443 }, +]; + +// GET /api/ping-services — RTT до Google, Cloudflare, Yandex, Instagram (для карточек на дашборде) +async function getPingServices(req, res) { + try { + const results = await Promise.all( + PING_SERVICES.map(async (svc) => { + const ms = await measureTcpRtt(svc.host, svc.port, 6000); + return { id: svc.id, name: svc.name, host: svc.host, ms }; + }) + ); + const byId = {}; + results.forEach((r) => { byId[r.id] = r; }); + res.json(byId); + } catch (e) { + console.error('ping-services error', e); + res.status(500).json({ + google: { id: 'google', name: 'Google', host: '8.8.8.8', ms: null }, + cloudflare: { id: 'cloudflare', name: 'Cloudflare', host: '1.1.1.1', ms: null }, + yandex: { id: 'yandex', name: 'Yandex', host: 'ya.ru', ms: null }, + instagram: { id: 'instagram', name: 'Instagram', host: 'instagram.com', ms: null }, + }); + } +} + function anyTrue(promises) { return new Promise((resolve) => { if (!Array.isArray(promises) || promises.length === 0) return resolve(false); @@ -524,5 +577,6 @@ module.exports = { getWsUrl, getUiSettings, postUiSettings, + getPingServices, }; diff --git a/backend/server.js b/backend/server.js index ec8ec1f..f119fc3 100644 --- a/backend/server.js +++ b/backend/server.js @@ -439,6 +439,7 @@ app.post('/api/update-bgp/background', bgpUpdateLimiter, miscRoutes.updateBgpBac app.get('/api/ws/url', miscRoutes.getWsUrl); app.get('/api/ui-settings', miscRoutes.getUiSettings); app.post('/api/ui-settings', miscRoutes.postUiSettings); +app.get('/api/ping-services', miscRoutes.getPingServices); // === IPSEC PASSWORDS === app.get('/api/ipsec-passwords', ipsecPasswordsRoutes.getIpsecPasswords); diff --git a/frontend/src/Dashboard.jsx b/frontend/src/Dashboard.jsx index 7d9cf68..31b279b 100644 --- a/frontend/src/Dashboard.jsx +++ b/frontend/src/Dashboard.jsx @@ -15,7 +15,11 @@ import { IconFilter, IconDownload, IconCreditCard, - IconSearch + IconSearch, + IconBrandGoogle, + IconBrandCloudflare, + IconBrandYandex, + IconBrandInstagram } from '@tabler/icons-react'; import PageHeader from './components/PageHeader.jsx'; import TopNStats from './components/TopNStats.jsx'; @@ -74,6 +78,34 @@ function MetricCard({ title, value, icon: Icon, color, description }) { ); } +/** Карточка сервиса с иконкой и пингом (как на референсном скрине) */ +const PING_SERVICES_CONFIG = [ + { id: 'google', name: 'Google', host: '8.8.8.8', Icon: IconBrandGoogle, color: 'blue' }, + { id: 'cloudflare', name: 'Cloudflare', host: '1.1.1.1', Icon: IconBrandCloudflare, color: 'orange' }, + { id: 'yandex', name: 'Yandex', host: 'ya.ru', Icon: IconBrandYandex, color: 'red' }, + { id: 'instagram', name: 'Instagram', host: 'instagram.com', Icon: IconBrandInstagram, color: 'pink' }, +]; + +function PingServiceCard({ config, ms, loading }) { + const { name, host, Icon, color } = config; + const value = loading ? '...' : (ms != null ? `${ms} мс` : '—'); + return ( +