diff --git a/public/app.js b/public/app.js index 6b16970..ea037f6 100644 --- a/public/app.js +++ b/public/app.js @@ -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 = `
@@ -640,11 +642,38 @@ function renderStatusPanel(data, panel) {
Heap${s.heapUsedMB} / ${s.heapTotalMB} MB
-
Polling
-
Mode${data.polling.enabled ? 'Background' : 'On-demand'}
- ${data.polling.enabled ? `
Interval${data.polling.intervalMs / 1000}s
` : ''} -
Client refresh${refreshLabel}
-
`; +
Data Refresh
`; + + 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 += `
Background poll${pollIntervalMs / 1000}s
`; + } else { + html += `
Background pollDisabled
`; + } + + if (hasForegroundClient) { + html += `
Effective modeForeground ${fastestClient.refreshRateMs / 1000}s
`; + } else if (activeRefreshers.length > 0) { + html += `
Effective mode${data.polling.enabled ? 'Background' : 'On-demand'}
`; + } else { + html += `
Effective modeIdle (no active clients)
`; + } + + html += `
Active clients${clients.length}
`; + 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 += `
${escapeHtml(c.user)}${rate} (${age}s ago)
`; + } + + html += ``; // Poll timings card const lp = data.polling.lastPoll; diff --git a/public/style.css b/public/style.css index 74e7bed..b1d0188 100644 --- a/public/style.css +++ b/public/style.css @@ -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; diff --git a/server/routes/dashboard.js b/server/routes/dashboard.js index 640d57b..f8dd4b9 100644 --- a/server/routes/dashboard.js +++ b/server/routes/dashboard.js @@ -90,6 +90,19 @@ function getRadarrLink(movie) { return `${movie._instanceUrl}/movie/${movie.titleSlug}`; } +// Track active dashboard clients: Map +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 });