From ac36d539c8e640dddc46f158e7c77d6c95902a29 Mon Sep 17 00:00:00 2001 From: Alex <25013571+alexhb1@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:44:46 +0000 Subject: [PATCH] Patch: Various fixes (#660) Various fixes from the last couple days: - Add manual approval option for book/audiobook requests (#651) - Add flagged HTTP headers - Add filesystem fallback - copy + delete when hardlink/move fails across filesystems (#647) - Dependency updates - Tweak frontend test config (simplified tsconfig for tests) - Fix overlapping sort scoring in release modal - duplicate scoring keys caused incorrect release ordering (#654 ) - Fix stale search session after download - search state was not refreshed when returning from a download (#659) - Fix multi-format release filtering - releases with multiple formats were incorrectly excluded by the format filter (#658) - Fix config persistence when action button is used - clicking "Test connection" reset unsaved settings (#657) - Fix request grid text positioning in admin request policy panel - Fix rTorrent path discovery (#653) - Fix `/login` API check (#650) --- requirements-base.txt | 1 + shelfmark/core/request_routes.py | 1 + shelfmark/core/requests_service.py | 21 + shelfmark/core/utils.py | 25 + shelfmark/download/clients/rtorrent.py | 25 +- shelfmark/download/clients/settings.py | 12 +- shelfmark/download/fs.py | 42 ++ shelfmark/main.py | 14 + shelfmark/release_sources/prowlarr/torznab.py | 9 +- src/frontend/package-lock.json | 605 +++--------------- src/frontend/package.json | 2 +- src/frontend/src/App.tsx | 13 + src/frontend/src/components/ReleaseModal.tsx | 32 +- .../src/components/activity/ActivityCard.tsx | 25 + .../components/activity/ActivitySidebar.tsx | 1 + .../settings/users/RequestPolicyGrid.tsx | 4 +- src/frontend/src/hooks/useAuth.ts | 19 +- src/frontend/src/hooks/useRequests.ts | 11 +- src/frontend/src/hooks/useSearch.ts | 20 +- src/frontend/src/hooks/useSettings.ts | 45 +- .../src/services/requestApiHelpers.ts | 16 +- .../src/tests/adminRequestApi.node.test.ts | 12 + .../src/tests/releaseFormats.node.test.ts | 40 ++ .../src/tests/useSettings.node.test.ts | 73 +++ src/frontend/src/utils/releaseFormats.ts | 35 + src/frontend/src/utils/settingsValues.ts | 32 + src/frontend/tsconfig.tests.json | 7 +- tests/core/test_download_processing.py | 22 + tests/core/test_hardlink.py | 28 + tests/core/test_request_routes_api.py | 75 +++ tests/core/test_requests_service.py | 59 ++ tests/prowlarr/test_rtorrent_client.py | 23 +- tests/prowlarr/test_torznab.py | 51 ++ 33 files changed, 789 insertions(+), 611 deletions(-) create mode 100644 src/frontend/src/tests/releaseFormats.node.test.ts create mode 100644 src/frontend/src/tests/useSettings.node.test.ts create mode 100644 src/frontend/src/utils/releaseFormats.ts create mode 100644 src/frontend/src/utils/settingsValues.ts create mode 100644 tests/prowlarr/test_torznab.py diff --git a/requirements-base.txt b/requirements-base.txt index 3211adea..d1edf950 100644 --- a/requirements-base.txt +++ b/requirements-base.txt @@ -3,6 +3,7 @@ flask-cors flask-socketio python-socketio requests[socks] +defusedxml beautifulsoup4 tqdm dnspython diff --git a/shelfmark/core/request_routes.py b/shelfmark/core/request_routes.py index b9a379f7..11d17de9 100644 --- a/shelfmark/core/request_routes.py +++ b/shelfmark/core/request_routes.py @@ -708,6 +708,7 @@ def register_request_routes( queue_release=queue_release, release_data=data.get("release_data"), admin_note=data.get("admin_note"), + manual_approval=data.get("manual_approval", False), ) except RequestServiceError as exc: return _error_response(str(exc), exc.status_code, code=exc.code) diff --git a/shelfmark/core/requests_service.py b/shelfmark/core/requests_service.py index 95b20484..df7f5bc0 100644 --- a/shelfmark/core/requests_service.py +++ b/shelfmark/core/requests_service.py @@ -413,6 +413,7 @@ def fulfil_request( queue_release: Callable[..., tuple[bool, str | None]], release_data: Any = None, admin_note: Any = None, + manual_approval: Any = False, ) -> dict[str, Any]: """Fulfil a pending request and queue the release under requesting-user identity.""" request_row = ensure_request_access( @@ -434,10 +435,30 @@ def fulfil_request( raise RequestServiceError("admin_note must be a string", status_code=400) normalized_admin_note = admin_note.strip() or None + if not isinstance(manual_approval, bool): + raise RequestServiceError("manual_approval must be a boolean", status_code=400) + selected_release_data = release_data if release_data is not None else request_row.get("release_data") if selected_release_data is not None and not isinstance(selected_release_data, dict): raise RequestServiceError("release_data must be an object", status_code=400) + if selected_release_data is None and manual_approval: + try: + return user_db.update_request( + request_id, + expected_current_status="pending", + status="fulfilled", + release_data=None, + delivery_state="complete", + delivery_updated_at=_now_timestamp(), + last_failure_reason=None, + admin_note=normalized_admin_note, + reviewed_by=admin_user_id, + reviewed_at=_now_timestamp(), + ) + except ValueError as exc: + raise RequestServiceError(str(exc), status_code=409, code="stale_transition") from exc + if request_row["request_level"] == "book" and selected_release_data is None: raise RequestServiceError( "release_data is required to fulfil book-level requests", diff --git a/shelfmark/core/utils.py b/shelfmark/core/utils.py index daf4e713..8f75b0ef 100644 --- a/shelfmark/core/utils.py +++ b/shelfmark/core/utils.py @@ -1,8 +1,11 @@ """Shared utility functions for the Shelfmark.""" import base64 +import importlib import os import re +from threading import Lock +from types import ModuleType from pathlib import Path from typing import Optional from urllib.parse import urlparse @@ -54,6 +57,28 @@ def normalize_http_url( return normalized +_xmlrpc_patch_lock = Lock() +_xmlrpc_patch_applied = False + + +def get_hardened_xmlrpc_client() -> ModuleType: + """Return ``xmlrpc.client`` after best-effort defusedxml monkey patching.""" + global _xmlrpc_patch_applied + if not _xmlrpc_patch_applied: + with _xmlrpc_patch_lock: + if not _xmlrpc_patch_applied: + try: + from defusedxml.xmlrpc import monkey_patch + + monkey_patch() + _xmlrpc_patch_applied = True + except Exception: + # Keep runtime behavior unchanged if defusedxml is unavailable. + _xmlrpc_patch_applied = False + + return importlib.import_module("xmlrpc.client") + + def normalize_base_path(value: Optional[str]) -> str: """Normalize a URL base path for reverse proxy subpath deployments.""" if not isinstance(value, str): diff --git a/shelfmark/download/clients/rtorrent.py b/shelfmark/download/clients/rtorrent.py index 80eaed28..670d6d1d 100644 --- a/shelfmark/download/clients/rtorrent.py +++ b/shelfmark/download/clients/rtorrent.py @@ -10,7 +10,7 @@ from urllib.parse import urlparse from shelfmark.core.config import config from shelfmark.core.logger import setup_logger -from shelfmark.core.utils import normalize_http_url +from shelfmark.core.utils import normalize_http_url, get_hardened_xmlrpc_client from shelfmark.download.network import get_ssl_verify from shelfmark.download.clients import ( DownloadClient, @@ -26,17 +26,17 @@ logger = setup_logger(__name__) def _create_rtorrent_server_proxy(url: str) -> Any: """Create an XML-RPC ServerProxy honoring certificate validation mode.""" - from xmlrpc.client import SafeTransport, ServerProxy + xmlrpc_client = get_hardened_xmlrpc_client() verify = get_ssl_verify(url) if url.startswith("https://") and not verify: ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE - transport = SafeTransport(context=ssl_context) - return ServerProxy(url, transport=transport) + transport = xmlrpc_client.SafeTransport(context=ssl_context) + return xmlrpc_client.ServerProxy(url, transport=transport) - return ServerProxy(url) + return xmlrpc_client.ServerProxy(url) @register_client("torrent") @@ -120,7 +120,7 @@ class RTorrentClient(DownloadClient): download_dir = self._download_dir or self._get_download_dir() if download_dir: logger.debug(f"Setting rTorrent download directory: {download_dir}") - commands.append(f"d.directory_base.set={download_dir}") + commands.append(f"d.directory.set={download_dir}") if torrent_info.torrent_data: logger.debug(f"Adding torrent data directly to rTorrent for: {name} with commands: {commands} with data size: {len(torrent_info.torrent_data)}") @@ -156,10 +156,9 @@ class RTorrentClient(DownloadClient): try: # rtorrent is somehow case sensitive and requires uppercase hashes for look download_id = download_id.upper() - torrent_list = self._rpc.d.multicall.filtered( + all_torrents = self._rpc.d.multicall2( + "", "", - "default", - f"equal={{d.hash=,cat={download_id}}}", "d.hash=", "d.state=", "d.completed_bytes=", @@ -169,6 +168,7 @@ class RTorrentClient(DownloadClient): "d.custom1=", "d.complete=", ) + torrent_list = [t for t in all_torrents if t and t[0] == download_id] logger.debug(f"Fetched torrent status from rTorrent for: {download_id} - {torrent_list}") if not torrent_list: logger.warning(f"Torrent not found in rTorrent: {download_id}") @@ -327,12 +327,13 @@ class RTorrentClient(DownloadClient): try: # rTorrent is case sensitive for hashes; use uppercase as in get_status() download_hash = download_id.upper() - details = self._rpc.d.multicall.filtered( + all_torrents = self._rpc.d.multicall2( "", - "default", - f"equal={{d.hash=,cat={download_hash}}}", + "", + "d.hash=", "d.base_path=", ) + details = [t[1:] for t in all_torrents if t and t[0] == download_hash] if not details: return None path = details[0][0] diff --git a/shelfmark/download/clients/settings.py b/shelfmark/download/clients/settings.py index 2fad373f..7a6221cd 100644 --- a/shelfmark/download/clients/settings.py +++ b/shelfmark/download/clients/settings.py @@ -12,7 +12,7 @@ from shelfmark.core.settings_registry import ( SelectField, TagListField, ) -from shelfmark.core.utils import normalize_http_url +from shelfmark.core.utils import normalize_http_url, get_hardened_xmlrpc_client from shelfmark.download.network import get_ssl_verify @@ -254,7 +254,6 @@ def _test_rtorrent_connection(current_values: Optional[Dict[str, Any]] = None) - from shelfmark.core.config import config import ssl from urllib.parse import urlparse - from xmlrpc.client import SafeTransport, ServerProxy current_values = current_values or {} @@ -270,6 +269,8 @@ def _test_rtorrent_connection(current_values: Optional[Dict[str, Any]] = None) - return {"success": False, "message": "rTorrent URL is invalid"} try: + xmlrpc_client = get_hardened_xmlrpc_client() + # Add HTTP auth to URL if credentials provided if username and password: parsed = urlparse(url) @@ -281,9 +282,12 @@ def _test_rtorrent_connection(current_values: Optional[Dict[str, Any]] = None) - ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE - rpc = ServerProxy(rpc_url, transport=SafeTransport(context=ssl_context)) + rpc = xmlrpc_client.ServerProxy( + rpc_url, + transport=xmlrpc_client.SafeTransport(context=ssl_context), + ) else: - rpc = ServerProxy(rpc_url) + rpc = xmlrpc_client.ServerProxy(rpc_url) version = rpc.system.client_version() return {"success": True, "message": f"Connected to rTorrent {version}"} diff --git a/shelfmark/download/fs.py b/shelfmark/download/fs.py index 9075b5bf..67737a74 100644 --- a/shelfmark/download/fs.py +++ b/shelfmark/download/fs.py @@ -200,6 +200,28 @@ def _perform_nfs_fallback(source: Path, dest: Path, is_move: bool) -> None: raise +def _is_enoent_error(error: Exception) -> bool: + return isinstance(error, FileNotFoundError) or ( + isinstance(error, OSError) and error.errno == errno.ENOENT + ) + + +def _can_use_partial_copy_after_enoent( + temp_path: Optional[Path], + expected_size: int, + action: str, +) -> bool: + """Recover when copy2 writes bytes but fails while copying source metadata.""" + if not temp_path or not run_blocking_io(temp_path.exists): + return False + + try: + _verify_transfer_size(temp_path, expected_size, action) + return True + except Exception: + return False + + def _claim_destination(path: Path) -> bool: """Atomically claim a destination path by creating a placeholder file. @@ -359,6 +381,16 @@ def atomic_move(source_path: Path, dest_path: Path, max_attempts: int = 100) -> copy_error, ) _perform_nfs_fallback(source_path, temp_path, is_move=False) + elif _is_enoent_error(copy_error) and _can_use_partial_copy_after_enoent( + temp_path, + expected_size, + "move", + ): + logger.warning( + "Source vanished during move-copy metadata step; preserving copied data: %s -> %s", + source_path, + temp_path, + ) else: raise @@ -525,6 +557,16 @@ def atomic_copy(source_path: Path, dest_path: Path, max_attempts: int = 100) -> fallback_error, ) raise e from fallback_error + elif _is_enoent_error(e) and _can_use_partial_copy_after_enoent( + temp_path, + expected_size, + "copy", + ): + logger.warning( + "Source vanished during copy2 metadata step; preserving copied data: %s -> %s", + source_path, + temp_path, + ) else: raise diff --git a/shelfmark/main.py b/shelfmark/main.py index f60d319c..d510e680 100644 --- a/shelfmark/main.py +++ b/shelfmark/main.py @@ -590,6 +590,20 @@ def proxy_auth_middleware(): logger.error(f"Proxy auth middleware error: {e}") return jsonify({"error": "Authentication error"}), 500 +@app.after_request +def set_security_headers(response: Response) -> Response: + """Add baseline security headers to every response.""" + response.headers.setdefault("X-Content-Type-Options", "nosniff") + response.headers.setdefault("X-Frame-Options", "DENY") + response.headers.setdefault( + "Content-Security-Policy", + "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws: wss:; frame-ancestors 'none'", + ) + response.headers.setdefault("Permissions-Policy", "camera=(), microphone=(), geolocation=()") + response.headers.setdefault("Cross-Origin-Embedder-Policy", "credentialless") + return response + + def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): diff --git a/shelfmark/release_sources/prowlarr/torznab.py b/shelfmark/release_sources/prowlarr/torznab.py index fe2df687..d2651642 100644 --- a/shelfmark/release_sources/prowlarr/torznab.py +++ b/shelfmark/release_sources/prowlarr/torznab.py @@ -8,7 +8,9 @@ isn't available via Prowlarr's JSON search endpoint. from __future__ import annotations from typing import Any, Dict, List, Optional -from xml.etree import ElementTree as ET + +from defusedxml import ElementTree as DefusedElementTree +from defusedxml.common import DefusedXmlException def _local_name(tag: str) -> str: @@ -67,8 +69,8 @@ def parse_torznab_xml(xml_text: str) -> List[Dict[str, Any]]: return [] try: - root = ET.fromstring(xml_text) - except ET.ParseError: + root = DefusedElementTree.fromstring(xml_text) + except (DefusedElementTree.ParseError, DefusedXmlException): return [] items = root.findall(".//item") @@ -168,4 +170,3 @@ def parse_torznab_xml(xml_text: str) -> List[Dict[str, Any]]: }) return results - diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index ec7ccc51..6b803628 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.30.2", + "react-router-dom": "^6.30.3", "socket.io-client": "^4.7.5" }, "devDependencies": { @@ -69,6 +69,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -711,24 +712,6 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -817,21 +800,10 @@ "node": ">= 8" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@remix-run/router": { - "version": "1.23.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", - "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -1216,6 +1188,7 @@ "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1233,6 +1206,7 @@ "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1269,32 +1243,6 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1361,13 +1309,6 @@ "postcss": "^8.1.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/baseline-browser-mapping": { "version": "2.8.25", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz", @@ -1391,16 +1332,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -1434,6 +1365,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -1517,26 +1449,6 @@ "node": ">= 6" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1554,21 +1466,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1621,13 +1518,6 @@ "dev": true, "license": "MIT" }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/electron-to-chromium": { "version": "1.5.249", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.249.tgz", @@ -1635,13 +1525,6 @@ "dev": true, "license": "ISC" }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, "node_modules/engine.io-client": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", @@ -1783,23 +1666,6 @@ "node": ">=8" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -1849,27 +1715,6 @@ "node": ">=6.9.0" } }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1935,16 +1780,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1968,35 +1803,13 @@ "node": ">=0.12.0" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -2099,32 +1912,6 @@ "node": ">=8.6" } }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2209,23 +1996,6 @@ "node": ">= 6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -2233,30 +2003,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2317,6 +2063,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2486,6 +2233,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -2498,6 +2246,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -2517,12 +2266,12 @@ } }, "node_modules/react-router": { - "version": "6.30.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", - "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.1" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -2532,13 +2281,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", - "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.1", - "react-router": "6.30.2" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" @@ -2688,42 +2437,6 @@ "semver": "bin/semver.js" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/socket.io-client": { "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", @@ -2796,123 +2509,19 @@ "node": ">=0.10.0" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/sucrase": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", - "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", - "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { @@ -2997,6 +2606,55 @@ "node": ">=0.8" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3082,6 +2740,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -3136,120 +2795,6 @@ } } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", diff --git a/src/frontend/package.json b/src/frontend/package.json index 2942273b..aec17ee1 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -14,7 +14,7 @@ "dependencies": { "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.30.2", + "react-router-dom": "^6.30.3", "socket.io-client": "^4.7.5" }, "devDependencies": { diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index b2625cee..3fee066c 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -1039,12 +1039,25 @@ function App() { record: RequestRecord, options?: { browseOnly?: boolean; + manualApproval?: boolean; } ) => { if (!requestRoleIsAdmin) { return; } + if (options?.manualApproval) { + try { + await fulfilSidebarRequest(requestId, undefined, undefined, true); + await refreshActivitySnapshot(); + showToast('Request approved', 'success'); + await fetchStatus(); + } catch (error) { + showToast(getErrorMessage(error, 'Failed to approve request'), 'error'); + } + return; + } + const shouldBrowse = Boolean(options?.browseOnly) || record.request_level === 'book'; if (!shouldBrowse && record.request_level === 'release') { diff --git a/src/frontend/src/components/ReleaseModal.tsx b/src/frontend/src/components/ReleaseModal.tsx index ea152fec..9660a167 100644 --- a/src/frontend/src/components/ReleaseModal.tsx +++ b/src/frontend/src/components/ReleaseModal.tsx @@ -31,6 +31,7 @@ import { releaseLanguageMatchesFilter, buildLanguageNormalizer, } from '../utils/languageFilters'; +import { getReleaseFormats } from '../utils/releaseFormats'; // Module-level cache for release search results // Key format: `${provider}:${provider_id}:${source}:${contentType}` @@ -266,9 +267,7 @@ function getTitleMatchScore(title: string, titleCandidate: string): number { if (normalizedTitle.startsWith(titleCandidate)) { score += 6000; - } - - if (normalizedTitle.includes(titleCandidate)) { + } else if (normalizedTitle.includes(titleCandidate)) { score += 3000; } @@ -1247,16 +1246,16 @@ export const ReleaseModal = ({ const availableFormats = useMemo(() => { const releases = releasesBySource[activeTab]?.releases || []; const formats = new Set(); - const effectiveLower = effectiveFormats.map((f) => f.toLowerCase()); + const effectiveLower = new Set(effectiveFormats.map((f) => f.toLowerCase())); releases.forEach((r) => { - if (r.format) { - const fmt = r.format.toLowerCase(); + const releaseFormats = getReleaseFormats(r); + releaseFormats.forEach((fmt) => { // Only include formats that are in the supported list - if (effectiveLower.includes(fmt)) { + if (effectiveLower.has(fmt)) { formats.add(fmt); } - } + }); }); return Array.from(formats).sort(); }, [releasesBySource, activeTab, effectiveFormats]); @@ -1390,22 +1389,23 @@ export const ReleaseModal = ({ // Filter and sort releases based on settings and user selection const filteredReleases = useMemo(() => { const releases = releasesBySource[activeTab]?.releases || []; - const effectiveLower = effectiveFormats.map((f) => f.toLowerCase()); + const effectiveLower = new Set(effectiveFormats.map((f) => f.toLowerCase())); const supportsIndexerFilter = columnConfig.supported_filters?.includes('indexer'); + const selectedFormat = formatFilter.toLowerCase(); // First, filter let filtered = releases.filter((r) => { // Format filtering - const fmt = r.format?.toLowerCase(); + const releaseFormats = getReleaseFormats(r); if (formatFilter) { - // User selected a specific format - must match exactly - if (!fmt || fmt !== formatFilter.toLowerCase()) return false; - } else if (fmt) { - // No specific filter - show only supported formats - if (!effectiveLower.includes(fmt)) return false; + // User selected a specific format - match if any format on the release matches + if (!releaseFormats.includes(selectedFormat)) return false; + } else if (releaseFormats.length > 0) { + // No specific filter - show only releases that include at least one supported format + if (!releaseFormats.some((fmt) => effectiveLower.has(fmt))) return false; } - // Releases with no format pass through when no filter is set (show all) + // Releases with no format info pass through when no filter is set (show all) // Language filtering - use r.language when provided by enriched indexers // Releases with no language (null/undefined) always pass diff --git a/src/frontend/src/components/activity/ActivityCard.tsx b/src/frontend/src/components/activity/ActivityCard.tsx index 9b4dedbe..249290d3 100644 --- a/src/frontend/src/components/activity/ActivityCard.tsx +++ b/src/frontend/src/components/activity/ActivityCard.tsx @@ -12,6 +12,7 @@ import { interface RequestApproveOptions { browseOnly?: boolean; + manualApproval?: boolean; } type RequestApproveHandler = ( @@ -431,6 +432,7 @@ export const ActivityCard = ({ ? 'Browse Releases To Retry' : 'Browse Releases To Approve' : 'Approve Attached File'; + const canMarkAsApprovedWithoutRelease = requiresBrowseBeforeApprove && !hasAttachedRelease; const provider = toOptionalText(bookData.provider)?.toLowerCase(); const providerId = toOptionalText(bookData.provider_id); @@ -467,6 +469,19 @@ export const ActivityCard = ({ } }; + const handleReviewManualApproval = async () => { + if (!reviewRecord || !reviewApproveHandler || isReviewSubmitting) { + return; + } + + setIsReviewSubmitting(true); + try { + await reviewApproveHandler(reviewRecord.id, reviewRecord, { manualApproval: true }); + } finally { + setIsReviewSubmitting(false); + } + }; + const canShowInlineReview = Boolean(isRequestDetailsOpen && reviewRecord && reviewApproveHandler); const rejectConfirmHandler = onRequestRejectConfirm || onRequestReject; const canShowInlineReject = Boolean( @@ -704,6 +719,16 @@ export const ActivityCard = ({ > {isReviewSubmitting ? 'Working...' : approveLabel} + {canMarkAsApprovedWithoutRelease && ( + + )} {canBrowseAlternatives && hasAttachedRelease && (