Status page test
This commit is contained in:
parent
378517faeb
commit
b39b7aff9c
|
|
@ -53,7 +53,7 @@
|
|||
<a href="/about">About (WIP)</a><span> </span>
|
||||
<a href="/directory">Directory</a><span> </span>
|
||||
<a href="/validator">Block Availability</a> <span> </span>
|
||||
<a href="/status" class="disabled" aria-disabled="true" tabindex="-1" style="pointer-events: none; opacity: 0.5;">Status (WIP)</a><span> </span>
|
||||
<a href="/status">Status</a><span> </span>
|
||||
<a href="/discord">Discord Server</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
296
public/status/index.html
Normal file
296
public/status/index.html
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/assets/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.1/css/all.min.css" />
|
||||
<title>AstroCom — Server Status</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #111;
|
||||
color: #f0f0f0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background-color: #1e1e1e;
|
||||
border: 1px solid #2e2e2e;
|
||||
border-radius: 10px;
|
||||
padding: 18px 20px;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.status-card .block-label {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.status-card .ping-badge {
|
||||
font-size: 0.78rem;
|
||||
padding: 3px 8px;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-card .status-indicator {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.indicator-online {
|
||||
background-color: #28a745;
|
||||
box-shadow: 0 0 6px #28a745;
|
||||
}
|
||||
|
||||
.indicator-offline {
|
||||
background-color: #dc3545;
|
||||
box-shadow: 0 0 6px #dc3545;
|
||||
}
|
||||
|
||||
.indicator-unknown {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
.status-card.card-online {
|
||||
border-left: 4px solid #28a745;
|
||||
}
|
||||
|
||||
.status-card.card-offline {
|
||||
border-left: 4px solid #dc3545;
|
||||
}
|
||||
|
||||
.status-card .ts-label {
|
||||
font-size: 0.72rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.summary-bar {
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #2e2e2e;
|
||||
border-radius: 10px;
|
||||
padding: 14px 20px;
|
||||
}
|
||||
|
||||
.summary-bar .dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
#refreshBtn {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
color: #888;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
color: #dc3545;
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.ping-good { background-color: #1a3d2b; color: #5cdb95; }
|
||||
.ping-warn { background-color: #3d2e1a; color: #f0a500; }
|
||||
.ping-bad { background-color: #3d1a1a; color: #ff6b6b; }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color:#0d0d0d; border-bottom: 1px solid #2e2e2e;">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand fw-bold" href="/">AstroCom</a>
|
||||
<span id="footer"></span>
|
||||
<div class="ms-auto d-flex align-items-center gap-2">
|
||||
<a href="/user" class="btn btn-outline-light btn-sm">User Login</a>
|
||||
<a href="/admin" class="btn btn-outline-light btn-sm">Admin Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid px-4 py-4">
|
||||
<!-- Header row -->
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h2 class="mb-0 fw-bold">Server Status</h2>
|
||||
<span class="last-updated" id="lastUpdated">Loading…</span>
|
||||
</div>
|
||||
<button class="btn btn-outline-secondary btn-sm" id="refreshBtn" title="Refresh">
|
||||
<i class="fa-solid fa-rotate-right me-1"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Summary bar -->
|
||||
<div class="summary-bar d-flex align-items-center gap-4 mb-4 flex-wrap" id="summaryBar">
|
||||
<span><span class="dot" style="background:#28a745;"></span><strong id="onlineCount">—</strong> Online</span>
|
||||
<span><span class="dot" style="background:#dc3545;"></span><strong id="offlineCount">—</strong> Offline</span>
|
||||
<span><strong id="totalCount">—</strong> Total Blocks</span>
|
||||
<span>Avg ping: <strong id="avgPing">—</strong></span>
|
||||
</div>
|
||||
|
||||
<!-- Grid -->
|
||||
<div id="statusGrid" class="grid-container">
|
||||
<div class="loading-spinner">
|
||||
<div class="spinner-border spinner-border-sm text-secondary" role="status"></div>
|
||||
<span>Fetching status…</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const AUTO_REFRESH_MS = 60000; // auto-refresh every 60 s
|
||||
|
||||
function formatRelative(ts) {
|
||||
const diff = Math.floor((Date.now() - ts) / 1000);
|
||||
if (diff < 5) return 'just now';
|
||||
if (diff < 60) return `${diff}s ago`;
|
||||
const m = Math.floor(diff / 60);
|
||||
if (m < 60) return `${m}m ago`;
|
||||
return `${Math.floor(m / 60)}h ago`;
|
||||
}
|
||||
|
||||
function pingClass(ms) {
|
||||
if (ms === null || ms === undefined) return '';
|
||||
if (ms < 80) return 'ping-good';
|
||||
if (ms < 200) return 'ping-warn';
|
||||
return 'ping-bad';
|
||||
}
|
||||
|
||||
function renderCard(block, data) {
|
||||
const online = data.online === true;
|
||||
const ping = data.ping;
|
||||
const ts = data.timestamp;
|
||||
|
||||
const indicator = `<span class="status-indicator ${online ? 'indicator-online' : 'indicator-offline'}"></span>`;
|
||||
const statusText = online ? 'Online' : 'Offline';
|
||||
|
||||
let pingHtml = '';
|
||||
if (online && ping !== null && ping !== undefined) {
|
||||
pingHtml = `<span class="ping-badge ${pingClass(ping)}">${Math.round(ping)} ms</span>`;
|
||||
} else if (!online && data.error) {
|
||||
pingHtml = `<span class="ping-badge" style="background:#2a1a1a;color:#ff6b6b;font-size:0.72rem;" title="${data.error}">
|
||||
<i class="fa-solid fa-triangle-exclamation me-1"></i>${data.error}
|
||||
</span>`;
|
||||
}
|
||||
|
||||
const tsHtml = ts ? `<div class="ts-label mt-1">Checked ${formatRelative(ts)}</div>` : '';
|
||||
|
||||
return `
|
||||
<div class="status-card ${online ? 'card-online' : 'card-offline'}">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="block-label">${block}</div>
|
||||
${pingHtml}
|
||||
</div>
|
||||
<div class="mt-2" style="font-size:0.9rem;">
|
||||
${indicator}<span style="vertical-align:middle;">${statusText}</span>
|
||||
</div>
|
||||
${tsHtml}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
const grid = document.getElementById('statusGrid');
|
||||
grid.innerHTML = `<div class="loading-spinner">
|
||||
<div class="spinner-border spinner-border-sm text-secondary" role="status"></div>
|
||||
<span>Fetching status…</span>
|
||||
</div>`;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/servers');
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
|
||||
const blocks = Object.keys(data).sort((a, b) => Number(a) - Number(b));
|
||||
|
||||
if (blocks.length === 0) {
|
||||
grid.innerHTML = `<div class="error-msg"><i class="fa-solid fa-circle-info me-2"></i>No servers found.</div>`;
|
||||
updateSummary([], data);
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = blocks.map(b => renderCard(b, data[b])).join('');
|
||||
updateSummary(blocks, data);
|
||||
|
||||
document.getElementById('lastUpdated').textContent =
|
||||
`Last updated: ${new Date().toLocaleTimeString()}`;
|
||||
} catch (err) {
|
||||
grid.innerHTML = `<div class="error-msg">
|
||||
<i class="fa-solid fa-circle-exclamation me-2"></i>
|
||||
Failed to load status: ${err.message}
|
||||
</div>`;
|
||||
document.getElementById('lastUpdated').textContent = 'Failed to fetch';
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSummary(blocks, data) {
|
||||
let online = 0, offline = 0, totalPing = 0, pingCount = 0;
|
||||
blocks.forEach(b => {
|
||||
if (data[b].online) {
|
||||
online++;
|
||||
if (data[b].ping !== null && data[b].ping !== undefined) {
|
||||
totalPing += data[b].ping;
|
||||
pingCount++;
|
||||
}
|
||||
} else {
|
||||
offline++;
|
||||
}
|
||||
});
|
||||
document.getElementById('onlineCount').textContent = online;
|
||||
document.getElementById('offlineCount').textContent = offline;
|
||||
document.getElementById('totalCount').textContent = blocks.length;
|
||||
document.getElementById('avgPing').textContent = pingCount
|
||||
? `${Math.round(totalPing / pingCount)} ms` : '—';
|
||||
}
|
||||
|
||||
document.getElementById('refreshBtn').addEventListener('click', () => {
|
||||
loadStatus();
|
||||
});
|
||||
|
||||
// Initial load + auto-refresh
|
||||
loadStatus();
|
||||
setInterval(loadStatus, AUTO_REFRESH_MS);
|
||||
</script>
|
||||
|
||||
<script src="/assets/js/bootstrap.min.js"></script>
|
||||
<script src="/assets/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/assets/js/jquery.min.js"></script>
|
||||
<script>
|
||||
$(function () { $("#footer").load("/footer"); });
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Reference in a new issue