292 lines
8 KiB
HTML
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 < 100) return 'ping-good';
|
|
if (ms < 500) 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>
|