# Changelog 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.36] - 2026-05-29 ### Fixed - **Test Timeout & Cross-Suite Background Event Pollution (V8 Coverage)** — Configured `fileParallelism: false` and `testTimeout: 15000` in `vitest.config.js`. This guarantees that slow code compilation/instrumentation under V8 coverage doesn't cause transient 5-second timeouts, and prevents asynchronous fire-and-forget background event loops (like Ombi webhook retry loops) in one test suite from running concurrently and overwriting cache singletons in other test suites. ## [1.7.35] - 2026-05-29 ### Added - **Orphaned *arr Queue Item Support (Issue #73)** — Added support for active Sonarr/Radarr queue items from unconfigured download clients ("orphaned" downloads). Added a new synthetic client (`'orphaned'`) with a custom viewBox vector graphics asset at `/images/clients/orphaned.svg` to represent unconfigured clients, and updated filter dropdown lists and active downloads grids to cleanly display them with a dimmed logo, custom dashed border styling, and informative hover tooltips. Resolves Gitea Issue [#73](https://git.i3omb.com/Gandalf/sofarr/issues/73). ### Enhanced - **Download Matching & JSDoc Hygiene (Issue #73)** — Refactored core active-download matching algorithms into unified, deduplicated helper functions (`normalizeTitle`, `titleMatches`, `buildArrDownload`) in `DownloadMatcher.js`, preventing hundreds of lines of duplicate code. Handled case-insensitive and type-safe `downloadId` lookup in `matchSabHistory` across both history and active queue records. Added safe progress arithmetic bounds checking to prevent division-by-zero or `NaN`. ### Fixed - **Security Metadata Isolation in buildArrDownload (Issue #73)** — Restricted access control for sensitive properties like `arrInstanceKey` (the raw instance API key) to ensure they are strictly stripped out of download objects for non-administrator users, preserving system security boundaries. ## [1.7.34] - 2026-05-28 ### Fixed - **Webhook Test Duplicate Error (Issue #71)** — Skipped duplicate/replay protection for `"Test"` event types in Sonarr and Radarr webhook handlers, resolving test button failures. Resolves Gitea Issue [#71](https://git.i3omb.com/Gandalf/sofarr/issues/71). ### Enhanced - **Unified Tab Header Layout & Typography Consistency (Issue #72)** — Added consistent titles, subtitles, and icons across Active Downloads, Recently Completed, and Requests tab panels, and refactored styling to use a unified flexbox design with CSS variables. Resolves Gitea Issue [#72](https://git.i3omb.com/Gandalf/sofarr/issues/72). ## [1.7.33] - 2026-05-28 ### Added - **Requests Tab Layout Enhancement (Issue #69)** — Redesigned and unified the Requests tab container and card layouts with the Active Downloads and Recently Completed tabs. Added styled media-type borders (`tv` and `movie`) using system color variables, styled the `.requests-container` with a surface card background (`var(--surface)`) and box shadow, converted `.requests-list` to a column flexbox (`display: flex; flex-direction: column; gap: 8px;`), aligned card items to the top (`align-items: flex-start`), tighter padding (`10px 14px`), and border-radius (`6px`), and scaled `.request-type-icon` to `48px` wide and `68px` high as a clean cover-art placeholder. All changes are strictly scoped to the requests tab element selectors, leaving active and recent downloads 100% untouched. Resolves Gitea Issue [#69](https://git.i3omb.com/Gandalf/sofarr/issues/69). ### Fixed - **Webhook Regression (Issue #70)** — Fixed a critical regression introduced in #62 where the Ombi webhook handler called `isReplay()` with 3 arguments instead of the new 4-argument signature (`eventType, instanceName, eventDate, contentId`). The handler now correctly passes `requestId` as the fourth `contentId` argument. This restores reliability to real Ombi webhooks, loopback fallbacks, and the Ombi test simulation buttons. Resolves Gitea Issue [#70](https://git.i3omb.com/Gandalf/sofarr/issues/70). ## [1.7.32] - 2026-05-28 ### Fixed - **TransmissionClient Hardening (Issue #63)** — Mapped the previously-unknown Transmission RPC status code `7` to `Checking` (best-effort; the RPC spec formally documents only codes 0–6, and the historical alias `TORRENT_IS_CHECKING` corresponds to code 2), so torrents reporting code 7 are now rendered with a useful status label instead of `Unknown`. Implemented three torrent control methods on `TransmissionClient` that were previously absent: `startTorrent(id)` (resumes via `torrent-start`), `stopTorrent(id)` (pauses via `torrent-stop`), and `removeTorrent(id, deleteData = false)` (removes via `torrent-remove`, optionally also deleting local files via Transmission's `delete-local-data` flag). All three accept either a single id (numeric or hash) or an array of ids, matching the Transmission RPC contract. Documented in `extractArrInfo()` that an `arrQueueId` cannot reliably be derived from filename alone — the cross-client matching path is hash-based via `DownloadMatcher.matchTorrents()` (Issue #65), which keys on `torrent.hashString` for Transmission. Added regression tests for status code 7 and all three control methods. Resolves Gitea Issue [#63](https://git.i3omb.com/Gandalf/sofarr/issues/63). - **Frontend Build Stability (Issue #66)** — Added explanatory inline comments to `client/vite.config.js` documenting two non-standard but deliberate build settings: `build.outDir = '../public'` (the Vite bundle is emitted into the Express-served `public/` directory at the repo root rather than the Vite-default `client/dist/`) and `build.emptyOutDir = false` (required so the hand-authored static assets committed under `public/` are not wiped by every `vite build`). The comments explicitly warn that changing either setting without also updating the Express static-serve configuration in `server/app.js` and the Dockerfile copy steps will break production serving. Removed a stale, untracked `client/dist/` directory (a leftover from an earlier default-Vite build) that was harmless — both `.gitignore` and `.dockerignore` already excluded it from version control and Docker contexts — but caused recurring confusion about which `index.html` was authoritative. Verified `client/index.html` correctly references `/src/main.js` as the Vite entrypoint. Resolves Gitea Issue [#66](https://git.i3omb.com/Gandalf/sofarr/issues/66). - **Download Matching & Deduplication (Issue #65)** — `DownloadMatcher.matchTorrents()` now attempts hash-first matching for every torrent before falling back to title-substring matching. The hash lookup compares `torrent.hash` (qBittorrent, rTorrent) or `torrent.hashString` (Transmission) against each *arr* queue/history record's `downloadId`, restoring deterministic matching for renamed downloads and torrents whose on-disk filename has diverged from the *arr* release title. Title-substring matching is retained verbatim as a fallback so unhashed clients and legacy fixtures continue to work. After the per-torrent matching pass, the returned list is deduplicated by the composite key `(arrType, arrQueueId)`: the first matched download wins, so a single torrent that maps to N *arr* queue records sharing one queue id (for example, a season pack exposed as multiple per-episode rows) produces a single dashboard card instead of N near-identical duplicates. A new integration suite at `tests/integration/download-matcher-season-pack.test.js` covers hash-first matching for qBittorrent (`hash`) and Transmission (`hashString`), the title-substring fallback path, and the deduplication step. Resolves Gitea Issue [#65](https://git.i3omb.com/Gandalf/sofarr/issues/65). - **qBittorrentClient Peer Data & Response Safety (Issue #64)** — `QBittorrentClient.normalizeDownload()` now exposes two new fields on every torrent record: `seeds` (sourced from qBittorrent's `num_seeds`, the count of connected seed peers) and `peers` (sourced from `num_leechs`, the count of connected leecher peers). The connected counts were chosen deliberately over the swarm totals `num_complete`/`num_incomplete` so the values remain consistent with what other clients (Transmission via `peersConnected`/`peersSendingToUs`, rTorrent via `d.peers_connected`) report on the same normalised contract. `QBittorrentClient.getMainData()` now also defensively returns the existing in-memory torrent map (rather than dereferencing a null) when the qBittorrent server responds with an empty body to `/api/v2/sync/maindata`, eliminating a crash class observed against transiently-restarting qBittorrent instances. A regression test verifies the new fields are populated from `num_seeds`/`num_leechs` and not from the swarm-total fields. Resolves Gitea Issue [#64](https://git.i3omb.com/Gandalf/sofarr/issues/64). - **Season Pack Queue Handling & Crash Prevention (Issue #61)** — Extracted a shared `buildArrQueueCache(queues, instances, mediaKey)` helper at `server/utils/arrQueueHelpers.js` covering both Sonarr and Radarr, replacing four previously-divergent inline `flatMap` blocks across the background poller (`server/utils/poller.js`) and the webhook event processor (`server/routes/webhook.js`) that built the `poll:sonarr-queue` and `poll:radarr-queue` cache entries. Sonarr queue records that share a `downloadId` (the canonical fingerprint for a season-pack release) are now annotated with `isSeasonPack: true` and `episodeCount: ` so downstream consumers — including the active-downloads matching service — can identify and de-duplicate season packs without re-deriving the grouping. The helper is wrapped in per-record and per-instance `try`/`catch` guards: malformed records (`null`, missing `data`, unknown instance ids) are skipped with a warning rather than throwing, eliminating a class of crashes that previously bubbled out of the `flatMap` and tore down the entire poll cycle or webhook refresh. Movies (Radarr) skip season-pack annotation by design. A new unit test suite at `tests/unit/utils/arrQueueHelpers.test.js` covers tagging, season-pack grouping, null-safety, and unknown-instance fallback. Resolves Gitea Issue [#61](https://git.i3omb.com/Gandalf/sofarr/issues/61). - **rTorrent Null-Safety, SABnzbd History Limit & Client Last-Error Visibility (Issue #68)** — Three related hardening improvements to the download-client layer. First, `RTorrentClient` now defends against the malformed-response scenarios observed against misconfigured or transiently-restarting rTorrent servers: `getActiveDownloads()` explicitly checks that `d.multicall2` returned an actual array (logging a warning and returning `[]` if not, rather than throwing on `.map`) and processes each torrent row in its own `try`/`catch` so a single malformed entry cannot poison the whole result set. All eleven field values retrieved from the multicall response are coerced to their expected types via explicit `Number()`/`String()` conversions in `normalizeDownload()`, so downstream arithmetic and string operations can no longer blow up on `null` or `undefined` values from plugins or older rTorrent versions. `_extractArrInfo()` now short-circuits safely on non-string filenames. `getClientStatus()` additionally coerces the global rate values through `Number.isFinite` before returning them. Second, the SABnzbd history limit (previously hard-coded to `10` records per poll) is now configurable via the `SAB_HISTORY_LIMIT` environment variable. Invalid or absent values fall back to the default of `10` with a log warning, ensuring backward compatibility. Third, all four download clients (`RTorrentClient`, `SABnzbdClient`, `QBittorrentClient`, `TransmissionClient`) now record structured `lastError` objects (`{ operation, message, at }`) on every failed API call via `_recordLastError()` and clear them on subsequent success via `_clearLastError()` — both helpers introduced on the `DownloadClient` base class alongside the public `getLastError()` accessor. The per-client last-error is surfaced through `DownloadClientRegistry.getAllClientStatuses()` and exposed on the `GET /api/status/status` admin endpoint under the new `downloadClients` array, letting the admin panel show a per-client failure indicator without log scraping. New regression tests cover all null-safety paths, the SAB history limit env variable (unset, valid, invalid, propagated to the API call), and the full lastError set/clear cycle for both rTorrent and SABnzbd. Resolves Gitea Issue [#68](https://git.i3omb.com/Gandalf/sofarr/issues/68). - **Webhook Reliability (Issue #62)** — Hardened the webhook replay protection to prevent false-duplicate detection while preserving protection against genuine retries. The replay key for Sonarr and Radarr now incorporates a content identifier (`downloadId`, falling back to `series.id` or `movie.id`) alongside the existing `eventType:instanceName:eventDate` components, so that multiple distinct events sharing the same timestamp (for example, several `Grab` events fired in the same second for episodes in a season pack) no longer collide and get silently dropped. Events without a content identifier (such as `Test`) fall back gracefully to the previous key shape so existing behaviour is preserved. The Ombi handler — which already uses a distinct `requestId`-bearing key — is unchanged. Additionally, the Sonarr and Radarr handlers now log an explicit warning when the inbound `instanceName` fails to match any configured instance and processing falls back to the first instance, improving diagnosability of misconfigured webhook senders. Resolves Gitea Issue [#62](https://git.i3omb.com/Gandalf/sofarr/issues/62). ## [1.7.31] - 2026-05-28 ### Fixed - **Frontend Connection Remediation** — Staged and committed dynamic proxy target configurations and startup pipeline orchestrations. Rebuilt the production build of `public/app.js` to ensure dynamic SSL bypass and dynamic local network address resolution are fully compiled and deployed. ## [1.7.30] - 2026-05-28 ### Added - **Testing Scope Expansion (Issue #60)** — Significantly expanded unit and integration test coverage targeting the background polling engine, rate limiting, and frontend rendering logic to secure core behaviors. - **Background Poller Tests (`poller.test.js`)**: Implemented robust unit tests verifying concurrency guards, subscriber registries (`onPollComplete`/`offPollComplete`), graceful error recovery, webhook bypasses, and global fallbacks using a hybrid testing approach with Vitest fake timers. - **Isolated Rate Limiting (`rateLimiter.test.js`)**: Designed a dedicated integration test that unsets `SKIP_RATE_LIMIT` and asserts `429 Too Many Requests` responses under rapid login requests while maintaining test suite stability. - **Active Downloads Decoration (`ombiDecoration.test.js`)**: Covered deep-link generation (`arrLink`) for active download matching under admin and non-admin states. - **Frontend UI Rendering (`downloads.test.js`)**: Expanded coverage for the downloads rendering engine, specifically targeting conditional administrative icon construction (`createServiceIcons()`) and client logo loading fallbacks (`createClientLogo()`). - **SSE Connection Lifecycle & Payload Contracts (`dashboard.test.js`)**: Added stream tests checking Server-Sent Event heartbeat emission, payload schema schemas, and client close event listeners (`req.on('close')`) to verify callback cleanup and prevent connection-based memory leaks. ## [1.7.29] - 2026-05-27 ### Fixed - **Missing Radarr/Sonarr Links on Active Downloads (Issue #59)** — Resolved a bug where Radarr and Sonarr deep-link buttons were missing from active/completed download cards on the main dashboard for administrator users. Implemented a generic link enrichment utility `decorateDownloadsWithArrLinks()` to query Radarr and Sonarr catalog APIs in parallel and match downloads via their database ID or title, circumventing minimal metadata in cached records that lacked `titleSlug`. Added a robust integration test to safeguard links decoration for dashboard downloads. ## [1.7.28] - 2026-05-27 ### Fixed - **Missing Sonarr Link on TV Requests (Issue #58)** — Resolved a bug where the Sonarr deep-link button was missing on TV request cards while Radarr links correctly appeared on movie request cards. Added support for all camelCase TVDB ID variants (`tvDbId`, `tvdbId`, `theTvdbId`, `theTvDbId`, `TvDbId`, `TheTvDbId`) on both backend link decoration (`server/utils/ombiHelpers.js`) and frontend rendering (`client/src/ui/requests.js`). Added a dedicated integration test to safeguard links decoration for TV requests. ## [1.7.27] - 2026-05-27 ### Fixed - **Frontend Dashboard Serve Regression (Issue #57)** — Resolved a critical regression where the Vite-built frontend dashboard UI was not served. Consolidated Express configurations by migrating static files serving and SPA routing into the `createApp` factory in `server/app.js`. Cleaned up `server/index.js` to import and instantiate the app from the factory, successfully eliminating over 300 lines of duplicate route and middleware registrations, and ensuring alignment between development testing and production runtime configurations. ## [1.7.26] - 2026-05-27 ### Fixed - **Missing Ombi & \*Arr Request Links (Issue #56)** — Resolved an issue where the Ombi and \*Arr lookup buttons failed to appear against request cards. Added `decorateRequestsWithArrLinks` to aggregate IDs and query Radarr/Sonarr libraries during SSE stream decoration and backend REST fetching. Also fixed a frontend condition failing to generate Ombi links for TV requests by checking a broader set of ID properties (`theTvDbId`, `theTmdbId`, `imdbId`). ## [1.7.25] - 2026-05-27 ### Fixed - **Ombi TV Request Status, User, and Date Resolution (Issue #53)** — Resolved the root cause where Ombi TV show requests consistently displayed "unknown" status, "unknown" user, and missing request dates. The Ombi API nests all TV request data (`requestedUser`, `approved`, `available`, `denied`, `requested`, `requestedDate`) inside `childRequests[]` sub-objects, while the application previously only inspected top-level properties. - `OmbiRetriever._hydrateRequest()` now hydrates `requestedUser` on each `childRequests` entry and promotes `requestedDate` from `childRequests[0]` to the top level. - `getRequestStatus()` (server and client) now aggregates status flags from `childRequests[]` when top-level properties are absent. - Client-side date display now falls back to `childRequests[0].requestedDate` as a defensive measure. ## [1.7.24] - 2026-05-27 ### Enhanced - **Gitea Actions Prioritization** — Optimized CI/CD workflow pipeline executions to prioritize critical `build-image` Docker compilation runs. Redundant security audits, tests, coverage generation, and RAML packages are now bypassed on `release/**` branches (which have already passed validation during development on `develop`). - **Workflow Concurrency Controls** — Configured active concurrency groups with `cancel-in-progress: true` inside both `ci.yml` and `build-image.yml` pipelines, ensuring obsolete running jobs are aborted instantly when newer commits are pushed. ## [1.7.23] - 2026-05-27 ### Enhanced - **Request Card Link Alignment (Issue #55)** — Aligned deep links on request cards (Ombi link and administrator Sonarr/Radarr deep links) with the elegant, inline `.service-icon` styling used across the downloads and history dashboard cards. Replaced bulky button elements with clean hoverable icons in a shared `.service-icons-container`, maintaining strict administrator-only visibility for *arr links. ## [1.7.22] - 2026-05-27 ### Fixed - **Theme Switcher Multi-Button Support (Issue #54)** — Resolved a frontend bug where themes other than Light failed to apply because the Javascript code queried a non-existent `#theme-toggle` element. Re-engineered the switcher to query all `.theme-btn` selectors inside `.theme-switcher`, managing and toggling the active class styling across `light`, `dark`, and `mono` themes, and cleanly persisting choices to local storage. - **Ombi TV Requester User Extraction Fallback (Issue #53)** — Resolved a bug where TV show requests from Ombi displayed as "unknown" user because requester attributes were nested under non-standard properties (`user`, `requestedBy`, `ombiUser`, `requestedByUser`, and nested seasons/child requests arrays). Added robust multi-layer extraction logic on both backend and frontend layers to resolve requester usernames under all TV request structures. - **Configurable Webhook Commit Delay & Retry Loop (Issue #53)** — Mitigated race conditions between Ombi webhook events and database commits by introducing a configurable `OMBI_WEBHOOK_REFRESH_DELAY_MS` delay (defaulting to `2000`) and a smart 3-attempt retry polling loop to verify updated requester data before cache updates. ### Added - **Admin Request Card *arr Library Lookup (Issue #53)** — Added library deep-link lookup for admin views. Request cards now query Sonarr and Radarr library caches and dynamically render deep-link navigation icons in the card actions container if the item exists. - **Request Date/Time Presentation** — Added request date and time display inside request cards formatted as `YYYY-MM-DD HH:MM`. - **Unknown (Ombi) Dotted Underline Tooltip** — Added user-friendly placeholder "Unknown (Ombi)" with dotted underline and explanatory hover tooltip when user details are unavailable from the Ombi database. - **Expanded Test Coverage** — Introduced two new frontend DOM test suites (`tests/frontend/ui/theme.test.js` and `tests/frontend/ui/requests.test.js`) and robust backend unit test assertions in `tests/unit/ombiHelpers.test.js`. --- ## [1.7.21] - 2026-05-26 ### Fixed - **Ombi Webhook Test Loopback Fallback** — Resolved a persistent failure on the Ombi webhook test button when Sofarr sits behind a reverse proxy or in loopback-restricted environments. When the outbound request to the public webhook URL (`SOFARR_BASE_URL`) fails due to loopback/NAT routing limits, the server now transparently falls back to a secure local loopback request (`127.0.0.1`) with smart TLS detection and SSL error bypass (`rejectUnauthorized: false`). - **Resilient Webhook Mocks in Tests** — Updated integration test assertions to verify the loopback fallback path under both HTTP and HTTPS configurations, ensuring full compatibility across dev, test, and production container environments. --- ## [1.7.21] - 2026-05-26 ### Fixed - **Webhook Loopback & Hairpin NAT Connectivity** — Implemented a robust local loopback fallback inside the Ombi webhook testing endpoint to bypass NAT loopback and DNS resolution issues common in setups behind reverse proxies. When the outbound request to the public URL fails, the server automatically routes the request internally via `127.0.0.1` using automatic TLS credentials detection and SSL validation bypass for loopback requests. Added comprehensive integration tests verifying the fallback behavior. - **Dedicated Webhook Base URL Support** — Added support for a new `SOFARR_WEBHOOK_BASE_URL` environment variable inside `server/routes/sonarr.js`, `server/routes/radarr.js`, and `server/routes/ombi.js`. This allows setups behind reverse proxies to declare an internal/custom base URL specifically for webhooks, enabling Sonarr, Radarr, and Ombi to send webhook events directly to the server via internal container networking, resolving `503 Service Unavailable` errors. --- ## [1.7.20] - 2026-05-26 ### Fixed - **Ombi Requesting User Hydration (Issue #51)** — Resolved a bug where Sofarr displayed "Unknown" for the requesting user on requests even when the Ombi database contains valid user information. Added automatic requestedUser object hydration on the server side by fetching the full user list from `/api/v1/Identity/Users` and caching it in memory. If a request is missing the nested `requestedUser` details but possesses a valid `requestedUserId`, Sofarr automatically resolves and binds the user's username/alias. Added robust unit tests safeguarding the client and the retriever. Resolves Gitea Issue [#51](https://git.i3omb.com/Gandalf/sofarr/issues/51). ### Changed - **Aligned Gitea Interaction Skill** — Updated `.windsurf/skills/gitea-interaction/SKILL.md` to align with the Antigravity `tea-interaction` hybrid interaction model guidelines, detailing when to use the editor extension versus the Gitea CLI. --- ## [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 - **Mobile overflow on Requests tab** — Request cards no longer extend off the right edge of the screen on mobile browsers. Removed `white-space: nowrap` from `.request-title` to allow text truncation with ellipsis, added `overflow-x: hidden` to `.requests-list` as a safety net, and added `@media (max-width: 768px)` rules to reduce padding and tighten gaps on mobile. Resolves Gitea Issue [#49](https://git.i3omb.com/Gandalf/sofarr/issues/49). --- ## [1.7.17] - 2026-05-24 ### Fixed - **Blocklist-Search Persistent Failure (Regression from v1.7.16)** — Identified and corrected the true root cause of the blocklist-and-search feature being non-functional. The v1.7.16 fix correctly cast both sides of the queue ID comparison to `String`, but the lookup was performed against `downloadClientRegistry.getAllDownloads()`, which returns **raw download-client data** (qBittorrent, SABnzbd, etc.) that never has `arrQueueId` populated — that field is only assigned by `DownloadMatcher.js` during the SSE build phase from the *arr cache. For qBittorrent torrents specifically, `QBittorrentClient.normalizeDownload()` does not set `arrQueueId` at all, so the lookup always returned `undefined` and the request was rejected with `403`. The permission check in `POST /api/dashboard/blocklist-search` now looks up the queue record directly from the Sonarr/Radarr queue cache (`poll:sonarr-queue` / `poll:radarr-queue`) where `record.id` is the numeric queue ID, using `String()` casting on both sides to handle the DOM-dataset (string) vs API response (number) type difference. Resolves Gitea Issue [#48](https://git.i3omb.com/Gandalf/sofarr/issues/48) (regression). --- ## [1.7.16] - 2026-05-24 ### Fixed - **Blocklist-Search Queue ID Type Mismatch** — Resolved a bug where the "Blocklist and search" action consistently returned `403 Download not found or permission denied` for all users. The server-side download lookup in `server/routes/dashboard.js` used strict equality (`===`) to compare `arrQueueId` values, but the value sent from the SPA client (read from a DOM `data-*` attribute) is always a `string`, while the value populated from the Radarr/Sonarr queue API is a `number`. Both sides are now cast to `String` before comparison, resolving the false-negative match failure. Resolves Gitea Issue [#48](https://git.i3omb.com/Gandalf/sofarr/issues/48). --- ## [1.7.15] - 2026-05-24 ### Fixed - **Ombi Webhook Authentication Failures** — Resolved a critical issue where Ombi webhook notifications failed to authenticate because Ombi's built-in notification agent does not support custom HTTP headers (such as `X-Sofarr-Webhook-Secret`). Added a query parameter authentication fallback (`?secret=`) to all `/api/webhook/*` endpoints (Sonarr, Radarr, and Ombi) and configured Ombi webhook registration to automatically append this secret query parameter. Resolves Gitea Issue [#47](https://git.i3omb.com/Gandalf/sofarr/issues/47). --- ## [1.7.14] - 2026-05-24 ### Fixed - **Undefined Reference Error in Background Poller** — Resolved a critical runtime exception in the background scheduler loop (`server/utils/poller.js`) where `logToFile` was called on cache updates but was never imported at the top of the file, previously triggering `[Poller] Poll error: logToFile is not defined` on every interval loop. --- ## [1.7.13] - 2026-05-24 ### Changed - **Comprehensive OpenAPI & Swagger Specification Remediation** — Bumped the API documentation version to `1.7.13` and fully documented all operational rate-limiting configurations, exemptions, and bypasses in `server/openapi.yaml` (including general cover-art exclusions, failed-only login trackers, webhook limiters, and rate-limit exempt root health probes). - **Aligned Health Check Endpoint Implementation** — Enhanced the express application factory `/health` endpoint to dynamically require and return the active version from `package.json`, keeping it fully aligned with the production entrypoint server logic. - **Synchronized Security & System Architecture Docs** — Aligned security matrices and threat mitigations in `SECURITY.md` and rate-limiting testing configurations in `ARCHITECTURE.md`. ### Added - **Swagger API Coverage Verification Integration** — Implemented comprehensive assertions within `tests/integration/swagger-coverage.test.js` to dynamically verify that all newly added logging and debug endpoints (`/api/debug/*`) are fully represented in the active specification, raising test suite coverage to 876 passing checks. --- ## [1.7.12] - 2026-05-24 ### Added - **Real-Time Server-Side Log Streaming (`GET /api/debug/server-logs`)** — Introduced an in-process output stream capturer that intercepts standard output (`stdout`) and standard error (`stderr`), strips terminal ANSI escape codes, maintains a rolling 1000-line buffer, and streams live operational logs via Server-Sent Events (SSE). Resolves Gitea Issue [#45](https://git.i3omb.com/Gandalf/sofarr/issues/45). - **Real-Time Client-Side Console Log Stream (`GET & POST /api/debug/client-logs`)** — Added a browser-side console override in the frontend SPA that intercepts `console.log`, `console.warn`, and `console.error` calls. Logs are queued and posted to `/api/debug/client-logs` in optimized batches every 2000ms (or when the queue reaches 20 items), which are then buffered and streamed over SSE to debugging developers. Resolves Gitea Issue [#46](https://git.i3omb.com/Gandalf/sofarr/issues/46). - **Subnet IP Filtering (`LOG_ALLOW_SUBNETS`)** — Implemented an optional CIDR-based subnet validation check using `ipaddr.js` to restrict access to debug logs endpoints to specific internal IP ranges (e.g. `127.0.0.1/32,192.168.1.0/24`). Works natively with reverse proxies when `TRUST_PROXY` is enabled. - **Multi-Layered Security Bypass & Auth** — Debug endpoints are guarded globally by `ENABLE_LOG_STREAM=true` (off by default) and require Emby admin sessions, standard HTTP Basic Auth fallback checks against Emby, or high-priority bypass via header `X-Webhook-Secret`. - **Swagger UI Specification Coverage** — Fully documented all four new endpoints, headers, and schemas in `server/openapi.yaml`, with automated verification integrated inside `swagger-coverage.test.js`. - **Architectural & Developer Guides** — Updated `ARCHITECTURE.md` (with system overview, diagrams, stdout/stderr hooks, and threat mitigations) and `README.md` (deploy instructions and querying parameters). --- ## [1.7.11] - 2026-05-24 ### Fixed - **Sonarr full-season and multi-episode blocklist-search failures** — Fixed a bug where clicking the "Blocklist & Search" button on television downloads representing full-season packs or multi-episode releases failed with a `400 Bad Request` error. We relaxed the API validation on `POST /api/dashboard/blocklist-search` to make `arrContentId` optional. In `DownloadMatcher.js`, we exposed `arrContentIds` (holding all episode IDs associated with the matched release) and `arrSeriesId` (holding the series ID). - **Enhanced blocklist re-search commands** — The backend now triggers multi-episode `EpisodeSearch` commands using the `episodeIds` array when blocklisting multi-episode releases, and falls back to a series-wide `SeriesSearch` command using the `seriesId` when no specific episode IDs can be resolved. - **OpenAPI Schema and Test Coverage** — Updated the OpenAPI specifications (`server/openapi.yaml`) to reflect optional/new parameters on `BlocklistSearchRequest`, and implemented new integration tests in `tests/integration/dashboard.test.js` to safeguard fallback and multi-episode search commands. --- ## [1.7.10] - 2026-05-24 ### Fixed - **Ombi webhook race condition fix** — Introduced a 2000ms delay in the webhook background worker for Ombi events before fetching new requests. This resolves a race condition where Ombi triggers the webhook before its database has committed the new request, which previously caused the request to be missing from the subsequent API fetch. Resolves Gitea Issue [#42](https://git.i3omb.com/Gandalf/sofarr/issues/42). - **Ombi webhook statistics in status panel** — Integrated Ombi webhook metrics into the admin status panel. The backend now retrieves the Ombi webhook configuration status and aggregates its events received and polls skipped metrics. The frontend displays these metrics (consistently formatted as `O:`) under the Webhooks status card. --- ## [1.7.9] - 2026-05-23 ### Fixed - **Ombi webhook PascalCase payload parsing (Re-release)** — Correctly packaged and released the Ombi webhook parsing logic to handle PascalCase property names (e.g., `NotificationType`, `RequestId`) sent by C#/.NET applications. This ensures standard Ombi webhook integrations function correctly. Resolves Gitea Issue [#42](https://git.i3omb.com/Gandalf/sofarr/issues/42). --- ## [1.7.8] - 2026-05-23 ### Fixed - **Ombi webhook PascalCase payload parsing** — Updated the Ombi webhook parsing logic to correctly handle payloads formatted with PascalCase property names (e.g. `NotificationType`, `RequestId`), which are often sent by C#/.NET applications. This ensures that new requests generated via Ombi's standard webhook integrations are correctly processed and displayed in the frontend dashboard. Resolves Gitea Issue [#42](https://git.i3omb.com/Gandalf/sofarr/issues/42). --- ## [1.7.7] - 2026-05-23 ### Fixed - **Ombi webhook NewRequest parsing support** — Added `'NewRequest'` to `VALID_EVENT_TYPES` and `OMBI_EVENTS` sets in `server/routes/webhook.js`. When a user submits a new media request in Ombi, the backend now successfully parses the webhook payload, bypasses the request cache, fetches the latest request list, and broadcasts it to all connected dashboard screens via SSE in real time. Previously, these webhook payloads were rejected with a `400 Bad Request` error. Resolves Gitea Issue [#42](https://git.i3omb.com/Gandalf/sofarr/issues/42). - **Ombi webhook integration tests** — Implemented a robust integration test suite in `tests/integration/webhook.test.js` validating `POST /api/webhook/ombi` payloads, secret keys, duplicate/replay protection, input checks, and cache refresh triggers. --- ## [1.7.6] - 2026-05-23 ### Fixed - **Bypass rate limiter for cover art** — Exempted `GET /api/dashboard/cover-art` requests from the global API rate limiter. Resolves Gitea Issue [#43](https://git.i3omb.com/Gandalf/sofarr/issues/43). - **Ombi request cache bypass** — Added a `force` cache-bypassing flag to `OmbiRetriever`'s cache refresh mechanism, enabling real-time cache updates upon receiving Ombi webhooks or manual request list reloads. Resolves Gitea Issue [#42](https://git.i3omb.com/Gandalf/sofarr/issues/42). --- ## [1.7.5] - 2026-05-23 ### Fixed - **Ombi webhook settings persistence** — Fixed a bug where enabling the Ombi webhook from the frontend was successfully processed by the server but not stored on the Ombi side. The payload submitted to Ombi now retrieves the database `id` of the settings row first and merges it back into the `POST` payload. This ensures Entity Framework Core on the Ombi backend performs an update on the correct database row, enabling the webhook and letting its status persist successfully. Resolves Gitea Issue [#41](https://git.i3omb.com/Gandalf/sofarr/issues/41). --- ## [1.7.4] - 2026-05-23 ### Fixed - **Ombi webhook registration in production** — Fixed a bug where `/api/ombi/webhook/enable` and other `/api/ombi/*` endpoints returned `404 (Not Found)` in production. The core Express app factory (`server/app.js`) registered the router, but the production server entry point (`server/index.js`) duplicated Express setup instead of utilizing the factory, omitting the `ombiRoutes` registration entirely. Re-registered the router in `server/index.js`, resolves Gitea Issue [#40](https://git.i3omb.com/Gandalf/sofarr/issues/40). --- ## [1.7.3] - 2026-05-23 ### Fixed - **Download client dropdown filter icons and type badges** — Refactored the download client selector dropdown in `client/src/ui/filters.js` to render utilizing the design system classes `.download-client-option`, `.download-client-icon`, `.download-client-option-label`, and `.download-client-type`. Options now display the client's brand SVG logo (e.g., `sabnzbd.svg`, `qbittorrent.svg` loaded from `/images/clients/`) next to the configured display name, along with a clean pill badge indicating the client type. This resolves visual ambiguity when multiple download clients share the same name (such as `"i3omb"` for SABnzbd and qBittorrent). --- ## [1.7.2] - 2026-05-22 ### Fixed - **Webhook configuration check** — Ombi webhook status now correctly checks for `SOFARR_BASE_URL` and `SOFARR_WEBHOOK_SECRET` environment variables. The webhook displays as disabled when either is missing, matching the existing *ARR webhook behavior. - **Common webhook config endpoint** — Added `GET /api/webhook/config` endpoint that returns whether both `SOFARR_BASE_URL` and `SOFARR_WEBHOOK_SECRET` are configured, along with a list of missing items. Used by the client to determine webhook enablement status across all webhook types. - **Client-side webhook status** — Updated `fetchWebhookStatus()` to call `/api/webhook/config` and use the result to determine if Sonarr and Radarr webhooks are enabled. Webhook status now depends on both the service-specific webhook being configured AND the Sofarr webhook config being valid. - **Webhook config endpoint authentication** — Added `requireAuth` middleware to `GET /api/webhook/config` to enforce authentication, matching the OpenAPI contract and preventing unauthenticated information disclosure. - **Ombi webhook enable/test config validation** — `POST /api/ombi/webhook/enable` and `POST /api/ombi/webhook/test` now validate `SOFARR_BASE_URL` and `SOFARR_WEBHOOK_SECRET` before making outbound Ombi API calls, returning `400` with descriptive errors when either is missing. Previously these routes would construct malformed URLs (`undefined/api/webhook/ombi`) if the environment variables were unset. - **Test environment cleanup** — `tests/integration/webhook.test.js` `afterEach` now cleans up `EMBY_URL`, `SOFARR_BASE_URL`, `SONARR_INSTANCES`, and `RADARR_INSTANCES` to ensure hermetic test isolation. --- ## [1.8.0] - 2026-05-21 ### Added #### Ombi PALDRA Integration - **OmbiRetriever** — New PALDRA-compliant retriever extending `ArrRetriever`, registered in `arrRetrieverRegistry` alongside Sonarr/Radarr. Manages Ombi request data with 5-minute TTL cache and lookup maps by TMDB/TVDB/IMDB IDs. - **OmbiClient** — Low-level Ombi API client for HTTP communication (movie/TV requests, search by external ID, connection test). - **`getOmbiInstances()`** — New config function in `server/utils/config.js` following the existing multi-instance JSON array pattern; supports both `OMBI_INSTANCES` and legacy `OMBI_URL`/`OMBI_API_KEY` formats. - **PALDRA registry Ombi methods** — `getOmbiRetrievers()`, `getOmbiRequests()`, `getOmbiRequestsByType()`, `findOmbiRequest()` added to `arrRetrieverRegistry`. - **External ID matching** — Downloads are matched to Ombi requests using TVDB ID → TMDB ID (TV) and TMDB ID → IMDB ID (movies); falls back to an Ombi search link when no request exists. - **`getOmbiLink()` / `getOmbiSearchLink()`** — New helpers in `DownloadAssembler.js` following the `getSonarrLink`/`getRadarrLink` pattern. - **Service icon layout** — Downloads and history cards now render inline SVG icons (Ombi for all users; Sonarr/Radarr for admins) instead of linked series/movie names. CSS `.service-icons-container` and `.service-icon` classes added to `public/style.css`. - **OpenAPI** — `NormalizedDownload` schema extended with `ombiLink`, `ombiRequestId`, `ombiTooltip` nullable string properties; `Ombi` tag added to the spec. - **`OMBI_INSTANCES` / `OMBI_URL` / `OMBI_API_KEY`** — New environment variables documented in `.env.sample`, `README.md`, `ARCHITECTURE.md`, and `SECURITY.md`. ### Changed - **`DownloadMatcher.js`** — `matchSabSlots`, `matchSabHistory`, and `matchTorrents` are now `async`; each matched download object is enriched with `ombiLink`, `ombiRequestId`, and `ombiTooltip` via `addOmbiMatching()`. - **`DownloadBuilder.js`** — `buildUserDownloads` accepts `ombiRetriever` and `ombiBaseUrl` in its options object and passes them through to matching context. - **Dashboard routes** — Both the REST endpoint and SSE stream now resolve the Ombi retriever from the PALDRA registry and include it in the download-building context. - **`arrRetrievers.js`** — PALDRA registry now imports `OmbiRetriever`, maps `'ombi'` in `retrieverClasses`, and initialises instances from `getOmbiInstances()`. - **`ARCHITECTURE.md`** — PALDRA section updated with OmbiRetriever description, registry API additions, and directory-structure entries. Technology stack table updated. - **`SECURITY.md`** — Threat model extended with Ombi API key exposure and rate-limit exhaustion mitigations. - **`README.md`** — Prerequisites and new *Ombi Integration (Optional)* configuration section added. --- ## [1.7.1] - 2026-05-21 ### Added #### RAML 1.0 Package Generation - **Automated RAML generation in CI/CD** — Added RAML 1.0 package generation to the existing `swagger` job in `.gitea/workflows/ci.yml`. The pipeline now generates a downloadable `raml-package` artifact on every push and PR, available from the Actions run page with 14-day retention. - **RAML generation scripts** — Created three new scripts in `scripts/`: - `generate-openapi.js` — Bootstraps the Express app in test mode, fetches the merged OpenAPI 3.1 spec from `/api/swagger.json`, and exports it to disk. - `downgrade-openapi.js` — Downgrades OpenAPI 3.1 to 3.0 for RAML compatibility (existing RAML converters don't support 3.1). - `simple-raml-converter.js` — Converts OpenAPI 3.0 to RAML 1.0 format using a custom converter (modern RAML converters are unmaintained). - `package-raml.js` — Creates a versioned tar.gz archive containing the RAML spec, original OpenAPI spec, version metadata, and README. - **RAML artifact structure** — Each artifact includes: - `api.raml` — RAML 1.0 specification - `openapi-merged.json` — Original merged OpenAPI 3.1.0 spec (for reference) - `version.json` — Metadata (version, commit SHA, timestamp, tool used) - `README.md` — Origin, conversion details, known limitations, and verification steps - **npm scripts** — Added three new scripts to `package.json`: - `generate:openapi` — Generates merged OpenAPI spec - `generate:raml` — Downgrades and converts to RAML - `package:raml` — Packages the RAML artifact - **Spectral ruleset** — Created minimal `.spectral.yml` ruleset to make the existing Spectral lint step functional (previously failing silently due to missing ruleset). - **OpenAPI JSON endpoint** — Added `/api/swagger.json` endpoint to `server/app.js` to serve the raw merged OpenAPI spec as JSON, enabling programmatic access. ### Changed - **Dependencies added** — `archiver` (^7.0.1) for creating tar.gz archives. --- ## [1.7.0] - 2026-05-21 ### Added #### Swagger UI & OpenAPI 3.1 Documentation - **Swagger UI at `/api/swagger`** — Interactive API documentation served via `swagger-ui-express`; publicly accessible with a custom authentication banner (`public/swagger-auth-banner.js`) that explains the cookie-based + CSRF-token authentication flow for testing endpoints directly in the browser. - **OpenAPI 3.1 specification** — Central `server/openapi.yaml` file containing base metadata, security schemes (`CookieAuth`, `CsrfToken`), and reusable component schemas: - `NormalizedDownload` — standardised download object returned by all PDCA clients - `DashboardPayload` — SSE payload shape (`{ user, isAdmin, downloads, downloadClients }`) - `ErrorResponse` — standard error envelope with redacted details - `BlocklistSearchRequest` — payload for the admin blocklist-and-search operation - `WebhookPayload` — Sonarr/Radarr webhook event structure - `HistoryItem` — deduplicated history record with upgrade-availability flag - `StatusResponse` — server metrics, polling timings, cache stats, and webhook metrics - **Hybrid documentation approach** — Per-endpoint details are documented directly in route handler files (`server/routes/*.js`) using JSDoc `@openapi` comments. `swagger-jsdoc` merges these comments with the central YAML at runtime, keeping documentation close to the code while maintaining shared schemas in one place. - **Comprehensive endpoint coverage** — All implemented endpoints are documented: - Authentication: `POST /api/auth/login`, `GET /api/auth/me`, `GET /api/auth/csrf`, `POST /api/auth/logout` - Dashboard: `GET /api/dashboard/stream` (SSE), `GET /api/dashboard/user-downloads` (deprecated), `GET /api/dashboard/cover-art`, `POST /api/dashboard/blocklist-search` - Status: `GET /api/status` - History: `GET /api/history/recent` - Webhooks: `POST /api/webhook/sonarr`, `POST /api/webhook/radarr` - Proxy routes: Sonarr, Radarr, SABnzbd, and Emby authenticated proxies - Public health: `GET /health`, `GET /ready` - **Machine-usable extensions** — Every documented endpoint includes: - `x-code-samples` with cURL, JavaScript fetch, and TypeScript examples - `x-integration-notes` section in descriptions for AI agents and automated tooling - Realistic request/response examples and full JSON Schema definitions - **Coverage validation test suite** — `tests/integration/swagger-coverage.test.js` (22 tests) validates that: - The OpenAPI spec loads without YAML parse errors - Every Express route appears in the merged spec - All schema and response examples are valid JSON - Required security schemes (`CookieAuth`, `CsrfToken`) are defined and referenced correctly - The Swagger UI HTML endpoint (`GET /api/swagger`) returns `200` - **CI/CD validation job** — Added "Swagger Validation & Coverage" job in `.gitea/workflows/ci.yml` that runs on every push: - Lints `server/openapi.yaml` with `@stoplight/spectral-cli` - Runs `npm test -- tests/integration/swagger-coverage.test.js` to verify coverage ### Changed - **Dependencies added** — `swagger-ui-express` (^5.0.1), `swagger-jsdoc` (^6.2.8), `yamljs` (^0.3.0), and `@stoplight/spectral-cli` (^6.16.0 dev dependency) for OpenAPI generation, UI serving, and spec linting. ### Security - **Swagger UI public access** — The Swagger UI endpoint (`/api/swagger`) is publicly accessible by design for convenience. All documented API endpoints still enforce authentication (`emby_user` cookie) and CSRF protection (`X-CSRF-Token` header for mutations) as before. The authentication banner in the UI explicitly instructs users to log in via `POST /api/auth/login` first before testing protected endpoints. --- ## [1.6.0] - 2026-05-21 ### Added - **Staged history loading with SSE push** — history data from Sonarr/Radarr is now fetched in stages; `history-update` SSE events push incremental results to the dashboard so the "Recently Completed" tab populates without waiting for the full fetch. - **Frontend unit tests** — added Vitest + jsdom test suite covering `client/src/` modules (formatters, storage, state). - **Comprehensive tests for staged history loading** — backend tests verify the new background-fetch behaviour, cache TTL handling, and SSE emission. - **Integration test coverage** — new integration and unit tests for `dashboard`, `emby`, `sonarr`, `radarr`, and `sabnzbd` routes. ### Changed - **Technical-debt remediation — service extraction** — extracted matching and assembly logic from the monolithic `dashboard.js` (~1,360 lines → ~284 lines) into dedicated, testable services: - `DownloadMatcher.js` — SABnzbd slot / torrent → *arr record matching - `DownloadAssembler.js` — cover-art, episode, link, blocklist-eligibility helpers - `DownloadBuilder.js` — orchestrator that reads the cache snapshot and builds the final user-facing download list - `TagMatcher.js` — tag extraction, sanitisation, and user-ownership matching - `WebhookStatus.js` — webhook configuration status aggregation - **Frontend architecture** — migrated from a monolithic `public/app.js` to vanilla ES modules under `client/src/`, bundled by Vite into `public/app.js`. - **History pagination** — replaced custom date-based cursor pagination with the retriever's built-in pagination (`pageSize=100`, up to 10 pages / 1,000 records total). Eliminates the 40-second response-time regression seen with large histories. - **Status endpoint path** — admin status route moved from `/api/status/status` to `/api/status` for consistency with the router mount point. - **Background-fetch safety** — the poller no longer overwrites the cache with empty data when a background history fetch fails or returns no records. - **SABnzbd progress calculation** — progress is now computed from `slot.mb` and `slot.mbleft` / `slot.mbmissing` because the SABnzbd queue API does not expose a `percentage` field. - **Speed formatting consistency** — `updateDownloadCard()` now calls `formatSpeed()` so all speed values display with uniform units (B/s, KB/s, MB/s, GB/s). - **Status-panel error handling** — the panel now surfaces error messages (e.g. `403` for non-admin users) instead of showing a blank box. ### Fixed - **CSRF token reference errors** — fixed `ReferenceError` bugs in `api.js`, `auth.js`, and the blocklist handler where `csrfToken` and `currentUser` were accessed as bare variables instead of via the shared `state` object. - **Logout button** — fixed undefined variable references in `handleLogoutClick` that prevented the logout flow from completing. - **Missing progress bar for SABnzbd** — SABnzbd downloads now render a correct progress bar instead of showing `undefined%`. - **Status route 404** — corrected the Express router mount so `GET /api/status` responds instead of returning HTML 404. - **Status button DOM ID** — fixed element-ID mismatch (`status-btn` vs `status-toggle`) that prevented the admin status button from toggling the panel. - **Tab selection** — fixed tab switching to use `data-tab` attributes after the DOM IDs were removed during the ES-module migration. - **CSP violations and `ignoreAvailable` reference error** — resolved frontend CSP compliance issues and a missing-variable error in the history tab. - **Docker client-build stage** — removed `client/` from `.dockerignore` so the multi-stage Dockerfile can run the Vite build during image creation. - **Unmatched torrent exclusion** — torrents that cannot be matched to a Sonarr/Radarr record are now correctly omitted from the download display. - **Blocklist button CSRF** — fixed a `ReferenceError` that occurred when a non-admin user clicked the blocklist button. ### Breaking Changes - **Unmatched torrents are no longer displayed** — torrents that do not match a Sonarr/Radarr queue or history record are excluded from the dashboard. Users who previously saw direct torrent-client downloads will no longer see them unless the *arr services track the item. - **Frontend build process changed** — the monolithic `public/app.js` source file has been replaced by a Vite build from `client/src/`. Any custom deployment scripts that copy or modify `public/app.js` directly must be updated to run `npm run build` (or use the provided Dockerfile, which already does this). - **Status API endpoint path changed** — the admin status endpoint moved from `/api/status/status` to `/api/status`. External integrations, monitoring checks, or scripts hitting the old path will receive `404`. --- ## [1.5.5] - 2026-05-20 ### Added - **Download client logos** — Added SVG logos for all supported download clients (SABnzbd, qBittorrent, Transmission, rTorrent, Deluge). Logos appear in the download client filter picker and on download cards. - **Download client logo in filter picker** — Multi-select download client filter now displays client logos alongside names for visual identification. - **Download client logo in download cards** — Download cards now display the client logo in the bottom-right corner (32×32px). Positioned absolutely within the card. - **SABnzbd speed display** — SABnzbd downloads now display the overall queue speed for the currently active download only. Speed is fetched from the client status API and applied to the downloading slot. - **Speed formatting** — Speed values are now formatted with appropriate units (B/s, KB/s, MB/s, GB/s) instead of raw bytes. ### Fixed - **Missing pieces display for SABnzbd** — Removed incorrect "missing x of y" text display for SABnzbd downloads. This information is only relevant for torrent clients (qBittorrent, rTorrent) and is now only shown for those clients. - **Logo duplication on page reload** — Fixed download client logos and user tags appearing twice during page load. Updated `updateDownloadCard()` to remove old elements before adding new ones. - **Logo positioning** — Fixed download client logos appearing stacked at bottom-right of browser window instead of bottom-right of each card. Added `position: relative` to `.download-card` to provide proper positioning context. --- ## [1.5.4] - 2026-05-19 ### Added - **Multi-select download client filter** — Active Downloads tab now includes a dropdown filter that allows users to select multiple download clients. Shows client name and type (e.g., "Main (qbitt)"). Includes Select All / Deselect All buttons. Selection persists across sessions via localStorage. - **Download client ordering** — Downloads are now sorted by client order (matching the configuration order). Downloads from the first configured client appear first. - **Client metadata in downloads** — Download objects now include `client`, `instanceId`, and `instanceName` fields for client identification and filtering. - **SSE payload extension** — SSE stream now includes `downloadClients` array with all configured clients for UI ordering/filtering. - **Automatic migration** — Existing single-select filter selection automatically migrates to new multi-select format on first load. ### Fixed - **SABnzbd size and speed display** — Fixed SABnzbd downloads showing undefined size and speed values. Now correctly uses `slot.mb` for size calculation and `slot.kbpersec` for per-slot speed from cached data. --- ## [1.5.3] - 2026-05-19 ### Fixed - **Status panel rendering regression** — the status panel was rendering as a small blank box due to an undefined `--background` CSS variable. Added the missing variable to all three themes (light, dark, mono). - **Status panel content destruction** — `showDashboard()` was calling `sp.innerHTML = ''` which destroyed the `status-content` div inside the status panel. Removed this destructive line. - **Webhooks panel visibility sync** — the webhooks panel was incorrectly visible on app load. Added explicit hiding of `webhooks-section` in `showDashboard()` to keep it in sync with the status panel (both show/hide together via `toggleStatusPanel()`). - **Webhooks panel DOM structure** — reverted webhooks-section to be a sibling of status-panel (not nested inside it), preventing innerHTML operations from affecting webhook elements. --- ## [1.5.2] - 2026-05-19 ### Fixed - **Status panel close button CSP compliance** — replaced inline `onclick` handler with `addEventListener` to comply with CSP nonce policy. --- ## [1.5.1] - 2026-05-19 ### Fixed - **Webhook endpoints not reachable in production** — `server/index.js` (the production entry point) was missing the `webhookRoutes` import and mount. Only `server/app.js` (the test factory) had the routes registered. As a result every `POST /api/webhook/*` request in a running container fell through to the `verifyCsrf` middleware and was rejected with `403 CSRF token missing`. Added `app.use('/api/webhook', webhookRoutes)` in `index.js` immediately after `authRoutes` and before `verifyCsrf`, matching the order in `app.js`. --- ## [1.5.0a] - 2026-05-19 ### Fixed - **Status panel close button** — the `×` button now correctly hides the status panel and stops the auto-refresh timer. The button was previously using an inline `onclick` attribute which was silently blocked by the server's CSP nonce policy. Replaced with `addEventListener` wired after `innerHTML` is set, consistent with all other button handlers in the application. --- ## [1.5.0] - 2026-05-19 ### Changed - **`ARCHITECTURE.md`** — consolidated the two existing architecture documents (root concise reference and `docs/ARCHITECTURE.md` deep-dive) into a single comprehensive reference at the project root. The merged document covers all 11 sections: Introduction, High-Level Architecture, Pluggable Architecture Layers (PDCA + PALDRA), Webhook System, Data Flow and Real-time Updates, Caching and Smart Polling, Key Subsystems, Directory Structure, Configuration and Environment Variables, Security Model, and Technology Stack. Includes full Mermaid diagrams for system overview, polling cycle, webhook path, UI state machine, and download matching pipeline. - **`docs/ARCHITECTURE.md`** — removed; content fully merged into root `ARCHITECTURE.md`. --- ## [1.4.0] - 2026-05-19 ### Added #### Webhook Integration (Phases 1–5.1) - **Webhook receiver endpoints** — `POST /api/webhook/sonarr` and `POST /api/webhook/radarr` accept push events from Sonarr and Radarr with shared-secret validation (`X-Sofarr-Webhook-Secret` header). Dashboard updates in < 1 second after any grab, import, or failure. - **Selective cache invalidation** — each incoming event is classified into *queue events* (`Grab`, `Download`, `DownloadFailed`, `ManualInteractionRequired`) or *history events* (`DownloadFolderImported`, `ImportFailed`, `EpisodeFileRenamed`, `MovieFileRenamed`). Only the affected cache key is refreshed via a lightweight re-poll of that instance, rather than a full poll cycle. - **SSE broadcast on webhook** — after refreshing the cache, `pollAllServices()` is called as a fire-and-forget, triggering an immediate SSE push to every connected browser. - **Notification management API proxy** — `GET /api/sonarr/api/v3/notification` and `POST /api/sonarr/api/v3/notification` (and Radarr equivalents) proxy the full *arr Notification API through sofarr with auth + CSRF enforcement. - **One-click webhook setup** — `POST /api/sonarr/webhook/enable` and `POST /api/radarr/webhook/enable` auto-configure the Sofarr webhook notification connection inside the respective *arr service. `POST /api/sonarr/webhook/test` and `/radarr/webhook/test` trigger a test event. - **Webhooks Configuration UI** — collapsible panel in the dashboard allowing users to enable, test, and view webhook status for each configured Sonarr/Radarr instance, with per-trigger type indicators showing which event types are active. - **`SOFARR_WEBHOOK_SECRET`** environment variable — required for webhook endpoints to accept requests. Generate with `openssl rand -hex 32`. - **`SOFARR_BASE_URL`** environment variable — public URL of sofarr, used by the one-click setup to tell Sonarr/Radarr where to POST events. #### Smart Polling Optimization (Phase 5) - **Webhook metrics tracking** — `cache.js` now maintains per-instance and global webhook metrics (`lastWebhookTimestamp`, `eventsReceived`, `pollsSkipped`) via `getWebhookMetrics()`, `updateWebhookMetrics()`, `incrementPollsSkipped()`, `getGlobalWebhookMetrics()`. - **Conditional poll skipping** — `poller.js` calls `shouldSkipInstancePolling()` before each Sonarr/Radarr fetch. If all instances of a type have received a webhook event within the fallback timeout window, their queue/history API calls are skipped entirely; existing cached data has its TTL extended instead. - **Webhook fallback** — if no webhook events have been received globally for `WEBHOOK_FALLBACK_TIMEOUT` minutes (default: 10), the poller forces a full poll regardless of per-instance state. Logged as `[Poller] Webhook fallback triggered`. - **Poll-skip logging** — logs `[Poller] Skipping sonarr/radarr polling for N instance(s) with active webhooks` when polling is skipped. - **`WEBHOOK_FALLBACK_TIMEOUT`** environment variable — minutes before fallback polling (default: `10`). - **`WEBHOOK_POLL_INTERVAL_MULTIPLIER`** environment variable — internal multiplier for TTL calculations when webhooks are active (default: `3`). - **Phase 5.1 metrics connection** — `webhook.js` calls `cache.updateWebhookMetrics(instance.url)` after every successfully validated event, activating the smart skip logic for that instance. #### Security Hardening (Phase 6) - **Dedicated webhook rate limiter** — 60 requests per minute per IP on `/api/webhook/*`, stricter than the global 300/15 min API limiter. Bypassed in tests via `SKIP_RATE_LIMIT=1`. - **Strict input validation** — `validatePayload()` rejects: non-object bodies, missing/non-string/overlong `eventType`, unrecognised event type values (allowlist of 18 known *arr event types), non-string `instanceName`. Returns `400` with a descriptive message. - **Replay protection** — `isReplay()` tracks recently-seen `(eventType, instanceName, date)` tuples in a `Map` with a 5-minute TTL. Duplicate events within the window return `200 { received: true, duplicate: true }` without triggering a cache refresh or SSE broadcast. - **35 new webhook integration tests** — cover secret validation (missing/wrong/unconfigured), payload validation (all invalid cases), replay protection, happy-path acceptance of all relevant event types, metrics increment, and secret-never-leaks assertions. #### Documentation (Phase 6) - **`README.md`** — updated architecture overview diagram, added Webhooks section with quick-setup guide, added PDCA/PALDRA/webhook architecture table, added Webhooks & Smart Polling env var section, added webhook API endpoints, updated test count. - **`CHANGELOG.md`** — this entry. - **`SECURITY.md`** — added webhook threat model rows, webhook-specific hardening checklist, and rate-limit table entry for webhook endpoints. - **`ARCHITECTURE.md`** (root) — new concise top-level architecture reference describing all pluggable layers and the full webhook + polling optimization flow. - **`.env.sample`** — added `WEBHOOK_FALLBACK_TIMEOUT` and `WEBHOOK_POLL_INTERVAL_MULTIPLIER` with explanatory comments; updated NOTES section. ### Changed - `poller.js` — `pollAllServices()` now conditionally skips Sonarr/Radarr fetches when webhooks are active; extends TTL of existing cache entries instead of overwriting with empty data. - `cache.js` — exports four new webhook metrics helpers alongside the existing `MemoryCache` singleton. - `webhook.js` — imported `express-rate-limit`; added `validatePayload()`, `isReplay()`, `VALID_EVENT_TYPES` allowlist, `recentEvents` Map, and per-route `webhookLimiter` middleware. --- ## [1.3.0] - 2026-05-17 ### Added - **History tab** — new "Recently Completed" tab showing imported and failed downloads from Sonarr/Radarr history for the last N days (configurable via the days input, persisted in `localStorage`). Auto-refreshes every 5 minutes. - **History deduplication** — when a failed download has subsequently been imported successfully, only the successful record is shown. If the most recent record for an item is a failure but the episode/movie is already on disk (upgrade attempt), the record is flagged as `availableForUpgrade`. - **"Upgrade available" badge** — failed history cards where the content is already on disk display an amber badge to indicate this is a failed upgrade rather than a missing item. - **"Hide upgrade failures" toggle** — checkbox in the history tab to filter out failed records that are already available on disk. State persists in `localStorage`. Tooltip explains the behaviour and matches the episode/multi-episode tooltip style. - **Blocklist & Search button** — admin-only button on download cards with an "Import Pending" caution. Removes the download from the client with `blocklist=true` (preventing re-grab of the same release) then immediately triggers an `EpisodeSearch`/`MoviesSearch` command in Sonarr/Radarr. Shows a confirmation dialog, loading/success/error states. Kicks a background poll on success. - **`POST /api/dashboard/blocklist-search`** — new admin-only endpoint backing the above button. Accepts `arrQueueId`, `arrType`, `arrInstanceUrl`, `arrInstanceKey`, `arrContentId`, `arrContentType`. - **Title link home navigation** — the sofarr logo/title in the header now navigates to the default view (Active Downloads tab, close status panel, reset "Show all users" toggle) without a page reload. - **Version footer link** — the version string in the dashboard footer links to the source repository. ### Changed - History records are now deduplicated server-side before being sent to the client — only the most relevant record per content item per instance is returned. - Import-issue badge tooltip now uses the themed `var(--surface)` / `var(--text-primary)` / `var(--border)` CSS variables, matching the episode and toggle tooltip style. - Poller now stores `_instanceKey` on Sonarr/Radarr queue records in the cache, enabling the backend to look up API credentials for blocklist operations without an additional configuration lookup. - **Blocklist & Search button** — now available on all admin downloads (not just those with import issues), and also available to non-admin users when: import issues are present, OR (for qBittorrent torrents only) the download is more than 1 hour old AND has less than 100% availability. - **Download object** — added `canBlocklist` boolean field to indicate whether the current user can blocklist a given download. - **qBittorrent torrent data** — added `addedOn` timestamp field to enable age-based blocklist eligibility checks. --- ## [1.2.2] - 2026-05-17 ### Changed - **Header logo** — uses the higher-resolution 192px favicon source rendered at 56px for better visual balance alongside the title text. --- ## [1.2.1] - 2026-05-17 ### Added - **Version footer** — the dashboard footer now displays the running app version (e.g. `sofarr v1.2.1`), fetched from the `/health` endpoint on page load. --- ## [1.2.0] - 2025-05-17 ### Security - **Docker secrets support** — all sensitive environment variables (`COOKIE_SECRET`, `EMBY_API_KEY`, `SABNZBD_API_KEY`, `SONARR_API_KEY`, `RADARR_API_KEY`, `QBITTORRENT_PASSWORD`) now support the standard `_FILE` variant for loading values from mounted secret files (e.g. `COOKIE_SECRET_FILE=/run/secrets/cookie_secret`). - **Weak secret warning** — server now warns at startup if `COOKIE_SECRET` is shorter than 32 characters. - **EMBY_URL validation** — validates the Emby URL scheme at startup and warns on misconfiguration. - **Improved error sanitization** — `sanitizeError()` now also redacts hostnames from full request URLs that may appear in axios error messages. - **Graceful shutdown** — `SIGTERM` and `SIGINT` handlers now stop the background poller and drain open HTTP connections before exiting. Prevents data loss and zombie processes on `docker stop`. ### Compliance - **MIT LICENSE file** added to project root. - **Copyright headers** added to key server source files (`index.js`, `poller.js`, `config.js`, `sanitizeError.js`, `loadSecrets.js`). - **`security.txt`** (`/.well-known/security.txt`) added for responsible disclosure. ### Configuration - **URL validation** added to `config.js` — all configured service instance URLs are validated for scheme (`http`/`https`) and well-formedness at startup; malformed URLs emit a warning instead of crashing. ### Docker / Deployment - **`docker-compose.yaml`** updated with commented Option B (Docker secrets `_FILE` pattern) alongside the existing plain-env Option A. - **`.dockerignore`** updated — `tests/`, `coverage/`, `vitest.config.js`, `CHANGELOG.md`, `SECURITY.md`, `LICENSE`, `.markdownlint.json` excluded from the production image. ### CI - **`docs-check` workflow** added — separate Gitea Actions workflow that lints all Markdown files and validates Mermaid diagram syntax on every push that touches `.md` files. Both jobs use `continue-on-error: true` so documentation issues never block a release. - **Mermaid diagrams** in `docs/ARCHITECTURE.md` fixed — replaced invalid `\n` in stateDiagram transition labels, Unicode arrows/dashes, and double-spaces in flowchart edge definitions. --- ## [1.1.2] - 2025-05-15 ### Changed - Server startup message now includes the current version (`sofarr v1.1.2`). --- ## [1.1.1] - 2025-05-14 ### Fixed - Docker/TrueNAS SCALE healthcheck: dynamic HTTP/HTTPS selection based on `TLS_ENABLED` environment variable. Prevents containers from being stuck in "starting" state when `TLS_ENABLED=false`. --- ## [1.1.0] - 2025-05-13 ### Added - **Episode display** — TV show download cards now show episode information (S01E01 format with title). Multi-episode packs show a "Multiple episodes" badge with a tooltip listing all episodes. - **Episode tooltip** — solid background colour (theme-dependent) for readability. - Sonarr queue and history API requests now include `includeEpisode=true`. --- ## [1.0.0] - 2025-05-01 ### Added - Initial release. - SABnzbd queue and history integration. - qBittorrent torrent integration. - Sonarr and Radarr queue/history matching with user tag filtering. - Emby/Jellyfin authentication. - Server-Sent Events (SSE) real-time dashboard. - Per-request CSP nonce, CSRF double-submit, HSTS, Permissions-Policy. - Background polling with configurable interval and on-demand fallback. - Docker multi-stage build, non-root user, read-only filesystem. - TLS support with bundled snakeoil certificate.