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:
+36
-7
@@ -213,7 +213,10 @@ async function fetchUserDownloads(isInitialLoad = false) {
|
|||||||
hideError();
|
hideError();
|
||||||
|
|
||||||
try {
|
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 response = await fetch(url);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
@@ -624,7 +627,6 @@ function renderStatusPanel(data, panel) {
|
|||||||
const uptime = `${hrs}h ${mins}m ${secs}s`;
|
const uptime = `${hrs}h ${mins}m ${secs}s`;
|
||||||
|
|
||||||
const totalKB = (data.cache.totalSizeBytes / 1024).toFixed(1);
|
const totalKB = (data.cache.totalSizeBytes / 1024).toFixed(1);
|
||||||
const refreshLabel = currentRefreshRate > 0 ? (currentRefreshRate / 1000) + 's' : 'Off';
|
|
||||||
|
|
||||||
let html = `
|
let html = `
|
||||||
<div class="status-header">
|
<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 class="status-row"><span>Heap</span><span>${s.heapUsedMB} / ${s.heapTotalMB} MB</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-card">
|
<div class="status-card">
|
||||||
<div class="status-card-title">Polling</div>
|
<div class="status-card-title">Data Refresh</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>` : ''}
|
const pollIntervalMs = data.polling.intervalMs;
|
||||||
<div class="status-row"><span>Client refresh</span><span>${refreshLabel}</span></div>
|
const clients = data.clients || [];
|
||||||
</div>`;
|
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
|
// Poll timings card
|
||||||
const lp = data.polling.lastPoll;
|
const lp = data.polling.lastPoll;
|
||||||
|
|||||||
@@ -862,6 +862,25 @@ body {
|
|||||||
font-size: 0.7rem;
|
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 {
|
.status-timings {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -90,6 +90,19 @@ function getRadarrLink(movie) {
|
|||||||
return `${movie._instanceUrl}/movie/${movie.titleSlug}`;
|
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
|
// Get user downloads for authenticated user
|
||||||
router.get('/user-downloads', async (req, res) => {
|
router.get('/user-downloads', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -106,6 +119,15 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
const showAll = isAdmin && req.query.showAll === 'true';
|
const showAll = isAdmin && req.query.showAll === 'true';
|
||||||
console.log(`[Dashboard] Serving downloads for user: ${user.name} (${username}), isAdmin: ${isAdmin}, showAll: ${showAll}`);
|
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
|
// 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
|
// The fetched data is cached (30s TTL) so subsequent requests from any user reuse it
|
||||||
if (!POLLING_ENABLED && !cache.get('poll:sab-queue')) {
|
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,
|
intervalMs: POLLING_ENABLED ? require('../utils/poller').POLL_INTERVAL : 0,
|
||||||
lastPoll: getLastPollTimings()
|
lastPoll: getLastPollTimings()
|
||||||
},
|
},
|
||||||
cache: cacheStats
|
cache: cacheStats,
|
||||||
|
clients: getActiveClients()
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: 'Failed to get status', details: err.message });
|
res.status(500).json({ error: 'Failed to get status', details: err.message });
|
||||||
|
|||||||
Reference in New Issue
Block a user