From 42d4caa474e1423bdd378086fcd168d790e41c90 Mon Sep 17 00:00:00 2001 From: Rephl3x Date: Tue, 3 Mar 2026 13:24:25 +1300 Subject: [PATCH] Hotfix: expand landing-page search to all requests --- .build_number | 2 +- backend/app/build_info.py | 4 +- backend/app/db.py | 13 ++- backend/app/routers/admin.py | 6 +- backend/app/routers/events.py | 4 + backend/app/routers/requests.py | 66 +++++++++--- frontend/app/admin/requests-all/page.tsx | 37 ++++++- frontend/app/globals.css | 7 ++ frontend/app/page.tsx | 126 ++++++++++++++++------- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- 11 files changed, 212 insertions(+), 59 deletions(-) diff --git a/.build_number b/.build_number index ef5618c..324abdd 100644 --- a/.build_number +++ b/.build_number @@ -1 +1 @@ -0203262044 +0303261323 diff --git a/backend/app/build_info.py b/backend/app/build_info.py index 4c1609c..4b893cd 100644 --- a/backend/app/build_info.py +++ b/backend/app/build_info.py @@ -1,2 +1,2 @@ -BUILD_NUMBER = "0203262044" -CHANGELOG = '2026-03-02|Process 1 build 0203261953\n2026-03-02|Process 1 build 0203261610\n2026-03-02|Process 1 build 0203261608\n2026-03-02|Add dedicated profile invites page and fix mobile admin layout\n2026-03-01|Persist Seerr media failure suppression and reduce sync error noise\n2026-03-01|Add repository line ending policy\n2026-03-01|Finalize diagnostics, logging controls, and email test support\n2026-03-01|Add invite email templates and delivery workflow\n2026-02-28|Finalize dev-1.3 upgrades and Seerr updates\n2026-02-27|admin docs and layout refresh, build 2702261314\n2026-02-27|Build 2702261153: fix jellyfin sync user visibility\n2026-02-26|Build 2602262241: live request page updates\n2026-02-26|Build 2602262204\n2026-02-26|Build 2602262159: restore jellyfin-first user source\n2026-02-26|Build 2602262049: split magent settings and harden local login\n2026-02-26|Build 2602262030: add magent settings and hardening\n2026-02-26|Build 2602261731: fix user resync after nuclear wipe\n2026-02-26|Build 2602261717: master invite policy and self-service invite controls\n2026-02-26|Build 2602261636: self-service invites and count fixes\n2026-02-26|Build 2602261605: invite trace and cross-system user lifecycle\n2026-02-26|Build 2602261536: refine invite layouts and tighten UI\n2026-02-26|Build 2602261523: live updates, invite cleanup and nuclear resync\n2026-02-26|Build 2602261442: tidy users and invite layouts\n2026-02-26|Build 2602261409: unify invite management controls\n2026-02-26|Build 2602260214: invites profiles and expiry admin controls\n2026-02-26|Build 2602260022: enterprise UI refresh and users bulk auto-search\n2026-02-25|Build 2502262321: fix auto-search quality and per-user toggle\n2026-02-02|Build 0202261541: allow FQDN service URLs\n2026-01-30|Build 3001262148: single container\n2026-01-29|Build 2901262244: format changelog\n2026-01-29|Build 2901262240: cache users\n2026-01-29|Tidy full changelog\n2026-01-29|Update full changelog\n2026-01-29|Bake build number and changelog\n2026-01-29|Hardcode build number in backend\n2026-01-29|release: 2901262102\n2026-01-29|release: 2901262044\n2026-01-29|release: 2901262036\n2026-01-27|Hydrate missing artwork from Jellyseerr (build 271261539)\n2026-01-27|Fallback to TMDB when artwork cache fails (build 271261524)\n2026-01-27|Add service test buttons (build 271261335)\n2026-01-27|Bump build number (process 2) 271261322\n2026-01-27|Add cache load spinner (build 271261238)\n2026-01-27|Fix snapshot title fallback (build 271261228)\n2026-01-27|Fix request titles in snapshots (build 271261219)\n2026-01-27|Bump build number to 271261202\n2026-01-27|Clarify request sync settings (build 271261159)\n2026-01-27|Fix backend cache stats import (build 271261149)\n2026-01-27|Improve cache stats performance (build 271261145)\n2026-01-27|Add cache control artwork stats\n2026-01-26|Fix sync progress bar animation\n2026-01-26|Fix cache title hydration\n2026-01-25|Build 2501262041\n2026-01-25|Harden request cache titles and cache-only reads\n2026-01-25|Serve bundled branding assets by default\n2026-01-25|Seed branding logo from bundled assets\n2026-01-25|Tidy request sync controls\n2026-01-25|Add Jellyfin login cache and admin-only stats\n2026-01-25|Add user stats and activity tracking\n2026-01-25|Move account actions into avatar menu\n2026-01-25|Improve mobile header layout\n2026-01-25|Automate build number tagging and sync\n2026-01-25|Add site banner, build number, and changelog\n2026-01-24|Improve request handling and qBittorrent categories\n2026-01-24|Map Prowlarr releases to Arr indexers for manual grab\n2026-01-24|Clarify how-it-works steps and fixes\n2026-01-24|Document fix buttons in how-it-works\n2026-01-24|Route grabs through Sonarr/Radarr only\n2026-01-23|Use backend branding assets for logo and favicon\n2026-01-23|Copy public assets into frontend image\n2026-01-23|Fix backend Dockerfile paths for root context\n2026-01-23|Add Docker Hub compose override\n2026-01-23|Remove password fields from users page\n2026-01-23|Use bundled branding assets\n2026-01-23|Add default branding assets when missing\n2026-01-23|Show available status on landing when in Jellyfin\n2026-01-23|Fix cache titles and move feedback link\n2026-01-23|Add feedback form and webhook\n2026-01-23|Hide header actions when signed out\n2026-01-23|Fallback manual grab to qBittorrent\n2026-01-23|Split search actions and improve download options\n2026-01-23|Fix cache titles via Jellyseerr media lookup\n2026-01-22|Update README with Docker-first guide\n2026-01-22|Update README\n2026-01-22|Ignore build artifacts\n2026-01-22|Initial commit' +BUILD_NUMBER = "0303261323" +CHANGELOG = '2026-03-02|Hotfix: add logged-out password reset flow\n2026-03-02|Process 1 build 0203261953\n2026-03-02|Process 1 build 0203261610\n2026-03-02|Process 1 build 0203261608\n2026-03-02|Add dedicated profile invites page and fix mobile admin layout\n2026-03-01|Persist Seerr media failure suppression and reduce sync error noise\n2026-03-01|Add repository line ending policy\n2026-03-01|Finalize diagnostics, logging controls, and email test support\n2026-03-01|Add invite email templates and delivery workflow\n2026-02-28|Finalize dev-1.3 upgrades and Seerr updates\n2026-02-27|admin docs and layout refresh, build 2702261314\n2026-02-27|Build 2702261153: fix jellyfin sync user visibility\n2026-02-26|Build 2602262241: live request page updates\n2026-02-26|Build 2602262204\n2026-02-26|Build 2602262159: restore jellyfin-first user source\n2026-02-26|Build 2602262049: split magent settings and harden local login\n2026-02-26|Build 2602262030: add magent settings and hardening\n2026-02-26|Build 2602261731: fix user resync after nuclear wipe\n2026-02-26|Build 2602261717: master invite policy and self-service invite controls\n2026-02-26|Build 2602261636: self-service invites and count fixes\n2026-02-26|Build 2602261605: invite trace and cross-system user lifecycle\n2026-02-26|Build 2602261536: refine invite layouts and tighten UI\n2026-02-26|Build 2602261523: live updates, invite cleanup and nuclear resync\n2026-02-26|Build 2602261442: tidy users and invite layouts\n2026-02-26|Build 2602261409: unify invite management controls\n2026-02-26|Build 2602260214: invites profiles and expiry admin controls\n2026-02-26|Build 2602260022: enterprise UI refresh and users bulk auto-search\n2026-02-25|Build 2502262321: fix auto-search quality and per-user toggle\n2026-02-02|Build 0202261541: allow FQDN service URLs\n2026-01-30|Build 3001262148: single container\n2026-01-29|Build 2901262244: format changelog\n2026-01-29|Build 2901262240: cache users\n2026-01-29|Tidy full changelog\n2026-01-29|Update full changelog\n2026-01-29|Bake build number and changelog\n2026-01-29|Hardcode build number in backend\n2026-01-29|release: 2901262102\n2026-01-29|release: 2901262044\n2026-01-29|release: 2901262036\n2026-01-27|Hydrate missing artwork from Jellyseerr (build 271261539)\n2026-01-27|Fallback to TMDB when artwork cache fails (build 271261524)\n2026-01-27|Add service test buttons (build 271261335)\n2026-01-27|Bump build number (process 2) 271261322\n2026-01-27|Add cache load spinner (build 271261238)\n2026-01-27|Fix snapshot title fallback (build 271261228)\n2026-01-27|Fix request titles in snapshots (build 271261219)\n2026-01-27|Bump build number to 271261202\n2026-01-27|Clarify request sync settings (build 271261159)\n2026-01-27|Fix backend cache stats import (build 271261149)\n2026-01-27|Improve cache stats performance (build 271261145)\n2026-01-27|Add cache control artwork stats\n2026-01-26|Fix sync progress bar animation\n2026-01-26|Fix cache title hydration\n2026-01-25|Build 2501262041\n2026-01-25|Harden request cache titles and cache-only reads\n2026-01-25|Serve bundled branding assets by default\n2026-01-25|Seed branding logo from bundled assets\n2026-01-25|Tidy request sync controls\n2026-01-25|Add Jellyfin login cache and admin-only stats\n2026-01-25|Add user stats and activity tracking\n2026-01-25|Move account actions into avatar menu\n2026-01-25|Improve mobile header layout\n2026-01-25|Automate build number tagging and sync\n2026-01-25|Add site banner, build number, and changelog\n2026-01-24|Improve request handling and qBittorrent categories\n2026-01-24|Map Prowlarr releases to Arr indexers for manual grab\n2026-01-24|Clarify how-it-works steps and fixes\n2026-01-24|Document fix buttons in how-it-works\n2026-01-24|Route grabs through Sonarr/Radarr only\n2026-01-23|Use backend branding assets for logo and favicon\n2026-01-23|Copy public assets into frontend image\n2026-01-23|Fix backend Dockerfile paths for root context\n2026-01-23|Add Docker Hub compose override\n2026-01-23|Remove password fields from users page\n2026-01-23|Use bundled branding assets\n2026-01-23|Add default branding assets when missing\n2026-01-23|Show available status on landing when in Jellyfin\n2026-01-23|Fix cache titles and move feedback link\n2026-01-23|Add feedback form and webhook\n2026-01-23|Hide header actions when signed out\n2026-01-23|Fallback manual grab to qBittorrent\n2026-01-23|Split search actions and improve download options\n2026-01-23|Fix cache titles via Jellyseerr media lookup\n2026-01-22|Update README with Docker-first guide\n2026-01-22|Update README\n2026-01-22|Ignore build artifacts\n2026-01-22|Initial commit' diff --git a/backend/app/db.py b/backend/app/db.py index cf1685c..1317a4b 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -1841,6 +1841,7 @@ def get_cached_requests( requested_by_norm: Optional[str] = None, requested_by_id: Optional[int] = None, since_iso: Optional[str] = None, + status_codes: Optional[list[int]] = None, ) -> list[Dict[str, Any]]: query = """ SELECT request_id, media_id, media_type, status, title, year, requested_by, @@ -1858,6 +1859,10 @@ def get_cached_requests( if since_iso: conditions.append("created_at >= ?") params.append(since_iso) + if status_codes: + placeholders = ", ".join("?" for _ in status_codes) + conditions.append(f"status IN ({placeholders})") + params.extend(status_codes) if conditions: query += " WHERE " + " AND ".join(conditions) query += " ORDER BY created_at DESC, request_id DESC LIMIT ? OFFSET ?" @@ -1865,11 +1870,12 @@ def get_cached_requests( with _connect() as conn: rows = conn.execute(query, tuple(params)).fetchall() logger.debug( - "requests_cache list: count=%s requested_by_norm=%s requested_by_id=%s since_iso=%s", + "requests_cache list: count=%s requested_by_norm=%s requested_by_id=%s since_iso=%s status_codes=%s", len(rows), requested_by_norm, requested_by_id, since_iso, + status_codes, ) results: list[Dict[str, Any]] = [] for row in rows: @@ -1903,6 +1909,7 @@ def get_cached_requests_count( requested_by_norm: Optional[str] = None, requested_by_id: Optional[int] = None, since_iso: Optional[str] = None, + status_codes: Optional[list[int]] = None, ) -> int: query = "SELECT COUNT(*) FROM requests_cache" params: list[Any] = [] @@ -1916,6 +1923,10 @@ def get_cached_requests_count( if since_iso: conditions.append("created_at >= ?") params.append(since_iso) + if status_codes: + placeholders = ", ".join("?" for _ in status_codes) + conditions.append(f"status IN ({placeholders})") + params.extend(status_codes) if conditions: query += " WHERE " + " AND ".join(conditions) with _connect() as conn: diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py index cb43d9c..6bab8a7 100644 --- a/backend/app/routers/admin.py +++ b/backend/app/routers/admin.py @@ -1012,6 +1012,7 @@ async def requests_all( take: int = 50, skip: int = 0, days: Optional[int] = None, + stage: str = "all", user: Dict[str, str] = Depends(get_current_user), ) -> Dict[str, Any]: if user.get("role") != "admin": @@ -1021,8 +1022,9 @@ async def requests_all( since_iso = None if days is not None and int(days) > 0: since_iso = (datetime.now(timezone.utc) - timedelta(days=int(days))).isoformat() - rows = get_cached_requests(limit=take, offset=skip, since_iso=since_iso) - total = get_cached_requests_count(since_iso=since_iso) + status_codes = requests_router.request_stage_filter_codes(stage) + rows = get_cached_requests(limit=take, offset=skip, since_iso=since_iso, status_codes=status_codes) + total = get_cached_requests_count(since_iso=since_iso, status_codes=status_codes) results = [] for row in rows: status = row.get("status") diff --git a/backend/app/routers/events.py b/backend/app/routers/events.py index ae8c547..d90a601 100644 --- a/backend/app/routers/events.py +++ b/backend/app/routers/events.py @@ -76,6 +76,7 @@ def _request_actions_brief(entries: Any) -> list[dict[str, Any]]: async def events_stream( request: Request, recent_days: int = 90, + recent_stage: str = "all", user: Dict[str, Any] = Depends(get_current_user_event_stream), ) -> StreamingResponse: recent_days = max(0, min(int(recent_days or 90), 3650)) @@ -103,6 +104,7 @@ async def events_stream( take=recent_take, skip=0, days=recent_days, + stage=recent_stage, user=user, ) results = recent_payload.get("results") if isinstance(recent_payload, dict) else [] @@ -110,6 +112,7 @@ async def events_stream( "type": "home_recent", "ts": datetime.now(timezone.utc).isoformat(), "days": recent_days, + "stage": recent_stage, "results": results if isinstance(results, list) else [], } except Exception as exc: @@ -117,6 +120,7 @@ async def events_stream( "type": "home_recent", "ts": datetime.now(timezone.utc).isoformat(), "days": recent_days, + "stage": recent_stage, "error": str(exc), } signature = json.dumps(payload, ensure_ascii=True, separators=(",", ":"), default=str) diff --git a/backend/app/routers/requests.py b/backend/app/routers/requests.py index 40fed8d..1e826cd 100644 --- a/backend/app/routers/requests.py +++ b/backend/app/routers/requests.py @@ -91,6 +91,17 @@ STATUS_LABELS = { 6: "Partially ready", } +REQUEST_STAGE_CODES = { + "all": None, + "pending": [1], + "approved": [2], + "declined": [3], + "ready": [4], + "working": [5], + "partial": [6], + "in_progress": [2, 5, 6], +} + def _cache_get(key: str) -> Optional[Dict[str, Any]]: cached = _detail_cache.get(key) @@ -152,6 +163,23 @@ def _status_label(value: Any) -> str: return "Unknown" +def normalize_request_stage_filter(value: Optional[str]) -> str: + if not isinstance(value, str): + return "all" + normalized = value.strip().lower().replace("-", "_").replace(" ", "_") + if not normalized: + return "all" + if normalized in {"processing", "inprogress"}: + normalized = "in_progress" + return normalized if normalized in REQUEST_STAGE_CODES else "all" + + +def request_stage_filter_codes(value: Optional[str]) -> Optional[list[int]]: + normalized = normalize_request_stage_filter(value) + codes = REQUEST_STAGE_CODES.get(normalized) + return list(codes) if codes else None + + def _normalize_username(value: Any) -> Optional[str]: if not isinstance(value, str): return None @@ -1063,6 +1091,7 @@ def _get_recent_from_cache( limit: int, offset: int, since_iso: Optional[str], + status_codes: Optional[list[int]] = None, ) -> List[Dict[str, Any]]: items = _recent_cache.get("items") or [] results = [] @@ -1078,6 +1107,8 @@ def _get_recent_from_cache( item_dt = _parse_iso_datetime(candidate) if not item_dt or item_dt < since_dt: continue + if status_codes and item.get("status") not in status_codes: + continue results.append(item) return results[offset : offset + limit] @@ -1521,6 +1552,7 @@ async def recent_requests( take: int = 6, skip: int = 0, days: int = 90, + stage: str = "all", user: Dict[str, str] = Depends(get_current_user), ) -> dict: runtime = get_runtime_settings() @@ -1542,9 +1574,17 @@ async def recent_requests( since_iso = None if days > 0: since_iso = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat() + status_codes = request_stage_filter_codes(stage) if _recent_cache_stale(): _refresh_recent_cache_from_db() - rows = _get_recent_from_cache(requested_by, requested_by_id, take, skip, since_iso) + rows = _get_recent_from_cache( + requested_by, + requested_by_id, + take, + skip, + since_iso, + status_codes=status_codes, + ) cache_mode = (runtime.artwork_cache_mode or "remote").lower() allow_title_hydrate = False allow_artwork_hydrate = client.configured() @@ -1733,6 +1773,8 @@ async def search_requests( request_id = None status = None status_label = None + requested_by = None + accessible = False media_info = item.get("mediaInfo") or {} media_info_id = media_info.get("id") requests = media_info.get("requests") @@ -1741,27 +1783,23 @@ async def search_requests( status = requests[0].get("status") status_label = _status_label(status) elif isinstance(media_info_id, int): - username_norm = _normalize_username(user.get("username", "")) - requested_by_id = user.get("jellyseerr_user_id") - requested_by = None if user.get("role") == "admin" else username_norm - requested_by_id = None if user.get("role") == "admin" else requested_by_id cached = get_cached_request_by_media_id( media_info_id, - requested_by_norm=requested_by, - requested_by_id=requested_by_id, ) if cached: request_id = cached.get("request_id") status = cached.get("status") status_label = _status_label(status) - if user.get("role") != "admin": - if isinstance(request_id, int): + if isinstance(request_id, int): + details = get_request_cache_payload(request_id) + if not isinstance(details, dict): details = await _get_request_details(client, request_id) - if not _request_matches_user(details, user.get("username", "")): - continue - else: - continue + requested_by = _request_display_name(details) + if user.get("role") == "admin": + accessible = True + elif isinstance(details, dict): + accessible = _request_matches_user(details, user.get("username", "")) results.append( { @@ -1772,6 +1810,8 @@ async def search_requests( "requestId": request_id, "status": status, "statusLabel": status_label, + "requestedBy": requested_by, + "accessible": accessible, } ) diff --git a/frontend/app/admin/requests-all/page.tsx b/frontend/app/admin/requests-all/page.tsx index cd2e685..b3a2d9c 100644 --- a/frontend/app/admin/requests-all/page.tsx +++ b/frontend/app/admin/requests-all/page.tsx @@ -15,6 +15,17 @@ type RequestRow = { createdAt?: string | null } +const REQUEST_STAGE_OPTIONS = [ + { value: 'all', label: 'All stages' }, + { value: 'pending', label: 'Waiting for approval' }, + { value: 'approved', label: 'Approved' }, + { value: 'in_progress', label: 'In progress' }, + { value: 'working', label: 'Working on it' }, + { value: 'partial', label: 'Partially ready' }, + { value: 'ready', label: 'Ready to watch' }, + { value: 'declined', label: 'Declined' }, +] + const formatDateTime = (value?: string | null) => { if (!value) return 'Unknown' const date = new Date(value) @@ -30,6 +41,7 @@ export default function AdminRequestsAllPage() { const [error, setError] = useState(null) const [pageSize, setPageSize] = useState(50) const [page, setPage] = useState(1) + const [stage, setStage] = useState('all') const pageCount = useMemo(() => { if (!total || pageSize <= 0) return 1 @@ -46,8 +58,15 @@ export default function AdminRequestsAllPage() { try { const baseUrl = getApiBase() const skip = (page - 1) * pageSize + const params = new URLSearchParams({ + take: String(pageSize), + skip: String(skip), + }) + if (stage !== 'all') { + params.set('stage', stage) + } const response = await authFetch( - `${baseUrl}/admin/requests/all?take=${pageSize}&skip=${skip}` + `${baseUrl}/admin/requests/all?${params.toString()}` ) if (!response.ok) { if (response.status === 401) { @@ -74,7 +93,7 @@ export default function AdminRequestsAllPage() { useEffect(() => { void load() - }, [page, pageSize]) + }, [page, pageSize, stage]) useEffect(() => { if (page > pageCount) { @@ -82,6 +101,10 @@ export default function AdminRequestsAllPage() { } }, [pageCount, page]) + useEffect(() => { + setPage(1) + }, [stage]) + return ( {total.toLocaleString()} total
+ +
+ + +
)}
@@ -467,9 +516,10 @@ export default function HomePage() {