From c6b5aaf3ded2236375162dd57791bcae7c48df1c Mon Sep 17 00:00:00 2001 From: Gronod Date: Fri, 15 May 2026 23:23:25 +0100 Subject: [PATCH] feat: show import-pending red lozenge when Sonarr/Radarr has issues - Detect trackedDownloadState=importPending or status=warning/error - Extract statusMessages and errorMessage from queue records - Display red 'Import Pending' badge on download card header - Hover reveals tooltip with the specific issue messages - Visible to all users (not admin-only) --- public/app.js | 8 ++++++++ public/style.css | 32 ++++++++++++++++++++++++++++++++ server/routes/dashboard.js | 31 +++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/public/app.js b/public/app.js index 24a9798..acc12da 100644 --- a/public/app.js +++ b/public/app.js @@ -384,6 +384,14 @@ function createDownloadCard(download) { header.appendChild(type); header.appendChild(status); + + if (download.importIssues && download.importIssues.length > 0) { + const issueBadge = document.createElement('span'); + issueBadge.className = 'import-issue-badge'; + issueBadge.textContent = 'Import Pending'; + issueBadge.setAttribute('data-tooltip', download.importIssues.join('\n')); + header.appendChild(issueBadge); + } const title = document.createElement('h3'); title.className = 'download-title'; diff --git a/public/style.css b/public/style.css index 6022b79..bcfd666 100644 --- a/public/style.css +++ b/public/style.css @@ -688,6 +688,38 @@ body { color: var(--text-muted); } +/* ===== Import Issue Badge ===== */ +.import-issue-badge { + padding: 2px 8px; + border-radius: 10px; + font-size: 0.65rem; + font-weight: 600; + background: #ffebee; + color: #c62828; + cursor: help; + position: relative; + white-space: nowrap; +} + +.import-issue-badge:hover::after { + content: attr(data-tooltip); + position: absolute; + top: calc(100% + 6px); + left: 0; + background: #424242; + color: #fff; + padding: 8px 12px; + border-radius: 6px; + font-size: 0.7rem; + font-weight: 400; + white-space: pre-line; + max-width: 320px; + z-index: 100; + box-shadow: 0 2px 8px rgba(0,0,0,0.25); + line-height: 1.4; + pointer-events: none; +} + .download-user-badge { padding: 2px 8px; border-radius: 10px; diff --git a/server/routes/dashboard.js b/server/routes/dashboard.js index 4deb9bf..7afafec 100644 --- a/server/routes/dashboard.js +++ b/server/routes/dashboard.js @@ -59,6 +59,29 @@ function tagMatchesUser(tag, username) { 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; @@ -355,6 +378,8 @@ router.get('/user-downloads', async (req, res) => { episodeInfo: sonarrMatch, userTag: userTag }; + const issues = getImportIssues(sonarrMatch); + if (issues) dlObj.importIssues = issues; if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; @@ -391,6 +416,8 @@ router.get('/user-downloads', async (req, res) => { movieInfo: radarrMatch, userTag: userTag }; + const issues = getImportIssues(radarrMatch); + if (issues) dlObj.importIssues = issues; if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = movie.path || null; @@ -536,6 +563,8 @@ router.get('/user-downloads', async (req, res) => { download.seriesName = series.title; download.episodeInfo = sonarrMatch; download.userTag = userTag; + const sonarrIssues = getImportIssues(sonarrMatch); + if (sonarrIssues) download.importIssues = sonarrIssues; if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = series.path || null; @@ -565,6 +594,8 @@ router.get('/user-downloads', async (req, res) => { download.movieName = movie.title; download.movieInfo = radarrMatch; download.userTag = userTag; + const radarrIssues = getImportIssues(radarrMatch); + if (radarrIssues) download.importIssues = radarrIssues; if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = movie.path || null;