الملفات
2026-07-28 03:54:42 +03:00

165 أسطر
6.7 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>لوحة مراقبة mithal.space</title>
<style>
:root{
--bg:#0b0f0d; --panel:#101512; --line:#1e2a24;
--green:#5fd88a; --red:#ff6b6b; --amber:#ffb454;
--text:#d7e0da; --dim:#7c8f85;
--mono:"IBM Plex Mono","SFMono-Regular",Consolas,monospace;
}
*{box-sizing:border-box;}
body{margin:0;background:radial-gradient(circle at 20% -10%, #142019 0%, transparent 60%), var(--bg);
color:var(--text);font-family:var(--mono);padding:28px 20px 60px;direction:rtl;}
.wrap{max-width:1040px;margin:0 auto;}
header{border-bottom:1px solid var(--line);padding-bottom:16px;margin-bottom:22px;}
h1{font-size:20px;margin:0;font-weight:600;}
h1 span{color:var(--green);}
.sub{color:var(--dim);font-size:12px;margin-top:4px;}
.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:20px;}
@media(max-width:820px){.grid{grid-template-columns:repeat(2,1fr);}}
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:16px;}
.card .label{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:1px;}
.card .value{font-size:24px;font-weight:700;margin-top:8px;}
.ok{color:var(--green);} .bad{color:var(--red);} .warn{color:var(--amber);}
.panel{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:18px;margin-bottom:16px;}
.panel h2{font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:1px;margin:0 0 14px;}
#chart{width:100%;height:160px;display:block;}
table{width:100%;border-collapse:collapse;font-size:12px;}
th,td{text-align:right;padding:8px 6px;border-bottom:1px solid var(--line);}
th{color:var(--dim);font-weight:500;}
.pill{padding:2px 8px;border-radius:20px;font-size:11px;font-weight:600;}
.pill.up{background:rgba(95,216,138,.12);color:var(--green);}
.pill.down{background:rgba(255,107,107,.12);color:var(--red);}
.note{color:var(--dim);font-size:11px;margin-top:6px;}
footer{color:var(--dim);font-size:11px;text-align:center;margin-top:30px;}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>لوحة مراقبة <span>mithal.space</span></h1>
<div class="sub">Ghaymah SRE Exam — Q5 · يقرأ بيانات من metrics.json (ينتجها monitor.py كل دقيقة)</div>
</header>
<div class="grid">
<div class="card">
<div class="label">Uptime (24 ساعة)</div>
<div class="value" id="uptimeValue"></div>
</div>
<div class="card">
<div class="label">آخر زمن استجابة</div>
<div class="value" id="latencyValue"></div>
</div>
<div class="card">
<div class="label">شهادة SSL</div>
<div class="value" id="sslValue"></div>
</div>
<div class="card">
<div class="label">زمن DNS</div>
<div class="value" id="dnsValue"></div>
</div>
</div>
<div class="panel">
<h2>زمن الاستجابة — آخر ساعة</h2>
<svg id="chart" viewBox="0 0 1000 160" preserveAspectRatio="none"></svg>
<div class="note">كل نقطة = فحص واحد (كل دقيقة تقريبًا)</div>
</div>
<div class="panel">
<h2>سجل آخر 10 فحوصات</h2>
<table>
<thead><tr><th>الوقت</th><th>الحالة</th><th>Status Code</th><th>زمن الاستجابة</th><th>DNS</th><th>Search</th></tr></thead>
<tbody id="logBody"><tr><td colspan="6">جاري تحميل البيانات من metrics.json...</td></tr></tbody>
</table>
</div>
<footer>مصدر البيانات: metrics.json الناتج عن monitor.py (نفس المجلد) · حدّث الصفحة لتحديث البيانات، أو استضف monitor.py على Ghaymah كمهمة مجدولة (cron/scheduled task)</footer>
</div>
<script>
async function loadData(){
let data = [];
try{
const res = await fetch('./metrics.json', {cache:'no-store'});
data = await res.json();
}catch(e){
document.getElementById('logBody').innerHTML =
'<tr><td colspan="6">تعذر تحميل metrics.json — تأكد أن monitor.py يعمل وأنه بجانب هذا الملف على نفس الاستضافة.</td></tr>';
return;
}
if(!data.length) return;
// Uptime % over last 24h (or all available data if less)
const last24h = data.slice(-1440);
const upCount = last24h.filter(d => d.up).length;
const uptimePct = ((upCount / last24h.length) * 100).toFixed(2);
const uptimeEl = document.getElementById('uptimeValue');
uptimeEl.textContent = uptimePct + '%';
uptimeEl.className = 'value ' + (uptimePct >= 99.9 ? 'ok' : uptimePct >= 99 ? 'warn' : 'bad');
const latest = data[data.length - 1];
const latencyEl = document.getElementById('latencyValue');
latencyEl.textContent = (latest.latency_ms ?? '—') + ' ms';
latencyEl.className = 'value ' + (latest.up ? 'ok' : 'bad');
const sslEl = document.getElementById('sslValue');
if(latest.ssl_valid){
sslEl.textContent = latest.ssl_days_remaining + ' يوم متبقي';
sslEl.className = 'value ' + (latest.ssl_days_remaining < 14 ? 'warn' : 'ok');
} else {
sslEl.textContent = 'غير صالحة';
sslEl.className = 'value bad';
}
document.getElementById('dnsValue').textContent = (latest.dns_ms ?? '—') + ' ms';
drawChart(data.slice(-60));
drawLog(data.slice(-10).reverse());
}
function drawChart(points){
const svg = document.getElementById('chart');
if(points.length < 2){ svg.innerHTML=''; return; }
const w = 1000, h = 160, pad = 10;
const vals = points.map(p => p.latency_ms || 0);
const maxMs = Math.max(...vals, 50);
const step = (w - pad*2) / (points.length - 1);
const line = points.map((p,i) => {
const x = pad + i*step;
const y = h - pad - (((p.latency_ms||0) / maxMs) * (h - pad*2));
return `${x},${y}`;
}).join(' ');
svg.innerHTML = `
<polyline fill="none" stroke="#5fd88a" stroke-width="2" points="${line}" />
${points.map((p,i)=>{
const x = pad + i*step;
const y = h - pad - (((p.latency_ms||0) / maxMs) * (h - pad*2));
return `<circle cx="${x}" cy="${y}" r="2.5" fill="${p.up?'#5fd88a':'#ff6b6b'}" />`;
}).join('')}
`;
}
function drawLog(rows){
document.getElementById('logBody').innerHTML = rows.map(r => `
<tr>
<td>${new Date(r.timestamp).toLocaleTimeString('ar-EG')}</td>
<td><span class="pill ${r.up?'up':'down'}">${r.up?'UP':'DOWN'}</span></td>
<td>${r.status_code ?? '—'}</td>
<td>${r.latency_ms ?? '—'} ms</td>
<td>${r.dns_ms ?? '—'} ms</td>
<td>${r.search_response_ms ?? '—'} ms</td>
</tr>
`).join('');
}
loadData();
setInterval(loadData, 60000);
</script>
</body>
</html>