mirror of https://github.com/yb/uptime-status
chore: update Dockerfile for dependency installation and add version update script; enhance nginx config for caching; add rebuild script; modify config.js and index.html for versioning
parent
c85d87b892
commit
169b59a8e0
|
@ -7,12 +7,15 @@ WORKDIR /app
|
||||||
# Копируем package.json и package-lock.json
|
# Копируем package.json и package-lock.json
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Устанавливаем зависимости
|
# Устанавливаем зависимости (включая dev для скриптов)
|
||||||
RUN npm ci --only=production
|
RUN npm ci
|
||||||
|
|
||||||
# Копируем исходный код
|
# Копируем исходный код
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Обновляем версию для кэш-бастинга
|
||||||
|
RUN node scripts/update-version.js
|
||||||
|
|
||||||
# Устанавливаем публичный путь для React
|
# Устанавливаем публичный путь для React
|
||||||
ENV PUBLIC_URL=/uptime
|
ENV PUBLIC_URL=/uptime
|
||||||
|
|
||||||
|
|
|
@ -28,17 +28,30 @@ server {
|
||||||
return 301 $scheme://$host/uptime/;
|
return 301 $scheme://$host/uptime/;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# config.js - не кэшируем, может изменяться
|
||||||
|
location = /uptime/config.js {
|
||||||
|
rewrite ^/uptime(/.*)$ $1 break;
|
||||||
|
proxy_pass http://127.0.0.1:34481;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
expires -1;
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
add_header Pragma "no-cache";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Версионированные файлы - долгое кэширование
|
||||||
|
location ~* ^/uptime/.*\?v=[\d\.]+$ {
|
||||||
|
rewrite ^/uptime(/.*)$ $1 break;
|
||||||
|
proxy_pass http://127.0.0.1:34481;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
# Статические файлы (обрабатываем в первую очередь)
|
# Статические файлы (обрабатываем в первую очередь)
|
||||||
location ~* ^/uptime/static/.*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf)$ {
|
location ~* ^/uptime/static/.*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf)$ {
|
||||||
rewrite ^/uptime(/.*)$ $1 break;
|
rewrite ^/uptime(/.*)$ $1 break;
|
||||||
proxy_pass http://127.0.0.1:34481;
|
proxy_pass http://127.0.0.1:34481;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
# Временно отключаем кэш для JS файлов во время разработки
|
|
||||||
location ~* \.js$ {
|
|
||||||
expires -1;
|
|
||||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
|
||||||
add_header Pragma "no-cache";
|
|
||||||
}
|
|
||||||
expires 1y;
|
expires 1y;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
window.Config = {
|
window.Config = {
|
||||||
// Название сайта
|
// Название сайта
|
||||||
SiteName: 'Nerjel',
|
SiteName: 'Nerjel Status',
|
||||||
|
|
||||||
// UptimeRobot Api Keys
|
// UptimeRobot Api Keys
|
||||||
// Поддерживает Monitor-Specific и Read-Only
|
// Поддерживает Monitor-Specific и Read-Only
|
||||||
|
@ -14,14 +14,14 @@ window.Config = {
|
||||||
|
|
||||||
// URL для проверки пинга (по порядку соответствуют API ключам)
|
// URL для проверки пинга (по порядку соответствуют API ключам)
|
||||||
PingUrls: [
|
PingUrls: [
|
||||||
'http://itachi.nj0.ru', // Для первого API ключа
|
'http://itachi.nj0.ru:60231', // Для первого API ключа
|
||||||
'http://hidan.nj0.ru', // Для второго API ключа
|
'http://hidan.nj0.ru:60231', // Для второго API ключа
|
||||||
'http://yugito.nj0.ru', // Для третьего API ключа
|
'http://yugito.nj0.ru:60231', // Для третьего API ключа
|
||||||
'http://lando.nj0.ru', // Для четвертого API ключа
|
'http://lando.nj0.ru:60231', // Для четвертого API ключа
|
||||||
],
|
],
|
||||||
|
|
||||||
// Количество дней в логах
|
// Количество дней в логах
|
||||||
CountDays: 20,
|
CountDays: 16,
|
||||||
|
|
||||||
// Показывать ли ссылки на проверяемые сайты
|
// Показывать ли ссылки на проверяемые сайты
|
||||||
ShowLink: false,
|
ShowLink: false,
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<link href="https://i.ibb.co/Df6CnYZg/a56f1a2d6cea.png" rel="icon" sizes="16x16" type="image/png">
|
<link href="https://i.ibb.co/Df6CnYZg/a56f1a2d6cea.png" rel="icon" sizes="16x16" type="image/png">
|
||||||
<link href="https://i.ibb.co/SXCgp0LM/6c403bc2fb73.png" rel="icon" sizes="32x32" type="image/png">
|
<link href="https://i.ibb.co/SXCgp0LM/6c403bc2fb73.png" rel="icon" sizes="32x32" type="image/png">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH">
|
||||||
<script src="/uptime/config.js"></script>
|
<script src="/uptime/config.js?v=2.0.1.1753641153734"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
|
<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
const CACHE_NAME = 'uptime-status-v2.0.1.1753557287951'
|
const CACHE_NAME = 'uptime-status-v2.0.1.1753641153734'
|
||||||
const CONFIG_FILE = '/config.js'
|
const CONFIG_FILE = '/config.js'
|
||||||
|
|
||||||
// Устанавливаем Service Worker
|
// Устанавливаем Service Worker
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Скрипт для полной пересборки с версионированием
|
||||||
|
# Использование: ./rebuild_new.sh
|
||||||
|
|
||||||
|
# Цвета для вывода
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${BLUE}=== 🚀 Полная пересборка Uptime Status ===${NC}"
|
||||||
|
|
||||||
|
# Проверка необходимых файлов
|
||||||
|
if [ ! -f "scripts/update-version.js" ] || [ ! -f "package.json" ]; then
|
||||||
|
echo -e "${RED}❌ Не найдены файлы для обновления версии${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 1. Остановка и удаление контейнера
|
||||||
|
echo -e "${YELLOW}🛑 Остановка и удаление контейнера...${NC}"
|
||||||
|
docker-compose down
|
||||||
|
docker rm -f uptime-status 2>/dev/null
|
||||||
|
|
||||||
|
# 2. Полная очистка образов
|
||||||
|
echo -e "${YELLOW}🗑️ Полная очистка старых образов...${NC}"
|
||||||
|
# Удаляем основной образ
|
||||||
|
docker rmi uptime-status 2>/dev/null || true
|
||||||
|
# Удаляем все образы с тегом uptime-status
|
||||||
|
docker rmi $(docker images | grep "uptime-status" | awk '{print $3}') 2>/dev/null || true
|
||||||
|
# Удаляем dangling образы
|
||||||
|
docker rmi $(docker images -f "dangling=true" -q) 2>/dev/null || true
|
||||||
|
|
||||||
|
# 3. Обновление версии
|
||||||
|
echo -e "${YELLOW}📝 Обновление версии и кэша...${NC}"
|
||||||
|
node scripts/update-version.js
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -e "${RED}❌ Ошибка при обновлении версии${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Сборка нового образа
|
||||||
|
echo -e "${YELLOW}🔨 Сборка нового образа (полная пересборка)...${NC}"
|
||||||
|
docker build -t uptime-status . --no-cache --pull
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -e "${RED}❌ Ошибка при сборке образа${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Запуск контейнера
|
||||||
|
echo -e "${YELLOW}🚀 Запуск нового контейнера...${NC}"
|
||||||
|
docker-compose up -d
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -e "${RED}❌ Ошибка при запуске контейнера${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. Ожидание запуска
|
||||||
|
echo -e "${YELLOW}⏳ Ожидание запуска (5 сек)...${NC}"
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# 7. Проверка статуса
|
||||||
|
echo -e "${GREEN}✅ Пересборка завершена!${NC}"
|
||||||
|
echo -e "${GREEN}📱 Приложение запущено на порту 34481${NC}"
|
||||||
|
|
||||||
|
# Показать статус
|
||||||
|
echo -e "${BLUE}📊 Статус контейнера:${NC}"
|
||||||
|
docker ps | head -1
|
||||||
|
docker ps | grep uptime-status
|
||||||
|
|
||||||
|
# Показать логи последних 10 строк
|
||||||
|
echo -e "${BLUE}📄 Последние логи:${NC}"
|
||||||
|
docker logs uptime-status --tail 10
|
||||||
|
|
||||||
|
# Показать информацию о версии
|
||||||
|
if [ -f "public/index.html" ]; then
|
||||||
|
VERSION=$(grep -o 'config\.js?v=[^"]*' public/index.html | cut -d'=' -f2)
|
||||||
|
echo -e "${GREEN}🔖 Версия: ${VERSION}${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}🎉 Готово! Приложение доступно по адресу: http://localhost:34481/uptime/${NC}"
|
|
@ -13,8 +13,8 @@ const versionString = `${version}.${timestamp}`
|
||||||
const indexPath = path.join(__dirname, '../public/index.html')
|
const indexPath = path.join(__dirname, '../public/index.html')
|
||||||
let indexContent = fs.readFileSync(indexPath, 'utf8')
|
let indexContent = fs.readFileSync(indexPath, 'utf8')
|
||||||
|
|
||||||
// Заменяем версию в config.js
|
// Заменяем версию в config.js (поддерживаем разные пути)
|
||||||
indexContent = indexContent.replace(/src="\.\/config\.js\?v=[^"]*"/, `src="./config.js?v=${versionString}"`)
|
indexContent = indexContent.replace(/src="[^"]*config\.js(\?v=[^"]*)?"/g, `src="/uptime/config.js?v=${versionString}"`)
|
||||||
|
|
||||||
fs.writeFileSync(indexPath, indexContent)
|
fs.writeFileSync(indexPath, indexContent)
|
||||||
|
|
||||||
|
|
|
@ -39,14 +39,11 @@ export const MESSAGES = {
|
||||||
// Футер
|
// Футер
|
||||||
footerText: 'Based on UptimeRobot API, check frequency 5 minutes',
|
footerText: 'Based on UptimeRobot API, check frequency 5 minutes',
|
||||||
|
|
||||||
// Обновления
|
|
||||||
updateAvailable: 'Update Available',
|
|
||||||
updateDescription: 'A new version of the application is available. Update now to get the latest improvements.',
|
|
||||||
updateNow: 'Update Now',
|
|
||||||
later: 'Later',
|
|
||||||
|
|
||||||
// Пинг
|
// Пинг
|
||||||
pingStatus: 'Current delay',
|
pingStatus: 'Current delay',
|
||||||
|
pingAvg: 'AVG',
|
||||||
|
pingMinMax: 'Min/Max',
|
||||||
|
pingMeasuring: 'Measuring ping...',
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
// Общие
|
// Общие
|
||||||
|
@ -87,14 +84,11 @@ export const MESSAGES = {
|
||||||
// Футер
|
// Футер
|
||||||
footerText: 'Сделано на основе API UptimeRobot, частота проверки 5 минут',
|
footerText: 'Сделано на основе API UptimeRobot, частота проверки 5 минут',
|
||||||
|
|
||||||
// Обновления
|
|
||||||
updateAvailable: 'Доступно обновление',
|
|
||||||
updateDescription: 'Доступна новая версия приложения. Обновите сейчас, чтобы получить последние улучшения.',
|
|
||||||
updateNow: 'Обновить сейчас',
|
|
||||||
later: 'Позже',
|
|
||||||
|
|
||||||
// Пинг
|
// Пинг
|
||||||
pingStatus: 'Текущая задержка',
|
pingStatus: 'Текущая задержка',
|
||||||
|
pingAvg: 'СР',
|
||||||
|
pingMinMax: 'Мин/Макс',
|
||||||
|
pingMeasuring: 'Измерение пинга...',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Измерение пинга серверов
|
// Измерение пинга серверов
|
||||||
const PING_TIMEOUT = 3000
|
const PING_TIMEOUT = 3000
|
||||||
|
|
||||||
// Пинг с замером времени
|
// Быстрый пинг через fetch с фолбэком на Image
|
||||||
export const measurePing = async (url, attempts = 3) => {
|
export const measurePing = async (url, attempts = 3) => {
|
||||||
const times = []
|
const times = []
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ export const measurePing = async (url, attempts = 3) => {
|
||||||
const timeout = setTimeout(() => controller.abort(), PING_TIMEOUT)
|
const timeout = setTimeout(() => controller.abort(), PING_TIMEOUT)
|
||||||
const start = performance.now()
|
const start = performance.now()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Сначала пробуем быстрый fetch
|
||||||
try {
|
try {
|
||||||
await fetch(url, {
|
await fetch(url, {
|
||||||
method: 'HEAD',
|
method: 'HEAD',
|
||||||
|
@ -17,8 +19,40 @@ export const measurePing = async (url, attempts = 3) => {
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
const time = performance.now() - start
|
} catch (fetchError) {
|
||||||
|
// Если fetch не работает, фолбэк на Image (медленнее, но работает)
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const img = new Image()
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
img.onload = null
|
||||||
|
img.onerror = null
|
||||||
|
img.src = '' // Останавливаем загрузку
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
cleanup()
|
||||||
|
reject(new Error('Image timeout'))
|
||||||
|
}, 2000) // Короткий таймаут для Image
|
||||||
|
|
||||||
|
img.onload = img.onerror = () => {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
cleanup()
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.signal.addEventListener('abort', () => {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
cleanup()
|
||||||
|
reject(new Error('Aborted'))
|
||||||
|
})
|
||||||
|
|
||||||
|
// Минимальный URL для быстрой проверки
|
||||||
|
img.src = url + (url.includes('?') ? '&' : '?') + 't=' + Date.now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = performance.now() - start
|
||||||
if (i > 0) times.push(Math.round(time)) // пропускаем первый
|
if (i > 0) times.push(Math.round(time)) // пропускаем первый
|
||||||
} catch {
|
} catch {
|
||||||
// Игнорируем ошибки
|
// Игнорируем ошибки
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { useMemo } from 'react'
|
||||||
import { LanguageProvider, useLanguage } from '../contexts/LanguageContext'
|
import { LanguageProvider, useLanguage } from '../contexts/LanguageContext'
|
||||||
import Header from './header'
|
import Header from './header'
|
||||||
import Link from './link'
|
import Link from './link'
|
||||||
import UpdateNotifier from './update-notifier'
|
|
||||||
import UptimeRobot from './uptimerobot'
|
import UptimeRobot from './uptimerobot'
|
||||||
|
|
||||||
const AppContent = () => {
|
const AppContent = () => {
|
||||||
|
@ -40,7 +39,6 @@ const AppContent = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UpdateNotifier />
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useLanguage } from '../contexts/LanguageContext'
|
|
||||||
|
|
||||||
const UpdateNotifier = () => {
|
|
||||||
const { t } = useLanguage()
|
|
||||||
const [updateAvailable, setUpdateAvailable] = useState(false)
|
|
||||||
const [registration, setRegistration] = useState(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.getRegistration().then((reg) => {
|
|
||||||
if (reg) {
|
|
||||||
setRegistration(reg)
|
|
||||||
|
|
||||||
// Проверяем обновления
|
|
||||||
reg.addEventListener('updatefound', () => {
|
|
||||||
const newWorker = reg.installing
|
|
||||||
if (newWorker) {
|
|
||||||
newWorker.addEventListener('statechange', () => {
|
|
||||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
|
||||||
console.log('Обновление доступно!')
|
|
||||||
setUpdateAvailable(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Слушаем сообщения от Service Worker
|
|
||||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
|
||||||
if (event.data.type === 'UPDATE_AVAILABLE') {
|
|
||||||
setUpdateAvailable(true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleUpdate = () => {
|
|
||||||
if (registration) {
|
|
||||||
// Отправляем сообщение SW для активации обновления
|
|
||||||
if (registration.waiting) {
|
|
||||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Перезагружаем страницу
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDismiss = () => {
|
|
||||||
setUpdateAvailable(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!updateAvailable) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="position-fixed bottom-0 start-50 translate-middle-x p-3" style={{ zIndex: 1050 }}>
|
|
||||||
<div className="toast show" role="alert" aria-live="assertive" aria-atomic="true">
|
|
||||||
<div className="toast-header">
|
|
||||||
<div className="bg-primary rounded me-2" style={{ width: '20px', height: '20px' }}></div>
|
|
||||||
<strong className="me-auto">{t('updateAvailable')}</strong>
|
|
||||||
<button type="button" className="btn-close" onClick={handleDismiss} aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div className="toast-body">
|
|
||||||
<p className="mb-2">{t('updateDescription')}</p>
|
|
||||||
<div className="d-flex gap-2">
|
|
||||||
<button type="button" className="btn btn-primary btn-sm" onClick={handleUpdate}>
|
|
||||||
{t('updateNow')}
|
|
||||||
</button>
|
|
||||||
<button type="button" className="btn btn-outline-secondary btn-sm" onClick={handleDismiss}>
|
|
||||||
{t('later')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UpdateNotifier
|
|
|
@ -275,7 +275,10 @@ const UptimeRobot = ({ apikey, pingUrl }) => {
|
||||||
<span className="my-text-content">{t('pingStatus')}</span>
|
<span className="my-text-content">{t('pingStatus')}</span>
|
||||||
{(() => {
|
{(() => {
|
||||||
const formatted = formatPing(pingResult)
|
const formatted = formatPing(pingResult)
|
||||||
const tooltipText = pingResult ? formatted.details : 'Измерение пинга...'
|
// Формируем tooltip без строки Details с переводами
|
||||||
|
const tooltipText = pingResult
|
||||||
|
? `${t('pingAvg')}: ${pingResult.avg}ms\n${t('pingMinMax')}: ${pingResult.min}/${pingResult.max}ms`
|
||||||
|
: t('pingMeasuring')
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`my-text-heading fw-semibold ping-stats-value ${formatted.class} ${
|
className={`my-text-heading fw-semibold ping-stats-value ${formatted.class} ${
|
||||||
|
|
Loading…
Reference in New Issue