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

pull/62/head
SawGoD 2025-07-27 23:27:25 +03:00
parent c85d87b892
commit 169b59a8e0
12 changed files with 168 additions and 124 deletions

View File

@ -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

View File

@ -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";
}

View File

@ -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,

View File

@ -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">

View File

@ -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

82
rebuild_new.sh Normal file
View File

@ -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}"

View File

@ -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)

View File

@ -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: 'Измерение пинга...',
},
}

View File

@ -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 {
// Игнорируем ошибки

View File

@ -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 />
</>
)
}

View File

@ -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

View File

@ -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} ${