From d87ad9f1c78c9009f5707218d2e6d007540001eb Mon Sep 17 00:00:00 2001 From: Gronod Date: Mon, 25 May 2026 08:28:16 +0100 Subject: [PATCH] fix: mobile request card overflow (#49) and admin arrLink active badges (#50), bump version to 1.7.19 --- CHANGELOG.md | 9 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- public/style.css | 8 ++++++- server/app.js | 2 +- server/index.js | 2 +- server/openapi.yaml | 23 ++++++++++++++++++- server/routes/dashboard.js | 34 +++++++++++++++++++++++++---- server/services/DownloadMatcher.js | 12 ++++++++++ tests/integration/dashboard.test.js | 1 + 10 files changed, 86 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eef4753..fd0472c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.7.19] - 2026-05-25 + +### Fixed + +- **Requests Mobile CSS Overflow Fix (Issue #49)** — Resolved the remaining viewport overflow on narrow screens by adding `min-width: 0` to `.request-card` to allow proper flexbox and grid shrinking, reducing mobile `.main-tabs` padding to `0 8px`, and tightening `.requests-container` mobile padding to `8px`. +- **Admin *arr Badge Links on Active Downloads (Issue #50)** — Fixed the missing Sonarr/Radarr badges and links on active downloads for admin users. Enabled robust `_instanceUrl` propagation during queue/history metadata compilation (`buildMetadataMaps`) and active matching (`DownloadMatcher.js`) to ensure link generation succeeds. Added complete schema documentation in OpenAPI. + +--- + ## [1.7.18] - 2026-05-24 ### Fixed diff --git a/package-lock.json b/package-lock.json index 85d83ac..1238653 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sofarr", - "version": "1.7.18", + "version": "1.7.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sofarr", - "version": "1.7.18", + "version": "1.7.19", "license": "MIT", "dependencies": { "axios": "^1.6.0", diff --git a/package.json b/package.json index b68f464..ca77866 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sofarr", - "version": "1.7.18", + "version": "1.7.19", "description": "A personal media download dashboard that shows your downloads 'so far' while you relax on the sofa waiting for your *arr services to finish", "main": "server/index.js", "scripts": { diff --git a/public/style.css b/public/style.css index 9f64d48..452da8c 100644 --- a/public/style.css +++ b/public/style.css @@ -1888,12 +1888,17 @@ body { /* ===== Mobile ===== */ @media (max-width: 768px) { + .main-tabs { + padding: 0 8px; + } + .requests-container { - padding: 12px; + padding: 8px; } .request-card { gap: 8px; + padding: 10px; } .request-meta { @@ -2258,6 +2263,7 @@ body { border: 1px solid var(--border); border-radius: 8px; transition: box-shadow 0.2s ease, border-color 0.2s ease; + min-width: 0; } .request-card:hover { diff --git a/server/app.js b/server/app.js index 2c04424..c30ef34 100644 --- a/server/app.js +++ b/server/app.js @@ -132,7 +132,7 @@ function createApp({ skipRateLimits = false } = {}) { * version: * type: string * description: sofarr version - * example: "1.7.16" + * example: "1.7.19" * x-code-samples: * - lang: curl * label: cURL diff --git a/server/index.js b/server/index.js index 1020fde..cc77b1b 100644 --- a/server/index.js +++ b/server/index.js @@ -249,7 +249,7 @@ app.use(express.json({ limit: '64kb' })); // prevent oversized JSON payloads * version: * type: string * description: sofarr version - * example: "1.7.16" + * example: "1.7.19" */ app.get('/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime(), version }); diff --git a/server/openapi.yaml b/server/openapi.yaml index df8f9b0..ae7a35a 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -22,7 +22,7 @@ info: ## SSE Streaming Real-time updates are available via Server-Sent Events at GET /api/dashboard/stream. - version: 1.7.18 + version: 1.7.19 contact: name: sofarr license: @@ -176,6 +176,27 @@ components: nullable: true description: Tooltip text for Ombi icon ("Request" or "Search") example: "Request" + arrLink: + type: string + nullable: true + format: uri + description: Sonarr/Radarr show/movie web UI link (admin-only) + example: "http://sonarr:8989/series/show-slug" + downloadPath: + type: string + nullable: true + description: Save path in download client (admin-only) + example: "/downloads/series/show-slug" + targetPath: + type: string + nullable: true + description: Target path in library (admin-only) + example: "/tv/show-slug" + arrInstanceKey: + type: string + nullable: true + description: Sonarr/Radarr instance API key (admin-only) + example: "api-key-here" DashboardPayload: type: object diff --git a/server/routes/dashboard.js b/server/routes/dashboard.js index 74e3e6e..3be2998 100644 --- a/server/routes/dashboard.js +++ b/server/routes/dashboard.js @@ -51,17 +51,43 @@ function readCacheSnapshot() { function buildMetadataMaps(snapshot) { const seriesMap = new Map(); for (const r of snapshot.sonarrQueue.data.records) { - if (r.series && r.seriesId) seriesMap.set(r.seriesId, r.series); + if (r.series && r.seriesId) { + if (!r.series._instanceUrl && r._instanceUrl) { + r.series._instanceUrl = r._instanceUrl; + } + seriesMap.set(r.seriesId, r.series); + } } for (const r of snapshot.sonarrHistory.data.records) { - if (r.series && r.seriesId && !seriesMap.has(r.seriesId)) seriesMap.set(r.seriesId, r.series); + if (r.series && r.seriesId) { + if (!r.series._instanceUrl && r._instanceUrl) { + r.series._instanceUrl = r._instanceUrl; + } + const existing = seriesMap.get(r.seriesId); + if (!existing || (!existing._instanceUrl && r.series._instanceUrl)) { + seriesMap.set(r.seriesId, r.series); + } + } } const moviesMap = new Map(); for (const r of snapshot.radarrQueue.data.records) { - if (r.movie && r.movieId) moviesMap.set(r.movieId, r.movie); + if (r.movie && r.movieId) { + if (!r.movie._instanceUrl && r._instanceUrl) { + r.movie._instanceUrl = r._instanceUrl; + } + moviesMap.set(r.movieId, r.movie); + } } for (const r of snapshot.radarrHistory.data.records) { - if (r.movie && r.movieId && !moviesMap.has(r.movieId)) moviesMap.set(r.movieId, r.movie); + if (r.movie && r.movieId) { + if (!r.movie._instanceUrl && r._instanceUrl) { + r.movie._instanceUrl = r._instanceUrl; + } + const existing = moviesMap.get(r.movieId); + if (!existing || (!existing._instanceUrl && r.movie._instanceUrl)) { + moviesMap.set(r.movieId, r.movie); + } + } } const sonarrTagMap = new Map(snapshot.sonarrTagsResults.flatMap(t => t.data || []).map(t => [t.id, t.label])); const radarrTagMap = new Map(snapshot.radarrTags.data.map(t => [t.id, t.label])); diff --git a/server/services/DownloadMatcher.js b/server/services/DownloadMatcher.js index 4baf005..de6fa24 100644 --- a/server/services/DownloadMatcher.js +++ b/server/services/DownloadMatcher.js @@ -215,6 +215,9 @@ async function matchSabSlots(slots, context) { if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = series.path || null; + if (series && !series._instanceUrl && sonarrMatch._instanceUrl) { + series._instanceUrl = sonarrMatch._instanceUrl; + } dlObj.arrLink = DownloadAssembler.getSonarrLink(series); dlObj.arrInstanceKey = sonarrMatch._instanceKey || null; } @@ -269,6 +272,9 @@ async function matchSabSlots(slots, context) { if (isAdmin) { dlObj.downloadPath = slot.storage || null; dlObj.targetPath = movie.path || null; + if (movie && !movie._instanceUrl && radarrMatch._instanceUrl) { + movie._instanceUrl = radarrMatch._instanceUrl; + } dlObj.arrLink = DownloadAssembler.getRadarrLink(movie); dlObj.arrInstanceKey = radarrMatch._instanceKey || null; } @@ -459,6 +465,9 @@ async function matchTorrents(torrents, context) { if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = series.path || null; + if (series && !series._instanceUrl && sonarrMatch._instanceUrl) { + series._instanceUrl = sonarrMatch._instanceUrl; + } download.arrLink = DownloadAssembler.getSonarrLink(series); download.arrInstanceKey = sonarrMatch._instanceKey || null; } @@ -505,6 +514,9 @@ async function matchTorrents(torrents, context) { if (isAdmin) { download.downloadPath = download.savePath || null; download.targetPath = movie.path || null; + if (movie && !movie._instanceUrl && radarrMatch._instanceUrl) { + movie._instanceUrl = radarrMatch._instanceUrl; + } download.arrLink = DownloadAssembler.getRadarrLink(movie); download.arrInstanceKey = radarrMatch._instanceKey || null; } diff --git a/tests/integration/dashboard.test.js b/tests/integration/dashboard.test.js index 032e1da..ae1aaad 100644 --- a/tests/integration/dashboard.test.js +++ b/tests/integration/dashboard.test.js @@ -349,6 +349,7 @@ describe('GET /api/dashboard/user-downloads', () => { expect(dl.arrQueueId).toBe(1002); expect(dl.arrType).toBe('sonarr'); expect(dl.arrInstanceUrl).toBe(SONARR_BASE); + expect(dl.arrLink).toBe(SONARR_BASE + '/series/admin-show'); expect(dl.downloadPath).toBeDefined(); });