fix(qbittorrent): guard against stale pagination responses#82
Open
jeromelefeuvre wants to merge 283 commits into
Shoshuo:mainShoshuo/Prismarr:mainfrom
jeromelefeuvre:fix/qbittorrent-pagination-racejeromelefeuvre/Prismarr:fix/qbittorrent-pagination-raceCopy head branch name to clipboard
Open
fix(qbittorrent): guard against stale pagination responses#82jeromelefeuvre wants to merge 283 commits intoShoshuo:mainShoshuo/Prismarr:mainfrom jeromelefeuvre:fix/qbittorrent-pagination-racejeromelefeuvre/Prismarr:fix/qbittorrent-pagination-raceCopy head branch name to clipboard
jeromelefeuvre wants to merge 283 commits into
Shoshuo:mainShoshuo/Prismarr:mainfrom
jeromelefeuvre:fix/qbittorrent-pagination-racejeromelefeuvre/Prismarr:fix/qbittorrent-pagination-raceCopy head branch name to clipboard
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ror, a11y) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The plays-over-time chart and the Radarr stats chart loaded Chart.js from cdn.jsdelivr.net, but the app's CSP script-src is 'self' 'unsafe-inline' data: so the CDN script was blocked and neither chart rendered. Vendor Chart.js v4.4.6 under public/static/chart and reference it via asset() so it loads same-origin under the strict CSP. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Invalidate on the actions that mutate normalized-list fields (membership, monitored, hasFile, quality): filmMonitor, filmDelete, filmAdd, filmFileDelete, filmsBulkEdit, filmsBulkDelete, filmEdit. Skip async-command actions (rename / bulk refresh / bulk search) — their effects land later via scan, not at request time.
Invalidate on the series actions that mutate normalized-list fields: seriesAdd, seriesImportBatch, seriesBulkEdit, seriesBulkDelete, serieEdit, serieMonitor, serieDelete, seriesFileDelete, seasonMonitor. Skip async-command actions (refresh / search / rename / rss-sync) and episode-level toggles that don't change the series list.
filmFileUpdate edits quality/languages/releaseGroup, which normalizeMovie embeds into the cached movie's quality + language fields. Without invalidation the change stayed hidden until the 45s TTL. Found in code review.
A Windows checkout (core.autocrlf=true) converts the s6-overlay control
files to CRLF, so a local `docker build` produces an image where
s6-rc-compile rejects every service ("type must be oneshot, longrun, or
bundle") and the container never boots. The repo already stores LF, so CI
builds were unaffected and this went unnoticed. Pin docker/** and *.sh to
eol=lf so the image builds correctly regardless of the contributor's git
settings.
perf(media): cache library list + concurrent batch fetch for Radarr/Sonarr pages
GitHub forces Node 20 actions off on 2026-06-16. Bump every action flagged in the GHCR run's deprecation annotation to its Node 24 major: actions/checkout v4 -> v5 (matches the v5 already used in ci/dockerhub) docker/setup-qemu-action v3 -> v4 docker/setup-buildx-action v3 -> v4 docker/metadata-action v5 -> v6 docker/login-action v3 -> v4 docker/build-push-action v6 -> v7 softprops/action-gh-release v2 -> v3 peter-evans/dockerhub-description v4 -> v5 All move the runtime to Node 24 (GitHub-hosted runners satisfy the v2.327.1+ requirement); none change the inputs these workflows use.
ci: bump GitHub Actions to Node 24 runtimes
docs: summarize fork changes vs upstream
Locks the fix from 1b38675: filmFileUpdate() must invalidate the bound instance's library cache on a successful movie-file metadata edit (quality / languages feed normalizeMovie's cached fields), and must NOT invalidate when the upstream write fails. Verified fail-first: with the invalidate() call removed the success case fails ('invalidate() expected once, never invoked').
test(media): regression test for films cache invalidation on file edit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fails A widget fragment fetch that failed (non-OK response or network error) was funneled into the same path as a deliberately-empty body and hidden with display:none - a crashed endpoint silently removed its section from the dashboard, the one surface whose job is answering "is everything OK?". Empty 200 bodies keep hiding the section (the "service not configured" contract is unchanged). Failures now render a muted "This section failed to load" + Retry state inside the card body, so the section header stays visible. Retry re-arms the data-dash-loaded guard, restores the spinner and refetches. The delegated click handler is bound once per session (window guard) so Turbo revisits don't stack listeners; the live-refresh pollers keep their fail-open behavior. Verified against the running app: forced failure renders in place, Retry restores real content, health poller self-heals its own section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… retry button The retry button rendered the literal key "common.retry" — the catalog nests it as common.action.retry. Caught on the live :beta render. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…or, Prowlarr switch Three fixes from the 2026-07-10 design critique (28/40): - Zero-value stat numerals lose their semantic hue: "0 errors"/"0 paused" (Deluge + qBittorrent, initial render and live poll), "0 failing" (Prowlarr) and "0 pending"/"0 declined" (Seerr) render text-secondary until the count is nonzero, so red/orange stay meaningful. - The 11px readability floor for inline font-size .5/.6rem text (mobile audit 2026-06-29) now applies at all breakpoints — the 8.8-9.6px cluster was equally unreadable on desktop. - Prowlarr indexer actions: the ON/OFF button (which read as a status label) becomes a Tabler switch whose position is the state, the destructive delete gets separated by a gap, and the low-value ADDED column hides below xxl so the actions column stops clipping at 1440px. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…~1550px) The d-xxl-table-cell breakpoint (1400px) sits below the width where the indexers table actually outgrows its card — at a 1440px viewport the actions column still clipped by ~86px. Measured live: the table fits from ~1526px up, so the ADDED column now hides under 1550px via a page media query instead of the Bootstrap utility. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion edges, quick-look close
Six fixes from the 2026-07-10 critique re-run (30/40):
- Calendar: the 8.8-9.9px month-view event text (the app's smallest) is
floored at 11px on ALL breakpoints — the existing mobile floor list is
unwrapped from its media query, matching the base.html.twig decision.
- Discover: countdown badges localize their prefix (EN "D-4", FR "J-4")
instead of hardcoded French "J-"; poster type chips use dedicated short
labels (EN "TV"/"Movie") so they stop colliding with the library badge
("TV Sho"), and card badges ride the 11px floor.
- Quick-look: the close button grows from Bootstrap's 14px/40%-opacity
default to a 32px circular target on a permanent dark scrim, so it
survives bright hero artwork.
- Radarr/Sonarr health banners drop the raw check-name prefix
("ImportMechanismCheck:") and show just the human message.
- SABnzbd: the speed-limit input gets a visible MB/s unit suffix (the
unit only existed in the placeholder, invisible once a value is set).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ormat too filmWarnings() hardcoded 'source : message' concatenation, so the check-name prefix survived there after warning_format dropped it. Caught on the live :beta — the banner still showed "ImportMechanismCheck : ...". Now uses the same trans call as the page-render path (line 127) and the Sonarr twin (line 887). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h batch 8 commits from the 2026-07-10 dual-agent design critique (28→30/40), all live-verified on :beta: retryable dashboard widget error states, earned semantic hues on zero-value stats, 11px text floor at all breakpoints (app-wide + calendar), Prowlarr switch + un-clipped actions, localized countdown badges, quick-look close button, clean health warnings, SABnzbd unit suffix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… dl-pulse loop Design-critique round-2, fork-only: - Shoshuo#3 Calendar events open the app-global quick-look modal IN PLACE instead of ejecting to /medias?open=N (honors "everything in place"). Reuses the same library quick-look endpoint the dashboard/search use; the old /medias route is kept as a graceful fallback. Day-detail popup events are now clickable too. Factored the modal fetch+inject into a shared window.prismarrQl.openUrl(url, fallback) and routed the existing [data-ql-trigger] handler through it. - Shoshuo#4 Episode blocks recolored into the blue/cyan family so they match the single blue "episode" legend dot; the old purple/pink/green/orange variants collided with cinema/digital/physical legend hues. - Shoshuo#8 Removed the infinite decorative dl-pulse glow/scale loop on the sidebar download badge (the app's only non-state-driven motion, no reduced-motion guard). Static glow retained; a single reduced-motion-guarded .dl-bumped pulse now fires only when the count rises, via one shared window.prismarrDlBadge helper (deduped 4 poller badge-update sites). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Design-critique round-2, priority-1 bug. 4 of 17 theme presets set data-bs-theme=light, but several custom fork components hardcoded near-white text / dark-only fills that turned invisible or washed out on a white surface. Audited every inline <style>/style="" block and fixed the genuine breaks (leaving white-on-colored-badge and already-gated rules alone): - calendrier: date numbers, day-of-week header, "+N more" and active filter pill switched to --tblr-secondary-color / --tblr-primary; pill hover uses primary-rgb tints; added a light-mode override that bumps the translucent event-block fills near-opaque so their pastel text keeps contrast over the white cell surface. - jellyseerr: detail-modal hero gradient bottom stop was var(--tblr-card-bg) (white in light) under white title text -> pinned to opaque dark. - prowlarr: inactive tag chips, capability labels, new-tag input and the #818cf8 protocol/tag badges tokenized. - radarr/sonarr/prowlarr updates: changelog list text -> secondary-color. - media/series_detail: header pinned to an opaque dark base (white hero text had no dark fallback); status badge tokenized. - media/series: JS-built "followed" badge tokenized. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Design-critique round-2. The media/explorer/watchlist/calendar "cards" open a modal or quick-look on mouse click but were non-focusable DIVs with no accessible name. Now they are focusable (tabindex=0), operable (Enter/Space), and named (aria-label), with a visible focus ring: - Shared global handler in _quicklook.html.twig: Enter/Space on a focused [data-kb-activate] element dispatches its click (bailing when focus is inside a real input/textarea/select/button/a/contenteditable). - Stamped tabindex=0 role=button data-kb-activate + aria-label on: media/films + media/series .media-card, decouverte explorer .ex-card (JS), decouverte index static + JS-built .media-card, watchlist .wl-card, and all four calendar event blocks (month/week/day-row/ day-popup) which already open the quick-look. - base.html.twig: one [data-kb-activate]:focus-visible outline (theme primary), not clipped by the cards' overflow:hidden. Existing img alts left as-is (decorative posters inside a now-labeled card are correct). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Owner-requested additive Tabler primitives (fork-only): - .avatar-list (stacked) in the shared Plex summary strip (_plex_summary, used by the dashboard widget + Tautulli now-playing): a glanceable "who's watching" row of initial-avatars built from the distinct session userDisplayNames — no backend change, degrades to nothing when idle. New key dashboard.plex.viewers (en/fr). - .ribbon "New" corner flag on films/series library cards for items added within 7 days. Kept SPARSE and semantic (not a blanket quality ribbon, which the critique warned reads as uniform noise); on films it swaps the top-left quality-badge slot so corners never double up. New key common.labels.new_badge (en/fr) + a scoped .media-card .ribbon size rule on both pages. Verified the Tabler .ribbon/.avatar-list classes exist in the shipped CSS bundle before wiring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lp (Shoshuo#7) Design-critique P3 grab-bag (fork-only), the high-value items: - dashboard.quicklook.seasons was hardcoded "{count} seasons" (rendered "1 seasons"); now an ICU plural (en + fr). - Sidebar logo linked to a hardcoded tmdb_index (Discover); now points at app_home, which already resolves the admin's display_home_page preference (dashboard/discovery/films/…/last-visited). - Admin display settings: the Theme preset and Primary colour pickers had no explanation of how they relate. Added help text — Primary colour is the accent override ("Auto" keeps the preset's own accent); Theme is the full scheme that ships its own accent (en + fr). Not changed: the 404/error page already extends base.html.twig with the full sidebar/theme chrome + an .empty state and home CTA (the critique's "degraded shell" flag predated a live check). Remaining P3s (Tautulli casing, topbar-title gaps, dual save paradigms, Explorer empty state) left as low-value polish for a later pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Shoshuo#2) Design-critique deferred item. TMDb release dates, certifications, alt titles and watch/providers all picked countries from hardcoded FR-first lists, so an English user saw French release dates and providers first. - New TmdbClient::regionPriority(locale, append) returns a country-code priority led by the locale's home region (en → US/GB/CA/AU, fr → FR/BE/LU/CA, + es/de/pt/it), followed by a broad common fallback chain and any extra countries present in the payload, de-duplicated and order-preserving. - Wired at all six sites (via $this->translator->getLocale()): TmdbController alt-titles / pickProviders / pickMovieCertification / pickTvCertification, DashboardController tmdbMovieReleaseDates / quick-look providers. - Unit test for regionPriority (6 cases: fr/en lead, locale suffix, append dedup, unknown + empty locale fallbacks) — green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…partial Shared component vocabulary, slice 1 of 3. deluge/qbittorrent/usenet each carried a near-identical view-switcher (list/table/compact) with its own .qbt-view-*/.usenet-view-* markup, CSS and JS selectors. - New _view_switcher.html.twig renders .view-switcher > .view-btn from a modes array (view/label/icon/title) + active + aria_label. - Global .view-switcher/.view-btn CSS in base.html.twig, active accent = var(--tblr-primary) (was a hardcoded #2f67ba on the torrent pages; now matches the theme like usenet already did). Descendant-scoped so the media pages' bare .view-btn is untouched. - All three pages migrated to the partial; local switcher CSS removed (layout rules .qbt-torrent-list[data-view]/.usenet-list[data-view] kept); JS click-delegation selectors → .view-btn. data-view values and default active unchanged. lint:twig OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shared component vocabulary, slice 2 of 3. - Global .stat-val/.stat-lbl in base.html.twig; removed the duplicate local copies from usenet/qbittorrent/deluge (kept their card-specific rules: .qbt-stat-card, .stat-speed/.stat-icon, VPN/total cards). - New _stat_tiles.html.twig renders a tiles[] list in either a 'strip' (one card, inline value+label) or 'grid' (card per tile) layout; hue accepts a Tabler token (→ text-*) or a raw color (→ inline style); id/data_stat/unit passthrough for live-updated values. - Migrated films + series (strip), prowlarr + usenet (grid) to the partial with their exact values/labels/hues. usenet's data-stat poll hooks preserved on .stat-val. Removed prowlarr's now-unused .pw-stat. - deluge/qbittorrent keep their rich live-updating stat-cards (IDs, SVG watermarks, VPN) — only the shared-vocabulary CSS moved to global. Harmonizations (intended): films/series value h3→stat-val, label uppercased; prowlarr gutter g-3→g-2. lint:twig OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ulary Shared component vocabulary, slice 3 of 3. films/series used Tabler outline-buttons with solid-colored active state (.filter-btn); deluge/ qbittorrent used transparent tinted-active pills (.qbt-filter-btn) — two looks for the same job. - Global .filter-pill in base.html.twig: transparent, hue-tinted when active, per-pill semantic colour via --pill-hue (color-mix), default theme primary. - New _filter_pills.html.twig renders the pill row from a pills[] list (filter/label/hue/count?/title?) + active + fill. - Migrated films, series (fill, inside their GET form — hidden status input + submit-on-click preserved) and deluge, qbittorrent (client-side refresh preserved). Every data-filter value unchanged; JS selectors retargeted .filter-btn/.qbt-filter-btn → .filter-pill; per-filter CSS removed. Semantic colours preserved as --pill-hue. - usenet/prowlarr keep their status dropdowns; downloads/watchlist tab filters (page-scoped, different pattern) untouched. Visual change: films/series active pills go solid→hue-tinted; torrent active bg now derives from the pill's text hue. lint:twig OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…k, a11y, Tabler r3, TMDb locale, shared component vocabulary Fork-only UI batch (branch ui/design-critique-round-2, 12 commits, CI green, live-verified on :beta): - #1 light-preset readability: tokenized hardcoded dark literals so the 4 light theme presets are legible (calendar, jellyseerr hero, prowlarr, updates changelogs, series header). - Shoshuo#3/Shoshuo#4 calendar: events open the in-place quick-look (no eject); episode blocks recolored to match the blue legend dot. - Shoshuo#5 a11y: media/explorer/watchlist/calendar cards are keyboard-focusable and operable (Enter/Space) with a focus ring. - Shoshuo#7 P3s: season ICU plural, logo honors display_home_page, Theme vs Primary-colour help text. - Shoshuo#8: retired the infinite dl-pulse loop (state-driven pulse instead). - Tabler r3: .avatar-list of Plex viewers, sparse "New" .ribbon on cards. - Shoshuo#2 TMDb: release/cert/provider region priority driven by locale, not hardcoded FR (TmdbClient::regionPriority + unit test). - Shoshuo#6 shared component vocabulary: unified _view_switcher, _stat_tiles and _filter_pills partials + global CSS across the service pages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dpoint
The dashboard's five live widgets (plex 10s, health/server/network 30s,
houndarr 60s) each ran their own setInterval and their own fragment
request, so at every 30s alignment up to four HTTP requests fired at
once (~13 requests/min steady state).
Replace the five bespoke refreshX functions + timers with one generic
coalescing scheduler: each widget node declares its cadence via
data-dash-poll, the scheduler ticks at the GCD of the present cadences,
and every widget due on a tick is batched into a single
/tableau-de-bord/widgets?w=... request that returns a { name: html } map.
Steady state drops to ~6 requests/min and the aligned burst collapses to
one request. Cadences are unchanged; initial hydration still fetches each
fragment in parallel for fast first paint; per-widget fail-open,
hidden-tab guard and Turbo-singleton behaviour are all preserved.
The combined endpoint reuses the existing widget actions verbatim
(widgetPlex/Health/Server/Houndarr/Network) via a private dispatch, so
their admin gates and empty-when-unconfigured contract are identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mbined /widgets endpoint Live-verified on :beta — plex-only at 10s, four-widget burst coalesced to one request at 30s, all five at 60s; every widget renders + refreshes, plex tab persists across swap, console clean. ~13→~6 poll requests/min. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Catch the docs up to everything shipped since 2026-07-10: - README: add the UniFi Network widget to the fork-only + Features lists (it shipped 2026-07-05 but was only in CHANGELOG/FORK-CHANGES). - CHANGELOG: log the 2026-07-11 design-critique round 2 (light-preset legibility, calendar quick-look + legend-true colours, keyboard- activatable cards, .ribbon/.avatar-list, locale-aware TMDb regions, shared component partials, dl-pulse retirement). - FORK-CHANGES: restamp to 2026-07-11 and add the missing waves — worker mode + OPcache + dashboard poller consolidation, mobile/settings polish, and the design-critique/accessibility pass. Docs only; no code or image change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes per-page inline init scripts that re-bound a document-level turbo:load listener on every Turbo body swap, accumulating stale slug-baked closures that corrupted other instances' saved keys. The module now loads once in <head>, is guarded against re-init, and reads the page type/slug from a data-libfilter-* marker on the filter form. Also hardens the EXCLUDE lookup with hasOwnProperty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each Radarr/Sonarr library tab reopens with the user's last-used filter + sort, remembered per instance in localStorage. Filters are saved off the URL on load and restored via location.replace only on a truly empty URL (so ?open= deep links are untouched); sidebar/subnav library links are rewritten on load so the common path lands filtered with no reload flash; Reset/Clear wipes the saved state. Implemented via a single head-loaded module driven by a data-libfilter-* marker on the filter form — no per-page scripts, no accumulating Turbo listeners. Search text is intentionally not remembered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
) The full-library endpoints (/api/v3/series, /api/v3/movie) are fetched via the shared get(), which capped CURLOPT_TIMEOUT at 4s (Sonarr) / 8s (Radarr). On a large or busy instance the response body doesn't finish in that window — libcurl reports "Operation timed out after 4002 ms with 0 bytes received" (TCP connected, no body yet), which trips the circuit breaker and surfaces the service as "unreachable/disconnected". Give only the two heavy full-library calls a 30s budget; connect timeout stays 4s so a genuinely-down service still trips the breaker fast. Radarr get() gains an optional $timeout param (default 8s) so all other call sites are unchanged. The library route already allows set_time_limit(120) and the MediaLibraryCache means only the cold fetch pays this cost. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
refreshData() aborts the previous in-flight fetch before firing a new one, but abort() is a no-op once a response has already arrived — common on localhost, where the round-trip can beat the click-to-JS-execution delay. A periodic poll response for the old page could then land after a user's page-change click and silently snap the view back to the previous page. Add a sequence counter so a response is only applied if no newer request has been issued since, regardless of whether abort() worked. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
jeromelefeuvre
force-pushed
the
fix/qbittorrent-pagination-race
branch
from
July 24, 2026 04:24
7916076 to
4d84db8
Compare
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
refreshData()aborts the previous in-flight fetch before firing a new one, butabort()is a no-op once a response has already arrived — common on localhost, where the round-trip can beat the click-to-JS-execution delay.abort()actually worked.Test plan
make lint-twigpasses on the changed template🤖 Generated with Claude Code