Added
- "Test connection" buttons in the setup wizard. Each service step (TMDb, Radarr, Sonarr, Prowlarr, Jellyseerr, qBittorrent) now has an inline
Test connectionbutton next to its inputs. Result is shown as a small status badge (green "Connected", red category like "Wrong API key" / "Cannot reach service" / "Endpoint not found"). Non-blocking — users can still continue without testing. Categories are surfaced fromHealthService::diagnose()so the same labels appear in/admin/settings.
Security
- SSRF guard on
HealthService::httpProbe(). The probe now hard-rejects any URL that isn'thttp://orhttps://, blocks the169.254.0.0/16link-local range used by AWS / GCP / Azure cloud-metadata endpoints, and pins cURL toCURLPROTO_HTTP | CURLPROTO_HTTPSfor both the initial request and any redirect. RFC1918 LAN ranges (10/8,172.16-31,192.168/16) are intentionally still allowed because Prismarr legitimately needs to reach Radarr/Sonarr/Prowlarr/Jellyseerr/qBittorrent on private addresses. This closes a dormant blind-SSRF that wasn't exploitable in v1.0.5 (the only call site was admin-only/admin/settings) but would have become exploitable as soon as the new public/setup/test/<service>endpoint shipped. - Rate limiter on
/setup/test/<service>. 30 attempts per minute per (client IP × service),sliding_windowpolicy. Neuters scripted port-scan attempts during the brief window where the wizard is publicly reachable (post-image-pull, pre-setup_completed=1). - Strict response envelope on
/setup/test/<service>—{ok, category}only, no echo of the URL probed, no echo of the API key submitted, no upstream response body. Headers forceCache-Control: no-store, no-cache, privateandX-Content-Type-Options: nosniffso the response cannot be cached by an intermediate proxy. - Auth gate (
ROLE_USER) on/setup/test/<service>. The wizard step that hosts the test buttons (TMDb / Managers / Indexers / Downloads) only renders after the admin has been created at step 2 and auto-logged-in via$security->login(), so requiringROLE_USERnever blocks a legitimate flow. It does close the small window between image start andsetup_completed=1where/setup/*is otherwisePUBLIC_ACCESS, eliminating the unauthenticated reachability of the probe endpoint entirely. Defense-in-depth on top of the existing CSRF token, rate limiter, service whitelist, and SSRF guard. - 7 new PHPUnit tests covering the new endpoint (guard / CSRF / rate limit / strict payload / field whitelist) and the SSRF guard (file:// / gopher:// / dict:// / link-local IPs blocked, RFC1918 + public HTTPS allowed).
Fixed
- "Test connection" button is no longer rendered for Gluetun on
/admin/settings.HealthService::probeFor()has no Gluetun handler so the probe always came back asunconfigured, which made the button look broken even when Gluetun was correctly set up. The button is hidden until/unless we add a real Gluetun probe. - Locale-aware byte units (issue #4). All filesize and transfer-rate displays now follow the active UI locale: English renders
GB / MB / KB / BandMB/s, French keepsGo / Mo / Ko / oandMo/s. Previously the FR abbreviations were hardcoded everywhere, including in the EN UI. Implemented as two new Twig filters (prismarr_bytes,prismarr_speed) and a global JS helper (window.prismarrBytes) so server- and client-rendered sizes stay consistent. Covered: root folders (Radarr / Sonarr), backups, Jellyseerr cache stats, qBittorrent dashboard totals, film/series detail cards, the post-download toast notification, and the qBittorrent torrent upload size-limit error message. - Stop logging spurious
tabler.min.css.map404s. The bundled Tabler CSS files referenced an upstream sourcemap (/*# sourceMappingURL=tabler.min.css.map */) that wasn't shipped with the image, so any browser opening DevTools triggered a 404 caught and logged as an exception by Symfony. The reference is now stripped from bothtabler.min.cssandtabler-themes.min.css. Cosmetic only — no impact on the rendered UI, just a quieterdocker logs prismarrfor users triaging real bugs. - Stop pinging unconfigured services (issue #9).
HealthService::isHealthy()now returnsnull(was: a stalefalse) when a service has no URL or API key in the DB, and skips the ping entirely — so users who only enabled a subset of the stack don't see "Jellyseerr ping failed" / "Gluetun ping failed" warnings every minute in their logs. The dashboard, topbar dropdown, and/api/health/servicesendpoint already handlednullas "not configured", so the new state propagates without any UI breakage. TheServiceNotConfiguredExceptionthrown by Radarr/Sonarr clients on missing config is also caught silently inside the dashboard'ssafeFetch()for the same reason. NewHealthService::isConfigured()helper exposes the check to the dashboard so it can hide widgets bound to disabled services entirely (mini-calendar, Jellyseerr requests, TMDb trending, recent additions) instead of rendering empty cards. - qBittorrent behind a reverse proxy (issue #10). Empty username/password are now a legitimate configuration: when qBittorrent sits behind a proxy that injects authentication itself (qui, traefik forward auth, …) Prismarr treats the credentials as optional.
HealthService::isConfigured()only requires the URL;HealthService::probeFor()falls back to a lightweightGET /api/v2/app/version(instead ofPOST /auth/loginwith an empty body, which qBit answersFails.);QBittorrentClient::login()returns a sentinel SID thatgetRaw()/postAction()recognize and skip theCookie: SID=…header for. The wizard step Downloads now displays an inline hint explaining the reverse-proxy setup, and/admin/settingsexposes a "Clear" button next to the qBit user/password fields so a deliberate wipe bypasses the empty-value guard that protects the other credentials from a Firefox/Chrome silent strip. 5 new PHPUnit tests coveringisConfigured()URL-only mode andlogin()sentinel behavior.
Changed
- Removed the "Coming soon" section in
/admin/settingssidebar. The disabled "Email notifications" and "Security · sessions" rows haven't been wired up to a real feature yet, and showing them as "v1.1" pollution every time the user opens settings adds noise without value. They will reappear when the corresponding features actually land. - Public roadmap link. Added a "Roadmap" entry in the
/admin/settingsAbout page (next to Source / Bug / Docs) and on the Updates page (next to GitHub / Docker Hub), plus an explicit mention in the README's Project status section. Points to the public GitHub project at https://github.com/users/Shoshuo/projects/3 so users can see what's queued and follow progress without needing to dig through the issue list. - "Monitored only" filter and persistent state on films / series pages (issue #14). Adds a "Monitored" pill to the existing status filter bar (next to All / Downloaded / Missing / Unmonitored) so users with a mix of monitored and unmonitored items can quickly narrow down to the ones actually being tracked. The active status filter is persisted in two layers: the URL (
/medias/films?filter=monitored,/medias/series?filter=continuing, …) so refreshes keep the view and shared links land on the same filter, and localStorage so sidebar navigation back to/medias/films(without the URL parameter) restores the user's last choice. The URL takes precedence when present so shared links always override the local preference. Quality / genre / language / sort / search will follow alongside the v1.1.0 server-side pagination refactor. - Renamed "Jellyseerr" to "Seerr" in the UI (issue #2). Overseerr and Jellyseerr were both archived in February 2026 and replaced by Seerr, a unified API-compatible fork. The wizard, sidebar, admin settings, dashboard and README now refer to "Seerr" instead. Internal identifiers (class names, route names, settings keys like
jellyseerr_url/jellyseerr_api_key) are unchanged so existing installs aren't disrupted, and the API endpoints Prismarr calls all exist verbatim in Seerr's spec — pointing your config at a fresh Seerr container instead of the archived Jellyseerr one keeps everything working without any setting edit. - Single source of truth for the running version (issue #11). The
/admin/settingsAbout card was reading aPRISMARR_VERSIONenv var that was never injected at build, so it always displayed the literal1.0.0-devfallback while the Updates card on the same page showed the real version fromApp\Service\AppVersion::VERSION. The About card now reads from the same constant. Bumped at every release tag along withCHANGELOG.md. The boot banner ininit.shstill respects thePRISMARR_VERSIONenv var, and the release workflow now passes--build-arg PRISMARR_VERSION=$TAG_WITHOUT_Vso the banner displays the correct version on official images instead of1.0.0-dev. - Headroom for large libraries (issue #13 + duplicate). The
filmsandseriespages used to crash withERR_EMPTY_RESPONSE(or a 500 / blank page) on libraries bigger than ~3,000–5,000 items, because the entire library is loaded in one shot then rendered in one Twig pass — easily blowing past the default 256 MB / 60 s ceiling. Four layers of fix:php.iniis bumped to 1024 MB / 120 s as a sane default for medium-large homelabs; thefilmsandseriescontrollers also callset_time_limit(120)defensively; users with even bigger libraries can override both at runtime via the newPHP_MEMORY_LIMITandPHP_MAX_EXECUTION_TIMEenv vars indocker-compose.yml(the init script writes them to/usr/local/etc/php/conf.d/zz-runtime.iniat boot, no image rebuild needed); and a newFatalErrorHandlerSubscriber, registered very early inpublic/index.phpso it runs before Symfony's own error handler, catchesE_ERROR(out-of-memory or max-execution-time) at PHP shutdown and emits a self-contained 503 HTML page that explains what happened and how to bump the limits, instead of letting the connection die mid-render. The proper fix — server-side pagination so the page never loads more than ~100 items at once — is deferred to v1.1.0: it's a substantial refactor of the films / series templates (1900+ lines each) plus the JS view-switching, and shipping it in 1.0.6 alongside the other fixes would risk regressions. The bumped limits cover libraries up to ~50,000 items in practice; users with bigger collections setPHP_MEMORY_LIMIT=2048M(or higher) in their compose. - Honor the
TZenv var (issue #12). The image used to shiptzdata-less and thephp.inihaddate.timezone = Europe/Parishardcoded, soTZ=Pacific/Honolulu(or any other zone) in yourdocker-compose.ymlwas ignored at every layer:dateinside the container, PHP's date helpers, the/admin/settingsserver time line. Nowtzdatais bundled in the image,php.inidefaults toUTC, and the init script reads$TZat boot to (a) symlink/etc/localtimeto the right zone file, (b) write/etc/timezone, and (c) drop a/usr/local/etc/php/conf.d/zz-tz.inithat overrides PHP's default. Invalid or missing$TZfalls back to UTC instead of pretending everyone is in Paris. The boot banner shows the resolved zone so users can confirm at a glance.