All checks were successful
Build and Push Docker Image / build (push) Successful in 28s
Create Release / release (push) Successful in 6s
Docs Check / Markdown lint (push) Successful in 56s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 46s
Docs Check / Mermaid diagram parse check (push) Successful in 1m31s
CI / Security audit (push) Successful in 1m50s
CI / Tests & coverage (push) Successful in 1m55s
15 KiB
15 KiB
Changelog
All notable changes to this project will be documented in this file. Format follows Keep a Changelog. This project adheres to Semantic Versioning.
[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
--backgroundCSS variable. Added the missing variable to all three themes (light, dark, mono). - Status panel content destruction —
showDashboard()was callingsp.innerHTML = ''which destroyed thestatus-contentdiv 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-sectioninshowDashboard()to keep it in sync with the status panel (both show/hide together viatoggleStatusPanel()). - 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
onclickhandler withaddEventListenerto 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 thewebhookRoutesimport and mount. Onlyserver/app.js(the test factory) had the routes registered. As a result everyPOST /api/webhook/*request in a running container fell through to theverifyCsrfmiddleware and was rejected with403 CSRF token missing. Addedapp.use('/api/webhook', webhookRoutes)inindex.jsimmediately afterauthRoutesand beforeverifyCsrf, matching the order inapp.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 inlineonclickattribute which was silently blocked by the server's CSP nonce policy. Replaced withaddEventListenerwired afterinnerHTMLis 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 anddocs/ARCHITECTURE.mddeep-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 rootARCHITECTURE.md.
[1.4.0] - 2026-05-19
Added
Webhook Integration (Phases 1–5.1)
- Webhook receiver endpoints —
POST /api/webhook/sonarrandPOST /api/webhook/radarraccept push events from Sonarr and Radarr with shared-secret validation (X-Sofarr-Webhook-Secretheader). 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/notificationandPOST /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/enableandPOST /api/radarr/webhook/enableauto-configure the Sofarr webhook notification connection inside the respective *arr service.POST /api/sonarr/webhook/testand/radarr/webhook/testtrigger 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_SECRETenvironment variable — required for webhook endpoints to accept requests. Generate withopenssl rand -hex 32.SOFARR_BASE_URLenvironment 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.jsnow maintains per-instance and global webhook metrics (lastWebhookTimestamp,eventsReceived,pollsSkipped) viagetWebhookMetrics(),updateWebhookMetrics(),incrementPollsSkipped(),getGlobalWebhookMetrics(). - Conditional poll skipping —
poller.jscallsshouldSkipInstancePolling()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_TIMEOUTminutes (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 webhookswhen polling is skipped. WEBHOOK_FALLBACK_TIMEOUTenvironment variable — minutes before fallback polling (default:10).WEBHOOK_POLL_INTERVAL_MULTIPLIERenvironment variable — internal multiplier for TTL calculations when webhooks are active (default:3).- Phase 5.1 metrics connection —
webhook.jscallscache.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 viaSKIP_RATE_LIMIT=1. - Strict input validation —
validatePayload()rejects: non-object bodies, missing/non-string/overlongeventType, unrecognised event type values (allowlist of 18 known *arr event types), non-stringinstanceName. Returns400with a descriptive message. - Replay protection —
isReplay()tracks recently-seen(eventType, instanceName, date)tuples in aMapwith a 5-minute TTL. Duplicate events within the window return200 { 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— addedWEBHOOK_FALLBACK_TIMEOUTandWEBHOOK_POLL_INTERVAL_MULTIPLIERwith 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 existingMemoryCachesingleton.webhook.js— importedexpress-rate-limit; addedvalidatePayload(),isReplay(),VALID_EVENT_TYPESallowlist,recentEventsMap, and per-routewebhookLimitermiddleware.
[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 anEpisodeSearch/MoviesSearchcommand 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. AcceptsarrQueueId,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
_instanceKeyon 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
canBlocklistboolean field to indicate whether the current user can blocklist a given download. - qBittorrent torrent data — added
addedOntimestamp 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/healthendpoint 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_FILEvariant 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_SECRETis 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 —
SIGTERMandSIGINThandlers now stop the background poller and drain open HTTP connections before exiting. Prevents data loss and zombie processes ondocker 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.yamlupdated with commented Option B (Docker secrets_FILEpattern) alongside the existing plain-env Option A..dockerignoreupdated —tests/,coverage/,vitest.config.js,CHANGELOG.md,SECURITY.md,LICENSE,.markdownlint.jsonexcluded from the production image.
CI
docs-checkworkflow added — separate Gitea Actions workflow that lints all Markdown files and validates Mermaid diagram syntax on every push that touches.mdfiles. Both jobs usecontinue-on-error: trueso documentation issues never block a release.- Mermaid diagrams in
docs/ARCHITECTURE.mdfixed — replaced invalid\nin 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_ENABLEDenvironment variable. Prevents containers from being stuck in "starting" state whenTLS_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.