412 أسطر
14 KiB
HTML
412 أسطر
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ar" dir="rtl">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Ghaymah SRE - لوحة المراقبة</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=Tajawal:wght@400;500;700;800&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bg-dark: #0b0f19;
|
|
--card-bg: rgba(18, 26, 44, 0.75);
|
|
--card-border: rgba(255, 255, 255, 0.08);
|
|
--primary: #3b82f6;
|
|
--primary-glow: rgba(59, 130, 246, 0.4);
|
|
--success: #10b981;
|
|
--success-glow: rgba(16, 185, 129, 0.4);
|
|
--danger: #ef4444;
|
|
--danger-glow: rgba(239, 68, 68, 0.4);
|
|
--text-main: #f3f4f6;
|
|
--text-muted: #9ca3af;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Tajawal', 'Outfit', sans-serif;
|
|
background-color: var(--bg-dark);
|
|
background-image:
|
|
radial-gradient(circle at 15% 20%, rgba(59, 130, 246, 0.12) 0%, transparent 40%),
|
|
radial-gradient(circle at 85% 80%, rgba(16, 185, 129, 0.08) 0%, transparent 40%);
|
|
color: var(--text-main);
|
|
min-height: 100vh;
|
|
padding: 2rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.container {
|
|
width: 100%;
|
|
max-width: 1100px;
|
|
}
|
|
|
|
header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 2.5rem;
|
|
padding-bottom: 1.5rem;
|
|
border-bottom: 1px solid var(--card-border);
|
|
}
|
|
|
|
.title-group h1 {
|
|
font-size: 1.8rem;
|
|
font-weight: 800;
|
|
background: linear-gradient(135deg, #ffffff 0%, #9ca3af 100%);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.title-group p {
|
|
color: var(--text-muted);
|
|
font-size: 0.95rem;
|
|
margin-top: 0.3rem;
|
|
}
|
|
|
|
.live-badge {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
background: rgba(16, 185, 129, 0.1);
|
|
border: 1px solid rgba(16, 185, 129, 0.3);
|
|
padding: 0.4rem 0.9rem;
|
|
border-radius: 9999px;
|
|
font-size: 0.85rem;
|
|
color: var(--success);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.pulse-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
background-color: var(--success);
|
|
border-radius: 50%;
|
|
box-shadow: 0 0 10px var(--success);
|
|
animation: pulse 1.8s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
|
|
70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
|
|
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
|
}
|
|
|
|
/* Grid Layout */
|
|
.metrics-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 2.5rem;
|
|
}
|
|
|
|
.card {
|
|
background: var(--card-bg);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 1.25rem;
|
|
padding: 1.5rem;
|
|
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
|
transition: transform 0.3s ease, border-color 0.3s ease;
|
|
}
|
|
|
|
.card:hover {
|
|
transform: translateY(-4px);
|
|
border-color: rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
color: var(--text-muted);
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.card-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 0.6rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.2rem;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
.card-value {
|
|
font-size: 2.2rem;
|
|
font-weight: 700;
|
|
color: #ffffff;
|
|
font-family: 'Outfit', sans-serif;
|
|
letter-spacing: -0.5px;
|
|
}
|
|
|
|
.card-subtext {
|
|
font-size: 0.85rem;
|
|
color: var(--text-muted);
|
|
margin-top: 0.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.3rem;
|
|
}
|
|
|
|
/* Status Badge */
|
|
.status-tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.35rem 0.9rem;
|
|
border-radius: 0.6rem;
|
|
font-weight: 700;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.status-healthy {
|
|
background: rgba(16, 185, 129, 0.15);
|
|
color: var(--success);
|
|
border: 1px solid rgba(16, 185, 129, 0.4);
|
|
box-shadow: 0 0 15px var(--success-glow);
|
|
}
|
|
|
|
.status-down {
|
|
background: rgba(239, 68, 68, 0.15);
|
|
color: var(--danger);
|
|
border: 1px solid rgba(239, 68, 68, 0.4);
|
|
box-shadow: 0 0 15px var(--danger-glow);
|
|
}
|
|
|
|
/* Activity Table */
|
|
.logs-section {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 1.25rem;
|
|
padding: 1.5rem;
|
|
backdrop-filter: blur(12px);
|
|
}
|
|
|
|
.logs-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.logs-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
text-align: right;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.logs-table th {
|
|
color: var(--text-muted);
|
|
font-weight: 600;
|
|
padding: 0.75rem 1rem;
|
|
border-bottom: 1px solid var(--card-border);
|
|
}
|
|
|
|
.logs-table td {
|
|
padding: 0.85rem 1rem;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
|
color: var(--text-main);
|
|
font-family: 'Outfit', 'Tajawal', sans-serif;
|
|
}
|
|
|
|
.badge-pill {
|
|
padding: 0.2rem 0.6rem;
|
|
border-radius: 0.4rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.pill-200 { background: rgba(16, 185, 129, 0.2); color: var(--success); }
|
|
.pill-err { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<!-- Header -->
|
|
<header>
|
|
<div class="title-group">
|
|
<h1>🌩️ Ghaymah SRE Monitor</h1>
|
|
<p>مراقبة أداء واستجابة التطبيق المباشرة (Real-time Health Dashboard)</p>
|
|
</div>
|
|
<div class="live-badge">
|
|
<span class="pulse-dot"></span>
|
|
<span id="lastUpdatedText">مباشر (تحديث كل 3 ثوانٍ)</span>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Metrics Cards -->
|
|
<div class="metrics-grid">
|
|
<!-- 1. Status Card -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<span>حالة النظام (Status)</span>
|
|
<div class="card-icon">⚡</div>
|
|
</div>
|
|
<div id="statusBadgeContainer">
|
|
<span id="statusTag" class="status-tag status-healthy">● متصل (Healthy)</span>
|
|
</div>
|
|
<div class="card-subtext" id="httpCodeSubtext">كود الاستجابة: HTTP 200 OK</div>
|
|
</div>
|
|
|
|
<!-- 2. Latency Card -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<span>زمن الاستجابة (Latency)</span>
|
|
<div class="card-icon">⏱️</div>
|
|
</div>
|
|
<div class="card-value"><span id="latencyValue">--</span> <small style="font-size: 1rem; font-weight: 400; color: var(--text-muted);">ms</small></div>
|
|
<div class="card-subtext">
|
|
<span>متوسط الاستجابة:</span>
|
|
<strong id="avgLatencyValue" style="color: var(--primary);">-- ms</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 3. Total Requests Card -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<span>إجمالي الطلبات (Total Requests)</span>
|
|
<div class="card-icon">📊</div>
|
|
</div>
|
|
<div class="card-value" id="totalRequestsValue">--</div>
|
|
<div class="card-subtext">
|
|
<span>مدة التشغيل (Uptime):</span>
|
|
<strong id="uptimeValue" style="color: #ffffff;">-- ثانية</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Check Logs -->
|
|
<div class="logs-section">
|
|
<div class="logs-header">
|
|
<h3>سجل الفحوصات الأخيرة (Health Check Logs)</h3>
|
|
<span style="font-size: 0.85rem; color: var(--text-muted);" id="apiTargetUrl">الهدف: http://localhost:8080/metrics</span>
|
|
</div>
|
|
<table class="logs-table">
|
|
<thead>
|
|
<tr>
|
|
<th>الوقت (UTC)</th>
|
|
<th>الحالة</th>
|
|
<th>كود HTTP</th>
|
|
<th>زمن الاستجابة</th>
|
|
<th>إجمالي الطلبات</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="logsTableBody">
|
|
<tr>
|
|
<td colspan="5" style="text-align: center; color: var(--text-muted); padding: 1.5rem;">جاري جلب البيانات...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const API_METRICS_URL = 'http://localhost:8080/metrics';
|
|
const FALLBACK_STATUS_URL = 'status.json';
|
|
const logsHistory = [];
|
|
|
|
async function fetchMetrics() {
|
|
let data = null;
|
|
let source = 'API';
|
|
|
|
try {
|
|
// Try fetching direct API endpoint
|
|
const response = await fetch(API_METRICS_URL, { cache: 'no-store' });
|
|
if (response.ok) {
|
|
data = await response.json();
|
|
}
|
|
} catch (err) {
|
|
// Fallback to status.json if direct API fails or CORS blocked
|
|
try {
|
|
const statusRes = await fetch(FALLBACK_STATUS_URL, { cache: 'no-store' });
|
|
if (statusRes.ok) {
|
|
data = await statusRes.json();
|
|
source = 'status.json';
|
|
}
|
|
} catch (e) {
|
|
data = null;
|
|
}
|
|
}
|
|
|
|
updateDashboardUI(data);
|
|
}
|
|
|
|
function updateDashboardUI(data) {
|
|
const statusTag = document.getElementById('statusTag');
|
|
const httpCodeSubtext = document.getElementById('httpCodeSubtext');
|
|
const latencyValue = document.getElementById('latencyValue');
|
|
const avgLatencyValue = document.getElementById('avgLatencyValue');
|
|
const totalRequestsValue = document.getElementById('totalRequestsValue');
|
|
const uptimeValue = document.getElementById('uptimeValue');
|
|
|
|
const nowIso = new Date().toLocaleTimeString('ar-SA');
|
|
|
|
if (data && (data.status === 'healthy' || data.status === 'ok')) {
|
|
statusTag.className = 'status-tag status-healthy';
|
|
statusTag.innerHTML = '● متصل (Healthy)';
|
|
httpCodeSubtext.textContent = 'كود الاستجابة: HTTP 200 OK';
|
|
|
|
const latency = data.last_request_latency_ms || data.check_latency_ms || 0;
|
|
latencyValue.textContent = latency;
|
|
avgLatencyValue.textContent = (data.average_latency_ms || 0) + ' ms';
|
|
totalRequestsValue.textContent = (data.total_requests || 0).toLocaleString();
|
|
uptimeValue.textContent = (data.uptime_seconds || 0) + ' ثانية';
|
|
|
|
addLogEntry(nowIso, 'Healthy', 200, latency + ' ms', data.total_requests || 0);
|
|
} else {
|
|
statusTag.className = 'status-tag status-down';
|
|
statusTag.innerHTML = '✖ غير متصل (Down)';
|
|
httpCodeSubtext.textContent = 'كود الاستجابة: HTTP 500 / Connection Error';
|
|
|
|
latencyValue.textContent = '--';
|
|
avgLatencyValue.textContent = '-- ms';
|
|
totalRequestsValue.textContent = '--';
|
|
uptimeValue.textContent = '0 ثانية';
|
|
|
|
addLogEntry(nowIso, 'Down', 500, '--', '--');
|
|
}
|
|
}
|
|
|
|
function addLogEntry(time, status, code, latency, reqCount) {
|
|
logsHistory.unshift({ time, status, code, latency, reqCount });
|
|
if (logsHistory.length > 7) logsHistory.pop();
|
|
|
|
const tbody = document.getElementById('logsTableBody');
|
|
tbody.innerHTML = logsHistory.map(entry => `
|
|
<tr>
|
|
<td>${entry.time}</td>
|
|
<td>
|
|
<span class="badge-pill ${entry.status === 'Healthy' ? 'pill-200' : 'pill-err'}">
|
|
${entry.status}
|
|
</span>
|
|
</td>
|
|
<td>HTTP ${entry.code}</td>
|
|
<td>${entry.latency}</td>
|
|
<td>${entry.reqCount}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
// Initial fetch & set interval (3 seconds)
|
|
fetchMetrics();
|
|
setInterval(fetchMetrics, 3000);
|
|
</script>
|
|
</body>
|
|
</html>
|