feat: add admin-only status page with cache stats
All checks were successful
Build and Push Docker Image / build (push) Successful in 29s
All checks were successful
Build and Push Docker Image / build (push) Successful in 29s
- New /api/dashboard/status endpoint (admin-only, 403 for non-admins) - Returns server info (uptime, Node version, memory usage) - Returns polling mode and interval - Returns cache stats: entry count, total size, per-key breakdown with item count, size in KB, and TTL remaining - Status button in admin controls header - Collapsible status panel with grid layout - Responsive: single column on mobile
This commit is contained in:
@@ -21,6 +21,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('logout-btn').addEventListener('click', handleLogout);
|
||||
document.getElementById('refresh-rate').addEventListener('change', handleRefreshRateChange);
|
||||
document.getElementById('show-all-toggle').addEventListener('change', handleShowAllToggle);
|
||||
document.getElementById('status-btn').addEventListener('click', toggleStatusPanel);
|
||||
});
|
||||
|
||||
function initThemeSwitcher() {
|
||||
@@ -568,6 +569,68 @@ function escapeHtml(str) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function toggleStatusPanel() {
|
||||
const panel = document.getElementById('status-panel');
|
||||
if (panel.style.display !== 'none') {
|
||||
panel.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
panel.style.display = 'block';
|
||||
panel.innerHTML = '<p class="status-loading">Loading status…</p>';
|
||||
try {
|
||||
const res = await fetch('/api/dashboard/status');
|
||||
if (!res.ok) throw new Error('Failed to fetch status');
|
||||
const data = await res.json();
|
||||
renderStatusPanel(data, panel);
|
||||
} catch (err) {
|
||||
panel.innerHTML = '<p class="status-error">Failed to load status.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderStatusPanel(data, panel) {
|
||||
const s = data.server;
|
||||
const hrs = Math.floor(s.uptimeSeconds / 3600);
|
||||
const mins = Math.floor((s.uptimeSeconds % 3600) / 60);
|
||||
const secs = s.uptimeSeconds % 60;
|
||||
const uptime = `${hrs}h ${mins}m ${secs}s`;
|
||||
|
||||
const totalKB = (data.cache.totalSizeBytes / 1024).toFixed(1);
|
||||
|
||||
let html = `
|
||||
<div class="status-header">
|
||||
<h3>Server Status</h3>
|
||||
<button class="status-close" onclick="document.getElementById('status-panel').style.display='none'">×</button>
|
||||
</div>
|
||||
<div class="status-grid">
|
||||
<div class="status-card">
|
||||
<div class="status-card-title">Server</div>
|
||||
<div class="status-row"><span>Uptime</span><span>${uptime}</span></div>
|
||||
<div class="status-row"><span>Node</span><span>${escapeHtml(s.nodeVersion)}</span></div>
|
||||
<div class="status-row"><span>Memory (RSS)</span><span>${s.memoryUsageMB} MB</span></div>
|
||||
<div class="status-row"><span>Heap</span><span>${s.heapUsedMB} / ${s.heapTotalMB} MB</span></div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<div class="status-card-title">Polling</div>
|
||||
<div class="status-row"><span>Mode</span><span>${data.polling.enabled ? 'Background' : 'On-demand'}</span></div>
|
||||
${data.polling.enabled ? `<div class="status-row"><span>Interval</span><span>${data.polling.intervalMs / 1000}s</span></div>` : ''}
|
||||
</div>
|
||||
<div class="status-card status-card-wide">
|
||||
<div class="status-card-title">Cache (${data.cache.entryCount} entries, ${totalKB} KB)</div>
|
||||
<table class="status-table">
|
||||
<thead><tr><th>Key</th><th>Items</th><th>Size</th><th>TTL</th></tr></thead>
|
||||
<tbody>`;
|
||||
|
||||
for (const e of data.cache.entries) {
|
||||
const sizeStr = e.sizeBytes > 1024 ? (e.sizeBytes / 1024).toFixed(1) + ' KB' : e.sizeBytes + ' B';
|
||||
const ttlStr = e.expired ? '<span class="status-expired">expired</span>' : (e.ttlRemainingMs / 1000).toFixed(0) + 's';
|
||||
const items = e.itemCount !== null ? e.itemCount : '—';
|
||||
html += `<tr><td><code>${escapeHtml(e.key)}</code></td><td>${items}</td><td>${sizeStr}</td><td>${ttlStr}</td></tr>`;
|
||||
}
|
||||
|
||||
html += `</tbody></table></div></div>`;
|
||||
panel.innerHTML = html;
|
||||
}
|
||||
|
||||
function formatSize(size) {
|
||||
if (!size) return 'N/A';
|
||||
// If already a formatted string (e.g., "21.5 GB"), return as-is
|
||||
|
||||
Reference in New Issue
Block a user