The requests table showed all names as "—" because Jellyseerr's /api/v1/request
list does NOT embed titles — they live on the Movie/Series records. Added
JellyseerrClient._resolve_title(media_type, tmdb_id) that fetches
/api/v1/movie/{tmdbId} (→ title) or /api/v1/tv/{tmdbId} (→ name), cached on
the client instance so subsequent polls are instant.
Also scoped the table fetch to open requests only (pending + approved) via
Jellyseerr's filter param, instead of fetching all 800+ historical requests.
open_requests() fetches pending+approved (paginated), resolves their titles
(small set → fast), and returns them sorted by date added desc.
Updated the frontend table's status filter to Open/Pending/Approved (the data
only contains open requests now).
Tests: title resolution end-to-end (movie tmdbId → title), caching across
polls, filter param used. 404/404 backend + 184/184 frontend + build green.
Replace the static "recent requests" list with a proper table of all Jellyseerr
requests, sorted by date added (newest first by default) with standard sorting
and filtering.
Backend:
- JellyseerrClient.requests(max_count=500): paginated GET /api/v1/request
(sort=added), mapped with type (movie/tv), status, media_status, and
created_at labels. Returns up to 500 so the table can sort/filter client-side.
- fetch_jellyseer_requests(service) reuses the per-service cached client
(shared with the stats widgets).
- new GET /api/jellyseerr/requests endpoint.
Frontend:
- JellyseerRequestsTable: TanStack Table (sorting via getSortedRowModel,
pagination via getPaginationRowModel) reusing the Table primitives +
TablePagination. Columns: Name / Type / Status / Media / Requested, all
sortable; default sort Requested desc. A search box filters by name and a
status dropdown defaults to "Open" (pending+approved+processing) with
All/Pending/Approved/Declined options. (The shared DataTable is deliberately
visibility-only, so this is a dedicated sortable table.)
- RequestsTab renders the stats grid + the new table (the compact recent list
stays on the Requests overview widget).
- useJellyseerRequests hook + fetchJellyseerRequests API client.
Tests: client requests() mapping + single-page stop; fetch helper not-configured;
RequestsTab test mocks both hooks. 404/404 backend + 184/184 frontend pass;
build (tsc -b && vite build) + ESLint clean.
Groundwork for Jellyseerr request stats in the Jellyfin service, behind a
small reusable abstraction so future stats services (Sonarr/Radarr) reuse it.
Backend:
- JellyseerrClient.request_count() -> /api/v1/request/count (normalized
total/pending/approved/declined/processing/available) and recent_requests()
-> /api/v1/request mapped to {name,type,status,media_status,created_at}
with numeric status enums labelled.
- widgets/stats_provider.py: StatsProvider protocol + registry keyed by
service_type (StatValue/StatsResult). A thin generic interface.
- widgets/jellyseerr_stats.py: JellyseerrStatsProvider registered for the
Jellyfin service; reuses one authenticated client per service (lru_cache) and
caches the StatsResult for ~10s under a lock, so multiple widgets + the tab
collapse onto one Jellyseerr fetch (same lesson as the qBittorrent client).
Accepts jellyseerr_api_key from secrets OR config during the upcoming
config->secret migration.
- Jellyfin service gains two widget kinds: `stat` (a Literal selector over the
six stats — the "extract one value into a widget" affordance) and
`stats_overview` (all stats + recent list).
- widgets router routes widget_kind in {stat, stats_overview} to a generic
StatsWidgetSource (dispatches to the service type's provider), independent of
service type.
- new /api/jellyseerr/stats router endpoint for the Requests tab (resolves the
Jellyfin service by id or first-enabled; shares the provider cache).
Tests: provider normalization, not-configured, TTL caching; stat selector +
overview + unknown-stat widget dispatch; 7 new tests. 400/400 backend pass;
ruff clean.