هذا الالتزام موجود في:
2026-09-17 22:11:14 +03:00
الأصل 7593b08483
التزام b9136d2a71
5 ملفات معدلة مع 712 إضافات و4 حذوفات

616
status-page/index.html Normal file
عرض الملف

@@ -0,0 +1,616 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Ghaymah Status</title>
<meta name="description" content="Real-time status and uptime for all Ghaymah services." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<style>
/* ─── Design tokens ─────────────────────────────── */
:root {
--bg: #f6f8fa;
--surface: #ffffff;
--border: #d0d7de;
--border-soft: #eaeef2;
--text-1: #1f2328;
--text-2: #656d76;
--text-3: #9198a1;
--green: #1a7f37;
--green-bg: #d1f4d9;
--green-bar: #3fb950;
--green-pulse: rgba(63,185,80,.35);
--yellow: #7d4e00;
--yellow-bg: #fff3cd;
--yellow-bar: #e3a31b;
--red: #b91c1c;
--red-bg: #fde8e8;
--red-bar: #f85149;
--bar-empty: #e1e4e8;
--radius: 8px;
--shadow: 0 1px 3px rgba(0,0,0,.07), 0 1px 2px rgba(0,0,0,.04);
}
/* ─── Reset ─────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--text-1);
line-height: 1.5;
min-height: 100vh;
}
a { color: inherit; text-decoration: none; }
/* ─── Layout ─────────────────────────────────────── */
.container { max-width: 860px; margin: 0 auto; padding: 0 24px; }
/* ─── Site header ────────────────────────────────── */
.site-header {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 14px 0;
}
.header-inner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.logo {
display: flex;
align-items: center;
gap: 10px;
font-size: 17px;
font-weight: 700;
color: var(--text-1);
}
.logo-icon {
width: 32px;
height: 32px;
background: linear-gradient(135deg, #1f6feb 0%, #388bfd 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.header-meta {
font-size: 12px;
color: var(--text-3);
}
/* ─── Status banner ──────────────────────────────── */
.banner {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 36px 0 32px;
}
.banner-inner {
display: flex;
align-items: flex-start;
gap: 20px;
}
.status-orb {
width: 22px;
height: 22px;
border-radius: 50%;
flex-shrink: 0;
margin-top: 4px;
position: relative;
}
.status-orb.operational { background: var(--green-bar); }
.status-orb.degraded { background: var(--yellow-bar); }
.status-orb.outage { background: var(--red-bar); }
.status-orb.operational::after {
content: '';
position: absolute;
inset: -5px;
border-radius: 50%;
background: var(--green-pulse);
animation: orb-pulse 2s ease-in-out infinite;
}
@keyframes orb-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.5); opacity: 0; }
}
.banner-title {
font-size: 26px;
font-weight: 700;
letter-spacing: -0.3px;
}
.banner-sub {
margin-top: 4px;
font-size: 14px;
color: var(--text-2);
}
/* ─── Main content ───────────────────────────────── */
.main { padding: 32px 0 48px; }
/* ─── Group card ─────────────────────────────────── */
.group-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 24px;
overflow: hidden;
}
.group-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
border-bottom: 1px solid var(--border-soft);
background: var(--bg);
}
.group-title {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--text-2);
}
.group-status-badge {
font-size: 11px;
font-weight: 500;
padding: 2px 10px;
border-radius: 20px;
}
.group-status-badge.operational { background: var(--green-bg); color: var(--green); }
.group-status-badge.degraded { background: var(--yellow-bg); color: var(--yellow); }
.group-status-badge.outage { background: var(--red-bg); color: var(--red); }
/* ─── Monitor row ────────────────────────────────── */
.monitor-row {
padding: 18px 20px;
border-bottom: 1px solid var(--border-soft);
}
.monitor-row:last-child { border-bottom: none; }
.monitor-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.monitor-name-wrap { display: flex; align-items: center; gap: 10px; }
.monitor-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.monitor-status-dot.operational { background: var(--green-bar); }
.monitor-status-dot.degraded { background: var(--yellow-bar); }
.monitor-status-dot.outage { background: var(--red-bar); }
.monitor-name {
font-size: 14px;
font-weight: 600;
color: var(--text-1);
}
.monitor-badge {
font-size: 11px;
font-weight: 500;
padding: 3px 10px;
border-radius: 20px;
}
.monitor-badge.operational { background: var(--green-bg); color: var(--green); }
.monitor-badge.degraded { background: var(--yellow-bg); color: var(--yellow); }
.monitor-badge.outage { background: var(--red-bg); color: var(--red); }
/* ─── Uptime bars ────────────────────────────────── */
.bar-labels {
display: flex;
justify-content: space-between;
font-size: 10px;
color: var(--text-3);
margin-bottom: 5px;
}
.bars-wrap {
display: flex;
align-items: center;
gap: 10px;
}
.bars {
display: flex;
gap: 2px;
flex: 1;
height: 30px;
}
.bar {
flex: 1;
border-radius: 2px;
background: var(--bar-empty);
transition: opacity .15s;
cursor: default;
position: relative;
}
.bar:hover { opacity: .72; }
.bar.up { background: var(--green-bar); }
.bar.down { background: var(--red-bar); }
.bar.partial { background: var(--yellow-bar); }
.pct-label {
font-size: 13px;
font-weight: 600;
min-width: 52px;
text-align: right;
white-space: nowrap;
color: var(--text-2);
}
.pct-label.great { color: var(--green); }
/* ─── Uptime summary row ─────────────────────────── */
.uptime-summary {
display: flex;
gap: 18px;
margin-top: 8px;
font-size: 11px;
color: var(--text-3);
}
.uptime-summary .val { font-weight: 600; color: var(--text-2); }
/* ─── Loading / error ────────────────────────────── */
.state-box {
text-align: center;
padding: 56px 24px;
color: var(--text-3);
}
.spinner {
width: 28px;
height: 28px;
border: 3px solid var(--border);
border-top-color: var(--green-bar);
border-radius: 50%;
animation: spin .7s linear infinite;
margin: 0 auto 14px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.error-card {
background: var(--red-bg);
border: 1px solid #fca5a5;
border-radius: var(--radius);
padding: 18px 20px;
color: var(--red);
font-size: 13px;
line-height: 1.6;
}
/* ─── Footer ─────────────────────────────────────── */
.site-footer {
border-top: 1px solid var(--border);
padding: 20px 0;
background: var(--surface);
}
.footer-inner {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: var(--text-3);
}
.footer-right { display: flex; align-items: center; gap: 8px; }
.live-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--green-bar);
animation: orb-pulse 2s ease-in-out infinite;
}
/* ─── Responsive ─────────────────────────────────── */
@media (max-width: 520px) {
.banner-title { font-size: 20px; }
.bars { height: 22px; }
}
</style>
</head>
<body>
<!-- ═══ SITE HEADER ═════════════════════════════════════════ -->
<header class="site-header">
<div class="container">
<div class="header-inner">
<a href="/" class="logo">
<div class="logo-icon">☁️</div>
Ghaymah Status
</a>
<span class="header-meta" id="headerTime"></span>
</div>
</div>
</header>
<!-- ═══ STATUS BANNER ═══════════════════════════════════════ -->
<section class="banner">
<div class="container">
<div class="banner-inner" id="bannerInner">
<div class="state-box" style="width:100%">
<div class="spinner"></div>
<div>Loading status…</div>
</div>
</div>
</div>
</section>
<!-- ═══ MAIN CONTENT ════════════════════════════════════════ -->
<main class="main">
<div class="container" id="mainContent"></div>
</main>
<!-- ═══ FOOTER ══════════════════════════════════════════════ -->
<footer class="site-footer">
<div class="container">
<div class="footer-inner">
<span>Powered by <strong>Uptime Kuma</strong> · Ghaymah Cloud</span>
<div class="footer-right">
<div class="live-dot"></div>
<span id="footerUpdated">Auto-refreshes every 60 s</span>
</div>
</div>
</div>
</footer>
<script>
/* ─────────────────────────────────────────────────────────────
CONFIG
─────────────────────────────────────────────────────────── */
const SLUG = 'ghaymah-status';
const BAR_COUNT = 90; // number of heartbeat bars to render
const REFRESH_MS = 60_000;
// API is proxied via nginx — same-origin, no CORS
const API_BASE = '/kuma-api';
/* ─────────────────────────────────────────────────────────────
FETCH
─────────────────────────────────────────────────────────── */
async function fetchAll() {
const [r1, r2] = await Promise.all([
fetch(`${API_BASE}/api/status-page/${SLUG}`),
fetch(`${API_BASE}/api/status-page/heartbeat/${SLUG}`)
]);
if (!r1.ok || !r2.ok) throw new Error(`HTTP ${r1.status} / ${r2.status}`);
return {
page: await r1.json(),
hb: await r2.json()
};
}
/* ─────────────────────────────────────────────────────────────
HELPERS
─────────────────────────────────────────────────────────── */
function statusClass(up) {
if (up === true) return 'operational';
if (up === false) return 'outage';
return 'degraded';
}
function statusLabel(cls) {
return { operational: 'Operational', degraded: 'Degraded', outage: 'Down' }[cls];
}
function pct(val) {
if (val === undefined || val === null) return null;
// Uptime Kuma returns fractions (01); convert to percentage
const num = parseFloat(val);
return (num <= 1 ? num * 100 : num).toFixed(2);
}
/* Build an array of BAR_COUNT bar states from raw heartbeat list.
Uptime Kuma returns the MOST RECENT heartbeats (newest last).
status 1 = up, 0 = down, 2 = pending / maintenance */
function buildBars(hbList) {
const bars = Array(BAR_COUNT).fill('empty');
if (!hbList || hbList.length === 0) return bars;
// Take the last BAR_COUNT entries (most recent are at the END)
const slice = hbList.slice(-BAR_COUNT);
// Place them right-aligned (newest = rightmost)
const offset = BAR_COUNT - slice.length;
slice.forEach((hb, i) => {
const s = hb.status;
bars[offset + i] = s === 1 ? 'up' : s === 0 ? 'down' : 'partial';
});
return bars;
}
/* ─────────────────────────────────────────────────────────────
RENDER
─────────────────────────────────────────────────────────── */
function renderMonitorRow(monitor, hbList, uptimeList) {
const bars = buildBars(hbList);
const lastHb = hbList && hbList.length ? hbList[hbList.length - 1] : null;
const isUp = lastHb?.status === 1;
const cls = lastHb ? (isUp ? 'operational' : 'outage') : 'degraded';
const label = statusLabel(cls);
const u24 = pct(uptimeList?.[`${monitor.id}_24`]);
const u720 = pct(uptimeList?.[`${monitor.id}_720`]);
const u8760 = pct(uptimeList?.[`${monitor.id}_8760`]);
const displayPct = u720 ?? u24 ?? '—';
const isGreat = displayPct !== '—' && parseFloat(displayPct) >= 99;
const barsHTML = bars.map(b =>
`<div class="bar ${b}" title="${b === 'up' ? '✓ Up' : b === 'down' ? '✗ Down' : b === 'partial' ? '~ Degraded' : 'No data'}"></div>`
).join('');
const summaryParts = [];
if (u24) summaryParts.push(`<span><span class="val">${u24}%</span> uptime (24h)</span>`);
if (u720) summaryParts.push(`<span><span class="val">${u720}%</span> uptime (30d)</span>`);
if (u8760) summaryParts.push(`<span><span class="val">${u8760}%</span> uptime (1y)</span>`);
return `
<div class="monitor-row">
<div class="monitor-top">
<div class="monitor-name-wrap">
<div class="monitor-status-dot ${cls}"></div>
<span class="monitor-name">${escHtml(monitor.name)}</span>
</div>
<span class="monitor-badge ${cls}">${label}</span>
</div>
<div class="bar-labels">
<span>${BAR_COUNT} checks ago</span>
<span>Now</span>
</div>
<div class="bars-wrap">
<div class="bars">${barsHTML}</div>
<span class="pct-label ${isGreat ? 'great' : ''}">${displayPct}${displayPct !== '—' ? '%' : ''}</span>
</div>
${summaryParts.length ? `<div class="uptime-summary">${summaryParts.join('')}</div>` : ''}
</div>`;
}
function renderGroupCard(group, heartbeatList, uptimeList) {
const monitors = group.monitorList || [];
// Determine group-level status
let anyDown = false, anyDegraded = false;
monitors.forEach(m => {
const hbs = heartbeatList?.[m.id] || [];
const last = hbs.length ? hbs[hbs.length - 1] : null;
if (!last) { anyDegraded = true; }
else if (last.status !== 1) anyDown = true;
});
const groupCls = anyDown ? 'outage' : anyDegraded ? 'degraded' : 'operational';
const rowsHTML = monitors.map(m =>
renderMonitorRow(m, heartbeatList?.[m.id] || [], uptimeList)
).join('');
return `
<div class="group-card">
<div class="group-header">
<span class="group-title">${escHtml(group.name)}</span>
<span class="group-status-badge ${groupCls}">${statusLabel(groupCls)}</span>
</div>
${rowsHTML}
</div>`;
}
function render({ page, hb }) {
const groups = page.publicGroupList || [];
const heartbeats = hb.heartbeatList || {};
const uptimes = hb.uptimeList || {};
// ── Overall status ──────────────────────────────────────
let allOp = true, anyOut = false;
groups.forEach(g => {
(g.monitorList || []).forEach(m => {
const hbs = heartbeats[m.id] || [];
const last = hbs.length ? hbs[hbs.length - 1] : null;
if (!last || last.status !== 1) { allOp = false; }
if (last && last.status === 0) anyOut = true;
});
});
const overallCls = allOp ? 'operational' : anyOut ? 'outage' : 'degraded';
const overallTitle = {
operational: 'All Systems Operational',
degraded: 'Some Systems Degraded',
outage: 'Major Service Disruption'
}[overallCls];
const overallSub = {
operational: 'All monitored services are functioning normally.',
degraded: 'Some services are experiencing intermittent issues. Our team is investigating.',
outage: 'One or more services are currently unavailable. Our team is working on a fix.'
}[overallCls];
document.getElementById('bannerInner').innerHTML = `
<div class="status-orb ${overallCls}"></div>
<div>
<div class="banner-title">${overallTitle}</div>
<div class="banner-sub">${overallSub}</div>
</div>`;
// ── Group cards ─────────────────────────────────────────
const html = groups.map(g => renderGroupCard(g, heartbeats, uptimes)).join('');
document.getElementById('mainContent').innerHTML = html || '<p style="color:var(--text-3);font-size:14px">No monitors found on this status page.</p>';
// ── Footer timestamp ────────────────────────────────────
const now = new Date();
document.getElementById('footerUpdated').textContent =
`Updated ${now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} · Auto-refreshes every 60 s`;
}
/* ─────────────────────────────────────────────────────────────
LOAD
─────────────────────────────────────────────────────────── */
async function load() {
try {
const data = await fetchAll();
render(data);
} catch (err) {
document.getElementById('bannerInner').innerHTML = `
<div class="error-card" style="width:100%">
⚠️ &nbsp;Unable to reach the monitoring service — it may be temporarily down or unreachable.<br>
<small style="opacity:.7">${escHtml(err.message)}</small>
</div>`;
}
}
function escHtml(str) {
return String(str).replace(/[&<>"']/g, c =>
({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[c]));
}
// Update clock in header
function updateClock() {
const el = document.getElementById('headerTime');
if (el) el.textContent = new Date().toLocaleString([], {
weekday: 'short', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit'
});
}
// Bootstrap
updateClock();
setInterval(updateClock, 30_000);
load();
setInterval(load, REFRESH_MS);
</script>
</body>
</html>