From dcd8082c8dfaf10fbe4d0d4fb2d7fab7b82f775e Mon Sep 17 00:00:00 2001 From: Zak Bearman Date: Sun, 13 Sep 2026 12:28:52 +1200 Subject: [PATCH] Filter recent requests by resolved display stage before pagination --- backend/app/routers/requests.py | 24 +++++++++++++++------ backend/tests/test_recent_stages.py | 33 +++++++++++++++++++++++++++++ frontend/app/MyRequests.tsx | 10 ++++++--- 3 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 backend/tests/test_recent_stages.py diff --git a/backend/app/routers/requests.py b/backend/app/routers/requests.py index 32e01da..e7add34 100644 --- a/backend/app/routers/requests.py +++ b/backend/app/routers/requests.py @@ -2760,14 +2760,17 @@ async def recent_requests( status_codes = request_stage_filter_codes(stage) if _recent_cache_stale(): _refresh_recent_cache_from_db() + # Jellyfin can promote working requests to ready. Filter the displayed + # stage before pagination, including working candidates in the ready view. + candidate_codes = [4, 5] if status_codes == [4] else status_codes + take = max(1, min(int(take), 200)) + skip = max(0, int(skip)) rows = _get_recent_from_cache( - requested_by, - requested_by_id, - take, - skip, - since_iso, - status_codes=status_codes, + requested_by, requested_by_id, + len(_recent_cache.get("items") or []), 0, since_iso, + status_codes=candidate_codes, ) + matched = 0 cache_mode = (runtime.artwork_cache_mode or "remote").lower() allow_title_hydrate = False allow_artwork_hydrate = client.configured() @@ -2880,6 +2883,13 @@ async def recent_requests( jellyfin_cache, ) status_label = _status_label_with_jellyfin(status, is_available) + if status_label == STATUS_LABELS[4]: + status = 4 + if status_codes and status not in status_codes: + continue + matched += 1 + if matched <= skip: + continue results.append( { "id": row.get("request_id"), @@ -2896,6 +2906,8 @@ async def recent_requests( }, } ) + if len(results) >= take: + break return {"results": results} diff --git a/backend/tests/test_recent_stages.py b/backend/tests/test_recent_stages.py new file mode 100644 index 0000000..5a15799 --- /dev/null +++ b/backend/tests/test_recent_stages.py @@ -0,0 +1,33 @@ +import unittest +from types import SimpleNamespace +from unittest.mock import AsyncMock, patch +from contextlib import ExitStack +from backend.app.routers import requests + + +class RecentStageTests(unittest.IsolatedAsyncioTestCase): + async def test_displayed_stage_controls_filter_and_pagination(self): + runtime = SimpleNamespace(jellyseerr_base_url='', jellyseerr_api_key='', + requests_data_source='prefer_cache', artwork_cache_mode='remote', + jellyfin_base_url='', jellyfin_api_key='') + rows = [dict(request_id=i, title=str(i), status=status, media_type='movie', + requested_by_id=10) for i, status in [(1,5),(2,5),(3,4),(4,6),(5,5),(6,2),(7,1),(8,3)]] + async def available(client, title, *args): return title in {'1','3','4','5'} + with ExitStack() as stack: + for name, value in [('get_runtime_settings', runtime), ('_recent_cache_stale', False), + ('active_repair_request_ids', {'5'}), ('get_request_cache_payload', None)]: + stack.enter_context(patch.object(requests, name, return_value=value)) + stack.enter_context(patch.dict(requests._recent_cache, {'items':rows})) + stack.enter_context(patch.object(requests, '_request_is_available_in_jellyfin', new=available)) + user={'role':'user','username':'viewer','jellyseerr_user_id':10} + expected={'working':[2,5], 'ready':[1,3], 'partial':[4], 'approved':[6], + 'pending':[7], 'declined':[8], 'in_progress':[2,4,5,6]} + for stage, ids in expected.items(): + result=await requests.recent_requests(take=20,skip=0,days=0,stage=stage,user=user) + self.assertEqual([r['id'] for r in result['results']],ids,stage) + result=await requests.recent_requests(take=1,skip=1,days=0,stage='working',user=user) + self.assertEqual([r['id'] for r in result['results']],[5]) + result=await requests.recent_requests(take=1,skip=0,days=0,stage='ready',user=user) + self.assertEqual(result['results'][0]['status'],4) + result=await requests.recent_requests(take=20,skip=0,days=0,stage='all',user={**user,'jellyseerr_user_id':99}) + self.assertEqual(result['results'],[]) diff --git a/frontend/app/MyRequests.tsx b/frontend/app/MyRequests.tsx index 736da3b..64f170a 100644 --- a/frontend/app/MyRequests.tsx +++ b/frontend/app/MyRequests.tsx @@ -85,6 +85,7 @@ export default function HomePage() { router.push('/login') return } + let cancelled = false const load = async () => { setRecentLoading(true) setRecentError(null) @@ -100,6 +101,7 @@ export default function HomePage() { throw new Error(`Auth failed: ${meResponse.status}`) } const me = await meResponse.json() + if (cancelled) return const userRole = me?.role ?? null setRole(userRole) setAuthReady(true) @@ -121,18 +123,20 @@ export default function HomePage() { throw new Error(`Recent requests failed: ${response.status}`) } const data = await response.json() + if (cancelled) return if (Array.isArray(data?.results)) { setRecent(normalizeRecentResults(data.results)) } } catch (error) { console.error(error) - setRecentError('Recent requests are not available right now.') + if (!cancelled) setRecentError('Recent requests are not available right now.') } finally { - setRecentLoading(false) + if (!cancelled) setRecentLoading(false) } } - load() + void load() + return () => { cancelled = true } }, [recentDays, recentStage]) useEffect(() => {