first commit
هذا الالتزام موجود في:
232
q1-deploy-monitor/dashboard.html
Normal file
232
q1-deploy-monitor/dashboard.html
Normal file
@@ -0,0 +1,232 @@
|
||||
<!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:#0b0f14;
|
||||
--panel:#111823;
|
||||
--panel-border:#1e2a38;
|
||||
--text:#dbe4ee;
|
||||
--muted:#7d8ba0;
|
||||
--ok:#33d17a;
|
||||
--bad:#f2495c;
|
||||
--warn:#f2b632;
|
||||
--accent:#3ea6ff;
|
||||
--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);
|
||||
min-height:100vh;
|
||||
padding:28px 20px 60px;
|
||||
}
|
||||
.wrap{max-width:960px;margin:0 auto;}
|
||||
header{
|
||||
display:flex;justify-content:space-between;align-items:flex-end;
|
||||
margin-bottom:22px;flex-wrap:wrap;gap:12px;
|
||||
}
|
||||
h1{font-size:1.25rem;margin:0;letter-spacing:.02em;font-weight:600;}
|
||||
.subtitle{color:var(--muted);font-size:.8rem;margin-top:4px;}
|
||||
.config{display:flex;gap:8px;align-items:center;}
|
||||
.config input{
|
||||
background:#0f151d;border:1px solid var(--panel-border);color:var(--text);
|
||||
padding:8px 10px;border-radius:6px;font-family:var(--mono);font-size:.8rem;width:280px;
|
||||
}
|
||||
.config button{
|
||||
background:var(--accent);border:none;color:#04101c;font-weight:700;
|
||||
padding:8px 14px;border-radius:6px;cursor:pointer;font-family:var(--mono);font-size:.8rem;
|
||||
}
|
||||
.config button:hover{filter:brightness(1.1);}
|
||||
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-bottom:16px;}
|
||||
.card{
|
||||
background:var(--panel);border:1px solid var(--panel-border);border-radius:10px;
|
||||
padding:16px 18px;position:relative;overflow:hidden;
|
||||
}
|
||||
.label{color:var(--muted);font-size:.72rem;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px;}
|
||||
.value{font-size:1.9rem;font-weight:700;line-height:1.1;}
|
||||
.value.small{font-size:1.1rem;}
|
||||
|
||||
.status-row{display:flex;align-items:center;gap:10px;}
|
||||
.dot{width:12px;height:12px;border-radius:50%;background:var(--muted);flex:0 0 auto;}
|
||||
.dot.ok{background:var(--ok);box-shadow:0 0 0 0 rgba(51,209,122,.6);animation:pulse 2s infinite;}
|
||||
.dot.bad{background:var(--bad);}
|
||||
@keyframes pulse{
|
||||
0%{box-shadow:0 0 0 0 rgba(51,209,122,.55);}
|
||||
70%{box-shadow:0 0 0 8px rgba(51,209,122,0);}
|
||||
100%{box-shadow:0 0 0 0 rgba(51,209,122,0);}
|
||||
}
|
||||
|
||||
.chart-card{grid-column:1/-1;}
|
||||
canvas{width:100%;height:140px;display:block;}
|
||||
|
||||
.log{
|
||||
background:var(--panel);border:1px solid var(--panel-border);border-radius:10px;
|
||||
padding:14px 18px;font-size:.76rem;color:var(--muted);max-height:170px;overflow-y:auto;
|
||||
}
|
||||
.log div{padding:2px 0;border-bottom:1px dashed #1a2431;}
|
||||
.log .fail{color:var(--bad);}
|
||||
.log .ok-line{color:var(--ok);}
|
||||
|
||||
footer{margin-top:18px;color:var(--muted);font-size:.7rem;text-align:center;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<div>
|
||||
<h1>لوحة مراقبة التطبيق</h1>
|
||||
<div class="subtitle">فحص كل <span id="intervalLabel">5</span> ثوانٍ · Ghaymah Containers</div>
|
||||
</div>
|
||||
<div class="config">
|
||||
<input id="apiUrl" type="text" placeholder="https://your-app.ghaymah.systems" />
|
||||
<button id="connectBtn">اتصال</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<div class="label">الحالة</div>
|
||||
<div class="status-row">
|
||||
<div class="dot" id="statusDot"></div>
|
||||
<div class="value small" id="statusText">غير متصل</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">زمن الاستجابة</div>
|
||||
<div class="value" id="latencyValue">--</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">عدد الطلبات</div>
|
||||
<div class="value" id="requestsValue">--</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="label">مدة التشغيل</div>
|
||||
<div class="value small" id="uptimeValue">--</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card chart-card">
|
||||
<div class="label">زمن الاستجابة (آخر 30 قراءة)</div>
|
||||
<canvas id="chart"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="log" id="log"></div>
|
||||
|
||||
<footer>يعتمد على نقاط الوصول /health و /metrics · يعمل بالكامل من المتصفح</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const POLL_MS = 5000;
|
||||
const HISTORY_LEN = 30;
|
||||
let latencyHistory = [];
|
||||
let timer = null;
|
||||
|
||||
const els = {
|
||||
dot: document.getElementById('statusDot'),
|
||||
statusText: document.getElementById('statusText'),
|
||||
latency: document.getElementById('latencyValue'),
|
||||
requests: document.getElementById('requestsValue'),
|
||||
uptime: document.getElementById('uptimeValue'),
|
||||
log: document.getElementById('log'),
|
||||
apiUrl: document.getElementById('apiUrl'),
|
||||
connectBtn: document.getElementById('connectBtn'),
|
||||
};
|
||||
|
||||
function fmtUptime(sec){
|
||||
if(sec == null) return '--';
|
||||
const h = Math.floor(sec/3600), m = Math.floor((sec%3600)/60), s = Math.floor(sec%60);
|
||||
return `${h}س ${m}د ${s}ث`;
|
||||
}
|
||||
|
||||
function addLog(msg, ok){
|
||||
const line = document.createElement('div');
|
||||
line.className = ok ? 'ok-line' : 'fail';
|
||||
const time = new Date().toLocaleTimeString('ar-EG');
|
||||
line.textContent = `[${time}] ${msg}`;
|
||||
els.log.prepend(line);
|
||||
while(els.log.children.length > 40) els.log.removeChild(els.log.lastChild);
|
||||
}
|
||||
|
||||
function drawChart(){
|
||||
const canvas = document.getElementById('chart');
|
||||
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(latencyHistory.length < 2){
|
||||
ctx.fillStyle = '#3a4a5c';
|
||||
ctx.font = '12px monospace';
|
||||
ctx.fillText('بانتظار بيانات كافية...', 10, h/2);
|
||||
return;
|
||||
}
|
||||
|
||||
const max = Math.max(...latencyHistory, 10) * 1.2;
|
||||
const stepX = w / (HISTORY_LEN - 1);
|
||||
|
||||
ctx.strokeStyle = '#3ea6ff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
latencyHistory.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((latencyHistory.length-1)*stepX, h);
|
||||
ctx.lineTo(0, h);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = 'rgba(62,166,255,0.12)';
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
async function poll(){
|
||||
const base = els.apiUrl.value.trim().replace(/\/$/, '');
|
||||
if(!base) return;
|
||||
|
||||
const start = performance.now();
|
||||
try{
|
||||
const res = await fetch(base + '/metrics', {cache:'no-store'});
|
||||
const elapsed = Math.round(performance.now() - start);
|
||||
if(!res.ok) throw new Error('HTTP ' + res.status);
|
||||
const data = await res.json();
|
||||
|
||||
els.dot.className = 'dot ok';
|
||||
els.statusText.textContent = 'يعمل';
|
||||
els.latency.textContent = (data.avg_response_time_ms ?? elapsed) + ' ms';
|
||||
els.requests.textContent = data.requests_total ?? '--';
|
||||
els.uptime.textContent = fmtUptime(data.uptime_seconds);
|
||||
|
||||
latencyHistory.push(elapsed);
|
||||
if(latencyHistory.length > HISTORY_LEN) latencyHistory.shift();
|
||||
drawChart();
|
||||
addLog(`الفحص ناجح - زمن الاستجابة ${elapsed}ms`, true);
|
||||
}catch(err){
|
||||
els.dot.className = 'dot bad';
|
||||
els.statusText.textContent = 'متوقف / لا يستجيب';
|
||||
addLog('فشل الفحص: ' + err.message, false);
|
||||
}
|
||||
}
|
||||
|
||||
els.connectBtn.addEventListener('click', () => {
|
||||
if(timer) clearInterval(timer);
|
||||
latencyHistory = [];
|
||||
poll();
|
||||
timer = setInterval(poll, POLL_MS);
|
||||
});
|
||||
|
||||
window.addEventListener('resize', drawChart);
|
||||
drawChart();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
المرجع في مشكلة جديدة
حظر مستخدم