diff --git a/public/config.js b/public/config.js index aa58fa3..eb1dbfe 100644 --- a/public/config.js +++ b/public/config.js @@ -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: [ { diff --git a/src/app.scss b/src/app.scss index 2029a80..9d5ae72 100644 --- a/src/app.scss +++ b/src/app.scss @@ -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; + } } // Спиннер diff --git a/src/common/i18n.js b/src/common/i18n.js index e17c178..05b90d7 100644 --- a/src/common/i18n.js +++ b/src/common/i18n.js @@ -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: 'Текущая задержка', }, } diff --git a/src/common/ping.js b/src/common/ping.js new file mode 100644 index 0000000..b7c6b51 --- /dev/null +++ b/src/common/ping.js @@ -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`, + } +} diff --git a/src/components/app.js b/src/components/app.js index 67fc43a..c5d50fc 100644 --- a/src/components/app.js +++ b/src/components/app.js @@ -27,9 +27,9 @@ const AppContent = () => {
- {apikeys.map((key) => ( + {apikeys.map((key, index) => (
- +
))}
diff --git a/src/components/uptimerobot.js b/src/components/uptimerobot.js index bffd439..0c0051d 100644 --- a/src/components/uptimerobot.js +++ b/src/components/uptimerobot.js @@ -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 (
@@ -229,6 +267,29 @@ const UptimeRobot = ({ apikey }) => {
+ + {/* Показываем пинг в блоке статистики, если URL настроен */} + {pingUrl && ( +
+
+ {t('pingStatus')} + {(() => { + const formatted = formatPing(pingResult) + const tooltipText = pingResult ? formatted.details : 'Измерение пинга...' + return ( + + 📡 {pingLoading ? '...' : formatted.text} + + ) + })()} +
+
+ )}