Build and Push Docker Image / build (push) Successful in 41s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 56s
CI / Security audit (push) Successful in 1m6s
CI / Tests & coverage (push) Successful in 1m27s
Sonarr tracks the exact SAB download ID (nzo_id). Now tries to match
by downloadId first, then falls back to title matching. Also adds
debug to show if matches are via downloadId vs title, and logs
downloadIds in history to verify the link exists.
Build and Push Docker Image / build (push) Successful in 39s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 53s
CI / Security audit (push) Successful in 1m10s
CI / Tests & coverage (push) Successful in 1m30s
When a match is found, logs whether it came from queue or history.
When no match, shows history counts and sample titles to verify
history is being checked properly.
Build and Push Docker Image / build (push) Successful in 44s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m6s
CI / Security audit (push) Successful in 1m21s
CI / Tests & coverage (push) Successful in 1m34s
SAB items often persist after Sonarr has processed them.
Previously only checked the active queue, now also checks
history records so completed downloads still appear.
Build and Push Docker Image / build (push) Successful in 13s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 46s
CI / Security audit (push) Has been cancelled
CI / Tests & coverage (push) Has been cancelled
SAB filenames use dots (dora.the.explorer.s02e08) but Sonarr titles
use spaces (Dora the Explorer - S02E08). Now tries matching with
both formats to improve match rate.
Also logs actual Sonarr titles when no match found for debugging.
Build and Push Docker Image / build (push) Successful in 29s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m4s
CI / Security audit (push) Successful in 1m29s
CI / Tests & coverage (push) Successful in 1m49s
Shows exactly which SAB items match/don't match to Sonarr/Radarr:
- ✓ Sonarr match: SAB name → Sonarr name
- ✓ Radarr match: SAB name → Radarr name
- ✗ No match: SAB name (with Sonarr queue count)
This will help diagnose why Sonarr Activity Queue shows matches but Sofarr doesn't.
Build and Push Docker Image / build (push) Successful in 46s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m8s
CI / Security audit (push) Successful in 1m33s
CI / Tests & coverage (push) Successful in 1m41s
1. Fixed download client collision:
- SABnzbd client with id 'i3omb' was being overwritten by qBittorrent
- Now uses unique key ':' like the arr retrievers
2. Fixed webhook metrics showing 0:
- instanceName from webhooks is generic ('Sonarr', 'Radarr')
- Not the configured instance name ('i3omb')
- Now updates metrics for ALL instances of that type
Build and Push Docker Image / build (push) Successful in 46s
Licence Check / Licence compatibility and copyright header verification (push) Has been cancelled
CI / Security audit (push) Has been cancelled
CI / Tests & coverage (push) Has been cancelled
The status panel was showing webhooks as disabled (null) when no events
had been received yet. Now it checks Sonarr/Radarr API to see if the
Sofarr webhook notification is actually configured.
- Added checkWebhookConfigured() to verify webhook exists in Sonarr/Radarr
- Shows 'enabled: true' with 0 events when webhook is configured
- Only shows null when webhook is not configured at all
Build and Push Docker Image / build (push) Successful in 34s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m3s
CI / Security audit (push) Successful in 1m20s
CI / Tests & coverage (push) Successful in 1m38s
Add debug logging to trace:
- When downloads payload is built
- Data sizes from cache (SAB, qBit, Sonarr, Radarr)
- Number of downloads found and their titles
This will help diagnose why Dora downloads aren't appearing.
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m8s
CI / Security audit (push) Successful in 1m28s
CI / Tests & coverage (push) Successful in 1m53s
Build and Push Docker Image / build (push) Successful in 35s
- Fixed webhooks section to load collapsed (content hidden, toggle arrow reset)
- Added webhook metrics card to status panel for admin users:
- Shows Sonarr/Radarr enabled/disabled status
- Shows events received and polls skipped counts
- Updated /api/dashboard/status endpoint to include webhook metrics
- Metrics are aggregated from all Sonarr/Radarr instances
Build and Push Docker Image / build (push) Successful in 44s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m6s
CI / Security audit (push) Successful in 1m17s
CI / Tests & coverage (push) Successful in 1m33s
The webhook notification payload was using string 'POST' for the method
field, but Sonarr/Radarr API expects numeric values:
- 1 = POST
- 2 = PUT
Also added onManualInteractionRequired: false to match the schema.
Fixes: Radarr/Sonarr rejecting webhook configuration with validation errors
Build and Push Docker Image / build (push) Successful in 31s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m8s
CI / Security audit (push) Successful in 1m21s
CI / Tests & coverage (push) Successful in 1m36s
The notification routes were using process.env.SONARR_URL directly,
which is undefined when using the newer SONARR_INSTANCES JSON format.
Changes:
- Added getFirstSonarrInstance() and getFirstRadarrInstance() helpers
- Updated /notifications, /notifications/test, and /notifications/sofarr-webhook
routes to use instance config from getSonarrInstances()/getRadarrInstances()
- Returns 503 error if no instances are configured
Fixes: 'Invalid URL' errors when calling Sonarr/Radarr notification APIs
Build and Push Docker Image / build (push) Successful in 27s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m3s
CI / Security audit (push) Successful in 1m19s
CI / Tests & coverage (push) Successful in 1m34s
Added detailed error logging to help diagnose 500 errors when calling
Sonarr/Radarr notification APIs. Logs include:
- Error message
- Response status (if available)
- Response data (if available)
This will help identify if the issue is:
- Missing SONARR_URL/RADARR_URL or API keys
- Network connectivity issues
- Sonarr/Radarr API version incompatibility
Build and Push Docker Image / build (push) Successful in 1m7s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 1m15s
CI / Security audit (push) Successful in 1m44s
CI / Tests & coverage (push) Successful in 1m53s
- Added POST /api/webhook/sonarr and POST /api/webhook/radarr endpoints
- Implemented webhook secret validation via SOFARR_WEBHOOK_SECRET environment variable
- Added logging for all incoming webhook events using existing logToFile utility
- Returns HTTP 200 immediately to prevent webhook retries
- Mounted webhook routes before CSRF middleware (called by external services)
- Non-breaking: no changes to polling, caching, SSE, or any existing behavior
- Lays groundwork for Phase 2 (cache + SSE integration) without implementing it yet
Build and Push Docker Image / build (push) Successful in 37s
CI / Security audit (push) Successful in 1m30s
CI / Tests & coverage (push) Successful in 1m47s
- Added addedOn timestamp to qBittorrent torrent mapping
- Added canBlocklist helper function: true for admins, true for non-admins when (importIssues OR (torrent >1h old AND availability<100%))
- Added canBlocklist field to all download objects in /user-downloads and SSE /stream routes (8 blocks total)
- Frontend button now shows when (isAdmin OR download.canBlocklist) && download.arrQueueId
Build and Push Docker Image / build (push) Successful in 29s
CI / Security audit (push) Successful in 1m24s
CI / Tests & coverage (push) Successful in 1m42s
- Poller now stores _instanceKey alongside _instanceUrl on Sonarr/Radarr queue records
- dashboard route threads arrQueueId/arrType/arrInstanceUrl/arrInstanceKey/arrContentId/arrContentType as admin-only fields on downloads with importIssues
- POST /api/dashboard/blocklist-search: admin-only, removes queue item with blocklist=true then triggers EpisodeSearch/MoviesSearch
- Button renders in download card header (admin + importIssues + arrQueueId only)
- Confirm dialog, loading/success/error states on the button
- Kicks a background poll on success so SSE reflects removed item promptly
Build and Push Docker Image / build (push) Successful in 25s
CI / Security audit (push) Successful in 45s
CI / Tests & coverage (push) Successful in 1m9s
Sonarr queue and history records do not expose episodeNumber at the
top level — it is only present inside the nested episode object
(record.episode.episodeNumber). Same for seasonNumber. The original
extractEpisode() read record.episodeNumber which was always undefined,
so gatherEpisodes() always returned an empty array.
Fix: prefer the nested episode object fields, falling back to the
top-level fields for forward-compatibility.
Build and Push Docker Image / build (push) Successful in 37s
CI / Security audit (push) Successful in 59s
CI / Tests & coverage (push) Successful in 54s
- Add includeEpisode:true to Sonarr queue and history API requests
in both the poller and historyFetcher
- Add extractEpisode() / gatherEpisodes() helpers in dashboard.js
and history.js to build a sorted, deduplicated episodes array
covering all records matching a download title (handles multi-
episode packs and series packs)
- Replace episodeInfo: sonarrMatch with episodes: gatherEpisodes()
across all 8 assignment sites in dashboard.js
- Add episodes field to /api/history/recent response items
- Frontend: formatEpisodeInfo() renders S01E05 for single episodes
or 'Multiple episodes' with hover tooltip listing all for packs
- CSS: .episode-info and .multi-episode tooltip styles
- ARCHITECTURE.md: update polling table and download/history schemas
Build and Push Docker Image / build (push) Successful in 36s
CI / Security audit (push) Successful in 49s
CI / Tests & coverage (push) Successful in 59s
secure:true cookies are only sent by browsers over HTTPS connections.
When NODE_ENV=production (always set in the Docker container) but no
TLS proxy is in front, the browser receives the cookie on login but
refuses to send it on subsequent HTTP requests — causing every
authenticated endpoint (/stream, /status, etc.) to return 401.
The correct signal is TRUST_PROXY: it is only set when a TLS-terminating
reverse proxy is confirmed to be in front. Affects emby_user and
csrf_token cookies across login, /csrf refresh, and logout.
Build and Push Docker Image / build (push) Successful in 26s
CI / npm audit (push) Has been cancelled
- index.html: checkbox between password field and login button
- app.js: reads #remember-me and passes rememberMe in POST body
- auth.js: rememberMe=true sets 30-day maxAge; false = session cookie
(expires when browser closes)
- style.css: .form-group--checkbox and .checkbox-label styles
Module-level const assignments (SONARR_API_KEY, RADARR_API_KEY,
SABNZBD_API_KEY, EMBY_URL, EMBY_API_KEY) captured values at startup
and would not pick up rotated credentials without a restart.
Replaced all module-level captures in emby.js, sabnzbd.js, sonarr.js,
radarr.js, and dashboard.js with inline process.env reads at each
call site. A process restart is still needed for dotenv-loaded values
but environment-injected vars (Docker, Kubernetes) are re-read live.
#13 Logout doesn't revoke Emby token:
- Added in-memory tokenStore (userId -> { accessToken })
- AccessToken stored server-side after successful login; never sent
to client
- POST /logout calls Emby POST /Sessions/Logout with the stored
token before clearing it; failure is warned but does not block
the local cookie clear
#14 Unbounded Emby session creation per login:
- DeviceId in the Emby auth request is now a stable SHA-256 hash
of the lowercase username (sofarr-<16 hex chars>)
- Emby treats the same DeviceId as the same device and reuses the
existing session slot instead of creating a new one each login
Added server/utils/sanitizeError.js which redacts:
- ?apikey= query parameters (SABnzbd passes key in URL)
- ?token= query parameters
- X-Api-Key / X-MediaBrowser-Token / X-Emby-Authorization header
values if they appear in the error message string
Applied to all catch blocks in emby.js, sabnzbd.js, sonarr.js,
radarr.js, and dashboard.js. Internal error.message still logged
server-side (unredacted) for debugging.
#7 isAdmin trusted from unsigned cookie:
- isAdmin is derived server-side from Emby Policy at login time
- Cookie is now signed (HMAC) when COOKIE_SECRET env var is set;
Express rejects tampered signatures (signedCookies returns false)
- dashboard.js /user-downloads and /status now use requireAuth
middleware (req.user) instead of re-parsing cookie directly
#8 cookie-parser used without signing secret:
- cookieParser(COOKIE_SECRET) in index.js when env var is set
- Hard-fails at startup in production if COOKIE_SECRET unset
- Warns in development
#9 Cookie JSON parsed without schema validation:
- parseSessionCookie() in auth.js and requireAuth.js both validate:
id (non-empty string), name (non-empty string), isAdmin (boolean)
- Invalid/tampered cookies return null / 401 respectively
Uses express-rate-limit@6 (pinned for Node 12 dev compat; Node 18
in prod container is unaffected). Limits each IP to 10 attempts per
15-minute window. Returns 429 with a safe error message on breach.
The full authResponse.data (containing AccessToken) and user object
were being logged via console.log → written to server.log on disk.
Replaced with a single safe log line showing only name and isAdmin.
Build and Push Docker Image / build (push) Successful in 28s
Server:
- Add getEmbyUsers(): fetches all Emby users, builds Map of
lowercase/sanitized name -> display name, cached 60s
- Add buildTagBadges(allTags, embyUserMap): classifies each tag
as { label, matchedUser: displayName|null } against the full
Emby user database
- Attach tagBadges[] to every download object when showAll=true
(all 10 construction sites across SABnzbd queue/history and
qBittorrent queue/history blocks)
- matchedUserTag still set to the tag matching the *current* user
for the non-showAll badge
Frontend:
- showAll mode: renders tagBadges[] — unmatched tags (no Emby user)
amber leftmost, matched tags show Emby display name in accent
colour rightmost
- Normal mode: renders matchedUserTag badge only (current user's tag)
Build and Push Docker Image / build (push) Successful in 27s
Previously extractUserTag returned the first tag in the list regardless
of whether it matched the logged-in user, so matchedUserTag was wrong
and unmatched tags weren't separated correctly.
- extractUserTag(tags, tagMap, username): finds tag label that matches
username via tagMatchesUser(); returns null if no match
- extractAllTags(): moved before extractUserTag for readability
- All 10 call sites in user-downloads pass username arg
- user-summary uses extractAllTags() directly (wants all tags, not just
the current user's) — as a bonus this now correctly counts items
tagged for multiple users
Build and Push Docker Image / build (push) Successful in 27s
- server: add extractAllTags() returning all tag labels for a series/movie
- server: showAll now includes items with ANY tag (not just user-matched);
non-admin path unchanged (must match current user's tag)
- server: replace userTag with allTags[] + matchedUserTag on every download object
- frontend: render all tags in header; unmatched tags amber (left), matched
user tag in accent colour (rightmost); only visible in showAll mode
- css: add --unmatched-tag-bg/color variables to all three themes (light,
dark, mono) and .download-user-badge.unmatched style
Build and Push Docker Image / build (push) Successful in 39s
#1 Session cookie: add secure (production-only) and sameSite=strict
to prevent transmission over HTTP and cross-site request abuse.
#2 Remove Emby AccessToken from cookie payload — it was stored in
the browser cookie but is never needed client-side; reduces blast
radius if cookie is ever exposed.
#3 Add requireAuth middleware to all proxy routes (/api/emby,
/api/sabnzbd, /api/sonarr, /api/radarr) — previously unauthenticated,
now require a valid emby_user session cookie.
#4 Remove open CORS wildcard (cors() with no options). The frontend
is served from the same origin so no CORS headers are required.
Also update clearCookie() to include matching cookie options.