213 أسطر
6.4 KiB
HTML
213 أسطر
6.4 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>لوحة مراقبة الـ API</title>
|
|
<style>
|
|
:root{
|
|
--bg:#f4f7f6;
|
|
--card:#ffffff;
|
|
--border:#e3e8ec;
|
|
--text:#25313c;
|
|
--muted:#7a8894;
|
|
--accent:#0056b3;
|
|
--up:#28a745;
|
|
--down:#dc3545;
|
|
--warn:#f0ad4e;
|
|
}
|
|
*{ box-sizing:border-box; }
|
|
body{
|
|
font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
background:var(--bg);
|
|
display:flex; justify-content:center; align-items:center;
|
|
min-height:100vh; margin:0; padding:24px;
|
|
}
|
|
.dashboard{
|
|
background:var(--card);
|
|
padding:30px;
|
|
border-radius:14px;
|
|
box-shadow:0 8px 24px rgba(15,35,55,0.08);
|
|
text-align:center;
|
|
width:420px;
|
|
max-width:100%;
|
|
border:1px solid var(--border);
|
|
}
|
|
h2{
|
|
color:var(--text);
|
|
margin:0 0 22px;
|
|
display:flex;
|
|
align-items:center;
|
|
justify-content:center;
|
|
gap:10px;
|
|
font-size:1.25rem;
|
|
}
|
|
.status-dot{
|
|
width:12px; height:12px; border-radius:50%;
|
|
background:#9aa5ad;
|
|
box-shadow:0 0 0 0 rgba(154,165,173,0.5);
|
|
flex-shrink:0;
|
|
}
|
|
.status-dot.online{ background:var(--up); animation:pulse-up 2s infinite; }
|
|
.status-dot.offline{ background:var(--down); animation:pulse-down 2s infinite; }
|
|
@keyframes pulse-up{
|
|
0%{ box-shadow:0 0 0 0 rgba(40,167,69,0.55); }
|
|
70%{ box-shadow:0 0 0 9px rgba(40,167,69,0); }
|
|
100%{ box-shadow:0 0 0 0 rgba(40,167,69,0); }
|
|
}
|
|
@keyframes pulse-down{
|
|
0%{ box-shadow:0 0 0 0 rgba(220,53,69,0.55); }
|
|
70%{ box-shadow:0 0 0 9px rgba(220,53,69,0); }
|
|
100%{ box-shadow:0 0 0 0 rgba(220,53,69,0); }
|
|
}
|
|
.metric{
|
|
background:#eef2f5;
|
|
margin:10px 0;
|
|
padding:14px 16px;
|
|
border-radius:9px;
|
|
font-size:1.05rem;
|
|
display:flex;
|
|
justify-content:space-between;
|
|
align-items:center;
|
|
}
|
|
.metric span{ font-weight:700; color:var(--accent); font-variant-numeric:tabular-nums; }
|
|
.metric span.status-online{ color:var(--up); }
|
|
.metric span.status-offline{ color:var(--down); }
|
|
.meta-row{
|
|
margin-top:16px;
|
|
font-size:0.75rem;
|
|
color:var(--muted);
|
|
display:flex;
|
|
justify-content:space-between;
|
|
}
|
|
.api-row{
|
|
margin-top:18px;
|
|
padding-top:14px;
|
|
border-top:1px dashed var(--border);
|
|
text-align:right;
|
|
}
|
|
.api-row label{
|
|
display:block;
|
|
font-size:0.72rem;
|
|
color:var(--muted);
|
|
margin-bottom:6px;
|
|
}
|
|
.api-row .input-group{
|
|
display:flex;
|
|
gap:6px;
|
|
}
|
|
.api-row input{
|
|
flex:1;
|
|
padding:8px 10px;
|
|
border:1px solid var(--border);
|
|
border-radius:7px;
|
|
font-size:0.8rem;
|
|
direction:ltr;
|
|
text-align:left;
|
|
}
|
|
.api-row button{
|
|
padding:8px 14px;
|
|
border:none;
|
|
border-radius:7px;
|
|
background:var(--accent);
|
|
color:#fff;
|
|
font-size:0.8rem;
|
|
cursor:pointer;
|
|
}
|
|
.api-row button:hover{ opacity:0.9; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="dashboard">
|
|
<h2>مراقبة النظام <span id="statusDot" class="status-dot"></span></h2>
|
|
|
|
<div class="metric">
|
|
الحالة: <span id="statusText">جاري التحميل...</span>
|
|
</div>
|
|
<div class="metric">
|
|
زمن الاستجابة: <span id="responseTime">— ms</span>
|
|
</div>
|
|
<div class="metric">
|
|
عدد الطلبات الإجمالي: <span id="requestCount">—</span>
|
|
</div>
|
|
|
|
<div class="meta-row">
|
|
<span id="uptimeText">التشغيل: —</span>
|
|
<span id="lastUpdated">آخر تحديث: —</span>
|
|
</div>
|
|
|
|
<div class="api-row">
|
|
<label for="apiInput">عنوان الـ API (health endpoint):</label>
|
|
<div class="input-group">
|
|
<input id="apiInput" type="text" placeholder="https://backend-06febdb2d9f4.hosted.ghaymah.systems/health">
|
|
<button id="apiSave">حفظ</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// This copy of the dashboard is meant to be hosted on its own — separately
|
|
// from the API — so it needs to be told where the API lives. Resolution
|
|
// order:
|
|
// 1. ?api=<url> query-string param, e.g. index.html?api=https://foo/health
|
|
// 2. the value typed into the input box below (kept only in memory /
|
|
// in the URL — nothing is written to disk or browser storage)
|
|
// 3. falls back to the hosted backend URL provided for this project.
|
|
const POLL_MS = 5000;
|
|
const DEFAULT_HEALTH_URL = 'https://backend-06febdb2d9f4.hosted.ghaymah.systems/health';
|
|
let healthUrl = new URLSearchParams(window.location.search).get('api') || DEFAULT_HEALTH_URL;
|
|
|
|
const apiInput = document.getElementById('apiInput');
|
|
apiInput.value = healthUrl === '/health' ? '' : healthUrl;
|
|
|
|
document.getElementById('apiSave').addEventListener('click', () => {
|
|
const val = apiInput.value.trim();
|
|
if (val) {
|
|
healthUrl = val;
|
|
const url = new URL(window.location.href);
|
|
url.searchParams.set('api', val);
|
|
window.history.replaceState({}, '', url); // shareable link, no storage used
|
|
fetchMetrics();
|
|
}
|
|
});
|
|
|
|
function fmtUptime(seconds){
|
|
if (seconds === undefined || seconds === null) return '—';
|
|
const h = Math.floor(seconds / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
const s = Math.floor(seconds % 60);
|
|
return `${h}h ${m}m ${s}s`;
|
|
}
|
|
|
|
async function fetchMetrics(){
|
|
try {
|
|
const res = await fetch(healthUrl, { cache: 'no-store' });
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
const data = await res.json();
|
|
|
|
const statusEl = document.getElementById('statusText');
|
|
statusEl.innerText = data.status || 'Online';
|
|
statusEl.className = 'status-online';
|
|
document.getElementById('statusDot').className = 'status-dot online';
|
|
|
|
document.getElementById('responseTime').innerText = (data.response_time_ms ?? 0) + ' ms';
|
|
document.getElementById('requestCount').innerText = data.request_count ?? '—';
|
|
document.getElementById('uptimeText').innerText = 'التشغيل: ' + fmtUptime(data.uptime_seconds);
|
|
document.getElementById('lastUpdated').innerText =
|
|
'آخر تحديث: ' + new Date().toLocaleTimeString('ar-EG');
|
|
|
|
} catch (error) {
|
|
const statusEl = document.getElementById('statusText');
|
|
statusEl.innerText = 'انقطاع الاتصال (Offline)';
|
|
statusEl.className = 'status-offline';
|
|
document.getElementById('statusDot').className = 'status-dot offline';
|
|
document.getElementById('responseTime').innerText = '—';
|
|
}
|
|
}
|
|
|
|
fetchMetrics();
|
|
setInterval(fetchMetrics, POLL_MS);
|
|
</script>
|
|
</body>
|
|
</html> |