feat(jellyseer): sortable/filterable requests table on the Requests tab

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.
This commit is contained in:
Developer
2026-07-12 17:18:21 +00:00
parent 54851779fb
commit 7665ef4d10
21 changed files with 447 additions and 77 deletions
@@ -2,7 +2,7 @@
dir: backend/src/media_library_viewer_api/routers
## role
FastAPI router package that defines all REST API endpoint modules for the media library viewer backend.
FastAPI router package that defines all HTTP API endpoints for the media library viewer backend, organized by domain (auth, backups, dashboards, files, jobs, media, monitoring, services, settings, tasks, widgets).
## parent
index: backend/src/media_library_viewer_api/.pi-map.index.md
map: backend/src/media_library_viewer_api/.pi-map.md
@@ -4,7 +4,7 @@ dir: backend/src/media_library_viewer_api/routers
index: backend/src/media_library_viewer_api/routers/.pi-map.index.md
## role
FastAPI router package that defines all REST API endpoint modules for the media library viewer backend.
FastAPI router package that defines all HTTP API endpoints for the media library viewer backend, organized by domain (auth, backups, dashboards, files, jobs, media, monitoring, services, settings, tasks, widgets).
## files
- __init__.py | Marks the directory as a Python package for routers.
- authentik_users.py | Provides a FastAPI router that proxies paginated user directory queries and email message enqueueing through an Authentik service client. | exp: class:MessageRequest, func:_build_client(service: ServiceRecord) → AuthentikClient, call:str(service.config.get("base_url") or "").rstrip, call:service.config.get, call:service.secrets.get, call:float, call:AuthentikClient, func:_empty(error: str) → dict[str, Any], func:get_authentik_users(service_id: str, search, page, page_size, store) → dict[str, Any], call:resolve_service_record, call:logger.info, call:_empty, call:_build_client, call:client.users, call:logger.exception, func:get_authentik_message_status(service_id: str, store, mail_queue) → dict[str, Any], call:resolve_service_record, call:mail_queue.status, func:post_authentik_message(service_id: str, body: MessageRequest, store, mail_queue) → dict[str, Any], call:resolve_service_record, call:r.strip, call:get_settings, call:validate_smtp_settings, call:mail_queue.enqueue, call:logger.info, call:len | dep: logging, typing, fastapi, pydantic, media_library_viewer_api.clients.authentik, media_library_viewer_api.config, media_library_viewer_api.dependencies, media_library_viewer_api.services.mail_queue, media_library_viewer_api.services.mailer, media_library_viewer_api.services.service_resolution, media_library_viewer_api.services.settings_store, media_library_viewer_api.widgets.sources
@@ -12,7 +12,7 @@ FastAPI router package that defines all REST API endpoint modules for the media
- dashboard.py | FastAPI router providing dashboard endpoints for media counts, library breakdowns, shortcuts CRUD, activity sessions, and backup summaries. | exp: func:get_counts(client, user_id) → dict[str, int], call:client.media_counts, call:logger.info, func:get_library_counts(client, user_id) → list[dict[str, Any]], call:client.libraries, call:logger.info, call:len, call:client.library_item_counts, func:get_shortcuts() → list[dict[str, Any]], call:store.list_shortcuts, call:logger.info, call:len, func:create_shortcut(payload: dict[str, Any]) → dict[str, Any], call:store.upsert_shortcut, call:logger.info, call:shortcut.get, func:update_shortcut(shortcut_id: str, payload: dict[str, Any]) → dict[str, Any], call:store.upsert_shortcut, call:logger.info, call:shortcut.get, func:delete_shortcut(shortcut_id: str) → dict[str, str], call:store.delete_shortcut, call:logger.info, func:get_activity(client) → list[dict[str, Any]], call:client.sessions, call:_map_sessions_to_activity_rows, call:rows.sort, call:state_rank.get, call:r.get, call:str(r.get("user", "")).lower, call:logger.info, call:len, func:get_now_playing(client) → list[dict[str, Any]], call:get_activity, func:get_backup_dashboard(store) → BackupDashboardSummary, call:build_backup_dashboard_summary | dep: logging, typing, fastapi, media_library_viewer_api.clients.jellyfin, media_library_viewer_api.dependencies, media_library_viewer_api.domain.dashboard, media_library_viewer_api.models.backups, media_library_viewer_api.services.settings_store
- dashboards.py | Provides CRUD API endpoints for managing named dashboards via a FastAPI router. | exp: func:list_dashboards(store) → list[NamedDashboard], call:store.list_dashboards, call:NamedDashboard, func:get_dashboard_by_slug(slug: str, store) → NamedDashboard, call:store.get_dashboard_by_slug, call:NamedDashboard, raise:HTTPException, func:create_dashboard(body: NamedDashboardInput, store) → NamedDashboard, call:store.upsert_dashboard, call:body.model_dump, call:NamedDashboard, func:update_dashboard(dashboard_id: str, body: NamedDashboardInput, store) → NamedDashboard, call:store.get_dashboard, call:store.upsert_dashboard, call:body.model_dump, call:NamedDashboard, raise:HTTPException, func:delete_dashboard(dashboard_id: str, store) → dict[str, str], call:store.get_dashboard, call:store.delete_dashboard, raise:HTTPException | dep: fastapi, media_library_viewer_api.dependencies, media_library_viewer_api.models.dashboards, media_library_viewer_api.services.settings_store
- files.py | FastAPI router providing endpoints for remote file operations including directory listing, ffprobe media analysis, stat, and path resolution via SSH. | exp: func:list_directory(path, ssh) → dict[str, Any], call:ssh.list_dir, call:logger.warning, call:json.loads, call:logger.info, call:len, raise:HTTPException, func:get_ffprobe(path, ssh) → dict[str, Any], call:ssh.ffprobe_json, call:logger.warning, call:logger.info, raise:HTTPException, func:get_stat(path, ssh) → dict[str, str], call:ssh.stat_path, call:logger.warning, call:logger.info, raise:HTTPException, func:resolve_path(path) → dict[str, str], call:get_settings, call:resolve_remote_media_path, call:logger.info | dep: json, logging, typing, fastapi, media_library_viewer_api.clients.ssh, media_library_viewer_api.config, media_library_viewer_api.dependencies, media_library_viewer_api.path_utils
- jellyseerr.py | FastAPI router that resolves a Jellyfin service instance and delegates to the Jellyseerr stats provider to return request counts and recent requests. | exp: func:_serialize(result) → dict, func:get_jellyseerr_stats(jellyfin_service_id, store) → dict, call:resolve_service_record, call:get_stats_provider, call:provider.fetch_stats, call:logger.exception, call:_serialize, raise:HTTPException | dep: logging, fastapi, media_library_viewer_api.dependencies, media_library_viewer_api.services.service_resolution, media_library_viewer_api.services.settings_store, media_library_viewer_api.widgets, media_library_viewer_api.widgets.stats_provider, media_library_viewer_api.widgets.jellyseerr_stats
- jellyseerr.py | FastAPI router providing Jellyseerr request stats and recent requests endpoints for the Jellyfin page. | exp: func:_serialize(result) → dict, func:get_jellyseerr_stats(jellyfin_service_id, store) → dict, call:resolve_service_record, call:get_stats_provider, call:provider.fetch_stats, call:logger.exception, call:_serialize, raise:HTTPException, func:get_jellyseerr_requests(jellyfin_service_id, store) → dict, call:resolve_service_record, call:fetch_jellyseer_requests, call:logger.exception, raise:HTTPException | dep: logging, fastapi, media_library_viewer_api.dependencies, media_library_viewer_api.services.service_resolution, media_library_viewer_api.services.settings_store, media_library_viewer_api.widgets, media_library_viewer_api.widgets.jellyseerr_stats, media_library_viewer_api.widgets.stats_provider
- jobs.py | FastAPI router that exposes endpoints to list available job templates and execute them on remote paths via SSH. | exp: class:RunJobRequest, func:get_templates() → list[dict[str, str]], call:JOB_TEMPLATES.items, call:logger.info, call:len, func:post_run_job(request: RunJobRequest, ssh) → dict[str, Any], call:logger.warning, call:logger.info, call:run_job, raise:HTTPException | dep: logging, typing, fastapi, pydantic, media_library_viewer_api.clients.ssh, media_library_viewer_api.dependencies, media_library_viewer_api.jobs
- media.py | FastAPI router providing endpoints to manage media index lifecycle operations including status checks, building (via subprocess workers), stopping, force-stopping, and querying the media library index. | exp: func:get_media_index() → MediaIndex, call:MediaIndex, func:_set_build_metadata(index: MediaIndex, state: dict[str, Any]) → None, call:state.items, call:index.set_metadata, func:_staging_db_path(index: MediaIndex) → Path, call:index.db_path.with_name, func:_pid_is_alive(pid: int | None) → bool, call:os.kill, func:_clean_stale_build_state(index: MediaIndex) → Any, call:index.status, call:_pid_is_alive, call:logger.warning, call:_set_build_metadata, func:_serialize_status(status: Any) → dict[str, Any], func:_worker_command(final_db_path: Path, staging_db_path: Path, service_id) → list[str], call:str, func:_start_worker(index: MediaIndex, service_id) → subprocess.Popen[bytes], call:_staging_db_path, call:staging_path.unlink, call:subprocess.Popen, call:_worker_command, call:os.environ.copy, func:get_index_status(index) → dict[str, Any], call:_clean_stale_build_state, call:logger.info, call:_serialize_status, func:post_build_index(jellyfin_service_id, index) → dict[str, Any], call:_clean_stale_build_state, call:_pid_is_alive, call:logger.warning, call:logger.info, call:_start_worker, call:_set_build_metadata, call:index.status, call:record_media_index_build, call:_serialize_status, raise:HTTPException, func:stop_build(index) → dict[str, Any], call:_clean_stale_build_state, call:logger.warning, call:logger.info, call:_set_build_metadata, call:index.status, call:_serialize_status, raise:HTTPException, func:force_stop_build(index) → dict[str, Any], call:_clean_stale_build_state, call:logger.warning, call:_pid_is_alive, call:_set_build_metadata, call:index.status, call:_serialize_status, call:logger.info, call:os.killpg, call:time.time, call:time.sleep, call:record_media_index_build, raise:HTTPException, func:query_media(libraries, types, search, hdr_filter, sort_key, sort_order, limit, offset, jellyfin_service_id, client, user_id, index) → dict[str, Any], call:lid.strip, call:libraries.split, call:client.libraries, call:t.strip, call:types.split, call:logger.info, call:len, call:",".join, call:index.query | dep: logging, os, signal, subprocess, sys, threading, time, pathlib, typing, fastapi, media_library_viewer_api.clients.jellyfin, media_library_viewer_api.dependencies, media_library_viewer_api.observability, media_library_viewer_api.services.media_index
- monitoring.py | FastAPI router providing observability endpoints for monitoring machines, Alertmanager alerts/status, Prometheus targets/status, and webhook ingestion. | exp: func:_base_url(service: ServiceRecord) → str, call:str(service.config.get("base_url") or "").rstrip, call:service.config.get, func:_timeout(service: ServiceRecord, default: int) → tuple[float, float], call:int, call:service.config.get, call:http_timeout, func:_auth_headers(service: ServiceRecord) → dict[str, str], call:str, call:service.secrets.get, func:_status_response(service: ServiceRecord | None, version, error) → dict[str, Any], func:_summary_from_alerts(alerts: list[dict[str, Any]]) → dict[str, Any], call:summarize_alerts, func:get_machines(store) → list[dict[str, Any]], call:store.list_machines, call:m.get, func:get_prometheus_targets(store) → list[dict[str, Any]], call:build_node_exporter_targets, call:logger.info, call:len, func:get_alertmanager_alerts(service_id, store) → dict[str, Any], call:resolve_service_record, call:requests.get, call:_base_url, call:_auth_headers, call:_timeout, call:response.raise_for_status, call:response.json, call:logger.exception, call:data.get, call:_summary_from_alerts, call:logger.info, func:get_alertmanager_status(service_id, store) → dict[str, Any], call:resolve_service_record, call:requests.get, call:_base_url, call:_auth_headers, call:_timeout, call:response.raise_for_status, call:response.json, call:logger.exception, call:data.get("versionInfo", {}).get, call:status.get, call:p.get, call:cluster.get, func:get_prometheus_status(service_id, store) → dict[str, Any], call:resolve_service_record, call:_status_response, call:str(service.config.get("grafana_url") or "").rstrip, call:service.config.get, call:service.secrets.get, call:int, call:requests.post, call:http_timeout, call:resp.raise_for_status, call:logger.exception, func:receive_alertmanager_webhook(payload) → dict[str, str], call:payload.get, call:logger.info, call:len | dep: logging, typing, requests, fastapi, media_library_viewer_api.clients.http_timeout, media_library_viewer_api.dependencies, media_library_viewer_api.services.service_resolution, media_library_viewer_api.services.settings_store, media_library_viewer_api.services.targets, media_library_viewer_api.widgets.sources, media_library_viewer_api.integrations.alertmanager, fastapi.APIRouter
@@ -21,9 +21,9 @@ FastAPI router package that defines all REST API endpoint modules for the media
- tasks.py | FastAPI router providing CRUD endpoints and execution for saved server tasks with SSH service resolution | exp: class:TaskInput, class:RunTaskRequest, func:_service_label(service: dict[str, Any] | None) → str, call:str, call:service.get, func:_resolve_service_for_task(store: SettingsStore, task: dict[str, Any], service_id: str | None) → dict[str, Any] | None, call:store.get_service, call:str(task.get("default_service_id") or "").strip, call:task.get, call:store.list_services, call:svc.get, func:_service_row_to_record(service_row: dict[str, Any]) → ServiceRecord, call:build_service_record, call:get_settings_store, func:list_tasks(store) → list[dict[str, Any]], call:store.list_tasks, func:create_task(task: TaskInput, store) → dict[str, Any], call:store.upsert_task, call:task.model_dump, func:update_task(task_id: str, task: TaskInput, store) → dict[str, Any], call:store.get_task, call:store.upsert_task, call:task.model_dump, raise:HTTPException, func:delete_task(task_id: str, store) → dict[str, str], call:store.get_task, call:store.delete_task, raise:HTTPException, func:list_task_runs(task_id: str, limit, store) → dict[str, Any], call:store.get_task, call:store.list_service_task_runs, call:len, raise:HTTPException, func:run_task(request: RunTaskRequest, service_id, store) → dict[str, Any], call:store.get_task, call:task.get, call:_resolve_service_for_task, call:service_row.get, call:_service_row_to_record, call:run_saved_task, call:_service_label, raise:HTTPException | dep: logging, typing, fastapi, pydantic, media_library_viewer_api.dependencies, media_library_viewer_api.services.settings_store, media_library_viewer_api.services.task_runner, media_library_viewer_api.widgets.sources
- widgets.py | Provides a FastAPI REST API for CRUD operations on dashboard widget instances and widget references (live-links), including data fetching through registered adapters. | exp: class:WidgetReferenceCreate, func:_validate_widget_input(body: WidgetInstanceInput, store: SettingsStore) → None, call:store.get_service, call:get_service_definition, call:definition.widget_kind, call:validate_config, call:is_builtin_kind, call:validate_builtin_config, raise:HTTPException, func:list_builtin_kinds() → list[BuiltinWidgetKindInfo], call:BuiltinWidgetKindInfo, call:BUILTIN_WIDGET_KINDS.values, func:list_instances(service_id, scope, store) → list[dict[str, Any]], call:WidgetInstance(**widget).model_dump, call:store.list_widgets, func:create_instance(body: WidgetInstanceInput, store) → dict[str, Any], call:_validate_widget_input, call:store.upsert_widget, call:body.model_dump, call:WidgetInstance(**widget).model_dump, func:update_instance(widget_id: str, body: WidgetInstanceInput, store) → dict[str, Any], call:store.get_widget, call:_validate_widget_input, call:store.upsert_widget, call:body.model_dump, call:WidgetInstance(**widget).model_dump, raise:HTTPException, func:delete_instance(widget_id: str, store) → dict[str, str], call:store.get_widget, call:store.delete_widget, raise:HTTPException, func:fetch_data(widget_id: str, store) → dict[str, Any], call:store.get_widget, call:widget.get, call:store.get_service, call:WidgetDataResponse( widget_id=widget_id, error=f"Service {service_id} not found", fetched_at=int(time.time()), ).model_dump, call:int, call:time.time, call:service_row.get, call:WidgetDataResponse( widget_id=widget_id, error="Service is disabled", fetched_at=int(time.time()), ).model_dump, call:get_stats_adapter, call:get_service_adapter, call:WidgetDataResponse( widget_id=widget_id, error=f"No adapter for service type {service_row['service_type']}", fetched_at=int(time.time()), ).model_dump, call:build_service_record, call:get_builtin_adapter, call:WidgetDataResponse( widget_id=widget_id, error=f"Unknown built-in widget kind: {widget_kind}", fetched_at=int(time.time()), ).model_dump, call:adapter.fetch, call:logger.exception, call:WidgetDataResponse( widget_id=widget_id, data=data if "error" not in data else None, error=data.get("error"), fetched_at=int(time.time()), ).model_dump, call:data.get, raise:HTTPException, func:list_references(dashboard_scope: str, store) → list[dict[str, Any]], call:store.list_widget_references, func:create_reference(body: WidgetReferenceCreate, store) → dict[str, Any], call:store.create_widget_reference, raise:HTTPException, func:delete_reference(reference_id: str, store) → dict[str, str], call:store.delete_widget_reference, func:update_reference(reference_id: str, sort_order: int, store) → dict[str, Any], call:store.update_widget_reference, raise:HTTPException, func:detach_reference(reference_id: str, store) → dict[str, Any], call:store.detach_widget_reference, call:WidgetInstance(**cloned).model_dump, raise:HTTPException | dep: logging, time, typing, fastapi, pydantic, media_library_viewer_api.dependencies, media_library_viewer_api.integrations.base, media_library_viewer_api.integrations.registry, media_library_viewer_api.models.widgets, media_library_viewer_api.services.settings_store, media_library_viewer_api.widgets.builtin, media_library_viewer_api.widgets.sources
## arch
Modular router-per-domain pattern where each file exposes a FastAPI APIRouter for a specific feature area, delegating business logic to service clients and providers.
Modular router-per-domain pattern where each file exposes a FastAPI APIRouter for a specific feature area; routers delegate business logic to service clients and adapters, using dependency injection for SSH/database access and standard Pydantic models for request/response validation.
## tags
call:, service, raise:httpexception, media_library_viewer_api, backup, get, call:store.get, ssh
call:, service, raise:httpexception, media_library_viewer_api, get, backup, call:store.get, ssh
## symbols
- MessageRequest
- RunJobRequest
@@ -17,6 +17,7 @@ from media_library_viewer_api.dependencies import get_settings_store
from media_library_viewer_api.services.service_resolution import resolve_service_record
from media_library_viewer_api.services.settings_store import SettingsStore
from media_library_viewer_api.widgets import jellyseerr_stats # noqa: F401 — ensure provider registration
from media_library_viewer_api.widgets.jellyseerr_stats import fetch_jellyseer_requests
from media_library_viewer_api.widgets.stats_provider import get_stats_provider
logger = logging.getLogger(__name__)
@@ -50,3 +51,20 @@ def get_jellyseerr_stats(
logger.exception("Jellyseerr stats endpoint failed")
raise HTTPException(status_code=502, detail=f"Jellyseerr fetch failed: {exc}") from exc
return _serialize(result)
@router.get("/requests")
def get_jellyseerr_requests(
jellyfin_service_id: str | None = None,
store: SettingsStore = Depends(get_settings_store),
) -> dict:
"""Return Jellyseerr requests for the Requests tab table (filter/sort client-side)."""
service = resolve_service_record(store, "jellyfin", jellyfin_service_id)
if service is None:
raise HTTPException(status_code=503, detail="No Jellyfin service is configured.")
try:
requests = fetch_jellyseer_requests(service)
except Exception as exc: # pragma: no cover - client guards internally
logger.exception("Jellyseerr requests endpoint failed")
raise HTTPException(status_code=502, detail=f"Jellyseerr fetch failed: {exc}") from exc
return {"requests": requests}