const express = require('express'); const router = express.Router(); const axios = require('axios'); const { mapTorrentToDownload } = require('../utils/qbittorrent'); const cache = require('../utils/cache'); const { pollAllServices, getLastPollTimings, POLLING_ENABLED } = require('../utils/poller'); const { getSonarrInstances, getRadarrInstances } = require('../utils/config'); const EMBY_URL = process.env.EMBY_URL; const EMBY_API_KEY = process.env.EMBY_API_KEY; // Helper function to extract poster/cover art URL from a movie or series object function getCoverArt(item) { if (!item || !item.images) return null; const poster = item.images.find(img => img.coverType === 'poster'); if (poster) return poster.remoteUrl || poster.url || null; // Fallback to fanart if no poster const fanart = item.images.find(img => img.coverType === 'fanart'); return fanart ? (fanart.remoteUrl || fanart.url || null) : null; } // Return all resolved tag labels for a series/movie. // For Radarr: tags is array of IDs, tagMap is id -> label mapping. // For Sonarr: tags are objects with a label property. function extractAllTags(tags, tagMap) { if (!tags || tags.length === 0) return []; if (tagMap) { return tags.map(id => tagMap.get(id)).filter(Boolean); } return tags.map(t => t && t.label).filter(Boolean); } // Return the tag label that matches the current username, or null. function extractUserTag(tags, tagMap, username) { const allLabels = extractAllTags(tags, tagMap); if (!allLabels.length) return null; if (username) { const match = allLabels.find(label => tagMatchesUser(label, username)); if (match) return match; } return null; } // Replicate Ombi's StringHelper.SanitizeTagLabel: lowercase, replace non-alphanumeric with hyphen, collapse, trim function sanitizeTagLabel(input) { if (!input) return ''; return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); } // Check if a tag matches the username: exact match first, then sanitized match function tagMatchesUser(tag, username) { if (!tag || !username) return false; const tagLower = tag.toLowerCase(); // Exact match (handles users whose tags weren't mangled) if (tagLower === username) return true; // Sanitized match (handles Ombi-mangled tags for email-style usernames) if (tagLower === sanitizeTagLabel(username)) return true; return false; } // Extract import issues from a Sonarr/Radarr queue record function getImportIssues(queueRecord) { if (!queueRecord) return null; const state = queueRecord.trackedDownloadState; const status = queueRecord.trackedDownloadStatus; if (state !== 'importPending' && status !== 'warning' && status !== 'error') return null; const messages = []; if (queueRecord.statusMessages && queueRecord.statusMessages.length > 0) { for (const sm of queueRecord.statusMessages) { if (sm.messages && sm.messages.length > 0) { messages.push(...sm.messages); } else if (sm.title) { messages.push(sm.title); } } } if (queueRecord.errorMessage) { messages.push(queueRecord.errorMessage); } if (messages.length === 0) return null; return messages; } // Helper to build Sonarr web UI link for a series function getSonarrLink(series) { if (!series || !series._instanceUrl || !series.titleSlug) return null; return `${series._instanceUrl}/series/${series.titleSlug}`; } // Helper to build Radarr web UI link for a movie function getRadarrLink(movie) { if (!movie || !movie._instanceUrl || !movie.titleSlug) return null; 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 { // Get authenticated user from cookie const userCookie = req.cookies.emby_user; if (!userCookie) { return res.status(401).json({ error: 'Not authenticated' }); } const user = JSON.parse(userCookie); const username = user.name.toLowerCase(); const usernameSanitized = sanitizeTagLabel(user.name); const isAdmin = !!user.isAdmin; 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')) { console.log(`[Dashboard] Cache expired and polling disabled, fetching on-demand...`); await pollAllServices(); } // Read all data from cache const sabQueueData = cache.get('poll:sab-queue') || { slots: [] }; const sabHistoryData = cache.get('poll:sab-history') || { slots: [] }; const sonarrTagsResults = cache.get('poll:sonarr-tags') || []; const sonarrQueueData = cache.get('poll:sonarr-queue') || { records: [] }; const sonarrHistoryData = cache.get('poll:sonarr-history') || { records: [] }; const radarrQueueData = cache.get('poll:radarr-queue') || { records: [] }; const radarrHistoryData = cache.get('poll:radarr-history') || { records: [] }; const radarrTagsData = cache.get('poll:radarr-tags') || []; const qbittorrentTorrents = cache.get('poll:qbittorrent') || []; // Wrap in the structure the rest of the code expects const sabnzbdQueue = { data: { queue: sabQueueData } }; const sabnzbdHistory = { data: { history: sabHistoryData } }; const sonarrQueue = { data: sonarrQueueData }; const sonarrHistory = { data: sonarrHistoryData }; const radarrQueue = { data: radarrQueueData }; const radarrHistory = { data: radarrHistoryData }; const radarrTags = { data: radarrTagsData }; // Build series/movie maps from embedded objects in queue records // (history is fetched without includeSeries/includeMovie for speed; // history matches fall back to the queue-built map via seriesId/movieId) const seriesMap = new Map(); for (const r of sonarrQueue.data.records) { if (r.series && r.seriesId) seriesMap.set(r.seriesId, r.series); } for (const r of sonarrHistory.data.records) { if (r.series && r.seriesId && !seriesMap.has(r.seriesId)) seriesMap.set(r.seriesId, r.series); } const moviesMap = new Map(); for (const r of radarrQueue.data.records) { if (r.movie && r.movieId) moviesMap.set(r.movieId, r.movie); } for (const r of radarrHistory.data.records) { if (r.movie && r.movieId && !moviesMap.has(r.movieId)) moviesMap.set(r.movieId, r.movie); } // Create tag maps (id -> label) const sonarrTagMap = new Map(sonarrTagsResults.flatMap(t => t.data || []).map(t => [t.id, t.label])); const radarrTagMap = new Map(radarrTags.data.map(t => [t.id, t.label])); console.log(`[Dashboard] Cache data - Series: ${seriesMap.size}, Movies: ${moviesMap.size}, qBit: ${qbittorrentTorrents.length}`); // Match SABnzbd downloads to Sonarr/Radarr activity const userDownloads = []; // Process SABnzbd queue const queueStatus = sabnzbdQueue.data.queue ? sabnzbdQueue.data.queue.status : null; const queueSpeed = sabnzbdQueue.data.queue ? sabnzbdQueue.data.queue.speed : null; const queueKbpersec = sabnzbdQueue.data.queue ? sabnzbdQueue.data.queue.kbpersec : null; console.log(`[Dashboard] Queue status: ${queueStatus}, speed: ${queueSpeed}, kbpersec: ${queueKbpersec}`); // Helper to determine status and speed function getSlotStatusAndSpeed(slot) { // If whole queue is paused, everything is paused with 0 speed if (queueStatus === 'Paused') { return { status: 'Paused', speed: '0' }; } // Use slot's actual status and queue speed return { status: slot.status || 'Unknown', speed: queueSpeed || queueKbpersec || '0' }; } if (sabnzbdQueue.data.queue && sabnzbdQueue.data.queue.slots) { for (const slot of sabnzbdQueue.data.queue.slots) { try { const nzbName = slot.filename || slot.nzbname; if (!nzbName) { console.log(`[Dashboard] Skipping slot with no filename/nzbname`); continue; } const slotState = getSlotStatusAndSpeed(slot); console.log(`[Dashboard] Slot ${nzbName}: status=${slotState.status}, speed=${slotState.speed}`); const nzbNameLower = nzbName.toLowerCase(); // Try to match with Sonarr const sonarrMatch = sonarrQueue.data.records.find(r => { const rTitle = (r.title || r.sourceTitle || '').toLowerCase(); return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle)); }); if (sonarrMatch && sonarrMatch.seriesId) { const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series; if (series) { const allTags = extractAllTags(series.tags, sonarrTagMap); const matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username); const hasAnyTag = allTags.length > 0; if (showAll ? hasAnyTag : !!matchedUserTag) { const dlObj = { type: 'series', title: nzbName, coverArt: getCoverArt(series), status: slotState.status, progress: slot.percentage, mb: slot.mb, mbmissing: slot.mbmissing, size: slot.size, speed: slotState.speed, eta: slot.timeleft, seriesName: series.title, episodeInfo: sonarrMatch, allTags, matchedUserTag: matchedUserTag || null }; const issues = getImportIssues(sonarrMatch); if (issues) dlObj.importIssues = issues; if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; dlObj.arrLink = getSonarrLink(series); } userDownloads.push(dlObj); } } } // Try to match with Radarr const radarrMatch = radarrQueue.data.records.find(r => { const rTitle = (r.title || r.sourceTitle || '').toLowerCase(); return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle)); }); if (radarrMatch && radarrMatch.movieId) { const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie; if (movie) { const allTags = extractAllTags(movie.tags, radarrTagMap); const matchedUserTag = extractUserTag(movie.tags, radarrTagMap, username); const hasAnyTag = allTags.length > 0; if (showAll ? hasAnyTag : !!matchedUserTag) { const dlObj = { type: 'movie', title: nzbName, coverArt: getCoverArt(movie), status: slotState.status, progress: slot.percentage, mb: slot.mb, mbmissing: slot.mbmissing, size: slot.size, speed: slotState.speed, eta: slot.timeleft, movieName: movie.title, movieInfo: radarrMatch, allTags, matchedUserTag: matchedUserTag || null }; const issues = getImportIssues(radarrMatch); if (issues) dlObj.importIssues = issues; if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = movie.path || null; dlObj.arrLink = getRadarrLink(movie); } userDownloads.push(dlObj); } } } } catch (err) { console.error(`[Dashboard] Error processing slot:`, err.message); console.error(`[Dashboard] Slot data:`, JSON.stringify(slot)); } } } // Process SABnzbd history if (sabnzbdHistory.data.history && sabnzbdHistory.data.history.slots) { for (const slot of sabnzbdHistory.data.history.slots) { try { const nzbName = slot.name || slot.nzb_name || slot.nzbname; if (!nzbName) { console.log(`[Dashboard] Skipping history slot with no name/nzb_name/nzbname`); continue; } const nzbNameLower = nzbName.toLowerCase(); // Try to match with Sonarr history const sonarrMatch = sonarrHistory.data.records.find(r => { const rTitle = (r.sourceTitle || r.title || '').toLowerCase(); return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle)); }); if (sonarrMatch && sonarrMatch.seriesId) { const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series; if (series) { const allTags = extractAllTags(series.tags, sonarrTagMap); const matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username); const hasAnyTag = allTags.length > 0; if (showAll ? hasAnyTag : !!matchedUserTag) { const dlObj = { type: 'series', title: nzbName, coverArt: getCoverArt(series), status: slot.status, size: slot.size, completedAt: slot.completed_time, seriesName: series.title, episodeInfo: sonarrMatch, allTags, matchedUserTag: matchedUserTag || null }; if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; dlObj.arrLink = getSonarrLink(series); } userDownloads.push(dlObj); } } } // Try to match with Radarr history const radarrMatch = radarrHistory.data.records.find(r => { const rTitle = (r.sourceTitle || r.title || '').toLowerCase(); return rTitle && (rTitle.includes(nzbNameLower) || nzbNameLower.includes(rTitle)); }); if (radarrMatch && radarrMatch.movieId) { const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie; if (movie) { const allTags = extractAllTags(movie.tags, radarrTagMap); const matchedUserTag = extractUserTag(movie.tags, radarrTagMap, username); const hasAnyTag = allTags.length > 0; if (showAll ? hasAnyTag : !!matchedUserTag) { const dlObj = { type: 'movie', title: nzbName, coverArt: getCoverArt(movie), status: slot.status, size: slot.size, completedAt: slot.completed_time, movieName: movie.title, movieInfo: radarrMatch, allTags, matchedUserTag: matchedUserTag || null }; if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = movie.path || null; dlObj.arrLink = getRadarrLink(movie); } userDownloads.push(dlObj); } } } } catch (err) { console.error(`[Dashboard] Error processing history slot:`, err.message); console.error(`[Dashboard] History slot data:`, JSON.stringify(slot)); } } } // Debug: show what queue records look like and which movies/series are tagged for this user console.log(`[Dashboard] Sonarr queue titles:`, sonarrQueue.data.records.map(r => ({ title: r.title, seriesId: r.seriesId }))); console.log(`[Dashboard] Radarr queue titles:`, radarrQueue.data.records.map(r => ({ title: r.title, movieId: r.movieId }))); console.log(`[Dashboard] Sonarr history titles:`, sonarrHistory.data.records.slice(0, 10).map(r => ({ title: r.title, seriesId: r.seriesId }))); console.log(`[Dashboard] Radarr history titles:`, radarrHistory.data.records.slice(0, 10).map(r => ({ title: r.title, movieId: r.movieId }))); console.log(`[Dashboard] Sonarr tag map:`, Array.from(sonarrTagMap.entries())); console.log(`[Dashboard] Radarr tag map:`, Array.from(radarrTagMap.entries())); // Show movies/series tagged for this user (from embedded objects in queue/history) const userMovies = Array.from(moviesMap.values()).filter(m => { return !!extractUserTag(m.tags, radarrTagMap, username); }); const userSeries = Array.from(seriesMap.values()).filter(s => { return !!extractUserTag(s.tags, sonarrTagMap, username); }); console.log(`[Dashboard] Movies tagged for ${username}:`, userMovies.map(m => m.title)); console.log(`[Dashboard] Series tagged for ${username}:`, userSeries.map(s => s.title)); // Process qBittorrent torrents - match to user-tagged Sonarr/Radarr activity console.log(`[Dashboard] Processing ${qbittorrentTorrents.length} qBittorrent torrents for user ${username}`); for (const torrent of qbittorrentTorrents) { try { const torrentName = torrent.name || ''; const torrentNameLower = torrentName.toLowerCase(); if (!torrentName) continue; console.log(`[Dashboard] Checking torrent "${torrentName}"`); // Try to match with Sonarr queue (user-tagged series) const sonarrMatch = sonarrQueue.data.records.find(r => { const rTitle = (r.title || r.sourceTitle || '').toLowerCase(); return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle)); }); if (sonarrMatch && sonarrMatch.seriesId) { const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series; if (series) { const allTags = extractAllTags(series.tags, sonarrTagMap); const matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username); const hasAnyTag = allTags.length > 0; if (showAll ? hasAnyTag : !!matchedUserTag) { console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr series "${series.title}"`); const download = mapTorrentToDownload(torrent); download.type = 'series'; download.coverArt = getCoverArt(series); download.seriesName = series.title; download.episodeInfo = sonarrMatch; download.allTags = allTags; download.matchedUserTag = matchedUserTag || null; const sonarrIssues = getImportIssues(sonarrMatch); if (sonarrIssues) download.importIssues = sonarrIssues; if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = series.path || null; download.arrLink = getSonarrLink(series); } userDownloads.push(download); continue; // Skip to next torrent } } } // Try to match with Radarr queue (user-tagged movies) const radarrMatch = radarrQueue.data.records.find(r => { const rTitle = (r.title || r.sourceTitle || '').toLowerCase(); return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle)); }); if (radarrMatch && radarrMatch.movieId) { const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie; if (movie) { const allTags = extractAllTags(movie.tags, radarrTagMap); const matchedUserTag = extractUserTag(movie.tags, radarrTagMap, username); const hasAnyTag = allTags.length > 0; if (showAll ? hasAnyTag : !!matchedUserTag) { console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr movie "${movie.title}"`); const download = mapTorrentToDownload(torrent); download.type = 'movie'; download.coverArt = getCoverArt(movie); download.movieName = movie.title; download.movieInfo = radarrMatch; download.allTags = allTags; download.matchedUserTag = matchedUserTag || null; const radarrIssues = getImportIssues(radarrMatch); if (radarrIssues) download.importIssues = radarrIssues; if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = movie.path || null; download.arrLink = getRadarrLink(movie); } userDownloads.push(download); continue; // Skip to next torrent } } } // Try to match with Sonarr history const sonarrHistoryMatch = sonarrHistory.data.records.find(r => { const rTitle = (r.sourceTitle || r.title || '').toLowerCase(); return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle)); }); if (sonarrHistoryMatch && sonarrHistoryMatch.seriesId) { const series = seriesMap.get(sonarrHistoryMatch.seriesId) || sonarrHistoryMatch.series; if (series) { const allTags = extractAllTags(series.tags, sonarrTagMap); const matchedUserTag = extractUserTag(series.tags, sonarrTagMap, username); const hasAnyTag = allTags.length > 0; if (showAll ? hasAnyTag : !!matchedUserTag) { console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr history "${series.title}"`); const download = mapTorrentToDownload(torrent); download.type = 'series'; download.coverArt = getCoverArt(series); download.seriesName = series.title; download.episodeInfo = sonarrHistoryMatch; download.allTags = allTags; download.matchedUserTag = matchedUserTag || null; if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = series.path || null; download.arrLink = getSonarrLink(series); } userDownloads.push(download); continue; } } } // Try to match with Radarr history const radarrHistoryMatch = radarrHistory.data.records.find(r => { const rTitle = (r.sourceTitle || r.title || '').toLowerCase(); return rTitle && (rTitle.includes(torrentNameLower) || torrentNameLower.includes(rTitle)); }); if (radarrHistoryMatch && radarrHistoryMatch.movieId) { const movie = moviesMap.get(radarrHistoryMatch.movieId) || radarrHistoryMatch.movie; if (movie) { const allTags = extractAllTags(movie.tags, radarrTagMap); const matchedUserTag = extractUserTag(movie.tags, radarrTagMap, username); const hasAnyTag = allTags.length > 0; if (showAll ? hasAnyTag : !!matchedUserTag) { console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr history "${movie.title}"`); const download = mapTorrentToDownload(torrent); download.type = 'movie'; download.coverArt = getCoverArt(movie); download.movieName = movie.title; download.movieInfo = radarrHistoryMatch; download.allTags = allTags; download.matchedUserTag = matchedUserTag || null; if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = movie.path || null; download.arrLink = getRadarrLink(movie); } userDownloads.push(download); continue; } } } } catch (err) { console.error(`[Dashboard] Error processing torrent:`, err.message); console.error(`[Dashboard] Torrent data:`, JSON.stringify(torrent)); } } console.log(`[Dashboard] Found ${userDownloads.length} downloads for user ${username}`); console.log(`[Dashboard] Sending ${userDownloads.length} downloads`); if (userDownloads.length > 0) { console.log(`[Dashboard] First download:`, JSON.stringify(userDownloads[0])); } res.json({ user: user.name, isAdmin: isAdmin, downloads: userDownloads }); } catch (error) { console.error(`[Dashboard] Error fetching user downloads:`, error.message); console.error(`[Dashboard] Full error:`, error); res.status(500).json({ error: 'Failed to fetch user downloads', details: error.message }); } }); // Get all users with their download counts router.get('/user-summary', async (req, res) => { try { const sonarrInstances = getSonarrInstances(); const radarrInstances = getRadarrInstances(); // Get all Emby users const usersResponse = await axios.get(`${EMBY_URL}/Users`, { headers: { 'X-MediaBrowser-Token': EMBY_API_KEY } }); // Get all series, movies, and tags from all instances const [sonarrSeriesResults, sonarrTagsResults, radarrMoviesResults, radarrTagsResults] = await Promise.all([ Promise.all(sonarrInstances.map(inst => axios.get(`${inst.url}/api/v3/series`, { headers: { 'X-Api-Key': inst.apiKey } }).then(r => r.data).catch(() => []) )), Promise.all(sonarrInstances.map(inst => axios.get(`${inst.url}/api/v3/tag`, { headers: { 'X-Api-Key': inst.apiKey } }).then(r => r.data).catch(() => []) )), Promise.all(radarrInstances.map(inst => axios.get(`${inst.url}/api/v3/movie`, { headers: { 'X-Api-Key': inst.apiKey } }).then(r => r.data).catch(() => []) )), Promise.all(radarrInstances.map(inst => axios.get(`${inst.url}/api/v3/tag`, { headers: { 'X-Api-Key': inst.apiKey } }).then(r => r.data).catch(() => []) )) ]); const allSeries = sonarrSeriesResults.flat(); const sonarrTagMap = new Map(sonarrTagsResults.flat().map(t => [t.id, t.label])); const allMovies = radarrMoviesResults.flat(); const radarrTagMap = new Map(radarrTagsResults.flat().map(t => [t.id, t.label])); // Count downloads per user const userDownloads = {}; usersResponse.data.forEach(user => { userDownloads[user.Name.toLowerCase()] = { username: user.Name, seriesCount: 0, movieCount: 0 }; }); // Process series tags allSeries.forEach(series => { const tags = extractAllTags(series.tags, sonarrTagMap); tags.forEach(userTag => { const uname = userTag.toLowerCase(); if (userDownloads[uname]) userDownloads[uname].seriesCount++; }); }); // Process movie tags allMovies.forEach(movie => { const tags = extractAllTags(movie.tags, radarrTagMap); tags.forEach(userTag => { const uname = userTag.toLowerCase(); if (userDownloads[uname]) userDownloads[uname].movieCount++; }); }); res.json(Object.values(userDownloads)); } catch (error) { res.status(500).json({ error: 'Failed to fetch user summary', details: error.message }); } }); // Admin-only status page with cache stats router.get('/status', (req, res) => { try { const userCookie = req.cookies.emby_user; if (!userCookie) { return res.status(401).json({ error: 'Not authenticated' }); } const user = JSON.parse(userCookie); if (!user.isAdmin) { return res.status(403).json({ error: 'Admin access required' }); } const cacheStats = cache.getStats(); const uptime = process.uptime(); res.json({ server: { uptimeSeconds: Math.floor(uptime), nodeVersion: process.version, memoryUsageMB: Math.round(process.memoryUsage().rss / 1024 / 1024 * 10) / 10, heapUsedMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024 * 10) / 10, heapTotalMB: Math.round(process.memoryUsage().heapTotal / 1024 / 1024 * 10) / 10 }, polling: { enabled: POLLING_ENABLED, intervalMs: POLLING_ENABLED ? require('../utils/poller').POLL_INTERVAL : 0, lastPoll: getLastPollTimings() }, cache: cacheStats, clients: getActiveClients() }); } catch (err) { res.status(500).json({ error: 'Failed to get status', details: err.message }); } }); module.exports = router;