7665ef4d10
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.
10 KiB
10 KiB
backend/src/media_library_viewer_api/widgets
dir: backend/src/media_library_viewer_api/widgets
index: backend/src/media_library_viewer_api/widgets/.pi-map.index.md
role
Provides widget data adapters, built-in widget definitions, and stats-provider abstractions for fetching and normalizing dashboard data from various external services.
files
- __init__.py | Marks the directory as a Python package for the widget subsystem.
- builtin.py | Defines built-in widget kinds that don't require external services, providing their configurations, schemas, and validation. | exp: class:StaticConfig, func:get_builtin_widget_kind(kind: str) → WidgetKind | None, call:BUILTIN_WIDGET_KINDS.get, func:is_builtin_kind(kind: str) → bool, func:builtin_widget_kind_models() → dict[str, type], call:Field, func:validate_builtin_config(kind: str, config: dict[str, Any]) → dict[str, Any], call:builtin_widget_kind_models, call:models.get, call:dict, call:model_cls.model_validate(config or {}).model_dump | dep: typing, media_library_viewer_api.integrations.base, pydantic
- jellyseerr_stats.py | Provides Jellyseerr request statistics and recent request lists for Jellyfin services via a cached, thread-safe stats provider. | exp: class:JellyseerrStatsProvider, method:init(self, ttl) → None, call:threading.Lock, method:fetch_stats(self, service: Any) → StatsResult, call:str, call:service.config.get, call:(service.secrets or {}).get, call:StatsResult, call:time.time, call:self._cache.get, call:_jellyseer_client, call:client.request_count, call:client.recent_requests, call:logger.warning, call:StatValue, call:int, call:counts.get, func:_jellyseer_client(cache_key: tuple[str, str, str]) → JellyseerrClient, call:JellyseerrClient, func:fetch_jellyseer_requests(service: Any) → list[dict[str, Any]], call:str, call:service.config.get, call:(service.secrets or {}).get, call:_jellyseer_client, call:client.requests | dep: logging, threading, time, functools, typing, media_library_viewer_api.clients.jellyseerr, media_library_viewer_api.widgets.stats_provider, functools.lru_cache, media_library_viewer_api.clients.jellyseerr.JellyseerrClient
- prometheus_range.py | Provides shared helper functions for Prometheus range queries, including step-size derivation, window presets, and normalization of both Prometheus and Grafana API responses into a frontend-consumable series format. | exp: func:step_for_window(window_seconds: int, target_points) → int, call:max, call:round, func:_dedup_label(label: str, seen: dict[str, int]) → str, func:normalize_prometheus_matrix(result: list[dict[str, Any]]) → list[dict[str, Any]], call:entry.get, call:sorted, call:metric.items, call:str(k).startswith, call:_dedup_label, call:" ".join, call:_safe_int, call:points.append, call:_safe_float, call:series.append, func:normalize_grafana_frames(raw: dict[str, Any]) → list[dict[str, Any]], call:raw.get, call:results.items, call:ref_data.get, call:frame.get("data", {}).get, call:len, call:frame.get("schema", {}).get, call:value_field.get("config", {}).get, call:sorted, call:frame_labels.items, call:str(k).startswith, call:" ".join, call:_dedup_label, call:zip, call:_safe_int, call:points.append, call:_safe_float, call:series.append, func:_safe_float(raw: Any) → float | None, call:float, func:_safe_int(ts: Any) → int | None, call:int, call:float | dep: typing
- sources.py | Provides widget source adapters that fetch and normalize data from various services (Grafana/Prometheus, Alertmanager, Jellyfin, SSH tasks, backups) for dashboard rendering. | exp: class:ServiceRecord, class:WidgetSource, method:fetch(self, service: ServiceRecord | None, widget_kind: str, config: dict[str, Any]) → dict[str, Any], class:BackupsWidgetSource, method:fetch(self, service: ServiceRecord | None, widget_kind: str, config: dict[str, Any]) → dict[str, Any], call:get_settings_store, call:build_backup_dashboard_summary, call:summary.model_dump, call:logger.exception, class:StaticWidgetSource, method:fetch(self, service: ServiceRecord | None, widget_kind: str, config: dict[str, Any]) → dict[str, Any], call:config.get, class:MetricSource, method:fetch(self, service: ServiceRecord | None, widget_kind: str, config: dict[str, Any]) → dict[str, Any], call:str(service.config.get("grafana_url") or "").rstrip, call:service.config.get, call:service.secrets.get, call:int, call:self._fetch_chart, call:self._fetch_gauge, call:self._fetch_mean, call:self._fetch_metric, call:logger.exception, method:_gateway_query(self, grafana_url: str, api_key: str, datasource_uid: str, timeout: int, promql: str, window_seconds, max_data_points) → dict[str, Any], call:step_for_window, call:requests.post, call:resp.raise_for_status, call:resp.json, call:asyncio.wait_for, call:asyncio.to_thread, call:logger.exception, method:_fetch_chart(self, grafana_url: str, api_key: str, datasource_uid: str, timeout: int, config: dict[str, Any]) → dict[str, Any], call:config.get, call:WINDOW_PRESETS.get, call:self._gateway_query, call:normalize_grafana_frames, method:_fetch_gauge(self, grafana_url: str, api_key: str, datasource_uid: str, timeout: int, config: dict[str, Any]) → dict[str, Any], call:config.get, call:self._gateway_query, call:normalize_grafana_frames, call:len, method:_fetch_mean(self, grafana_url: str, api_key: str, datasource_uid: str, timeout: int, config: dict[str, Any]) → dict[str, Any], call:config.get, call:WINDOW_PRESETS.get, call:self._gateway_query, call:normalize_grafana_frames, call:len, call:sum, method:_fetch_metric(self, grafana_url: str, api_key: str, datasource_uid: str, timeout: int, config: dict[str, Any]) → dict[str, Any], call:config.get, call:self._gateway_query, call:normalize_grafana_frames, class:AlertmanagerWidgetSource, method:fetch(self, service: ServiceRecord | None, widget_kind: str, config: dict[str, Any]) → dict[str, Any], call:str(service.config.get("base_url") or "").rstrip, call:service.config.get, call:int, call:config.get, call:service.secrets.get, call:asyncio.wait_for, call:asyncio.to_thread, call:response.raise_for_status, call:response.json, call:payload.get, call:isinstance, call:summarize_alerts, call:logger.exception, class:JellyfinWidgetSource, method:fetch(self, service: ServiceRecord | None, widget_kind: str, config: dict[str, Any]) → dict[str, Any], call:str, call:service.config.get, call:service.secrets.get, call:int, call:asyncio.wait_for, call:asyncio.to_thread, call:s.get("PlayState", {}).get, call:_map_sessions_to_activity_rows, call:logger.exception, class:SshTaskWidgetSource, method:fetch(self, service: ServiceRecord | None, widget_kind: str, config: dict[str, Any]) → dict[str, Any], call:get_settings_store, call:config.get, call:store.get_task, call:task.get, call:int, call:service.config.get, call:asyncio.wait_for, call:asyncio.to_thread, call:_record_timeout, call:logger.exception, class:QbittorrentWidgetSource, method:fetch(self, service: ServiceRecord | None, widget_kind: str, config: dict[str, Any]) → dict[str, Any], call:str, call:service.config.get, call:service.secrets.get, call:int, call:_qbittorrent_client, call:asyncio.wait_for, call:asyncio.to_thread, call:data.get, call:torrents.values, call:t.get, call:by_state.get, call:len, call:server_state.get, call:time.time, call:QbittorrentSampleStore, call:store.append, call:store.window, call:logger.exception, class:StatsWidgetSource, method:fetch(self, service: ServiceRecord | None, widget_kind: str, config: dict[str, Any]) → dict[str, Any], call:get_stats_provider, call:int, call:service.config.get, call:asyncio.wait_for, call:asyncio.to_thread, call:logger.exception, call:str, call:config.get, call:next, func:build_service_record(store: SettingsStore, service_row: dict[str, Any]) → ServiceRecord, call:ServiceRecord, call:service_row.get, call:decrypt_secrets, call:bool, func:_record_timeout(service: ServiceRecord | None, config: dict[str, Any], timeout: int) → None, call:get_settings_store, call:store.record_service_task_run, call:str, call:config.get, call:logger.exception, func:_qbittorrent_client(cache_key: tuple[str, str, str, str, int]) → QbittorrentClient, call:QbittorrentClient, func:get_service_adapter(service_type: str) → WidgetSource | None, call:SERVICE_ADAPTERS.get, func:get_builtin_adapter(kind: str) → WidgetSource | None, call:BUILTIN_ADAPTERS.get, func:get_stats_adapter() → WidgetSource | None | dep: asyncio, logging, time, dataclasses, functools, typing, requests, media_library_viewer_api.clients.jellyfin, media_library_viewer_api.clients.qbittorrent, media_library_viewer_api.domain.dashboard, media_library_viewer_api.integrations.alertmanager, media_library_viewer_api.services.qbittorrent_store, media_library_viewer_api.services.settings_store, media_library_viewer_api.services.task_runner, media_library_viewer_api.widgets, media_library_viewer_api.widgets.prometheus_range, media_library_viewer_api.widgets.stats_provider, media_library_viewer_api.services.secrets, JellyfinClient, QbittorrentClient, SettingsStore, summarize_alerts, run_saved_task, normalize_grafana_frames, get_stats_provider
- stats_provider.py | Defines a generic stats-provider abstraction with a protocol interface and registry for fetching and exposing named numeric metrics from various services. | exp: class:StatValue, class:StatsResult, class:StatsProvider, method:fetch_stats(self, service: Any) → StatsResult, func:register_stats_provider(service_type: str, provider: StatsProvider) → None, func:get_stats_provider(service_type: str) → StatsProvider | None, call:STATS_PROVIDERS.get | dep: dataclasses, typing
arch
Modular adapter pattern with protocol-based provider interfaces, a stats-provider registry, and per-service normalization layers that transform heterogeneous API responses into a unified frontend-consumable format.
tags
fetch, widget, stats, call:, call:str, source, call:self., call:logger.exception
symbols
- StaticConfig
- JellyseerrStatsProvider
- ServiceRecord
- WidgetSource
- BackupsWidgetSource
- StaticWidgetSource
- MetricSource
- AlertmanagerWidgetSource
workflows
- change widgets behavior read: __init__.py, builtin.py, jellyseerr_stats.py