feat: status page shows effective refresh mode across all active clients
- Server tracks each client's refresh rate via query param on /user-downloads
- Active clients expire after 30s of no requests
- Status panel 'Data Refresh' card shows:
- Background poll interval (or Disabled)
- Effective mode: Background if all clients >= poll rate,
Foreground (with rate) if any client is faster, Idle if no clients
- Active client list with per-user refresh rate and last-seen age
- Foreground mode shown with orange badge for visibility
- Client refresh rate sent on every dashboard request
This commit is contained in:
@@ -213,7 +213,10 @@ async function fetchUserDownloads(isInitialLoad = false) {
|
||||
hideError();
|
||||
|
||||
try {
|
||||
const url = showAll ? '/api/dashboard/user-downloads?showAll=true' : '/api/dashboard/user-downloads';
|
||||
const params = new URLSearchParams();
|
||||
if (showAll) params.set('showAll', 'true');
|
||||
params.set('refreshRate', currentRefreshRate);
|
||||
const url = '/api/dashboard/user-downloads?' + params.toString();
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
@@ -624,7 +627,6 @@ 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">
|
||||
@@ -640,11 +642,38 @@ function renderStatusPanel(data, panel) {
|
||||
<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 class="status-row"><span>Client refresh</span><span>${refreshLabel}</span></div>
|
||||
</div>`;
|
||||
<div class="status-card-title">Data Refresh</div>`;
|
||||
|
||||
const pollIntervalMs = data.polling.intervalMs;
|
||||
const clients = data.clients || [];
|
||||
const activeRefreshers = clients.filter(c => c.refreshRateMs > 0);
|
||||
const fastestClient = activeRefreshers.length > 0
|
||||
? activeRefreshers.reduce((min, c) => c.refreshRateMs < min.refreshRateMs ? c : min)
|
||||
: null;
|
||||
const hasForegroundClient = fastestClient && data.polling.enabled && fastestClient.refreshRateMs < pollIntervalMs;
|
||||
|
||||
if (data.polling.enabled) {
|
||||
html += `<div class="status-row"><span>Background poll</span><span>${pollIntervalMs / 1000}s</span></div>`;
|
||||
} else {
|
||||
html += `<div class="status-row"><span>Background poll</span><span>Disabled</span></div>`;
|
||||
}
|
||||
|
||||
if (hasForegroundClient) {
|
||||
html += `<div class="status-row"><span>Effective mode</span><span class="status-fg-badge">Foreground ${fastestClient.refreshRateMs / 1000}s</span></div>`;
|
||||
} else if (activeRefreshers.length > 0) {
|
||||
html += `<div class="status-row"><span>Effective mode</span><span>${data.polling.enabled ? 'Background' : 'On-demand'}</span></div>`;
|
||||
} else {
|
||||
html += `<div class="status-row"><span>Effective mode</span><span>Idle (no active clients)</span></div>`;
|
||||
}
|
||||
|
||||
html += `<div class="status-row"><span>Active clients</span><span>${clients.length}</span></div>`;
|
||||
for (const c of clients) {
|
||||
const rate = c.refreshRateMs > 0 ? (c.refreshRateMs / 1000) + 's' : 'Off';
|
||||
const age = Math.round((Date.now() - c.lastSeen) / 1000);
|
||||
html += `<div class="status-row status-row-sub"><span>${escapeHtml(c.user)}</span><span>${rate} (${age}s ago)</span></div>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
|
||||
// Poll timings card
|
||||
const lp = data.polling.lastPoll;
|
||||
|
||||
@@ -862,6 +862,25 @@ body {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.status-fg-badge {
|
||||
background: #fff3e0;
|
||||
color: #e65100;
|
||||
padding: 1px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-row-sub {
|
||||
padding-left: 12px;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.status-row-sub span:first-child {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.status-timings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -90,6 +90,19 @@ function getRadarrLink(movie) {
|
||||
return `${movie._instanceUrl}/movie/${movie.titleSlug}`;
|
||||
}
|
||||
|
||||
// Track active dashboard clients: Map<username, { refreshRateMs, lastSeen }>
|
||||
const activeClients = new Map();
|
||||
const CLIENT_STALE_MS = 30000; // consider client gone after 30s of no requests
|
||||
|
||||
function getActiveClients() {
|
||||
const now = Date.now();
|
||||
// Prune stale clients
|
||||
for (const [key, client] of activeClients.entries()) {
|
||||
if (now - client.lastSeen > CLIENT_STALE_MS) activeClients.delete(key);
|
||||
}
|
||||
return Array.from(activeClients.values());
|
||||
}
|
||||
|
||||
// Get user downloads for authenticated user
|
||||
router.get('/user-downloads', async (req, res) => {
|
||||
try {
|
||||
@@ -106,6 +119,15 @@ router.get('/user-downloads', async (req, res) => {
|
||||
const showAll = isAdmin && req.query.showAll === 'true';
|
||||
console.log(`[Dashboard] Serving downloads for user: ${user.name} (${username}), isAdmin: ${isAdmin}, showAll: ${showAll}`);
|
||||
|
||||
// Track this client's refresh rate
|
||||
const clientRefreshRate = parseInt(req.query.refreshRate, 10);
|
||||
if (clientRefreshRate > 0) {
|
||||
activeClients.set(username, { user: user.name, refreshRateMs: clientRefreshRate, lastSeen: Date.now() });
|
||||
} else {
|
||||
// Client has refresh off or didn't send — still mark as seen but with no rate
|
||||
activeClients.set(username, { user: user.name, refreshRateMs: 0, lastSeen: Date.now() });
|
||||
}
|
||||
|
||||
// When polling is disabled, fetch on-demand if cache has expired
|
||||
// The fetched data is cached (30s TTL) so subsequent requests from any user reuse it
|
||||
if (!POLLING_ENABLED && !cache.get('poll:sab-queue')) {
|
||||
@@ -628,7 +650,8 @@ router.get('/status', (req, res) => {
|
||||
intervalMs: POLLING_ENABLED ? require('../utils/poller').POLL_INTERVAL : 0,
|
||||
lastPoll: getLastPollTimings()
|
||||
},
|
||||
cache: cacheStats
|
||||
cache: cacheStats,
|
||||
clients: getActiveClients()
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: 'Failed to get status', details: err.message });
|
||||
|
||||
Reference in New Issue
Block a user