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
|
||||
COPY package*.json ./
|
||||
|
||||
# Устанавливаем зависимости
|
||||
RUN npm ci --only=production
|
||||
# Устанавливаем зависимости (включая dev для скриптов)
|
||||
RUN npm ci
|
||||
|
||||
# Копируем исходный код
|
||||
COPY . .
|
||||
|
||||
# Обновляем версию для кэш-бастинга
|
||||
RUN node scripts/update-version.js
|
||||
|
||||
# Устанавливаем публичный путь для React
|
||||
ENV PUBLIC_URL=/uptime
|
||||
|
||||
|
|
|
@ -28,17 +28,30 @@ server {
|
|||
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)$ {
|
||||
rewrite ^/uptime(/.*)$ $1 break;
|
||||
proxy_pass http://127.0.0.1:34481;
|
||||
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;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
window.Config = {
|
||||
// Название сайта
|
||||
SiteName: 'Nerjel',
|
||||
SiteName: 'Nerjel Status',
|
||||
|
||||
// UptimeRobot Api Keys
|
||||
// Поддерживает Monitor-Specific и Read-Only
|
||||
|
@ -14,14 +14,14 @@ window.Config = {
|
|||
|
||||
// URL для проверки пинга (по порядку соответствуют API ключам)
|
||||
PingUrls: [
|
||||
'http://itachi.nj0.ru', // Для первого API ключа
|
||||
'http://hidan.nj0.ru', // Для второго API ключа
|
||||
'http://yugito.nj0.ru', // Для третьего API ключа
|
||||
'http://lando.nj0.ru', // Для четвертого API ключа
|
||||
'http://itachi.nj0.ru:60231', // Для первого API ключа
|
||||
'http://hidan.nj0.ru:60231', // Для второго API ключа
|
||||
'http://yugito.nj0.ru:60231', // Для третьего API ключа
|
||||
'http://lando.nj0.ru:60231', // Для четвертого API ключа
|
||||
],
|
||||
|
||||
// Количество дней в логах
|
||||
CountDays: 20,
|
||||
CountDays: 16,
|
||||
|
||||
// Показывать ли ссылки на проверяемые сайты
|
||||
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/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">
|
||||
<script src="/uptime/config.js"></script>
|
||||
<script src="/uptime/config.js?v=2.0.1.1753641153734"></script>
|
||||
</head>
|
||||
<body>
|
||||
<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'
|
||||
|
||||
// Устанавливаем 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')
|
||||
let indexContent = fs.readFileSync(indexPath, 'utf8')
|
||||
|
||||
// Заменяем версию в config.js
|
||||
indexContent = indexContent.replace(/src="\.\/config\.js\?v=[^"]*"/, `src="./config.js?v=${versionString}"`)
|
||||
// Заменяем версию в config.js (поддерживаем разные пути)
|
||||
indexContent = indexContent.replace(/src="[^"]*config\.js(\?v=[^"]*)?"/g, `src="/uptime/config.js?v=${versionString}"`)
|
||||
|
||||
fs.writeFileSync(indexPath, indexContent)
|
||||
|
||||
|
|
|
@ -39,14 +39,11 @@ export const MESSAGES = {
|
|||
// Футер
|
||||
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',
|
||||
pingAvg: 'AVG',
|
||||
pingMinMax: 'Min/Max',
|
||||
pingMeasuring: 'Measuring ping...',
|
||||
},
|
||||
ru: {
|
||||
// Общие
|
||||
|
@ -87,14 +84,11 @@ export const MESSAGES = {
|
|||
// Футер
|
||||
footerText: 'Сделано на основе API UptimeRobot, частота проверки 5 минут',
|
||||
|
||||
// Обновления
|
||||
updateAvailable: 'Доступно обновление',
|
||||
updateDescription: 'Доступна новая версия приложения. Обновите сейчас, чтобы получить последние улучшения.',
|
||||
updateNow: 'Обновить сейчас',
|
||||
later: 'Позже',
|
||||
|
||||
// Пинг
|
||||
pingStatus: 'Текущая задержка',
|
||||
pingAvg: 'СР',
|
||||
pingMinMax: 'Мин/Макс',
|
||||
pingMeasuring: 'Измерение пинга...',
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Измерение пинга серверов
|
||||
const PING_TIMEOUT = 3000
|
||||
|
||||
// Пинг с замером времени
|
||||
// Быстрый пинг через fetch с фолбэком на Image
|
||||
export const measurePing = async (url, attempts = 3) => {
|
||||
const times = []
|
||||
|
||||
|
@ -11,14 +11,48 @@ export const measurePing = async (url, attempts = 3) => {
|
|||
const start = performance.now()
|
||||
|
||||
try {
|
||||
await fetch(url, {
|
||||
method: 'HEAD',
|
||||
mode: 'no-cors',
|
||||
cache: 'no-store',
|
||||
signal: controller.signal,
|
||||
})
|
||||
const time = performance.now() - start
|
||||
// Сначала пробуем быстрый fetch
|
||||
try {
|
||||
await fetch(url, {
|
||||
method: 'HEAD',
|
||||
mode: 'no-cors',
|
||||
cache: 'no-store',
|
||||
signal: controller.signal,
|
||||
})
|
||||
} 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)) // пропускаем первый
|
||||
} catch {
|
||||
// Игнорируем ошибки
|
||||
|
|
|
@ -2,7 +2,6 @@ import { useMemo } from 'react'
|
|||
import { LanguageProvider, useLanguage } from '../contexts/LanguageContext'
|
||||
import Header from './header'
|
||||
import Link from './link'
|
||||
import UpdateNotifier from './update-notifier'
|
||||
import UptimeRobot from './uptimerobot'
|
||||
|
||||
const AppContent = () => {
|
||||
|
@ -40,7 +39,6 @@ const AppContent = () => {
|
|||
</p>
|
||||
</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>
|
||||
{(() => {
|
||||
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 (
|
||||
<span
|
||||
className={`my-text-heading fw-semibold ping-stats-value ${formatted.class} ${
|
||||
|
|
Loading…
Reference in New Issue