الملفات
2026-07-27 00:05:12 +03:00

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>