github Shoshuo/Prismarr v1.0.6
Prismarr v1.0.6

4 hours ago

Added

  • "Test connection" buttons in the setup wizard. Each service step (TMDb, Radarr, Sonarr, Prowlarr, Jellyseerr, qBittorrent) now has an inline Test connection button 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 from HealthService::diagnose() so the same labels appear in /admin/settings.

Security

  • SSRF guard on HealthService::httpProbe(). The probe now hard-rejects any URL that isn't http:// or https://, blocks the 169.254.0.0/16 link-local range used by AWS / GCP / Azure cloud-metadata endpoints, and pins cURL to CURLPROTO_HTTP | CURLPROTO_HTTPS for 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_window policy. 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 force Cache-Control: no-store, no-cache, private and X-Content-Type-Options: nosniff so 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 requiring ROLE_USER never blocks a legitimate flow. It does close the small window between image start and setup_completed=1 where /setup/* is otherwise PUBLIC_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 as unconfigured, 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 / B and MB/s, French keeps Go / Mo / Ko / o and Mo/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.map 404s. 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 both tabler.min.css and tabler-themes.min.css. Cosmetic only — no impact on the rendered UI, just a quieter docker logs prismarr for users triaging real bugs.
  • Stop pinging unconfigured services (issue #9). HealthService::isHealthy() now returns null (was: a stale false) 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/services endpoint already handled null as "not configured", so the new state propagates without any UI breakage. The ServiceNotConfiguredException thrown by Radarr/Sonarr clients on missing config is also caught silently inside the dashboard's safeFetch() for the same reason. New HealthService::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 lightweight GET /api/v2/app/version (instead of POST /auth/login with an empty body, which qBit answers Fails.); QBittorrentClient::login() returns a sentinel SID that getRaw() / postAction() recognize and skip the Cookie: SID=… header for. The wizard step Downloads now displays an inline hint explaining the reverse-proxy setup, and /admin/settings exposes 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 covering isConfigured() URL-only mode and login() sentinel behavior.

Changed

  • Removed the "Coming soon" section in /admin/settings sidebar. 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/settings About 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/settings About card was reading a PRISMARR_VERSION env var that was never injected at build, so it always displayed the literal 1.0.0-dev fallback while the Updates card on the same page showed the real version from App\Service\AppVersion::VERSION. The About card now reads from the same constant. Bumped at every release tag along with CHANGELOG.md. The boot banner in init.sh still respects the PRISMARR_VERSION env var, and the release workflow now passes --build-arg PRISMARR_VERSION=$TAG_WITHOUT_V so the banner displays the correct version on official images instead of 1.0.0-dev.
  • Headroom for large libraries (issue #13 + duplicate). The films and series pages used to crash with ERR_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.ini is bumped to 1024 MB / 120 s as a sane default for medium-large homelabs; the films and series controllers also call set_time_limit(120) defensively; users with even bigger libraries can override both at runtime via the new PHP_MEMORY_LIMIT and PHP_MAX_EXECUTION_TIME env vars in docker-compose.yml (the init script writes them to /usr/local/etc/php/conf.d/zz-runtime.ini at boot, no image rebuild needed); and a new FatalErrorHandlerSubscriber, registered very early in public/index.php so it runs before Symfony's own error handler, catches E_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 set PHP_MEMORY_LIMIT=2048M (or higher) in their compose.
  • Honor the TZ env var (issue #12). The image used to ship tzdata-less and the php.ini had date.timezone = Europe/Paris hardcoded, so TZ=Pacific/Honolulu (or any other zone) in your docker-compose.yml was ignored at every layer: date inside the container, PHP's date helpers, the /admin/settings server time line. Now tzdata is bundled in the image, php.ini defaults to UTC, and the init script reads $TZ at boot to (a) symlink /etc/localtime to the right zone file, (b) write /etc/timezone, and (c) drop a /usr/local/etc/php/conf.d/zz-tz.ini that overrides PHP's default. Invalid or missing $TZ falls back to UTC instead of pretending everyone is in Paris. The boot banner shows the resolved zone so users can confirm at a glance.

Don't miss a new Prismarr release

NewReleases is sending notifications on new releases.