mirror of https://github.com/yb/uptime-status
chore: update config values, fix script path, and enhance timeline styles
parent
51ed0aa1bc
commit
7dd042ef7c
|
@ -8,11 +8,12 @@ window.Config = {
|
|||
'm800673107-e0c2ebe9751e77346e8481a0', // Read-Only ключ
|
||||
'm800673135-585a7f95c55b61c43bc818b4', // Read-Only ключ
|
||||
'm800911467-ae3c9c2dc001bd9dc4a6bd1a', // Read-Only ключ
|
||||
'm801031885-db86f05252c99d9bc8d58a76', // Read-Only ключ
|
||||
// 'm800679644-4ee3480057a34ce157103cba', // Read-Only ключ
|
||||
],
|
||||
|
||||
// Количество дней в логах
|
||||
CountDays: 45,
|
||||
CountDays: 24,
|
||||
|
||||
// Показывать ли ссылки на проверяемые сайты
|
||||
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="./config.js"></script>
|
||||
<script src="/uptime/config.js"></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.1752084531934'
|
||||
const CACHE_NAME = 'uptime-status-v2.0.1.1753557287951'
|
||||
const CONFIG_FILE = '/config.js'
|
||||
|
||||
// Устанавливаем Service Worker
|
||||
|
|
190
src/app.scss
190
src/app.scss
|
@ -284,37 +284,131 @@
|
|||
}
|
||||
|
||||
.timeline {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
position: relative;
|
||||
height: 40px;
|
||||
padding: 4px;
|
||||
background-color: var(--my-dark-gray-color);
|
||||
border-radius: var(--bs-border-radius);
|
||||
overflow: hidden;
|
||||
padding: 12px 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
flex: 1;
|
||||
min-width: 3px;
|
||||
border-radius: 2px;
|
||||
.timeline-graph {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.timeline-segment {
|
||||
stroke-dasharray: 100;
|
||||
stroke-dashoffset: 100;
|
||||
animation: drawLine 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes drawLine {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-point {
|
||||
position: absolute;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.3s ease;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 2;
|
||||
border: 3px solid #fff;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.timeline-point-animate {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.3);
|
||||
animation: timelinePointAppear 0.4s ease-out forwards;
|
||||
}
|
||||
|
||||
.timeline-item:hover {
|
||||
transform: scaleY(1.1);
|
||||
@keyframes timelinePointAppear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0.3);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-item.ok {
|
||||
.timeline-point:hover {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.timeline-point:hover::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: pointPing 1.2s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.timeline-point.ok:hover::before {
|
||||
background-color: #1aad3a;
|
||||
box-shadow: 0 0 15px rgba(26, 173, 58, 0.4);
|
||||
}
|
||||
|
||||
.timeline-point.down:hover::before {
|
||||
background-color: #ea4e43;
|
||||
box-shadow: 0 0 15px rgba(234, 78, 67, 0.4);
|
||||
}
|
||||
|
||||
.timeline-point.none:hover::before {
|
||||
background-color: var(--my-alpha-gray-color);
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
@keyframes pointPing {
|
||||
0%, 100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.5);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-point.ok {
|
||||
background-color: #1aad3a;
|
||||
}
|
||||
|
||||
.timeline-item.down {
|
||||
.timeline-point.down {
|
||||
background-color: #ea4e43;
|
||||
}
|
||||
|
||||
.timeline-item.none {
|
||||
.timeline-point.none {
|
||||
background-color: var(--my-alpha-gray-color);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.timeline-point.latest-ok {
|
||||
box-shadow: 0 0 15px rgba(26, 173, 58, 0.6), 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.timeline-point.latest-down {
|
||||
box-shadow: 0 0 15px rgba(234, 78, 67, 0.6), 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.timeline-labels {
|
||||
|
@ -345,6 +439,70 @@
|
|||
// Tooltip стили
|
||||
.tooltip {
|
||||
font-size: 0.875rem;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
font-family: inherit !important;
|
||||
line-height: 1.4 !important;
|
||||
padding: 8px 12px !important;
|
||||
max-width: 250px !important;
|
||||
}
|
||||
|
||||
// Стилизация ReactTooltip
|
||||
.__react_component_tooltip {
|
||||
border-radius: 8px !important;
|
||||
background-color: var(--bs-dark) !important;
|
||||
color: var(--bs-light) !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
font-family: inherit !important;
|
||||
font-size: 0.875rem !important;
|
||||
line-height: 1.4 !important;
|
||||
padding: 8px 12px !important;
|
||||
max-width: 250px !important;
|
||||
opacity: 0.95 !important;
|
||||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
.__react_component_tooltip.type-dark {
|
||||
background-color: var(--bs-dark) !important;
|
||||
color: var(--bs-light) !important;
|
||||
}
|
||||
|
||||
.__react_component_tooltip.type-dark.place-top:after {
|
||||
border-top-color: var(--bs-dark) !important;
|
||||
}
|
||||
|
||||
.__react_component_tooltip.type-dark.place-bottom:after {
|
||||
border-bottom-color: var(--bs-dark) !important;
|
||||
}
|
||||
|
||||
.__react_component_tooltip.type-dark.place-left:after {
|
||||
border-left-color: var(--bs-dark) !important;
|
||||
}
|
||||
|
||||
.__react_component_tooltip.type-dark.place-right:after {
|
||||
border-right-color: var(--bs-dark) !important;
|
||||
}
|
||||
|
||||
// Поддержка темной темы для tooltip
|
||||
[data-bs-theme="dark"] .__react_component_tooltip {
|
||||
background-color: rgba(248, 249, 250, 0.95) !important;
|
||||
color: var(--bs-dark) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .__react_component_tooltip.type-dark.place-top:after {
|
||||
border-top-color: rgba(248, 249, 250, 0.95) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .__react_component_tooltip.type-dark.place-bottom:after {
|
||||
border-bottom-color: rgba(248, 249, 250, 0.95) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .__react_component_tooltip.type-dark.place-left:after {
|
||||
border-left-color: rgba(248, 249, 250, 0.95) !important;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .__react_component_tooltip.type-dark.place-right:after {
|
||||
border-right-color: rgba(248, 249, 250, 0.95) !important;
|
||||
}
|
||||
|
||||
// Логотипы для разных тем
|
||||
|
|
|
@ -8,6 +8,10 @@ import UptimeRobot from './uptimerobot'
|
|||
const AppContent = () => {
|
||||
const { t } = useLanguage()
|
||||
const apikeys = useMemo(() => {
|
||||
if (!window.Config) {
|
||||
console.error('window.Config не найден. Убедитесь, что config.js загружен.')
|
||||
return []
|
||||
}
|
||||
const { ApiKeys } = window.Config
|
||||
if (Array.isArray(ApiKeys)) return ApiKeys
|
||||
if (typeof ApiKeys === 'string') return [ApiKeys]
|
||||
|
|
|
@ -87,28 +87,123 @@ const UptimeRobot = ({ apikey }) => {
|
|||
|
||||
<div className="timeline-container mb-3">
|
||||
<div className="timeline">
|
||||
{site.daily
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((data, index) => {
|
||||
{(() => {
|
||||
// Создаем точки для SVG линии-графика
|
||||
const svgPoints = site.daily
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((data, index) => {
|
||||
let yPercent = 50 // По умолчанию по центру
|
||||
if (data.uptime >= 100) {
|
||||
yPercent = 35 // Выше центра для OK
|
||||
} else if (data.uptime <= 0 && data.down.times === 0) {
|
||||
yPercent = 50 // По центру для нет данных
|
||||
} else {
|
||||
yPercent = 65 // Ниже центра для DOWN
|
||||
}
|
||||
const xPercent = (index / (site.daily.length - 1)) * 100
|
||||
return {
|
||||
x: xPercent,
|
||||
y: yPercent,
|
||||
status: data.uptime >= 100 ? 'ok' : data.uptime <= 0 && data.down.times === 0 ? 'none' : 'down',
|
||||
}
|
||||
})
|
||||
|
||||
// Создаем сегменты линии с разными цветами
|
||||
const segments = svgPoints.slice(0, -1).map((point, index) => {
|
||||
const nextPoint = svgPoints[index + 1]
|
||||
|
||||
// Определяем цвет сегмента на основе статусов точек
|
||||
let segmentColor = 'var(--my-alpha-gray-color)'
|
||||
if (point.status === 'ok' && nextPoint.status === 'ok') {
|
||||
segmentColor = '#1aad3a'
|
||||
} else if (point.status === 'down' || nextPoint.status === 'down') {
|
||||
segmentColor = '#ea4e43'
|
||||
} else if (point.status === 'ok' || nextPoint.status === 'ok') {
|
||||
segmentColor = '#1aad3a'
|
||||
}
|
||||
|
||||
// Задержка для появления сегментов
|
||||
const segmentDelay = index * 50 + 100 // Линии появляются после точек
|
||||
|
||||
return (
|
||||
<path
|
||||
key={index}
|
||||
className="timeline-segment"
|
||||
d={`M ${point.x} ${point.y} L ${nextPoint.x} ${nextPoint.y}`}
|
||||
fill="none"
|
||||
stroke={segmentColor}
|
||||
strokeWidth="2"
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
style={{ animationDelay: `${segmentDelay}ms` }}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<svg className="timeline-graph" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
{segments}
|
||||
</svg>
|
||||
)
|
||||
})()}
|
||||
{(() => {
|
||||
// Находим индекс самой правой точки с данными
|
||||
const dataPoints = site.daily.slice().reverse()
|
||||
let latestDataIndex = -1
|
||||
for (let i = dataPoints.length - 1; i >= 0; i--) {
|
||||
if (!(dataPoints[i].uptime <= 0 && dataPoints[i].down.times === 0)) {
|
||||
latestDataIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return dataPoints.map((data, index) => {
|
||||
let statusClass = ''
|
||||
let text = data.date.format('DD.MM.YYYY ')
|
||||
let topPosition = '50%' // По умолчанию по центру
|
||||
|
||||
if (data.uptime >= 100) {
|
||||
statusClass = 'ok'
|
||||
topPosition = '35%' // Выше центра для OK
|
||||
text += `${t('availability')} ${formatNumber(data.uptime)}%`
|
||||
} else if (data.uptime <= 0 && data.down.times === 0) {
|
||||
statusClass = 'none'
|
||||
topPosition = '50%' // По центру для нет данных
|
||||
text += t('noData')
|
||||
} else {
|
||||
statusClass = 'down'
|
||||
topPosition = '65%' // Ниже центра для DOWN
|
||||
text += `Сбоев ${data.down.times}, суммарно ${formatDuration(data.down.duration)}, ${t(
|
||||
'availability'
|
||||
).toLowerCase()} ${formatNumber(data.uptime)}%`
|
||||
}
|
||||
|
||||
return <div key={index} className={`timeline-item ${statusClass}`} data-tip={text} title={text} />
|
||||
})}
|
||||
// Добавляем свечение для самой правой точки с данными
|
||||
if (index === latestDataIndex && statusClass !== 'none') {
|
||||
statusClass += ` latest-${statusClass}`
|
||||
}
|
||||
|
||||
const dayPosition = (index / (site.daily.length - 1)) * 100
|
||||
// Случайная задержка от 50мс до 200мс для каждой точки
|
||||
const randomDelay = 50 + Math.random() * 150
|
||||
const animationDelay = index * 30 + randomDelay // Базовая задержка + случайная
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`timeline-point ${statusClass} timeline-point-animate`}
|
||||
style={{
|
||||
left: `${dayPosition}%`,
|
||||
top: topPosition,
|
||||
animationDelay: `${animationDelay}ms`,
|
||||
}}
|
||||
data-tip={text}
|
||||
title={text}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})()}
|
||||
</div>
|
||||
<div className="timeline-labels d-flex justify-content-between mt-2">
|
||||
<small className="my-text-content">{t('daysAgo', { days: CountDays })}</small>
|
||||
|
@ -137,7 +232,7 @@ const UptimeRobot = ({ apikey }) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<ReactTooltip className="tooltip" place="top" type="dark" effect="solid" />
|
||||
<ReactTooltip className="tooltip" place="top" type="dark" effect="solid" multiline={true} delayShow={200} delayHide={100} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue