You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Whole-server lockup on Unraid when the data volume sits on a FUSE share (/mnt/user). PHP's native file session handler holds an exclusive flock on the session file for the entire request, and the dashboard fires ~6 widget fragments in parallel, so they all serialised on that one lock while their slow Radarr/Sonarr calls ran. On Unraid the session file lives on the shfs/FUSE share, where flock contention is expensive enough to peg every core and freeze the whole machine (mapping the volume to /mnt/cache "fixed" it only by bypassing FUSE). A new SessionLockReleaseSubscriber now closes the session right after authentication on read-only GET requests, releasing the lock immediately so the parallel fragments stop fighting over it. POSTs, the setup wizard and internal routes keep the session open and write normally. Unraid users should still map the data volume to /mnt/cache/... rather than /mnt/user/....
Gluetun integration with API key set, and incorrect endpoints. The Gluetun client authenticated using Authorization: Bearer <key>, but Gluetun expects it as X-API-Key, so it would return a 401 error when an API key is required. Additionally, referring to the older Control Server Docs, WireGuard doesn't get its own endpoint, so the /v1/wireguard/status and /v1/wireguard/portforwarded calls were incorrect. The client now sends X-API-Key and uses the unified /v1/vpn/status and /v1/portforward endpoints, with the legacy /v1/openvpn/ paths as a fallback. With that, the protocol selector in the settings becomes redundant and was removed.
Radarr/Sonarr sidebar entry unusable with 4+ instances (#44). The dropdown toggle shown for 4 or more instances of the same service used the native Bootstrap data-bs-toggle, but nothing in the app initializes Bootstrap's dropdown JS anymore — every other dropdown had already moved to a shared click-delegate. The triangle rendered but never opened, making every instance beyond the first three unreachable. The toggle now uses the same delegate as the rest of the app, and its label shows the active instance's name instead of staying stuck on the generic service name.
A torrent client page could show another client's downloads mid-session. Turbo Drive re-executes the incoming page's <script> with a fresh closure while the outgoing page's — and its live 3-second poll timer — kept running; since the qBittorrent, Deluge and Transmission pages render the same element IDs, the orphaned poller kept overwriting the page you'd navigated to with its own client's torrents. The poll timer now lives on a single shared handle so the incoming page can kill the outgoing one's, and these three pages always do a full reload in and out instead of a Turbo visit.
Performance
Service-health pings are now cached across requests.HealthService::statusFor()'s 10 s memo lived in a per-object array, which the classic (non-worker) FrankenPHP throws away with each request — so every topbar/dashboard health poll, from every open tab, re-pinged all configured services with sequential blocking calls. The verdicts are now shared through cache.app (same 10 s TTL): the whole install performs at most one probe sweep per window, and invalidate() (admin "Test connection" / settings save) rotates a generation token so stale verdicts can't outlive a reconfiguration.
Static assets get real browser-cache headers. Caddy now serves AssetMapper-compiled /assets/* (content-hashed filenames) with Cache-Control: public, max-age=31536000, immutable, and the unfingerprinted /static/* vendor bundles (Tabler, Chart.js) plus /img/* with a one-day TTL — repeat page loads stop re-negotiating ~600 KB of CSS/JS.
Prod cache pre-warmed at image build. The Dockerfile runs cache:warmup after asset-map:compile, so the first request after a container (re)start no longer pays the 1–3 s container/route/Twig compile. Env vars stay runtime-resolved placeholders, so the boot-generated APP_SECRET doesn't invalidate the baked cache.
Faster Radarr / Sonarr library pages. The heavy getMovies() / getSeries() payload is now cached per instance for 45 s (MediaLibraryCache) instead of being re-fetched and re-normalised on every visit, and the per-page status / queue / indexers / health / calendar calls run in a single curl_multi batch (multiGet()) rather than sequentially. Cold loads are unchanged, but revisits within the window are roughly 3× faster, and a slow or unreachable instance now costs one timeout window for the whole page instead of stacking one timeout per call. Empty results are not cached and library mutations invalidate the entry, so user changes still show immediately. Same per-handle semantics as the existing get() (SSRF protocol guard, connect/total timeouts, per-instance circuit breaker).
Internal
CI / release workflows modernised. Bumped the pinned GitHub Actions to current majors (checkout@v5, setup-qemu@v4, setup-buildx@v4, login@v4, metadata@v6, build-push@v7, dockerhub-description@v5, action-gh-release@v3) across ci, beta, release and dockerhub-readme, added a workflow_dispatch trigger to ci so the suite can be run on demand, and guarded the Docker Hub README sync with if: github.repository == 'Shoshuo/Prismarr' so forks don't fail the job on missing secrets.
Added
Deluge tab (optional). A full torrent-management page for Deluge (deluge-web URL + password in the setup wizard or /admin/settings, with per-service kill switch, Test connection and health chip), mirroring the qBittorrent tab: live torrent table (2s-refresh API with server-side pagination/filter/sort/search), state filters, read-only Label filter (Label plugin — labels are set by Sonarr/Radarr and never written from Prismarr), seeding-focused Ratio / Uploaded / Completed columns, a detail panel (status, files, trackers, peers) with Radarr/Sonarr resolve, single + bulk pause/resume/delete(±data)/recheck, reannounce, move storage, session-wide Pause All / Resume All (core.pause_session), add via magnet/URL/.torrent upload (SSRF-guarded, bencode-validated) with save-path, per-torrent and global speed limits, and a sidebar badge with download-complete toasts. The JSON-RPC client judges success on the response envelope (deluge-web answers HTTP 200 even on failure), auto-reconnects a daemon-disconnected web UI, supports an authenticating-reverse-proxy mode (empty password), and runs behind the same circuit breaker + SSRF protocol locks as every other service client.
Transmission torrent client integration. A new download-client page (setup wizard step, admin settings entry, sidebar badge/poll, health circuit breaker) alongside qBittorrent, modeled on the same conventions. Transmission's RPC session handshake is handled transparently: the first request without a valid X-Transmission-Session-Id gets an expected HTTP 409 carrying the real token, which the client caches and retries with — the setup wizard's Test button and the dashboard health check both treat that 409 as "reachable," while a genuine 401 (bad RPC password) is reported as an auth failure. The user/password fields are optional, matching qBittorrent's reverse-proxy-friendly config shape. The page covers the list/table/compact torrent views, filtering, search, sort, pagination, bulk actions (pause/resume/delete/recheck, plus pause-all/resume-all), a per-torrent detail modal (general/files/trackers/peers) and add-by-URL / add-by-file, with a read-only category filter derived from Transmission's own labels.
Plex activity via Tautulli (optional). A new read-only Tautulli integration (URL + API key in /admin/settings, behind the per-service health circuit breaker) surfaces current Plex activity. The dashboard gets a "Current Plex activity" widget — active streams, Direct Play / Direct Stream / Transcode counts, total / LAN / WAN bandwidth and a per-session card (quality, HDR/SDR badge, source→target codec when transcoding) — that hydrates on its own and refreshes every 10 s. A dedicated Plex Activity page (own sidebar entry) adds a now-playing strip, watch statistics with a 7 / 30 / 90-day toggle (top movies / shows / users / platforms), plays-over-time graphs with a Media-type ⇄ Stream-type toggle plus by-hour and by-day-of-week breakdowns and a platform × stream-type "problem clients" chart, a dense watch-history grid, and per-library item counts. Each title opens an in-app info modal (synopsis, ratings, cast/crew). The API key never leaves the server, every response is sanitised before it reaches the browser, and each section fails open independently so a down/misconfigured Tautulli never breaks the dashboard or page. Chart.js is self-hosted (public/static/chart/) for CSP compliance and reused by the existing Radarr stats chart.
In-place quick-look on the dashboard. Clicking any media tile — hero spotlight, upcoming, weekly trending, watchlist or latest additions — now opens a read-only detail modal right on the dashboard (poster, year, status, rating, genres, synopsis) instead of navigating away, with a deep-link to manage the item in Radarr/Sonarr or open it in Discover. Library items resolve from the already-cached dashboard aggregate (zero extra upstream calls on a warm page, falling back to a direct fetch on a miss); TMDb items resolve straight from TMDb. The fragment is read-only and fails open to a small graceful body, and the modal is Turbo-safe (declarative trigger, escaped values).
Plex Activity page — statistics, graphs & a per-user filter. The activity page's statistics gain Most Popular Movies / Shows tiles (ranked by distinct viewers), a Most Concurrent Streams tile, and a Play Count ⇄ Play Duration toggle that reformats every count-based tile and chart into watch time. Four new graphs are added — plays by source resolution, plays by stream resolution, streams by user, and concurrent streams over time (always a count, never reformatted as duration) — alongside a new privacy-safe Users overview table (friendly name, relative last-seen, last played, play count, total watch time). A page-wide per-user filter dropdown scopes the statistics, all graphs and the history grid to a single user, or all users (the Users overview table always lists everyone). The metric and user-id are clamped/validated server-side (metric ∈ plays|duration, user filter digits-only), every new client method and endpoint fails open to its neutral shape, and the allow-list keeps email / IP / avatar / Plex login off the wire — the opaque Tautulli user-id is a filter token only and never rendered. All data comes from read-only Tautulli commands (get_home_stats, get_users_table, get_user_names, get_plays_by_*, get_concurrent_streams_by_stream_type); strings are i18n'd (en/fr).
Dashboard theming (glance-style presets). A single admin-chosen instance theme (Settings → Display, beside the accent picker) restyles the whole UI from a curated catalogue of 17 presets adapted from glance's theme list. Each preset is a full colour model — HSL background / primary / positive / negative plus contrast and text-saturation multipliers and a light/dark flag — resolved server-side into concrete CSS variables (no flash of unstyled colour, --tblr-primary-rgb precomputed) and injected into the base layout. Picking a preset fixes the palette for everyone; the new default "Classic" mode keeps the original per-browser light/dark toggle instead, so existing installs are a visual no-op until a theme is explicitly picked. The accent picker gains a "theme default" option that follows the chosen theme's primary (or the original indigo in Classic mode). A theme change forces one full reload so the new colours apply immediately (normal Turbo navigation stays fast); strings are i18n'd (en/fr).
Rich detail modal from top-bar search, with release dates. Clicking a search result now opens the quick-look detail modal (instead of jumping straight to the quick-add form), enriched with release/air dates: movies show theater / digital / physical dates (the next upcoming one emphasised), TV shows a Continuing/Ended status with first-aired and next-episode (or end) dates. The action button is context-aware — Manage (deep-link) for items already in your library, Add (the existing quick-add flow) for ones that aren't. In-library results use the authoritative Radarr/Sonarr dates; not-yet-added results read from TMDb (theater/digital/physical parsed from TMDb's release_dates, FR→US fallback; series air dates from next/last_episode_to_air). The quick-look modal is now app-global (extracted to a shared partial), so it's available from search on every page and the dashboard tiles reuse it. Labels are i18n'd (en/fr); the modal stays read-only and fails open.
Dashboard layout customization. Reorder and hide/show the dashboard's content sections (upcoming releases, requests, health, Plex activity, watchlist, trending, recent additions) from Settings → Display, or directly on the dashboard itself via an on-page edit mode — drag to reorder, per-section hide toggle. The Hero banner stays pinned at the top and is never reordered. Order and visibility resolve server-side (DashboardLayoutService) from the flat settings table; unknown or duplicate keys are dropped, and any section missing from a saved order is appended in its default position, so a section added in a future release always shows up instead of silently vanishing.
Changed
Download queue widget starts collapsed on Radarr/Sonarr. The films/series queue card now loads collapsed by default instead of open; the existing toggle (click the header) still expands it, and the 2s auto-refresh no longer forces it back open.
Plex items open the app-global quick-look modal. Clicking a title on the Plex Activity page or the dashboard's Plex widget now opens the same rich detail modal used everywhere else (poster, synopsis, ratings, watchlist button, Manage/Discover deep-links) instead of the bespoke Tautulli metadata pop-up. The click resolves the item's TMDb id server-side (/tautulli/api/quicklook/{ratingKey}, one get_metadata call — episodes/seasons resolve to their show, with a grandparent hop for older Tautulli payloads that lack show-level guids); only the numeric TMDb id reaches the browser, keeping raw Plex guids inside the existing allow-list sanitization. Items with no TMDb match (music, home videos) fall back to the legacy Plex metadata modal, so every click keeps working.
Dashboard service-health chips show latency.HealthService::statusFor() returns a status word plus a round-trip reading (cached 10 s like the existing bool path, which now delegates to it); the dashboard chips render five states — up / slow / very_slow / down / degraded — with a coloured dot, so a reachable-but-slow service is visibly distinct from a healthy one. isHealthy() keeps its old contract for every existing caller.