الملفات
ghaymah-exam-ahmed-abdelazi…/q1-deploy-monitor/dashboard.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>