الملفات
Ghayamah-Uptime/status-page/index.html
2026-09-17 22:11:14 +03:00

617 أسطر
23 KiB
HTML
خام اللوم التاريخ

هذا الملف يحتوي على أحرف Unicode غامضة

هذا الملف يحتوي على أحرف Unicode قد تُخلط مع أحرف أخرى. إذا كنت تعتقد أن هذا مقصود، يمكنك تجاهل هذا التحذير بأمان. استخدم زر الهروب للكشف عنها.

<!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>