Redesign health service + create backup service

This commit is contained in:
2025-12-18 03:35:13 +07:00
parent e43e579329
commit 57bad3b4a8
16 changed files with 1486 additions and 98 deletions

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Status</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* {
margin: 0;
@@ -19,7 +20,7 @@
}
.container {
max-width: 900px;
max-width: 1100px;
margin: 0 auto;
padding: 40px 20px;
}
@@ -39,6 +40,13 @@
background-clip: text;
}
h2 {
font-size: 1.3rem;
font-weight: 600;
margin: 30px 0 16px 0;
color: #94a3b8;
}
.overall-status {
display: inline-flex;
align-items: center;
@@ -174,8 +182,9 @@
.service-metrics {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.metric {
@@ -212,6 +221,132 @@
color: #fca5a5;
}
/* Latency chart */
.latency-chart {
height: 60px;
margin-top: 12px;
}
/* SSL Card */
.ssl-card {
background: rgba(30, 41, 59, 0.5);
border: 1px solid rgba(100, 116, 139, 0.2);
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
}
.ssl-card.warning {
border-color: rgba(250, 204, 21, 0.3);
}
.ssl-card.danger {
border-color: rgba(239, 68, 68, 0.3);
}
.ssl-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.ssl-title {
font-size: 1.1rem;
font-weight: 600;
color: #f1f5f9;
}
.ssl-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
}
.ssl-badge.valid {
background: rgba(34, 197, 94, 0.15);
color: #22c55e;
}
.ssl-badge.expiring {
background: rgba(250, 204, 21, 0.15);
color: #facc15;
}
.ssl-badge.expired {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.ssl-info {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
}
/* Incidents */
.incidents-list {
background: rgba(30, 41, 59, 0.5);
border: 1px solid rgba(100, 116, 139, 0.2);
border-radius: 16px;
overflow: hidden;
}
.incident-item {
padding: 16px 20px;
border-bottom: 1px solid rgba(100, 116, 139, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.incident-item:last-child {
border-bottom: none;
}
.incident-info {
display: flex;
align-items: center;
gap: 12px;
}
.incident-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.incident-dot.resolved {
background: #22c55e;
}
.incident-dot.open {
background: #ef4444;
animation: pulse 2s infinite;
}
.incident-service {
font-weight: 500;
color: #f1f5f9;
}
.incident-message {
font-size: 0.85rem;
color: #94a3b8;
}
.incident-time {
font-size: 0.85rem;
color: #64748b;
}
.no-incidents {
padding: 30px;
text-align: center;
color: #64748b;
}
.refresh-btn {
display: inline-flex;
align-items: center;
@@ -292,8 +427,42 @@
</p>
</header>
{% if ssl_status %}
<div class="ssl-card {% if ssl_status.days_until_expiry <= 0 %}danger{% elif ssl_status.days_until_expiry <= 14 %}warning{% endif %}">
<div class="ssl-header">
<span class="ssl-title">SSL Certificate</span>
<span class="ssl-badge {% if ssl_status.days_until_expiry <= 0 %}expired{% elif ssl_status.days_until_expiry <= 14 %}expiring{% else %}valid{% endif %}">
{% if ssl_status.days_until_expiry <= 0 %}
Expired
{% elif ssl_status.days_until_expiry <= 14 %}
Expiring Soon
{% else %}
Valid
{% endif %}
</span>
</div>
<div class="ssl-info">
<div class="metric">
<div class="metric-label">Domain</div>
<div class="metric-value">{{ ssl_status.domain }}</div>
</div>
<div class="metric">
<div class="metric-label">Issuer</div>
<div class="metric-value">{{ ssl_status.issuer }}</div>
</div>
<div class="metric">
<div class="metric-label">Days Left</div>
<div class="metric-value {% if ssl_status.days_until_expiry <= 0 %}bad{% elif ssl_status.days_until_expiry <= 14 %}warning{% else %}good{% endif %}">
{{ ssl_status.days_until_expiry }}
</div>
</div>
</div>
</div>
{% endif %}
<div class="services-grid">
{% for name, service in services.items() %}
{% if service.status.value != 'unknown' or name != 'external' %}
<div class="service-card">
<div class="service-header">
<span class="service-name">{{ service.display_name }}</span>
@@ -322,7 +491,17 @@
</div>
</div>
<div class="metric">
<div class="metric-label">Uptime</div>
<div class="metric-label">Avg 24h</div>
<div class="metric-value {% if service.avg_latency_24h and service.avg_latency_24h < 200 %}good{% elif service.avg_latency_24h and service.avg_latency_24h < 500 %}warning{% elif service.avg_latency_24h %}bad{% endif %}">
{% if service.avg_latency_24h %}
{{ "%.0f"|format(service.avg_latency_24h) }} ms
{% else %}
{% endif %}
</div>
</div>
<div class="metric">
<div class="metric-label">Uptime 24h</div>
<div class="metric-value {% if service.uptime_percent >= 99 %}good{% elif service.uptime_percent >= 95 %}warning{% else %}bad{% endif %}">
{{ "%.1f"|format(service.uptime_percent) }}%
</div>
@@ -333,20 +512,49 @@
<div class="metric-value">{{ service.version }}</div>
</div>
{% endif %}
{% if service.last_incident %}
<div class="metric">
<div class="metric-label">Last Incident</div>
<div class="metric-value warning">{{ service.last_incident.strftime('%d.%m %H:%M') }}</div>
</div>
{% endif %}
</div>
{% if service.latency_history and service.latency_history|length > 1 %}
<div class="latency-chart">
<canvas id="chart-{{ name }}"></canvas>
</div>
{% endif %}
{% if service.message %}
<div class="service-message">{{ service.message }}</div>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
<h2>Recent Incidents</h2>
<div class="incidents-list">
{% if incidents and incidents|length > 0 %}
{% for incident in incidents %}
<div class="incident-item">
<div class="incident-info">
<span class="incident-dot {% if incident.resolved_at %}resolved{% else %}open{% endif %}"></span>
<div>
<div class="incident-service">{{ incident.service_name | title }}</div>
<div class="incident-message">{{ incident.message or 'Service unavailable' }}</div>
</div>
</div>
<div class="incident-time">
{{ incident.started_at[:16].replace('T', ' ') }}
{% if incident.resolved_at %}
- Resolved
{% else %}
- Ongoing
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<div class="no-incidents">
No recent incidents
</div>
{% endif %}
</div>
<center>
<button class="refresh-btn" onclick="refreshStatus(this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -363,6 +571,55 @@
</div>
<script>
// Initialize latency charts
{% for name, service in services.items() %}
{% if service.latency_history and service.latency_history|length > 1 %}
(function() {
const ctx = document.getElementById('chart-{{ name }}').getContext('2d');
const data = {{ service.latency_history | tojson }};
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(d => ''),
datasets: [{
data: data.map(d => d.latency_ms),
borderColor: '#00d4ff',
backgroundColor: 'rgba(0, 212, 255, 0.1)',
fill: true,
tension: 0.4,
pointRadius: 0,
borderWidth: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (ctx) => ctx.raw.toFixed(0) + ' ms'
}
}
},
scales: {
x: { display: false },
y: {
display: false,
beginAtZero: true
}
},
interaction: {
intersect: false,
mode: 'index'
}
}
});
})();
{% endif %}
{% endfor %}
async function refreshStatus(btn) {
btn.classList.add('loading');
btn.disabled = true;