All checks were successful
Build and Push Docker Image / build (push) Successful in 41s
Docs Check / Markdown lint (push) Successful in 48s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 57s
CI / Security audit (push) Successful in 1m23s
CI / Tests & coverage (push) Successful in 1m36s
Docs Check / Mermaid diagram parse check (push) Successful in 1m43s
12 KiB
12 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.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.