AstroCom-API/public/status/index.html
2026-06-29 10:25:11 -06:00

292 lines
8 KiB
HTML

<!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="/assets/css/theme.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>
.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: #1e1e1e;
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 bg-dark">
<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>