feat: live-updating status panel with per-task poll timings
- Each service fetch is individually timed (SABnzbd, Sonarr, Radarr, qBit) - Status panel shows timing bar chart with ms per task and total - Shows 'Last Poll' age that updates live - Shows client refresh rate (1s/5s/10s/Off) - Status panel auto-refreshes in sync with dashboard refresh cycle - Changing refresh rate restarts the status panel refresh too - TTL counters update live on each refresh
This commit is contained in:
@@ -51,6 +51,14 @@ function handleRefreshRateChange(e) {
|
||||
const rate = parseInt(e.target.value);
|
||||
currentRefreshRate = rate;
|
||||
startAutoRefresh();
|
||||
// Restart status panel refresh if it's open
|
||||
const statusPanel = document.getElementById('status-panel');
|
||||
if (statusPanel && statusPanel.style.display !== 'none') {
|
||||
if (statusRefreshHandle) { clearInterval(statusRefreshHandle); statusRefreshHandle = null; }
|
||||
if (currentRefreshRate > 0) {
|
||||
statusRefreshHandle = setInterval(refreshStatusPanel, currentRefreshRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleShowAllToggle(e) {
|
||||
@@ -569,21 +577,42 @@ function escapeHtml(str) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
let statusRefreshHandle = null;
|
||||
|
||||
async function toggleStatusPanel() {
|
||||
const panel = document.getElementById('status-panel');
|
||||
if (panel.style.display !== 'none') {
|
||||
panel.style.display = 'none';
|
||||
if (statusRefreshHandle) { clearInterval(statusRefreshHandle); statusRefreshHandle = null; }
|
||||
return;
|
||||
}
|
||||
panel.style.display = 'block';
|
||||
panel.innerHTML = '<p class="status-loading">Loading status…</p>';
|
||||
await refreshStatusPanel();
|
||||
// Auto-refresh in sync with dashboard refresh rate
|
||||
if (statusRefreshHandle) clearInterval(statusRefreshHandle);
|
||||
if (currentRefreshRate > 0) {
|
||||
statusRefreshHandle = setInterval(refreshStatusPanel, currentRefreshRate);
|
||||
}
|
||||
}
|
||||
|
||||
function closeStatusPanel() {
|
||||
document.getElementById('status-panel').style.display = 'none';
|
||||
if (statusRefreshHandle) { clearInterval(statusRefreshHandle); statusRefreshHandle = null; }
|
||||
}
|
||||
|
||||
async function refreshStatusPanel() {
|
||||
const panel = document.getElementById('status-panel');
|
||||
if (!panel || panel.style.display === 'none') return;
|
||||
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>';
|
||||
// Don't overwrite panel on transient error during auto-refresh
|
||||
if (!panel.innerHTML || panel.innerHTML.includes('status-loading')) {
|
||||
panel.innerHTML = '<p class="status-error">Failed to load status.</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,11 +624,12 @@ function renderStatusPanel(data, panel) {
|
||||
const uptime = `${hrs}h ${mins}m ${secs}s`;
|
||||
|
||||
const totalKB = (data.cache.totalSizeBytes / 1024).toFixed(1);
|
||||
const refreshLabel = currentRefreshRate > 0 ? (currentRefreshRate / 1000) + 's' : 'Off';
|
||||
|
||||
let html = `
|
||||
<div class="status-header">
|
||||
<h3>Server Status</h3>
|
||||
<button class="status-close" onclick="document.getElementById('status-panel').style.display='none'">×</button>
|
||||
<button class="status-close" onclick="closeStatusPanel()">×</button>
|
||||
</div>
|
||||
<div class="status-grid">
|
||||
<div class="status-card">
|
||||
@@ -613,7 +643,31 @@ function renderStatusPanel(data, panel) {
|
||||
<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-row"><span>Client refresh</span><span>${refreshLabel}</span></div>
|
||||
</div>`;
|
||||
|
||||
// Poll timings card
|
||||
const lp = data.polling.lastPoll;
|
||||
if (lp) {
|
||||
const pollAge = Math.round((Date.now() - new Date(lp.timestamp).getTime()) / 1000);
|
||||
html += `
|
||||
<div class="status-card status-card-wide">
|
||||
<div class="status-card-title">Last Poll (${lp.totalMs}ms total, ${pollAge}s ago)</div>
|
||||
<div class="status-timings">`;
|
||||
for (const t of lp.tasks) {
|
||||
const barWidth = lp.totalMs > 0 ? Math.max(2, (t.ms / lp.totalMs) * 100) : 0;
|
||||
html += `
|
||||
<div class="timing-row">
|
||||
<span class="timing-label">${escapeHtml(t.label)}</span>
|
||||
<div class="timing-bar-bg"><div class="timing-bar" style="width:${barWidth.toFixed(1)}%"></div></div>
|
||||
<span class="timing-value">${t.ms}ms</span>
|
||||
</div>`;
|
||||
}
|
||||
html += `</div></div>`;
|
||||
}
|
||||
|
||||
// Cache table
|
||||
html += `
|
||||
<div class="status-card status-card-wide">
|
||||
<div class="status-card-title">Cache (${data.cache.entryCount} entries, ${totalKB} KB)</div>
|
||||
<table class="status-table">
|
||||
|
||||
@@ -862,6 +862,51 @@ body {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.status-timings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.timing-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.timing-label {
|
||||
width: 110px;
|
||||
flex-shrink: 0;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.timing-bar-bg {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: var(--border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timing-bar {
|
||||
height: 100%;
|
||||
background: var(--accent);
|
||||
border-radius: 4px;
|
||||
min-width: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.timing-value {
|
||||
width: 50px;
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.status-loading, .status-error {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
|
||||
Reference in New Issue
Block a user