221 أسطر
6.8 KiB
HTML
221 أسطر
6.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>لوحة مراقبة التطبيق</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0f1420;
|
|
--card: #171f30;
|
|
--border: #2a3448;
|
|
--text: #e7ecf5;
|
|
--muted: #8b96ab;
|
|
--green: #2ecc71;
|
|
--red: #e74c3c;
|
|
--accent: #4f8cff;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
font-family: 'Segoe UI', Tahoma, Arial, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
padding: 24px;
|
|
}
|
|
h1 { font-size: 20px; margin-bottom: 4px; }
|
|
.sub { color: var(--muted); font-size: 13px; margin-bottom: 20px; }
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.controls input {
|
|
flex: 1;
|
|
min-width: 220px;
|
|
padding: 10px 12px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border);
|
|
background: var(--card);
|
|
color: var(--text);
|
|
font-size: 14px;
|
|
}
|
|
.controls button {
|
|
padding: 10px 18px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
background: var(--accent);
|
|
color: white;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
}
|
|
.controls button:hover { opacity: 0.9; }
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: 16px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.card {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 18px;
|
|
}
|
|
.card .label { color: var(--muted); font-size: 13px; margin-bottom: 8px; }
|
|
.card .value { font-size: 26px; font-weight: 600; }
|
|
.status-dot {
|
|
display: inline-block;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
margin-left: 8px;
|
|
}
|
|
.up { color: var(--green); }
|
|
.down { color: var(--red); }
|
|
.dot-up { background: var(--green); box-shadow: 0 0 8px var(--green); }
|
|
.dot-down { background: var(--red); box-shadow: 0 0 8px var(--red); }
|
|
|
|
.log {
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
max-height: 260px;
|
|
overflow-y: auto;
|
|
font-family: 'Consolas', monospace;
|
|
font-size: 12.5px;
|
|
}
|
|
.log div { padding: 4px 0; border-bottom: 1px solid #202a3d; }
|
|
.log .ok { color: var(--green); }
|
|
.log .fail { color: var(--red); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<h1>لوحة مراقبة التطبيق</h1>
|
|
<div class="sub">يفحص /health و /metrics مباشرة من التطبيق كل 10 ثوانٍ</div>
|
|
|
|
<div class="controls">
|
|
<input id="apiUrl" type="text" placeholder="https://your-app.ghaymah.systems" value="">
|
|
<button onclick="startMonitoring()">بدء المراقبة</button>
|
|
</div>
|
|
|
|
<div class="grid">
|
|
<div class="card">
|
|
<div class="label">الحالة</div>
|
|
<div class="value" id="statusValue"><span class="status-dot" id="statusDot"></span><span id="statusText">--</span></div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="label">زمن الاستجابة</div>
|
|
<div class="value" id="latencyValue">-- ms</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="label">عدد الطلبات (على السيرفر)</div>
|
|
<div class="value" id="requestCountValue">--</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="label">مدة التشغيل</div>
|
|
<div class="value" id="uptimeValue">--</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="log" id="log"></div>
|
|
|
|
<script>
|
|
let timer = null;
|
|
|
|
function fmtUptime(sec) {
|
|
if (sec == null) return '--';
|
|
const h = Math.floor(sec / 3600);
|
|
const m = Math.floor((sec % 3600) / 60);
|
|
const s = sec % 60;
|
|
return `${h}h ${m}m ${s}s`;
|
|
}
|
|
|
|
function addLogLine(text, ok) {
|
|
const log = document.getElementById('log');
|
|
const line = document.createElement('div');
|
|
line.className = ok ? 'ok' : 'fail';
|
|
line.textContent = text;
|
|
log.prepend(line);
|
|
while (log.children.length > 50) log.removeChild(log.lastChild);
|
|
}
|
|
|
|
async function checkOnce(baseUrl) {
|
|
const start = performance.now();
|
|
try {
|
|
const res = await fetch(baseUrl.replace(/\/$/, '') + '/health', { cache: 'no-store' });
|
|
const elapsed = Math.round(performance.now() - start);
|
|
const ok = res.ok;
|
|
// /health may return JSON (custom API) or plain text "ok" (static site) - handle both
|
|
const raw = await res.text();
|
|
let data = {};
|
|
try { data = JSON.parse(raw); } catch (e) { /* plain text health response, fine */ }
|
|
|
|
document.getElementById('statusDot').className = 'status-dot ' + (ok ? 'dot-up' : 'dot-down');
|
|
document.getElementById('statusText').textContent = ok ? 'متصل (UP)' : 'متوقف (DOWN)';
|
|
document.getElementById('statusText').className = ok ? 'up' : 'down';
|
|
document.getElementById('latencyValue').textContent = elapsed + ' ms';
|
|
document.getElementById('uptimeValue').textContent = data.uptime_seconds ? fmtUptime(data.uptime_seconds) : '--';
|
|
|
|
addLogLine(`[${new Date().toLocaleTimeString()}] ${ok ? 'OK' : 'FAIL'} - ${elapsed}ms`, ok);
|
|
|
|
// Try a JSON /metrics endpoint first (custom API apps like demo-api)
|
|
let gotMetrics = false;
|
|
try {
|
|
const mRes = await fetch(baseUrl.replace(/\/$/, '') + '/metrics', { cache: 'no-store' });
|
|
if (mRes.ok) {
|
|
const mData = await mRes.json();
|
|
document.getElementById('requestCountValue').textContent = mData.request_count ?? '--';
|
|
gotMetrics = true;
|
|
}
|
|
} catch (e) { /* no /metrics endpoint, try nginx_status next */ }
|
|
|
|
// Fallback: nginx stub_status (used by static sites served via nginx)
|
|
if (!gotMetrics) {
|
|
try {
|
|
const sRes = await fetch(baseUrl.replace(/\/$/, '') + '/nginx_status', { cache: 'no-store' });
|
|
if (sRes.ok) {
|
|
const text = await sRes.text();
|
|
// Format: "server accepts handled requests\n a b c "
|
|
const match = text.match(/\d+\s+\d+\s+(\d+)/);
|
|
document.getElementById('requestCountValue').textContent = match ? match[1] : '--';
|
|
}
|
|
} catch (e) { /* neither metrics source available */ }
|
|
}
|
|
|
|
} catch (err) {
|
|
document.getElementById('statusDot').className = 'status-dot dot-down';
|
|
document.getElementById('statusText').textContent = 'متوقف (DOWN)';
|
|
document.getElementById('statusText').className = 'down';
|
|
document.getElementById('latencyValue').textContent = '-- ms';
|
|
addLogLine(`[${new Date().toLocaleTimeString()}] FAIL - ${err.message}`, false);
|
|
}
|
|
}
|
|
|
|
function startMonitoring() {
|
|
const url = document.getElementById('apiUrl').value.trim();
|
|
if (!url) return;
|
|
if (timer) clearInterval(timer);
|
|
checkOnce(url);
|
|
timer = setInterval(() => checkOnce(url), 10000);
|
|
}
|
|
|
|
// Auto-fill with this site's own address (dashboard is served from the same app)
|
|
// and start monitoring immediately.
|
|
window.addEventListener('DOMContentLoaded', () => {
|
|
const input = document.getElementById('apiUrl');
|
|
if (!input.value) {
|
|
input.value = window.location.origin;
|
|
}
|
|
startMonitoring();
|
|
});
|
|
|
|
</script>
|
|
</body>
|
|
</html>
|