feat: add ping functionality and update config for server monitoring

pull/62/head
SawGoD 2025-07-27 14:43:34 +03:00
parent 935fa4970c
commit c27aed85e4
6 changed files with 239 additions and 4 deletions

View File

@ -12,12 +12,28 @@ window.Config = {
// 'm800679644-4ee3480057a34ce157103cba', // Read-Only ключ
],
// URL для проверки пинга (по порядку соответствуют API ключам)
PingUrls: [
'http://itachi.nj0.ru', // Для первого API ключа
'http://hidan.nj0.ru', // Для второго API ключа
'http://yugito.nj0.ru', // Для третьего API ключа
'http://lando.nj0.ru', // Для четвертого API ключа
],
// Количество дней в логах
CountDays: 20,
// Показывать ли ссылки на проверяемые сайты
ShowLink: false,
// Настройки пинга
PingSettings: {
enabled: true,
timeout: 3000,
attempts: 3,
interval: 15000, // Обновление каждые 15 сек
},
// Меню навигации
Navi: [
{

View File

@ -484,12 +484,62 @@
padding: 0.5rem 0;
}
// Пинг в блоке статистики
.ping-stats-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.5rem 0;
}
.ping-stats-value {
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
cursor: pointer;
transition: all 0.2s ease;
}
.ping-stats-value:hover {
transform: scale(1.05);
}
.ping-stats-value.ping-excellent {
color: #22c55e;
}
.ping-stats-value.ping-good {
color: #16a34a;
}
.ping-stats-value.ping-ok {
color: #f59e0b;
}
.ping-stats-value.ping-poor {
color: #ef4444;
}
.ping-stats-value.ping-fail {
color: #dc2626;
}
.ping-stats-value.ping-loading {
color: var(--my-text-content-color);
font-style: italic;
}
@media (min-width: 768px) {
.stats-item {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
// На ПК ping-статистика тоже должна быть горизонтальной
.ping-stats-item {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
// Спиннер

View File

@ -44,6 +44,9 @@ export const MESSAGES = {
updateDescription: 'A new version of the application is available. Update now to get the latest improvements.',
updateNow: 'Update Now',
later: 'Later',
// Пинг
pingStatus: 'Current delay',
},
ru: {
// Общие
@ -89,6 +92,9 @@ export const MESSAGES = {
updateDescription: 'Доступна новая версия приложения. Обновите сейчас, чтобы получить последние улучшения.',
updateNow: 'Обновить сейчас',
later: 'Позже',
// Пинг
pingStatus: 'Текущая задержка',
},
}

102
src/common/ping.js Normal file
View File

@ -0,0 +1,102 @@
// Измерение пинга серверов
const PING_TIMEOUT = 3000
// Пинг с замером времени
export const measurePing = async (url, attempts = 3) => {
const times = []
for (let i = 0; i < attempts; i++) {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), PING_TIMEOUT)
const start = performance.now()
try {
await fetch(url, {
method: 'HEAD',
mode: 'no-cors',
cache: 'no-store',
signal: controller.signal,
})
const time = performance.now() - start
if (i > 0) times.push(Math.round(time)) // пропускаем первый
} catch {
// Игнорируем ошибки
}
clearTimeout(timeout)
}
if (times.length === 0) return null
const min = Math.min(...times)
const max = Math.max(...times)
const avg = Math.round(times.reduce((a, b) => a + b, 0) / times.length)
return { avg, min, max, times }
}
// Проверка пинга для списка серверов
export const checkServersLatency = async (servers) => {
const results = {}
for (const server of servers) {
try {
const result = await measurePing(server.url)
results[server.name] = result
} catch (error) {
console.error(`Ошибка пинга для ${server.name}:`, error)
results[server.name] = null
}
}
return results
}
// Парсинг имени сервера для извлечения флага и типа
export const parseServerName = (name) => {
let cleanName = name
let countryCode = null
let serverType = null
// Извлекаем тип сервера (CDN/DED/API)
const typeMatch = name.match(/\[(CDN|DED|API)\]/i)
if (typeMatch) {
serverType = typeMatch[1].toLowerCase()
cleanName = cleanName.replace(typeMatch[0], '').trim()
}
// Извлекаем код страны
const flagMatch = name.match(/\[([A-Z]{2})\]/)
if (flagMatch) {
countryCode = flagMatch[1].toLowerCase()
cleanName = cleanName.replace(flagMatch[0], '').trim()
}
return {
cleanName,
countryCode,
serverType,
originalName: name,
}
}
// Цветовая классификация пинга
export const getPingClass = (ping) => {
if (!ping) return 'ping-fail'
if (ping.avg <= 50) return 'ping-excellent'
if (ping.avg <= 100) return 'ping-good'
if (ping.avg <= 200) return 'ping-ok'
return 'ping-poor'
}
// Форматирование отображения пинга
export const formatPing = (ping) => {
if (!ping) return { text: 'timeout', class: 'ping-fail' }
return {
text: `${ping.avg}ms`,
class: getPingClass(ping),
details: `AVG: ${ping.avg}ms\nMin/Max: ${ping.min}/${ping.max}ms\nDetails: ${ping.times.join('ms, ')}ms`,
}
}

View File

@ -27,9 +27,9 @@ const AppContent = () => {
</div>
<div id="uptime" className="row g-3 mb-5">
{apikeys.map((key) => (
{apikeys.map((key, index) => (
<div key={key} className="col-12">
<UptimeRobot apikey={key} />
<UptimeRobot apikey={key} pingUrl={window.Config?.PingUrls?.[index]} />
</div>
))}
</div>

View File

@ -1,12 +1,13 @@
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import ReactTooltip from 'react-tooltip'
import { getCountryCodeFromServerName, getCountryName } from '../common/country-flags'
import { formatDuration, formatNumber } from '../common/helper'
import { formatPing, measurePing } from '../common/ping'
import { GetMonitors } from '../common/uptimerobot'
import { useLanguage } from '../contexts/LanguageContext'
import Link from './link'
const UptimeRobot = ({ apikey }) => {
const UptimeRobot = ({ apikey, pingUrl }) => {
const { t } = useLanguage()
const status = {
@ -18,6 +19,25 @@ const UptimeRobot = ({ apikey }) => {
const { CountDays, ShowLink } = window.Config
const [monitors, setMonitors] = useState(null)
const [loading, setLoading] = useState(true)
const [pingResult, setPingResult] = useState(null)
const [pingLoading, setPingLoading] = useState(false)
const pingIntervalRef = useRef(null)
// Функция для обновления пинга
const updatePing = async () => {
if (!pingUrl || !window.Config?.PingSettings?.enabled) return
setPingLoading(true)
try {
const result = await measurePing(pingUrl, window.Config.PingSettings.attempts || 3)
setPingResult(result)
} catch (error) {
console.error('Ошибка измерения пинга:', error)
setPingResult(null)
} finally {
setPingLoading(false)
}
}
useEffect(() => {
// Debouncing - задержка 500мс перед запросом
@ -32,6 +52,24 @@ const UptimeRobot = ({ apikey }) => {
return () => clearTimeout(timeoutId)
}, [apikey, CountDays])
// Эффект для пинга
useEffect(() => {
if (pingUrl && window.Config?.PingSettings?.enabled) {
// Сразу измеряем пинг
updatePing()
// Устанавливаем интервал
const interval = window.Config.PingSettings.interval || 15000
pingIntervalRef.current = setInterval(updatePing, interval)
}
return () => {
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current)
}
}
}, [pingUrl])
if (loading) {
return (
<div className="my-block-big">
@ -229,6 +267,29 @@ const UptimeRobot = ({ apikey }) => {
</span>
</div>
</div>
{/* Показываем пинг в блоке статистики, если URL настроен */}
{pingUrl && (
<div className="col-md-6 ping-stats-col">
<div className="stats-item ping-stats-item">
<span className="my-text-content">{t('pingStatus')}</span>
{(() => {
const formatted = formatPing(pingResult)
const tooltipText = pingResult ? formatted.details : 'Измерение пинга...'
return (
<span
className={`my-text-heading fw-semibold ping-stats-value ${formatted.class} ${
pingLoading ? 'ping-loading' : ''
}`}
data-tip={tooltipText}
>
📡 {pingLoading ? '...' : formatted.text}
</span>
)
})()}
</div>
</div>
)}
</div>
<ReactTooltip className="tooltip" place="top" type="dark" effect="solid" multiline={true} delayShow={200} delayHide={100} />