الملفات
ghaymah-exam-amirkasseb-sre/q5-mithal-monitor/index.html
2026-07-26 19:57:48 +03:00

386 أسطر
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>mithal.space — لوحة المراقبة</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.4/chart.umd.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root{
--bg: #0a0e17;
--panel: #121826;
--panel-border: #202838;
--text: #e7ebf3;
--muted: #7c8aa5;
--ok: #34d399;
--warn: #f2b84b;
--danger: #ef5a6f;
--accent: #4fd1ff;
--mono: 'JetBrains Mono', monospace;
--sans: 'Inter', sans-serif;
}
*{ box-sizing:border-box; }
body{
margin:0;
background: radial-gradient(1200px 600px at 80% -10%, #12203a 0%, var(--bg) 55%);
color: var(--text);
font-family: var(--sans);
min-height:100vh;
padding: 28px 20px 60px;
}
header{
display:flex; align-items:center; justify-content:space-between;
max-width:1180px; margin:0 auto 24px; flex-wrap:wrap; gap:12px;
}
.brand{ display:flex; align-items:center; gap:12px; }
.pulse-dot{
width:14px; height:14px; border-radius:50%;
background: var(--ok);
box-shadow: 0 0 0 0 rgba(52,211,153,0.6);
animation: pulse 1.8s infinite;
}
.pulse-dot.down{ background: var(--danger); box-shadow:0 0 0 0 rgba(239,90,111,0.6); animation: pulse-down 1.8s infinite; }
@keyframes pulse{
0%{ box-shadow: 0 0 0 0 rgba(52,211,153,0.55); }
70%{ box-shadow: 0 0 0 12px rgba(52,211,153,0); }
100%{ box-shadow: 0 0 0 0 rgba(52,211,153,0); }
}
@keyframes pulse-down{
0%{ box-shadow: 0 0 0 0 rgba(239,90,111,0.55); }
70%{ box-shadow: 0 0 0 12px rgba(239,90,111,0); }
100%{ box-shadow: 0 0 0 0 rgba(239,90,111,0); }
}
h1{ font-size:20px; margin:0; letter-spacing:0.2px; }
h1 span{ color: var(--muted); font-weight:400; font-size:14px; display:block; font-family: var(--mono); margin-top:2px; }
.controls{ display:flex; gap:8px; align-items:center; }
.controls label{
font-family: var(--mono); font-size:12px; color:var(--muted);
background: var(--panel); border:1px solid var(--panel-border);
padding:8px 12px; border-radius:8px; cursor:pointer;
transition: all 0.2s ease;
}
.controls label:hover{ background: #1a2234; color: var(--text); }
.controls input[type=file]{ display:none; }
.last-updated{ font-family: var(--mono); font-size:12px; color:var(--muted); }
main{ max-width:1180px; margin:0 auto; display:grid; gap:16px; grid-template-columns: repeat(12,1fr); }
.card{
background: var(--panel); border:1px solid var(--panel-border);
border-radius:14px; padding:18px 20px;
}
.card h2{
margin:0 0 10px; font-size:12px; text-transform:uppercase; letter-spacing:1px;
color: var(--muted); font-weight:600;
}
.metric{ font-family: var(--mono); font-size:34px; font-weight:700; line-height:1; }
.metric small{ font-size:13px; color:var(--muted); font-weight:400; margin-right:6px; }
.sub{ margin-top:8px; font-family: var(--mono); font-size:12px; color:var(--muted); word-break: break-word; }
.ok-text{ color:var(--ok); } .warn-text{ color:var(--warn); } .danger-text{ color:var(--danger); }
.col-4{ grid-column: span 4; }
.col-12{ grid-column: span 12; }
.col-6{ grid-column: span 6; }
@media (max-width: 800px){
.col-4, .col-6{ grid-column: span 12; }
}
canvas{ max-height:260px; width:100%; }
table{ width:100%; border-collapse:collapse; font-family: var(--mono); font-size:13px; }
th, td{ text-align:right; padding:9px 8px; border-bottom:1px solid var(--panel-border); }
th{ color:var(--muted); font-weight:500; font-size:11px; text-transform:uppercase; letter-spacing:0.5px; }
.badge{ display:inline-block; padding:2px 9px; border-radius:20px; font-size:11px; font-weight:700; }
.badge.up{ background: rgba(52,211,153,0.15); color:var(--ok); }
.badge.down{ background: rgba(239,90,111,0.15); color:var(--danger); }
.empty-state{
text-align:center; padding:60px 20px; color:var(--muted); font-family: var(--mono); font-size:13px;
}
.empty-state b{ color:var(--text); }
</style>
</head>
<body>
<header>
<div class="brand">
<div class="pulse-dot" id="pulseDot"></div>
<h1>mithal.space<span>لوحة مراقبة الموقع — Uptime · Latency · SSL · DNS · Search</span></h1>
</div>
<div class="controls">
<span class="last-updated" id="lastUpdated">لا توجد بيانات بعد</span>
<label for="fileInput">📂 تحميل data.jsonl</label>
<input type="file" id="fileInput" accept=".jsonl,.json,.txt">
</div>
</header>
<main id="dashboard">
<div class="card col-12" id="emptyCard" style="display: block;">
<div class="empty-state" id="emptyState">
شغّل <b>monitor.py</b> لجمع البيانات، ثم اخدم هذا المجلد عبر خادم محلي
(مثال: <b>python3 -m http.server 8080</b>) ليتم تحميل <b>data.jsonl</b> تلقائياً،
أو استخدم زر «تحميل data.jsonl» أعلاه لاختيار الملف يدوياً.
</div>
</div>
<div class="card col-4 main-content" style="display:none;">
<h2>Uptime (24 ساعة)</h2>
<div class="metric" id="uptimeMetric"></div>
<div class="sub" id="uptimeSub"></div>
</div>
<div class="card col-4 main-content" style="display:none;">
<h2>شهادة SSL</h2>
<div class="metric" id="sslMetric"></div>
<div class="sub" id="sslSub"></div>
</div>
<div class="card col-4 main-content" style="display:none;">
<h2>DNS (متوسط 24س) / آخر بحث</h2>
<div class="metric" id="dnsMetric"></div>
<div class="sub" id="dnsSub"></div>
</div>
<div class="card col-12 main-content" style="display:none;">
<h2>زمن الاستجابة — آخر ساعة (Latency)</h2>
<canvas id="latencyChart"></canvas>
</div>
<div class="card col-12 main-content" style="display:none;">
<h2>سجل آخر 10 فحوصات</h2>
<table>
<thead>
<tr><th>الوقت</th><th>الحالة</th><th>Latency</th><th>DNS</th><th>SSL (أيام)</th><th>Search</th></tr>
</thead>
<tbody id="logBody"></tbody>
</table>
</div>
</main>
<script>
const REFRESH_MS = 60000; // إعادة تحميل تلقائي كل دقيقة
let records = [];
let latencyChart = null;
function parseJsonl(text){
return text.split('\n')
.map(l => l.trim())
.filter(Boolean)
.map(l => { try { return JSON.parse(l); } catch(e){ return null; } })
.filter(Boolean);
}
function fmtTime(iso){
if (!iso) return '—';
const d = new Date(iso);
if (isNaN(d.getTime())) return iso;
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const seconds = String(d.getSeconds()).padStart(2, '0');
return `${d.toLocaleDateString('ar-EG')} ${hours}:${minutes}:${seconds}`;
}
function fmtShortTime(iso){
if (!iso) return '—';
const d = new Date(iso);
if (isNaN(d.getTime())) return iso;
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
const seconds = String(d.getSeconds()).padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}
function statusColorClass(days){
if (days === null || days === undefined) return 'danger-text';
if (days <= 7) return 'danger-text';
if (days <= 30) return 'warn-text';
return 'ok-text';
}
function initChart() {
const chartCanvas = document.getElementById('latencyChart');
if (!chartCanvas) return;
const ctx = chartCanvas.getContext('2d');
latencyChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'HTTP latency (ms)',
data: [],
borderColor: '#4fd1ff',
backgroundColor: 'rgba(79,209,255,0.08)',
tension: 0.3,
pointRadius: 2,
spanGaps: true,
fill: true,
},
{
label: 'Search latency (ms)',
data: [],
borderColor: '#f2b84b',
backgroundColor: 'rgba(242,184,75,0.06)',
tension: 0.3,
pointRadius: 2,
spanGaps: true,
fill: true,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { labels: { color: '#7c8aa5', font: { family: 'JetBrains Mono', size: 11 } } }
},
scales: {
x: { ticks: { color: '#7c8aa5', maxTicksLimit: 12 }, grid: { color: '#202838' } },
y: { ticks: { color: '#7c8aa5' }, grid: { color: '#202838' }, title:{display:true, text:'ms', color:'#7c8aa5'} }
}
}
});
}
function render(){
const emptyCard = document.getElementById('emptyCard');
const mainCards = document.querySelectorAll('.main-content');
if (!records.length){
if (emptyCard) emptyCard.style.display = 'block';
mainCards.forEach(c => c.style.display = 'none');
return;
}
if (emptyCard) emptyCard.style.display = 'none';
mainCards.forEach(c => c.style.display = 'block');
const sorted = [...records].sort((a,b) => new Date(a.timestamp) - new Date(b.timestamp));
const latest = sorted[sorted.length - 1];
const nowMs = new Date(latest.timestamp).getTime();
// --- Uptime % over last 24h ---
const last24h = sorted.filter(r => nowMs - new Date(r.timestamp).getTime() <= 24*3600*1000);
const upCount = last24h.filter(r => r.http && r.http.up).length;
const uptimePct = last24h.length ? ((upCount / last24h.length) * 100).toFixed(2) : 'N/A';
const uptimeClass = uptimePct === 'N/A' ? '' : (uptimePct >= 99.5 ? 'ok-text' : uptimePct >= 95 ? 'warn-text' : 'danger-text');
const uptimeEl = document.getElementById('uptimeMetric');
if (uptimeEl) {
uptimeEl.className = `metric ${uptimeClass}`;
uptimeEl.innerHTML = `${uptimePct}${uptimePct !== 'N/A' ? '<small>%</small>' : ''}`;
}
const uptimeSubEl = document.getElementById('uptimeSub');
if (uptimeSubEl) {
uptimeSubEl.textContent = `${upCount}/${last24h.length} فحص ناجح آخر 24 ساعة`;
}
// --- SSL ---
const ssl = latest.ssl || {};
const hasValidDays = ssl.valid && ssl.days_remaining !== null && ssl.days_remaining !== undefined;
const sslClass = hasValidDays ? statusColorClass(ssl.days_remaining) : 'danger-text';
const sslMetricText = hasValidDays ? ssl.days_remaining : '—';
const sslSubText = hasValidDays ? (`تنتهي في ${ssl.expiry_date}`) : (ssl.error ? `غير سارية (${ssl.error})` : 'غير سارية أو تعذّر التحقق');
const sslMetricEl = document.getElementById('sslMetric');
if (sslMetricEl) {
sslMetricEl.className = `metric ${sslClass}`;
sslMetricEl.innerHTML = `${sslMetricText}${hasValidDays ? '<small>يوم متبقي</small>' : ''}`;
}
const sslSubEl = document.getElementById('sslSub');
if (sslSubEl) {
sslSubEl.textContent = sslSubText;
}
// --- DNS avg over last 24h ---
const dnsTimes = last24h.map(r => r.dns && r.dns.time_ms).filter(v => v !== null && v !== undefined);
const dnsAvg = dnsTimes.length ? (dnsTimes.reduce((a,b)=>a+b,0)/dnsTimes.length).toFixed(1) : 'N/A';
const dnsMetricEl = document.getElementById('dnsMetric');
if (dnsMetricEl) {
dnsMetricEl.innerHTML = `${dnsAvg}${dnsAvg !== 'N/A' ? '<small>ms</small>' : ''}`;
}
const dnsSubEl = document.getElementById('dnsSub');
if (dnsSubEl) {
dnsSubEl.textContent = `زمن البحث الأخير: ${latest.search && latest.search.latency_ms !== null && latest.search.latency_ms !== undefined ? latest.search.latency_ms + ' ms' : 'فشل الطلب'}`;
}
// --- pulse dot & last updated ---
const dot = document.getElementById('pulseDot');
if (dot) dot.classList.toggle('down', !latest.http || !latest.http.up);
const lastUpdatedEl = document.getElementById('lastUpdated');
if (lastUpdatedEl) lastUpdatedEl.textContent = 'آخر تحديث: ' + fmtTime(latest.timestamp);
// --- update Chart smoothly ---
const lastHour = sorted.filter(r => nowMs - new Date(r.timestamp).getTime() <= 3600*1000);
const chartSource = lastHour.length ? lastHour : sorted.slice(-30);
const labels = chartSource.map(r => fmtShortTime(r.timestamp));
const httpData = chartSource.map(r => r.http ? r.http.latency_ms : null);
const searchData = chartSource.map(r => r.search ? r.search.latency_ms : null);
if (!latencyChart) {
initChart();
}
if (latencyChart) {
latencyChart.data.labels = labels;
latencyChart.data.datasets[0].data = httpData;
latencyChart.data.datasets[1].data = searchData;
latencyChart.update();
}
// --- log table (last 10) ---
const logBody = document.getElementById('logBody');
if (logBody) {
const last10 = sorted.slice(-10).reverse();
logBody.innerHTML = last10.map(r => {
const rSslValid = r.ssl && r.ssl.valid && r.ssl.days_remaining !== null && r.ssl.days_remaining !== undefined;
return `
<tr>
<td>${fmtTime(r.timestamp)}</td>
<td><span class="badge ${r.http && r.http.up ? 'up' : 'down'}">${r.http && r.http.up ? 'UP' : 'DOWN'}</span></td>
<td>${r.http && r.http.latency_ms !== null && r.http.latency_ms !== undefined ? r.http.latency_ms + ' ms' : '—'}</td>
<td>${r.dns && r.dns.time_ms !== null && r.dns.time_ms !== undefined ? r.dns.time_ms + ' ms' : '—'}</td>
<td class="${statusColorClass(rSslValid ? r.ssl.days_remaining : null)}">${rSslValid ? r.ssl.days_remaining : '—'}</td>
<td>${r.search && r.search.latency_ms !== null && r.search.latency_ms !== undefined ? r.search.latency_ms + ' ms' : '—'}</td>
</tr>
`;
}).join('');
}
}
async function loadFromServer(){
try{
const res = await fetch('data.jsonl', { cache: 'no-store' });
if (!res.ok) throw new Error('not found');
const text = await res.text();
records = parseJsonl(text);
render();
} catch(e){
// Fallback when served directly as file:// or if fetch fails
}
}
document.getElementById('fileInput').addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
records = parseJsonl(evt.target.result);
render();
};
reader.readAsText(file);
});
initChart();
loadFromServer();
setInterval(loadFromServer, REFRESH_MS);
</script>
</body>
</html>