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)
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
- 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
#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.
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.
Debug logging at line 389/393 still referenced radarrMovies.data and
sonarrSeries.data which were removed in the previous commit. Updated
to use moviesMap/seriesMap built from embedded queue/history objects.
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
- Server tracks each client's refresh rate via query param on /user-downloads
- Active clients expire after 30s of no requests
- Status panel 'Data Refresh' card shows:
- Background poll interval (or Disabled)
- Effective mode: Background if all clients >= poll rate,
Foreground (with rate) if any client is faster, Idle if no clients
- Active client list with per-user refresh rate and last-seen age
- Foreground mode shown with orange badge for visibility
- Client refresh rate sent on every dashboard request
- 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
- SABnzbd, Sonarr, and Radarr history now fetch 20 items instead of 100
- Only recent completions are needed for the dashboard
- Reduces response payload and serialization time
- 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
- Detect trackedDownloadState=importPending or status=warning/error
- Extract statusMessages and errorMessage from queue records
- Display red 'Import Pending' badge on download card header
- Hover reveals tooltip with the specific issue messages
- Visible to all users (not admin-only)
- Replicate Ombi's SanitizeTagLabel logic (lowercase, replace non-alnum with hyphen, collapse, trim)
- Try exact tag match first (handles users with normal usernames)
- Fall back to sanitized comparison (handles email usernames like robcunn@live.co.uk → robcunn-live-co-uk)
- Applied to all tag matching locations
- Series title links to Sonarr series page (/series/{titleSlug})
- Movie title links to Radarr movie page (/movie/{titleSlug})
- Links open in new tab, only shown for admin users
- Instance URL preserved through data aggregation for multi-instance support
- 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)
- Admin users (Emby IsAdministrator) see a 'Show all users' toggle
- When toggled, all tagged downloads are shown regardless of user
- Each download card shows the tagged user's name as a badge
- Non-admin users see only their own downloads (unchanged behavior)
- Backend accepts ?showAll=true query param (admin-only)
- 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