244 أسطر
8.8 KiB
HTML
244 أسطر
8.8 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:#0a0e14;
|
|
--panel:#10161f;
|
|
--border:#1c2530;
|
|
--text:#dde5ee;
|
|
--muted:#7b8a9e;
|
|
--ok:#2fd480;
|
|
--bad:#ef4f60;
|
|
--warn:#f2b632;
|
|
--accent:#5aa9ff;
|
|
--mono:'SFMono-Regular',Consolas,'Liberation Mono',Menlo,monospace;
|
|
}
|
|
*{box-sizing:border-box;}
|
|
body{
|
|
margin:0;background:var(--bg);color:var(--text);font-family:var(--mono);
|
|
padding:26px 18px 50px;
|
|
}
|
|
.wrap{max-width:1000px;margin:0 auto;}
|
|
header{display:flex;justify-content:space-between;align-items:flex-end;flex-wrap:wrap;gap:10px;margin-bottom:20px;}
|
|
h1{font-size:1.2rem;margin:0;}
|
|
.target{color:var(--muted);font-size:.78rem;margin-top:4px;}
|
|
.refresh{color:var(--muted);font-size:.72rem;}
|
|
|
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-bottom:14px;}
|
|
.card{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:16px 18px;}
|
|
.label{color:var(--muted);font-size:.7rem;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;}
|
|
|
|
/* Uptime gauge */
|
|
.gauge-card{display:flex;align-items:center;gap:16px;}
|
|
.gauge{width:82px;height:82px;flex:0 0 auto;}
|
|
.gauge-value{font-size:1.6rem;font-weight:700;}
|
|
.gauge-sub{color:var(--muted);font-size:.72rem;}
|
|
|
|
.ssl-value{font-size:1.5rem;font-weight:700;}
|
|
.ssl-badge{display:inline-block;padding:2px 8px;border-radius:5px;font-size:.68rem;margin-top:6px;}
|
|
.ssl-badge.good{background:rgba(47,212,128,.15);color:var(--ok);}
|
|
.ssl-badge.warn{background:rgba(242,182,50,.15);color:var(--warn);}
|
|
.ssl-badge.bad{background:rgba(239,79,96,.15);color:var(--bad);}
|
|
|
|
.stat-row{display:flex;justify-content:space-between;font-size:.78rem;padding:4px 0;color:var(--muted);}
|
|
.stat-row b{color:var(--text);}
|
|
|
|
.chart-card canvas{width:100%;height:160px;display:block;}
|
|
|
|
table{width:100%;border-collapse:collapse;font-size:.76rem;}
|
|
th{text-align:right;color:var(--muted);font-weight:600;padding:8px 6px;border-bottom:1px solid var(--border);}
|
|
td{padding:7px 6px;border-bottom:1px dashed var(--border);}
|
|
.pill{display:inline-block;width:8px;height:8px;border-radius:50%;margin-left:6px;}
|
|
.pill.up{background:var(--ok);}
|
|
.pill.down{background:var(--bad);}
|
|
|
|
footer{text-align:center;color:var(--muted);font-size:.68rem;margin-top:18px;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="wrap">
|
|
<header>
|
|
<div>
|
|
<h1>لوحة مراقبة mithal.space</h1>
|
|
<div class="target">Latency · Uptime · SSL · DNS · Search Response</div>
|
|
</div>
|
|
<div class="refresh">آخر تحديث: <span id="lastUpdate">--</span></div>
|
|
</header>
|
|
|
|
<div class="grid">
|
|
<div class="card gauge-card">
|
|
<svg class="gauge" viewBox="0 0 42 42">
|
|
<circle cx="21" cy="21" r="18" fill="none" stroke="#1c2530" stroke-width="4"></circle>
|
|
<circle id="uptimeArc" cx="21" cy="21" r="18" fill="none" stroke="var(--ok)" stroke-width="4"
|
|
stroke-dasharray="0 113" stroke-linecap="round" transform="rotate(-90 21 21)"></circle>
|
|
</svg>
|
|
<div>
|
|
<div class="label">Uptime (24 ساعة)</div>
|
|
<div class="gauge-value" id="uptimeValue">--%</div>
|
|
<div class="gauge-sub" id="uptimeChecks">-- فحص</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="label">شهادة SSL</div>
|
|
<div class="ssl-value" id="sslDays">-- يوم</div>
|
|
<div id="sslBadge" class="ssl-badge">--</div>
|
|
<div class="stat-row"><span>تنتهي في</span><b id="sslExpiry">--</b></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="label">آخر قياس</div>
|
|
<div class="stat-row"><span>Latency</span><b id="latestLatency">--</b></div>
|
|
<div class="stat-row"><span>DNS</span><b id="latestDns">--</b></div>
|
|
<div class="stat-row"><span>Search</span><b id="latestSearch">--</b></div>
|
|
<div class="stat-row"><span>HTTP Status</span><b id="latestStatus">--</b></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card chart-card" style="margin-bottom:14px;">
|
|
<div class="label">زمن الاستجابة (آخر ساعة)</div>
|
|
<canvas id="latencyChart"></canvas>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="label">آخر 10 فحوصات</div>
|
|
<table>
|
|
<thead>
|
|
<tr><th>الوقت</th><th>الحالة</th><th>Latency</th><th>DNS</th><th>Search</th><th>SSL (أيام)</th></tr>
|
|
</thead>
|
|
<tbody id="logBody"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<footer>يقرأ من /api/metrics · يُحدَّث كل 30 ثانية</footer>
|
|
</div>
|
|
|
|
<script>
|
|
const API = '/api/metrics';
|
|
const POLL_MS = 30000;
|
|
|
|
function fmtMs(v){ return (v == null) ? '--' : Math.round(v) + ' ms'; }
|
|
function fmtTime(iso){
|
|
if(!iso) return '--';
|
|
const d = new Date(iso);
|
|
return d.toLocaleTimeString('ar-EG', {hour:'2-digit', minute:'2-digit', second:'2-digit'});
|
|
}
|
|
|
|
function setUptimeGauge(percent){
|
|
const circumference = 2 * Math.PI * 18; // ~113
|
|
const arc = document.getElementById('uptimeArc');
|
|
const p = percent == null ? 0 : percent;
|
|
const filled = (p / 100) * circumference;
|
|
arc.setAttribute('stroke-dasharray', `${filled} ${circumference}`);
|
|
arc.style.stroke = p >= 99 ? 'var(--ok)' : (p >= 95 ? 'var(--warn)' : 'var(--bad)');
|
|
}
|
|
|
|
function drawLatencyChart(series){
|
|
const canvas = document.getElementById('latencyChart');
|
|
const ctx = canvas.getContext('2d');
|
|
const dpr = window.devicePixelRatio || 1;
|
|
const w = canvas.clientWidth, h = canvas.clientHeight;
|
|
canvas.width = w * dpr; canvas.height = h * dpr;
|
|
ctx.scale(dpr, dpr);
|
|
ctx.clearRect(0,0,w,h);
|
|
|
|
if(!series || series.length < 2){
|
|
ctx.fillStyle = '#3a4a5c';
|
|
ctx.font = '12px monospace';
|
|
ctx.fillText('بيانات غير كافية بعد...', 10, h/2);
|
|
return;
|
|
}
|
|
|
|
const values = series.map(p => p.latency_ms);
|
|
const max = Math.max(...values, 10) * 1.2;
|
|
const stepX = w / (series.length - 1);
|
|
|
|
ctx.strokeStyle = '#5aa9ff';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
values.forEach((v,i) => {
|
|
const x = i * stepX;
|
|
const y = h - (v / max) * (h - 10) - 5;
|
|
i === 0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
|
|
});
|
|
ctx.stroke();
|
|
|
|
ctx.lineTo((values.length-1)*stepX, h);
|
|
ctx.lineTo(0, h);
|
|
ctx.closePath();
|
|
ctx.fillStyle = 'rgba(90,169,255,0.12)';
|
|
ctx.fill();
|
|
}
|
|
|
|
function renderSSL(ssl){
|
|
const badge = document.getElementById('sslBadge');
|
|
const days = ssl.days_remaining;
|
|
document.getElementById('sslDays').textContent = (days == null) ? '--' : `${days} يوم`;
|
|
document.getElementById('sslExpiry').textContent = ssl.expires_at ? new Date(ssl.expires_at).toLocaleDateString('ar-EG') : '--';
|
|
|
|
if(ssl.valid === false){
|
|
badge.className = 'ssl-badge bad'; badge.textContent = 'غير صالحة';
|
|
} else if(days == null){
|
|
badge.className = 'ssl-badge'; badge.textContent = 'بلا بيانات';
|
|
} else if(days > 14){
|
|
badge.className = 'ssl-badge good'; badge.textContent = 'صالحة';
|
|
} else if(days > 3){
|
|
badge.className = 'ssl-badge warn'; badge.textContent = 'تقترب من الانتهاء';
|
|
} else {
|
|
badge.className = 'ssl-badge bad'; badge.textContent = 'خطر - قرب الانتهاء';
|
|
}
|
|
}
|
|
|
|
function renderLog(checks){
|
|
const body = document.getElementById('logBody');
|
|
body.innerHTML = '';
|
|
checks.forEach(c => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${fmtTime(c.timestamp)}</td>
|
|
<td><span class="pill ${c.up ? 'up' : 'down'}"></span>${c.up ? 'يعمل' : 'متوقف'} (${c.status_code ?? '--'})</td>
|
|
<td>${fmtMs(c.latency_ms)}</td>
|
|
<td>${fmtMs(c.dns_ms)}</td>
|
|
<td>${fmtMs(c.search_response_ms)}</td>
|
|
<td>${c.ssl_days_remaining ?? '--'}</td>
|
|
`;
|
|
body.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
async function poll(){
|
|
try{
|
|
const res = await fetch(API, {cache:'no-store'});
|
|
const data = await res.json();
|
|
|
|
document.getElementById('uptimeValue').textContent = (data.uptime_24h_percent ?? '--') + '%';
|
|
document.getElementById('uptimeChecks').textContent = `${data.checks_in_24h} فحص`;
|
|
setUptimeGauge(data.uptime_24h_percent);
|
|
|
|
renderSSL(data.ssl || {});
|
|
drawLatencyChart(data.latency_series);
|
|
renderLog(data.last_checks || []);
|
|
|
|
const latest = data.latest || {};
|
|
document.getElementById('latestLatency').textContent = fmtMs(latest.latency_ms);
|
|
document.getElementById('latestDns').textContent = fmtMs(latest.dns_ms);
|
|
document.getElementById('latestSearch').textContent = fmtMs(latest.search_response_ms);
|
|
document.getElementById('latestStatus').textContent = latest.status_code ?? '--';
|
|
|
|
document.getElementById('lastUpdate').textContent = fmtTime(data.generated_at);
|
|
}catch(err){
|
|
document.getElementById('lastUpdate').textContent = 'خطأ في الاتصال';
|
|
}
|
|
}
|
|
|
|
poll();
|
|
setInterval(poll, POLL_MS);
|
|
window.addEventListener('resize', () => poll());
|
|
</script>
|
|
</body>
|
|
</html>
|