feat(PingServices): add endpoint and dashboard component for measuring RTT to key services (Google, Cloudflare, Yandex, Instagram)
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m39s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m39s
This commit is contained in:
@@ -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) {
|
function anyTrue(promises) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!Array.isArray(promises) || promises.length === 0) return resolve(false);
|
if (!Array.isArray(promises) || promises.length === 0) return resolve(false);
|
||||||
@@ -524,5 +577,6 @@ module.exports = {
|
|||||||
getWsUrl,
|
getWsUrl,
|
||||||
getUiSettings,
|
getUiSettings,
|
||||||
postUiSettings,
|
postUiSettings,
|
||||||
|
getPingServices,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -439,6 +439,7 @@ app.post('/api/update-bgp/background', bgpUpdateLimiter, miscRoutes.updateBgpBac
|
|||||||
app.get('/api/ws/url', miscRoutes.getWsUrl);
|
app.get('/api/ws/url', miscRoutes.getWsUrl);
|
||||||
app.get('/api/ui-settings', miscRoutes.getUiSettings);
|
app.get('/api/ui-settings', miscRoutes.getUiSettings);
|
||||||
app.post('/api/ui-settings', miscRoutes.postUiSettings);
|
app.post('/api/ui-settings', miscRoutes.postUiSettings);
|
||||||
|
app.get('/api/ping-services', miscRoutes.getPingServices);
|
||||||
|
|
||||||
// === IPSEC PASSWORDS ===
|
// === IPSEC PASSWORDS ===
|
||||||
app.get('/api/ipsec-passwords', ipsecPasswordsRoutes.getIpsecPasswords);
|
app.get('/api/ipsec-passwords', ipsecPasswordsRoutes.getIpsecPasswords);
|
||||||
|
|||||||
@@ -15,7 +15,11 @@ import {
|
|||||||
IconFilter,
|
IconFilter,
|
||||||
IconDownload,
|
IconDownload,
|
||||||
IconCreditCard,
|
IconCreditCard,
|
||||||
IconSearch
|
IconSearch,
|
||||||
|
IconBrandGoogle,
|
||||||
|
IconBrandCloudflare,
|
||||||
|
IconBrandYandex,
|
||||||
|
IconBrandInstagram
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import PageHeader from './components/PageHeader.jsx';
|
import PageHeader from './components/PageHeader.jsx';
|
||||||
import TopNStats from './components/TopNStats.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 (
|
||||||
|
<div className="card h-100 position-relative">
|
||||||
|
<div className="card-body d-flex align-items-center">
|
||||||
|
<span className={`avatar avatar-lg me-3 bg-${color}-lt text-${color} border-0 rounded`}>
|
||||||
|
<Icon size={28} stroke={1.5} />
|
||||||
|
</span>
|
||||||
|
<div className="flex-grow-1 min-w-0">
|
||||||
|
<div className="h4 mb-0 fw-bold">{value}</div>
|
||||||
|
<div className="text-muted small text-truncate" title={`${name} (${host})`}>
|
||||||
|
{name} <span className="opacity-75">({host})</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Dashboard() {
|
function Dashboard() {
|
||||||
// Глобальный поиск удалён по требованию UX
|
// Глобальный поиск удалён по требованию UX
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
@@ -92,6 +124,8 @@ function Dashboard() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [lastFetchTime, setLastFetchTime] = useState(null);
|
const [lastFetchTime, setLastFetchTime] = useState(null);
|
||||||
|
const [pingServices, setPingServices] = useState(null);
|
||||||
|
const [pingLoading, setPingLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchStats() {
|
async function fetchStats() {
|
||||||
@@ -190,6 +224,22 @@ function Dashboard() {
|
|||||||
fetchStats();
|
fetchStats();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
setPingLoading(true);
|
||||||
|
api.get('/ping-services')
|
||||||
|
.then(({ data }) => {
|
||||||
|
if (!cancelled) setPingServices(data || null);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) setPingServices(null);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setPingLoading(false);
|
||||||
|
});
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="alert alert-danger" role="alert">
|
<div className="alert alert-danger" role="alert">
|
||||||
@@ -220,6 +270,19 @@ function Dashboard() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Пинг до сервисов: Google, Cloudflare, Yandex, Instagram */}
|
||||||
|
<div className="row g-3 mb-4">
|
||||||
|
{PING_SERVICES_CONFIG.map((config) => (
|
||||||
|
<div key={config.id} className="col-6 col-md-3">
|
||||||
|
<PingServiceCard
|
||||||
|
config={config}
|
||||||
|
ms={pingServices?.[config.id]?.ms ?? null}
|
||||||
|
loading={pingLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Основные метрики */}
|
{/* Основные метрики */}
|
||||||
<div className="row g-3 mb-4">
|
<div className="row g-3 mb-4">
|
||||||
<div className="col-md-3">
|
<div className="col-md-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user