Ghaymah intern
هذا الالتزام موجود في:
220
q1-deploy-monitor/dashboard/index.html
Normal file
220
q1-deploy-monitor/dashboard/index.html
Normal file
@@ -0,0 +1,220 @@
|
||||
<!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>
|
||||
المرجع في مشكلة جديدة
حظر مستخدم