- 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
cache.js: Map values serialise as '{}' under JSON.stringify, causing
emby:users to show 0 bytes and null item count in the status panel.
Convert Maps via Object.fromEntries before stringifying, and report
Map.size as itemCount.
index.js: JS and CSS served with Cache-Control: no-cache so browsers
always revalidate on load. ETag still prevents re-downloading unchanged
files — only a new deploy triggers an actual download.
server/utils/logger.js was still writing to ../../server.log relative
to __dirname (/app/server.log) which is root-owned. The non-root node
user (UID 1000) cannot write there, causing an EACCES crash on startup.
Fix: use DATA_DIR env var (same as index.js) so all log writes go to
/app/data/server.log which is owned by the node user.
better-sqlite3 is a native C++ addon that requires compilation on Alpine
(musl libc, no pre-built binaries exist) and fails on Debian slim too
because prebuild-install cannot detect the libc type correctly.
Replace with a pure-JS JSON file token store (server/utils/tokenStore.js):
- Atomic writes via temp file + rename (no corruption on crash)
- Same API: storeToken/getToken/clearToken
- TTL enforcement on read and hourly prune
- Zero native code, zero build tools required
Dockerfile:
- Revert to node:22-alpine (was node:22-slim)
- Remove build tools (python3/make/g++) — no longer needed
- Restore wget HEALTHCHECK (available in Alpine busybox)
docker-compose.yaml: restore wget healthcheck
package.json: remove better-sqlite3 dependency
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.
Fast poll (every cycle): SABnzbd, Sonarr/Radarr queue + history,
qBittorrent — all lightweight with no include* params.
Slow cache (5 min TTL): Sonarr series, Radarr movies, tags —
fetched only when cache expires. These rarely change.
This eliminates the 2s+ includeSeries/includeMovie joins from
every poll cycle. First poll is still slow (cold cache), but
subsequent polls should complete in <500ms.
Sonarr/Radarr history with include* params forces expensive DB
joins (~2s each). History records still have seriesId/movieId
for matching; the series/movie objects come from the queue-built
maps instead.
Trade-off: completed downloads only show if the series/movie
is also currently in the queue. Active downloads unaffected.
- Sonarr/Radarr history: pageSize 20 -> 10
- SABnzbd history: limit 20 -> 10
- Drop includeEpisode from Sonarr queue and history (never rendered)
- These reduce DB join overhead and response payload size
The poller was fetching the entire series and movie libraries on every
poll cycle (~9s each). Queue and history records already embed the full
series/movie object via includeSeries/includeMovie params.
Changes:
- Remove 'Sonarr Series' and 'Radarr Movies' timed fetches from poller
- Tag queue/history records with _instanceUrl in the poller instead
- Build seriesMap/moviesMap from embedded objects in dashboard
- Remove poll:sonarr-series and poll:radarr-movies cache keys
- Fix missing axios and config imports in dashboard
- Net result: ~18s saved per poll cycle, ~2 fewer API calls
- Each service fetch is individually timed (SABnzbd, Sonarr, Radarr, qBit)
- Status panel shows timing bar chart with ms per task and total
- Shows 'Last Poll' age that updates live
- Shows client refresh rate (1s/5s/10s/Off)
- Status panel auto-refreshes in sync with dashboard refresh cycle
- Changing refresh rate restarts the status panel refresh too
- TTL counters update live on each refresh
- New /api/dashboard/status endpoint (admin-only, 403 for non-admins)
- Returns server info (uptime, Node version, memory usage)
- Returns polling mode and interval
- Returns cache stats: entry count, total size, per-key breakdown
with item count, size in KB, and TTL remaining
- Status button in admin controls header
- Collapsible status panel with grid layout
- Responsive: single column on mobile
- Set POLL_INTERVAL=0, off, false, or disabled to disable background polling
- When disabled, data is fetched on-demand when a user opens the dashboard
- On-demand results cached for 30s so other users benefit from fresh data
- A user with a faster refresh rate keeps the cache warm for everyone
- When polling is enabled, behaviour is unchanged (default 5s)
- New poller.js polls all services on a configurable interval
- POLL_INTERVAL env var (default 5000ms / 5 seconds)
- All data stored in cache with TTL of 3x poll interval
- Dashboard endpoint now reads from cache only (no network calls)
- API responses are near-instant regardless of service count
- First poll runs immediately on server start
- Reuse client instances so auth cookies survive across requests
- Eliminates redundant login round-trips on every dashboard refresh
- Clients still re-authenticate automatically if session expires (403)
- Add MemoryCache utility with get/set/invalidate/clear
- Cache Sonarr series, Sonarr tags, Radarr movies, Radarr tags
- 60-second TTL - first request fetches, subsequent requests served from cache
- Queue, history, and torrent data remain uncached (changes frequently)
- On cache hit, these 4 heavy API calls resolve instantly
- Prefer content_path over save_path for qBittorrent torrents
- content_path is the full path to the single file or top-level
folder for multi-file torrents
- save_path is just the base download directory
- Admin users see download path (SABnzbd storage / qBittorrent save_path)
- Admin users see target path (Sonarr series folder / Radarr movie folder)
- Paths displayed in monospace font at bottom of card details
- Non-admin users unaffected (paths not sent in API response)
- Fix seriesMap key (use Sonarr internal id, not tvdbId)
- Fix Sonarr tag resolution (use tag map like Radarr)
- Use sourceTitle for history record matching
- Fall back to embedded movie/series objects when API timeouts
- Add includeMovie/includeSeries params to queue/history API calls
- Add coverArt field to all download responses (TMDB poster URLs)
- Add cover art display to frontend download cards
- Fix user-summary route to use instance config and tag maps