diff --git a/Dockerfile b/Dockerfile index c1c0321..ad288e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-slim AS frontend-builder +FROM node:24-slim AS frontend-builder WORKDIR /frontend @@ -6,8 +6,8 @@ ENV NODE_ENV=production \ BACKEND_INTERNAL_URL=http://127.0.0.1:8000 \ NEXT_PUBLIC_API_BASE=/api -COPY frontend/package.json ./ -RUN npm install +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci --include=dev COPY frontend/app ./app COPY frontend/public ./public @@ -17,7 +17,7 @@ COPY frontend/tsconfig.json ./tsconfig.json RUN npm run build -FROM python:3.12-slim +FROM python:3.14-slim WORKDIR /app @@ -27,7 +27,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ RUN apt-get update \ && apt-get install -y --no-install-recommends curl gnupg supervisor \ - && curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/README.md b/README.md index d1e4aa5..3a72055 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Magent -Magent is a friendly, AI-assisted request tracker for Jellyseerr + Arr services. It shows a clear timeline of where a request is stuck, explains what is happening in plain English, and offers safe actions to help fix issues. +Magent is a friendly, AI-assisted request tracker for Seerr + Arr services. It shows a clear timeline of where a request is stuck, explains what is happening in plain English, and offers safe actions to help fix issues. ## How it works -1) Requests are pulled from Jellyseerr and stored locally. +1) Requests are pulled from Seerr and stored locally. 2) Magent joins that request to Sonarr/Radarr, Prowlarr, qBittorrent, and Jellyfin using TMDB/TVDB IDs and download hashes. 3) A state engine normalizes noisy service statuses into a simple, user-friendly state. 4) The UI renders a timeline and a central status box for each request. @@ -14,7 +14,7 @@ Magent is a friendly, AI-assisted request tracker for Jellyseerr + Arr services. - Request search by title/year or request ID. - Recent requests list with posters and status. -- Timeline view across Jellyseerr, Arr, Prowlarr, qBittorrent, Jellyfin. +- Timeline view across Seerr, Arr, Prowlarr, qBittorrent, Jellyfin. - Central status box with clear reason + next steps. - Safe action buttons (search, resume, re-add, etc.). - Admin settings for service URLs, API keys, profiles, and root folders. @@ -160,7 +160,7 @@ If you prefer the browser to call the backend directly, set `NEXT_PUBLIC_API_BAS ### No recent requests -- Confirm Jellyseerr credentials in Settings. +- Confirm Seerr credentials in Settings. - Run a full sync from Settings -> Requests. ### Docker images not updating diff --git a/backend/app/ai/triage.py b/backend/app/ai/triage.py index ae0e33a..ae4499c 100644 --- a/backend/app/ai/triage.py +++ b/backend/app/ai/triage.py @@ -9,12 +9,12 @@ def triage_snapshot(snapshot: Snapshot) -> TriageResult: if snapshot.state == NormalizedState.requested: root_cause = "approval" - summary = "The request is waiting for approval in Jellyseerr." + summary = "The request is waiting for approval in Seerr." recommendations.append( TriageRecommendation( action_id="wait_for_approval", title="Ask an admin to approve the request", - reason="Jellyseerr has not marked this request as approved.", + reason="Seerr has not marked this request as approved.", risk="low", ) ) diff --git a/backend/app/build_info.py b/backend/app/build_info.py index e55a98a..91d32d5 100644 --- a/backend/app/build_info.py +++ b/backend/app/build_info.py @@ -1,4 +1,4 @@ -BUILD_NUMBER = "2702261314" -CHANGELOG = '2026-01-22\\n- Initial commit\\n- Ignore build artifacts\\n- Update README\\n- Update README with Docker-first guide\\n\\n2026-01-23\\n- Fix cache titles via Jellyseerr media lookup\\n- Split search actions and improve download options\\n- Fallback manual grab to qBittorrent\\n- Hide header actions when signed out\\n- Add feedback form and webhook\\n- Fix cache titles and move feedback link\\n- Show available status on landing when in Jellyfin\\n- Add default branding assets when missing\\n- Use bundled branding assets\\n- Remove password fields from users page\\n- Add Docker Hub compose override\\n- Fix backend Dockerfile paths for root context\\n- Copy public assets into frontend image\\n- Use backend branding assets for logo and favicon\\n\\n2026-01-24\\n- Route grabs through Sonarr/Radarr only\\n- Document fix buttons in how-it-works\\n- Clarify how-it-works steps and fixes\\n- Map Prowlarr releases to Arr indexers for manual grab\\n- Improve request handling and qBittorrent categories\\n\\n2026-01-25\\n- Add site banner, build number, and changelog\\n- Automate build number tagging and sync\\n- Improve mobile header layout\\n- Move account actions into avatar menu\\n- Add user stats and activity tracking\\n- Add Jellyfin login cache and admin-only stats\\n- Tidy request sync controls\\n- Seed branding logo from bundled assets\\n- Serve bundled branding assets by default\\n- Harden request cache titles and cache-only reads\\n- Build 2501262041\\n\\n2026-01-26\\n- Fix cache title hydration\\n- Fix sync progress bar animation\\n\\n2026-01-27\\n- Add cache control artwork stats\\n- Improve cache stats performance (build 271261145)\\n- Fix backend cache stats import (build 271261149)\\n- Clarify request sync settings (build 271261159)\\n- Bump build number to 271261202\\n- Fix request titles in snapshots (build 271261219)\\n- Fix snapshot title fallback (build 271261228)\\n- Add cache load spinner (build 271261238)\\n- Bump build number (process 2) 271261322\\n- Add service test buttons (build 271261335)\\n- Fallback to TMDB when artwork cache fails (build 271261524)\\n- Hydrate missing artwork from Jellyseerr (build 271261539)\\n\\n2026-01-29\\n- release: 2901262036\\n- release: 2901262044\\n- release: 2901262102\\n- Hardcode build number in backend\\n- Bake build number and changelog\\n- Update full changelog\\n- Tidy full changelog\\n- Build 2901262240: cache users\n\n2026-01-30\n- Merge backend and frontend into one container' +BUILD_NUMBER = "2802262051" +CHANGELOG = '2026-01-22\\n- Initial commit\\n- Ignore build artifacts\\n- Update README\\n- Update README with Docker-first guide\\n\\n2026-01-23\\n- Fix cache titles via Seerr media lookup\\n- Split search actions and improve download options\\n- Fallback manual grab to qBittorrent\\n- Hide header actions when signed out\\n- Add feedback form and webhook\\n- Fix cache titles and move feedback link\\n- Show available status on landing when in Jellyfin\\n- Add default branding assets when missing\\n- Use bundled branding assets\\n- Remove password fields from users page\\n- Add Docker Hub compose override\\n- Fix backend Dockerfile paths for root context\\n- Copy public assets into frontend image\\n- Use backend branding assets for logo and favicon\\n\\n2026-01-24\\n- Route grabs through Sonarr/Radarr only\\n- Document fix buttons in how-it-works\\n- Clarify how-it-works steps and fixes\\n- Map Prowlarr releases to Arr indexers for manual grab\\n- Improve request handling and qBittorrent categories\\n\\n2026-01-25\\n- Add site banner, build number, and changelog\\n- Automate build number tagging and sync\\n- Improve mobile header layout\\n- Move account actions into avatar menu\\n- Add user stats and activity tracking\\n- Add Jellyfin login cache and admin-only stats\\n- Tidy request sync controls\\n- Seed branding logo from bundled assets\\n- Serve bundled branding assets by default\\n- Harden request cache titles and cache-only reads\\n- Build 2501262041\\n\\n2026-01-26\\n- Fix cache title hydration\\n- Fix sync progress bar animation\\n\\n2026-01-27\\n- Add cache control artwork stats\\n- Improve cache stats performance (build 271261145)\\n- Fix backend cache stats import (build 271261149)\\n- Clarify request sync settings (build 271261159)\\n- Bump build number to 271261202\\n- Fix request titles in snapshots (build 271261219)\\n- Fix snapshot title fallback (build 271261228)\\n- Add cache load spinner (build 271261238)\\n- Bump build number (process 2) 271261322\\n- Add service test buttons (build 271261335)\\n- Fallback to TMDB when artwork cache fails (build 271261524)\\n- Hydrate missing artwork from Seerr (build 271261539)\\n\\n2026-01-29\\n- release: 2901262036\\n- release: 2901262044\\n- release: 2901262102\\n- Hardcode build number in backend\\n- Bake build number and changelog\\n- Update full changelog\\n- Tidy full changelog\\n- Build 2901262240: cache users\n\n2026-01-30\n- Merge backend and frontend into one container' diff --git a/backend/app/clients/jellyseerr.py b/backend/app/clients/jellyseerr.py index 7c75011..23314d6 100644 --- a/backend/app/clients/jellyseerr.py +++ b/backend/app/clients/jellyseerr.py @@ -1,4 +1,5 @@ from typing import Any, Dict, Optional +import httpx from .base import ApiClient @@ -18,9 +19,6 @@ class JellyseerrClient(ApiClient): }, ) - async def get_media(self, media_id: int) -> Optional[Dict[str, Any]]: - return await self.get(f"/api/v1/media/{media_id}") - async def get_movie(self, tmdb_id: int) -> Optional[Dict[str, Any]]: return await self.get(f"/api/v1/movie/{tmdb_id}") @@ -50,3 +48,14 @@ class JellyseerrClient(ApiClient): async def delete_user(self, user_id: int) -> Optional[Dict[str, Any]]: return await self.delete(f"/api/v1/user/{user_id}") + + async def login_local(self, email: str, password: str) -> Optional[Dict[str, Any]]: + payload = {"email": email, "password": password} + try: + return await self.post("/api/v1/auth/local", payload=payload) + except httpx.HTTPStatusError as exc: + # Backward compatibility for older Seerr/Overseerr deployments + # that still expose /auth/login instead of /auth/local. + if exc.response is not None and exc.response.status_code in {404, 405}: + return await self.post("/api/v1/auth/login", payload=payload) + raise diff --git a/backend/app/db.py b/backend/app/db.py index edc0a45..c4f18d9 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -703,7 +703,7 @@ def get_all_users() -> list[Dict[str, Any]]: } ) # Admin user management uses Jellyfin as the source of truth for non-admin - # user objects. Jellyseerr rows are treated as enrichment-only and hidden + # user objects. Seerr rows are treated as enrichment-only and hidden # from admin/user-management views to avoid duplicate accounts in the UI. def _provider_rank(user: Dict[str, Any]) -> int: provider = str(user.get("auth_provider") or "local").strip().lower() diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py index a1c2395..bc0e4e4 100644 --- a/backend/app/routers/admin.py +++ b/backend/app/routers/admin.py @@ -696,12 +696,13 @@ async def _fetch_all_jellyseerr_users( return save_jellyseerr_users_cache(users) return users +@router.post("/seerr/users/sync") @router.post("/jellyseerr/users/sync") async def jellyseerr_users_sync() -> Dict[str, Any]: runtime = get_runtime_settings() client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) if not client.configured(): - raise HTTPException(status_code=400, detail="Jellyseerr not configured") + raise HTTPException(status_code=400, detail="Seerr not configured") jellyseerr_users = await _fetch_all_jellyseerr_users(client, use_cache=False) if not jellyseerr_users: return {"status": "ok", "matched": 0, "skipped": 0, "total": 0} @@ -733,12 +734,13 @@ def _pick_jellyseerr_username(user: Dict[str, Any]) -> Optional[str]: return None +@router.post("/seerr/users/resync") @router.post("/jellyseerr/users/resync") async def jellyseerr_users_resync() -> Dict[str, Any]: runtime = get_runtime_settings() client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) if not client.configured(): - raise HTTPException(status_code=400, detail="Jellyseerr not configured") + raise HTTPException(status_code=400, detail="Seerr not configured") jellyseerr_users = await _fetch_all_jellyseerr_users(client, use_cache=False) if not jellyseerr_users: return {"status": "ok", "imported": 0, "cleared": 0} @@ -772,7 +774,7 @@ async def requests_sync() -> Dict[str, Any]: runtime = get_runtime_settings() client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) if not client.configured(): - raise HTTPException(status_code=400, detail="Jellyseerr not configured") + raise HTTPException(status_code=400, detail="Seerr not configured") state = await requests_router.start_requests_sync( runtime.jellyseerr_base_url, runtime.jellyseerr_api_key ) @@ -785,7 +787,7 @@ async def requests_sync_delta() -> Dict[str, Any]: runtime = get_runtime_settings() client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) if not client.configured(): - raise HTTPException(status_code=400, detail="Jellyseerr not configured") + raise HTTPException(status_code=400, detail="Seerr not configured") state = await requests_router.start_requests_delta_sync( runtime.jellyseerr_base_url, runtime.jellyseerr_api_key ) @@ -902,7 +904,7 @@ async def requests_cache(limit: int = 50) -> Dict[str, Any]: logger.info("Requests cache titles repaired via settings view: %s", repaired) hydrated = await _hydrate_cache_titles_from_jellyseerr(limit) if hydrated: - logger.info("Requests cache titles hydrated via Jellyseerr: %s", hydrated) + logger.info("Requests cache titles hydrated via Seerr: %s", hydrated) rows = get_request_cache_overview(limit) return {"rows": rows} @@ -1073,7 +1075,7 @@ async def user_system_action(username: str, payload: Dict[str, Any]) -> Dict[str "username": user.get("username"), "local": {"status": "pending"}, "jellyfin": {"status": "skipped", "detail": "Jellyfin not configured"}, - "jellyseerr": {"status": "skipped", "detail": "Jellyseerr not configured or no linked user ID"}, + "jellyseerr": {"status": "skipped", "detail": "Seerr not configured or no linked user ID"}, "invites": {"status": "pending", "disabled": 0}, } diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index b8e2450..7b3e2ca 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -566,21 +566,21 @@ async def jellyfin_login(request: Request, form_data: OAuth2PasswordRequestForm } +@router.post("/seerr/login") @router.post("/jellyseerr/login") async def jellyseerr_login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()) -> dict: _enforce_login_rate_limit(request, form_data.username) runtime = get_runtime_settings() client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) if not client.configured(): - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Jellyseerr not configured") - payload = {"email": form_data.username, "password": form_data.password} + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Seerr not configured") try: - response = await client.post("/api/v1/auth/login", payload=payload) + response = await client.login_local(form_data.username, form_data.password) except Exception as exc: raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc if not isinstance(response, dict): _record_login_failure(request, form_data.username) - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Jellyseerr credentials") + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Seerr credentials") jellyseerr_user_id = _extract_jellyseerr_user_id(response) ci_matches = get_users_by_username_ci(form_data.username) preferred_match = _pick_preferred_ci_user_match(ci_matches, form_data.username) diff --git a/backend/app/routers/requests.py b/backend/app/routers/requests.py index 22064d6..3c86d47 100644 --- a/backend/app/routers/requests.py +++ b/backend/app/routers/requests.py @@ -76,7 +76,6 @@ _artwork_prefetch_state: Dict[str, Any] = { "finished_at": None, } _artwork_prefetch_task: Optional[asyncio.Task] = None -_media_endpoint_supported: Optional[bool] = None STATUS_LABELS = { 1: "Waiting for approval", @@ -419,23 +418,6 @@ async def _hydrate_title_from_tmdb( return None, None -async def _hydrate_media_details(client: JellyseerrClient, media_id: Optional[int]) -> Optional[Dict[str, Any]]: - if not media_id: - return None - global _media_endpoint_supported - if _media_endpoint_supported is False: - return None - try: - details = await client.get_media(int(media_id)) - except httpx.HTTPStatusError as exc: - if exc.response is not None and exc.response.status_code == 405: - _media_endpoint_supported = False - logger.info("Jellyseerr media endpoint rejected GET requests; skipping media lookups.") - return None - _media_endpoint_supported = True - return details if isinstance(details, dict) else None - - async def _hydrate_artwork_from_tmdb( client: JellyseerrClient, media_type: Optional[str], tmdb_id: Optional[int] ) -> tuple[Optional[str], Optional[str]]: @@ -511,7 +493,7 @@ async def _sync_all_requests(client: JellyseerrClient) -> int: skip = 0 stored = 0 cache_mode = (get_runtime_settings().artwork_cache_mode or "remote").lower() - logger.info("Jellyseerr sync starting: take=%s", take) + logger.info("Seerr sync starting: take=%s", take) _sync_state.update( { "status": "running", @@ -527,11 +509,11 @@ async def _sync_all_requests(client: JellyseerrClient) -> int: try: response = await client.get_recent_requests(take=take, skip=skip) except httpx.HTTPError as exc: - logger.warning("Jellyseerr sync failed at skip=%s: %s", skip, exc) + logger.warning("Seerr sync failed at skip=%s: %s", skip, exc) _sync_state.update({"status": "failed", "message": f"Sync failed: {exc}"}) break if not isinstance(response, dict): - logger.warning("Jellyseerr sync stopped: non-dict response at skip=%s", skip) + logger.warning("Seerr sync stopped: non-dict response at skip=%s", skip) _sync_state.update({"status": "failed", "message": "Invalid response"}) break if _sync_state["total"] is None: @@ -546,7 +528,7 @@ async def _sync_all_requests(client: JellyseerrClient) -> int: _sync_state["total"] = total items = response.get("results") or [] if not isinstance(items, list) or not items: - logger.info("Jellyseerr sync completed: no more results at skip=%s", skip) + logger.info("Seerr sync completed: no more results at skip=%s", skip) break for item in items: if not isinstance(item, dict): @@ -559,38 +541,18 @@ async def _sync_all_requests(client: JellyseerrClient) -> int: cached = get_request_cache_by_id(request_id) if cached and cached.get("title"): cached_title = cached.get("title") - if not payload.get("title") or not payload.get("media_id"): - logger.debug("Jellyseerr sync hydrate request_id=%s", request_id) + needs_details = ( + not payload.get("title") + or not payload.get("media_id") + or not payload.get("tmdb_id") + or not payload.get("media_type") + ) + if needs_details: + logger.debug("Seerr sync hydrate request_id=%s", request_id) details = await _get_request_details(client, request_id) if isinstance(details, dict): payload = _parse_request_payload(details) item = details - if ( - not payload.get("title") - and payload.get("media_id") - and (not payload.get("tmdb_id") or not payload.get("media_type")) - ): - media_details = await _hydrate_media_details(client, payload.get("media_id")) - if isinstance(media_details, dict): - media_title = media_details.get("title") or media_details.get("name") - if media_title: - payload["title"] = media_title - if not payload.get("year") and media_details.get("year"): - payload["year"] = media_details.get("year") - if not payload.get("tmdb_id") and media_details.get("tmdbId"): - payload["tmdb_id"] = media_details.get("tmdbId") - if not payload.get("media_type") and media_details.get("mediaType"): - payload["media_type"] = media_details.get("mediaType") - if isinstance(item, dict): - existing_media = item.get("media") - if isinstance(existing_media, dict): - merged = dict(media_details) - for key, value in existing_media.items(): - if value is not None: - merged[key] = value - item["media"] = merged - else: - item["media"] = media_details poster_path, backdrop_path = _extract_artwork_paths(item) if cache_mode == "cache" and not (poster_path or backdrop_path): details = await _get_request_details(client, request_id) @@ -629,12 +591,12 @@ async def _sync_all_requests(client: JellyseerrClient) -> int: stored += 1 _sync_state["stored"] = stored if len(items) < take: - logger.info("Jellyseerr sync completed: stored=%s", stored) + logger.info("Seerr sync completed: stored=%s", stored) break skip += take _sync_state["skip"] = skip _sync_state["message"] = f"Synced {stored} requests" - logger.info("Jellyseerr sync progress: stored=%s skip=%s", stored, skip) + logger.info("Seerr sync progress: stored=%s skip=%s", stored, skip) _sync_state.update( { "status": "completed", @@ -659,7 +621,7 @@ async def _sync_delta_requests(client: JellyseerrClient) -> int: stored = 0 unchanged_pages = 0 cache_mode = (get_runtime_settings().artwork_cache_mode or "remote").lower() - logger.info("Jellyseerr delta sync starting: take=%s", take) + logger.info("Seerr delta sync starting: take=%s", take) _sync_state.update( { "status": "running", @@ -675,16 +637,16 @@ async def _sync_delta_requests(client: JellyseerrClient) -> int: try: response = await client.get_recent_requests(take=take, skip=skip) except httpx.HTTPError as exc: - logger.warning("Jellyseerr delta sync failed at skip=%s: %s", skip, exc) + logger.warning("Seerr delta sync failed at skip=%s: %s", skip, exc) _sync_state.update({"status": "failed", "message": f"Delta sync failed: {exc}"}) break if not isinstance(response, dict): - logger.warning("Jellyseerr delta sync stopped: non-dict response at skip=%s", skip) + logger.warning("Seerr delta sync stopped: non-dict response at skip=%s", skip) _sync_state.update({"status": "failed", "message": "Invalid response"}) break items = response.get("results") or [] if not isinstance(items, list) or not items: - logger.info("Jellyseerr delta sync completed: no more results at skip=%s", skip) + logger.info("Seerr delta sync completed: no more results at skip=%s", skip) break page_changed = False for item in items: @@ -698,37 +660,17 @@ async def _sync_delta_requests(client: JellyseerrClient) -> int: cached_title = cached.get("title") if cached else None if cached and incoming_updated and cached.get("updated_at") == incoming_updated and cached.get("title"): continue - if not payload.get("title") or not payload.get("media_id"): + needs_details = ( + not payload.get("title") + or not payload.get("media_id") + or not payload.get("tmdb_id") + or not payload.get("media_type") + ) + if needs_details: details = await _get_request_details(client, request_id) if isinstance(details, dict): payload = _parse_request_payload(details) item = details - if ( - not payload.get("title") - and payload.get("media_id") - and (not payload.get("tmdb_id") or not payload.get("media_type")) - ): - media_details = await _hydrate_media_details(client, payload.get("media_id")) - if isinstance(media_details, dict): - media_title = media_details.get("title") or media_details.get("name") - if media_title: - payload["title"] = media_title - if not payload.get("year") and media_details.get("year"): - payload["year"] = media_details.get("year") - if not payload.get("tmdb_id") and media_details.get("tmdbId"): - payload["tmdb_id"] = media_details.get("tmdbId") - if not payload.get("media_type") and media_details.get("mediaType"): - payload["media_type"] = media_details.get("mediaType") - if isinstance(item, dict): - existing_media = item.get("media") - if isinstance(existing_media, dict): - merged = dict(media_details) - for key, value in existing_media.items(): - if value is not None: - merged[key] = value - item["media"] = merged - else: - item["media"] = media_details poster_path, backdrop_path = _extract_artwork_paths(item) if cache_mode == "cache" and not (poster_path or backdrop_path): details = await _get_request_details(client, request_id) @@ -772,15 +714,15 @@ async def _sync_delta_requests(client: JellyseerrClient) -> int: else: unchanged_pages = 0 if len(items) < take or unchanged_pages >= 2: - logger.info("Jellyseerr delta sync completed: stored=%s", stored) + logger.info("Seerr delta sync completed: stored=%s", stored) break skip += take _sync_state["skip"] = skip _sync_state["message"] = f"Delta synced {stored} requests" - logger.info("Jellyseerr delta sync progress: stored=%s skip=%s", stored, skip) + logger.info("Seerr delta sync progress: stored=%s skip=%s", stored, skip) deduped = prune_duplicate_requests_cache() if deduped: - logger.info("Jellyseerr delta sync removed duplicate rows: %s", deduped) + logger.info("Seerr delta sync removed duplicate rows: %s", deduped) _sync_state.update( { "status": "completed", @@ -1118,7 +1060,7 @@ async def run_daily_requests_full_sync() -> None: runtime = get_runtime_settings() client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) if not client.configured(): - logger.info("Daily full sync skipped: Jellyseerr not configured.") + logger.info("Daily full sync skipped: Seerr not configured.") continue if _sync_task and not _sync_task.done(): logger.info("Daily full sync skipped: another sync is running.") @@ -1144,7 +1086,7 @@ async def start_requests_sync(base_url: Optional[str], api_key: Optional[str]) - if _sync_task and not _sync_task.done(): return dict(_sync_state) if not base_url: - _sync_state.update({"status": "failed", "message": "Jellyseerr not configured"}) + _sync_state.update({"status": "failed", "message": "Seerr not configured"}) return dict(_sync_state) client = JellyseerrClient(base_url, api_key) _sync_state.update( @@ -1163,7 +1105,7 @@ async def start_requests_sync(base_url: Optional[str], api_key: Optional[str]) - try: await _sync_all_requests(client) except Exception as exc: - logger.exception("Jellyseerr sync failed") + logger.exception("Seerr sync failed") _sync_state.update( { "status": "failed", @@ -1181,7 +1123,7 @@ async def start_requests_delta_sync(base_url: Optional[str], api_key: Optional[s if _sync_task and not _sync_task.done(): return dict(_sync_state) if not base_url: - _sync_state.update({"status": "failed", "message": "Jellyseerr not configured"}) + _sync_state.update({"status": "failed", "message": "Seerr not configured"}) return dict(_sync_state) client = JellyseerrClient(base_url, api_key) _sync_state.update( @@ -1200,7 +1142,7 @@ async def start_requests_delta_sync(base_url: Optional[str], api_key: Optional[s try: await _sync_delta_requests(client) except Exception as exc: - logger.exception("Jellyseerr delta sync failed") + logger.exception("Seerr delta sync failed") _sync_state.update( { "status": "failed", @@ -1514,7 +1456,7 @@ async def recent_requests( allow_remote = mode == "always_js" if allow_remote: if not client.configured(): - raise HTTPException(status_code=400, detail="Jellyseerr not configured") + raise HTTPException(status_code=400, detail="Seerr not configured") try: await _ensure_requests_cache(client) except httpx.HTTPStatusError as exc: @@ -1690,7 +1632,7 @@ async def search_requests( runtime = get_runtime_settings() client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) if not client.configured(): - raise HTTPException(status_code=400, detail="Jellyseerr not configured") + raise HTTPException(status_code=400, detail="Seerr not configured") try: response = await client.search(query=query, page=page) diff --git a/backend/app/routers/status.py b/backend/app/routers/status.py index 2c5a941..d2c60a2 100644 --- a/backend/app/routers/status.py +++ b/backend/app/routers/status.py @@ -41,7 +41,7 @@ async def services_status() -> Dict[str, Any]: services = [] services.append( await _check( - "Jellyseerr", + "Seerr", jellyseerr.configured(), lambda: jellyseerr.get_recent_requests(take=1, skip=0), ) @@ -109,8 +109,13 @@ async def test_service(service: str) -> Dict[str, Any]: service_key = service.strip().lower() checks = { + "seerr": ( + "Seerr", + jellyseerr.configured(), + lambda: jellyseerr.get_recent_requests(take=1, skip=0), + ), "jellyseerr": ( - "Jellyseerr", + "Seerr", jellyseerr.configured(), lambda: jellyseerr.get_recent_requests(take=1, skip=0), ), diff --git a/backend/app/services/jellyfin_sync.py b/backend/app/services/jellyfin_sync.py index 8742a4b..be8d931 100644 --- a/backend/app/services/jellyfin_sync.py +++ b/backend/app/services/jellyfin_sync.py @@ -29,7 +29,7 @@ async def sync_jellyfin_users() -> int: if not isinstance(users, list): return 0 save_jellyfin_users_cache(users) - # Jellyfin is the canonical source for local user objects; Jellyseerr IDs are + # Jellyfin is the canonical source for local user objects; Seerr IDs are # matched as enrichment when possible. jellyseerr_users = get_cached_jellyseerr_users() candidate_map = build_jellyseerr_candidate_map(jellyseerr_users or []) diff --git a/backend/app/services/snapshot.py b/backend/app/services/snapshot.py index 0a2f7d5..3bfcb2b 100644 --- a/backend/app/services/snapshot.py +++ b/backend/app/services/snapshot.py @@ -242,14 +242,14 @@ async def build_snapshot(request_id: str) -> Snapshot: allow_remote = mode == "always_js" and jellyseerr.configured() if not jellyseerr.configured() and not cached_request: - timeline.append(TimelineHop(service="Jellyseerr", status="not_configured")) + timeline.append(TimelineHop(service="Seerr", status="not_configured")) timeline.append(TimelineHop(service="Sonarr/Radarr", status="not_configured")) timeline.append(TimelineHop(service="Prowlarr", status="not_configured")) timeline.append(TimelineHop(service="qBittorrent", status="not_configured")) snapshot.timeline = timeline return snapshot if cached_request is None and not allow_remote: - timeline.append(TimelineHop(service="Jellyseerr", status="cache_miss")) + timeline.append(TimelineHop(service="Seerr", status="cache_miss")) snapshot.timeline = timeline snapshot.state = NormalizedState.unknown snapshot.state_reason = "Request not found in cache" @@ -260,20 +260,20 @@ async def build_snapshot(request_id: str) -> Snapshot: try: jelly_request = await jellyseerr.get_request(request_id) logging.getLogger(__name__).debug( - "snapshot jellyseerr fetch: request_id=%s mode=%s", request_id, mode + "snapshot Seerr fetch: request_id=%s mode=%s", request_id, mode ) except Exception as exc: - timeline.append(TimelineHop(service="Jellyseerr", status="error", details={"error": str(exc)})) + timeline.append(TimelineHop(service="Seerr", status="error", details={"error": str(exc)})) snapshot.timeline = timeline snapshot.state = NormalizedState.failed - snapshot.state_reason = "Failed to reach Jellyseerr" + snapshot.state_reason = "Failed to reach Seerr" return snapshot if not jelly_request: - timeline.append(TimelineHop(service="Jellyseerr", status="not_found")) + timeline.append(TimelineHop(service="Seerr", status="not_found")) snapshot.timeline = timeline snapshot.state = NormalizedState.unknown - snapshot.state_reason = "Request not found in Jellyseerr" + snapshot.state_reason = "Request not found in Seerr" return snapshot jelly_status = jelly_request.get("status", "unknown") @@ -338,7 +338,7 @@ async def build_snapshot(request_id: str) -> Snapshot: timeline.append( TimelineHop( - service="Jellyseerr", + service="Seerr", status=jelly_status_label, details={ "requestedBy": jelly_request.get("requestedBy", {}).get("displayName") diff --git a/backend/app/services/user_cache.py b/backend/app/services/user_cache.py index 35e6629..360557e 100644 --- a/backend/app/services/user_cache.py +++ b/backend/app/services/user_cache.py @@ -114,7 +114,7 @@ def save_jellyseerr_users_cache(users: List[Dict[str, Any]]) -> List[Dict[str, A } ) _save_cached_users(JELLYSEERR_CACHE_KEY, JELLYSEERR_CACHE_AT_KEY, normalized) - logger.debug("Cached Jellyseerr users: %s", len(normalized)) + logger.debug("Cached Seerr users: %s", len(normalized)) return normalized diff --git a/backend/requirements.txt b/backend/requirements.txt index ea7c5c7..9414392 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,9 +1,9 @@ -fastapi==0.115.0 -uvicorn==0.30.6 -httpx==0.27.2 -pydantic==2.9.2 -pydantic-settings==2.5.2 -python-jose[cryptography]==3.3.0 +fastapi==0.134.0 +uvicorn==0.41.0 +httpx==0.28.1 +pydantic==2.12.5 +pydantic-settings==2.13.1 +python-jose[cryptography]==3.5.0 passlib==1.7.4 -python-multipart==0.0.9 -Pillow==10.4.0 +python-multipart==0.0.22 +Pillow==12.1.1 diff --git a/frontend/app/admin/SettingsPage.tsx b/frontend/app/admin/SettingsPage.tsx index e13db1a..0777e66 100644 --- a/frontend/app/admin/SettingsPage.tsx +++ b/frontend/app/admin/SettingsPage.tsx @@ -22,7 +22,8 @@ const SECTION_LABELS: Record = { magent: 'Magent', general: 'General', notifications: 'Notifications', - jellyseerr: 'Jellyseerr', + seerr: 'Seerr', + jellyseerr: 'Seerr', jellyfin: 'Jellyfin', artwork: 'Artwork cache', cache: 'Cache Control', @@ -89,7 +90,8 @@ const SECTION_DESCRIPTIONS: Record = { 'Application runtime, binding, reverse proxy, and manual SSL settings for the Magent UI/API.', notifications: 'Notification providers and delivery channel settings used by Magent messaging features.', - jellyseerr: 'Connect the request system where users submit content.', + seerr: 'Connect Seerr where users submit content requests.', + jellyseerr: 'Connect Seerr where users submit content requests.', jellyfin: 'Control Jellyfin login and availability checks.', artwork: 'Cache posters/backdrops and review artwork coverage.', cache: 'Manage saved requests cache and refresh behavior.', @@ -106,6 +108,7 @@ const SETTINGS_SECTION_MAP: Record = { magent: 'magent', general: 'magent', notifications: 'magent', + seerr: 'jellyseerr', jellyseerr: 'jellyseerr', jellyfin: 'jellyfin', artwork: null, @@ -234,6 +237,8 @@ const MAGENT_GROUPS_BY_SECTION: Record> = { } const SETTING_LABEL_OVERRIDES: Record = { + jellyseerr_base_url: 'Seerr base URL', + jellyseerr_api_key: 'Seerr API key', magent_application_url: 'Application URL', magent_application_port: 'Application port', magent_api_url: 'API URL', @@ -278,6 +283,7 @@ const labelFromKey = (key: string) => SETTING_LABEL_OVERRIDES[key] ?? key .replaceAll('_', ' ') + .replace('jellyseerr', 'Seerr') .replace('base url', 'URL') .replace('api key', 'API key') .replace('quality profile id', 'Quality profile ID') @@ -289,7 +295,7 @@ const labelFromKey = (key: string) => .replace('requests full sync time', 'Daily full refresh time (24h)') .replace('requests cleanup time', 'Daily history cleanup time (24h)') .replace('requests cleanup days', 'History retention window (days)') - .replace('requests data source', 'Request source (cache vs Jellyseerr)') + .replace('requests data source', 'Request source (cache vs Seerr)') .replace('jellyfin public url', 'Jellyfin public URL') .replace('jellyfin sync to arr', 'Sync Jellyfin to Sonarr/Radarr') .replace('artwork cache mode', 'Artwork cache mode') @@ -352,6 +358,21 @@ export default function SettingsPage({ section }: SettingsPageProps) { const [liveStreamConnected, setLiveStreamConnected] = useState(false) const requestsSyncRef = useRef(null) const artworkPrefetchRef = useRef(null) + const computeProgressPercent = ( + completedValue: unknown, + totalValue: unknown, + statusValue: unknown + ): number => { + if (String(statusValue).toLowerCase() === 'completed') { + return 100 + } + const completed = Number(completedValue) + const total = Number(totalValue) + if (!Number.isFinite(completed) || !Number.isFinite(total) || total <= 0 || completed <= 0) { + return 0 + } + return Math.max(0, Math.min(100, Math.round((completed / total) * 100))) + } const loadSettings = useCallback(async () => { const baseUrl = getApiBase() @@ -642,7 +663,7 @@ export default function SettingsPage({ section }: SettingsPageProps) { magent_notify_webhook_url: 'Generic webhook endpoint for custom integrations or automation flows.', jellyseerr_base_url: - 'Base URL for your Jellyseerr server (FQDN or IP). Scheme is optional.', + 'Base URL for your Seerr server (FQDN or IP). Scheme is optional.', jellyseerr_api_key: 'API key used to read requests and status.', jellyfin_base_url: 'Jellyfin server URL for logins and lookups (FQDN or IP). Scheme is optional.', @@ -677,7 +698,7 @@ export default function SettingsPage({ section }: SettingsPageProps) { requests_cleanup_time: 'Daily time to trim old request history.', requests_cleanup_days: 'History older than this is removed during cleanup.', requests_data_source: - 'Pick where Magent should read requests from. Cache-only avoids Jellyseerr lookups on reads.', + 'Pick where Magent should read requests from. Cache-only avoids Seerr lookups on reads.', log_level: 'How much detail is written to the activity log.', log_file: 'Where the activity log is stored.', site_build_number: 'Build number shown in the account menu (auto-set from releases).', @@ -805,6 +826,13 @@ export default function SettingsPage({ section }: SettingsPageProps) { const syncRequests = async () => { setRequestsSyncStatus(null) + setRequestsSync({ + status: 'running', + stored: 0, + total: 0, + skip: 0, + message: 'Starting sync', + }) try { const baseUrl = getApiBase() const response = await authFetch(`${baseUrl}/admin/requests/sync`, { @@ -829,6 +857,13 @@ export default function SettingsPage({ section }: SettingsPageProps) { const syncRequestsDelta = async () => { setRequestsSyncStatus(null) + setRequestsSync({ + status: 'running', + stored: 0, + total: 0, + skip: 0, + message: 'Starting delta sync', + }) try { const baseUrl = getApiBase() const response = await authFetch(`${baseUrl}/admin/requests/sync/delta`, { @@ -853,6 +888,12 @@ export default function SettingsPage({ section }: SettingsPageProps) { const prefetchArtwork = async () => { setArtworkPrefetchStatus(null) + setArtworkPrefetch({ + status: 'running', + processed: 0, + total: 0, + message: 'Starting artwork caching', + }) try { const baseUrl = getApiBase() const response = await authFetch(`${baseUrl}/admin/requests/artwork/prefetch`, { @@ -877,6 +918,12 @@ export default function SettingsPage({ section }: SettingsPageProps) { const prefetchArtworkMissing = async () => { setArtworkPrefetchStatus(null) + setArtworkPrefetch({ + status: 'running', + processed: 0, + total: 0, + message: 'Starting missing artwork caching', + }) try { const baseUrl = getApiBase() const response = await authFetch( @@ -1202,7 +1249,7 @@ export default function SettingsPage({ section }: SettingsPageProps) { setMaintenanceBusy(true) if (typeof window !== 'undefined') { const ok = window.confirm( - 'This will perform a nuclear reset: clear cached requests/history, wipe non-admin users, invites, and profiles, then re-sync users and requests from Jellyseerr. Continue?' + 'This will perform a nuclear reset: clear cached requests/history, wipe non-admin users, invites, and profiles, then re-sync users and requests from Seerr. Continue?' ) if (!ok) { setMaintenanceBusy(false) @@ -1264,7 +1311,7 @@ export default function SettingsPage({ section }: SettingsPageProps) { const cacheSourceLabel = formValues.requests_data_source === 'always_js' - ? 'Jellyseerr direct' + ? 'Seerr direct' : formValues.requests_data_source === 'prefer_cache' ? 'Saved requests only' : 'Saved requests only' @@ -1485,22 +1532,16 @@ export default function SettingsPage({ section }: SettingsPageProps) {
@@ -1517,22 +1558,16 @@ export default function SettingsPage({ section }: SettingsPageProps) {
@@ -1860,7 +1895,7 @@ export default function SettingsPage({ section }: SettingsPageProps) { })) } > - + @@ -2005,7 +2040,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {

Maintenance

- Emergency tools. Use with care: flush + resync now performs a nuclear wipe of non-admin users, invite links, profiles, cached requests, and history before re-syncing Jellyseerr users/requests. + Emergency tools. Use with care: flush + resync now performs a nuclear wipe of non-admin users, invite links, profiles, cached requests, and history before re-syncing Seerr users/requests.
{maintenanceStatus &&
{maintenanceStatus}
}
diff --git a/frontend/app/admin/[section]/page.tsx b/frontend/app/admin/[section]/page.tsx index 7483b0b..4401483 100644 --- a/frontend/app/admin/[section]/page.tsx +++ b/frontend/app/admin/[section]/page.tsx @@ -2,6 +2,7 @@ import { notFound } from 'next/navigation' import SettingsPage from '../SettingsPage' const ALLOWED_SECTIONS = new Set([ + 'seerr', 'jellyseerr', 'jellyfin', 'artwork', @@ -20,12 +21,13 @@ const ALLOWED_SECTIONS = new Set([ ]) type PageProps = { - params: { section: string } + params: Promise<{ section: string }> } -export default function AdminSectionPage({ params }: PageProps) { - if (!ALLOWED_SECTIONS.has(params.section)) { +export default async function AdminSectionPage({ params }: PageProps) { + const { section } = await params + if (!ALLOWED_SECTIONS.has(section)) { notFound() } - return + return } diff --git a/frontend/app/admin/system/page.tsx b/frontend/app/admin/system/page.tsx index 5c9d596..9fb17f4 100644 --- a/frontend/app/admin/system/page.tsx +++ b/frontend/app/admin/system/page.tsx @@ -21,7 +21,7 @@ const REQUEST_FLOW: FlowStage[] = [ }, { title: 'Request intake', - input: 'Jellyseerr request ID', + input: 'Seerr request ID', action: 'Magent snapshots request + media metadata', output: 'Unified request state', }, diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 591c592..4fe750f 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -2197,7 +2197,7 @@ button span { pointer-events: none; } -.step-jellyseerr::before { +.step-seerr::before { background: linear-gradient(135deg, rgba(255, 187, 92, 0.35), transparent 60%); } diff --git a/frontend/app/how-it-works/page.tsx b/frontend/app/how-it-works/page.tsx index 0a81982..5d352ae 100644 --- a/frontend/app/how-it-works/page.tsx +++ b/frontend/app/how-it-works/page.tsx @@ -14,7 +14,7 @@ export default function HowItWorksPage() {
-

Jellyseerr

+

Seerr

The request box

This is where you ask for a movie or show. It keeps the request and whether it is @@ -55,7 +55,7 @@ export default function HowItWorksPage() {

The pipeline (request to ready)

  1. - Request created in Jellyseerr. + Request created in Seerr.
  2. Approved and sent to Sonarr/Radarr. @@ -108,7 +108,7 @@ export default function HowItWorksPage() {

    Request actions and when to use them

    -
    +
    1

    Re-add to Arr

    Use when a request is approved but never entered the Arr queue.

    diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 5b86b89..971d593 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -352,7 +352,7 @@ export default function HomePage() {
    {(() => { const order = [ - 'Jellyseerr', + 'Seerr', 'Sonarr', 'Radarr', 'Prowlarr', diff --git a/frontend/app/requests/[id]/page.tsx b/frontend/app/requests/[id]/page.tsx index 9375780..c5e1026 100644 --- a/frontend/app/requests/[id]/page.tsx +++ b/frontend/app/requests/[id]/page.tsx @@ -2,7 +2,7 @@ import Image from 'next/image' import { useEffect, useState } from 'react' -import { useRouter } from 'next/navigation' +import { useParams, useRouter } from 'next/navigation' import { authFetch, clearToken, getApiBase, getEventStreamToken, getToken } from '../../lib/auth' type TimelineHop = { @@ -140,7 +140,7 @@ const friendlyState = (value: string) => { } const friendlyTimelineStatus = (service: string, status: string) => { - if (service === 'Jellyseerr') { + if (service === 'Seerr') { const map: Record = { Pending: 'Waiting for approval', Approved: 'Approved', @@ -195,7 +195,9 @@ const friendlyTimelineStatus = (service: string, status: string) => { return status } -export default function RequestTimelinePage({ params }: { params: { id: string } }) { +export default function RequestTimelinePage() { + const params = useParams<{ id: string | string[] }>() + const requestId = Array.isArray(params?.id) ? params.id[0] : params?.id const router = useRouter() const [snapshot, setSnapshot] = useState(null) const [loading, setLoading] = useState(true) @@ -208,6 +210,9 @@ export default function RequestTimelinePage({ params }: { params: { id: string } const [historyActions, setHistoryActions] = useState([]) useEffect(() => { + if (!requestId) { + return + } const load = async () => { try { if (!getToken()) { @@ -216,9 +221,9 @@ export default function RequestTimelinePage({ params }: { params: { id: string } } const baseUrl = getApiBase() const [snapshotResponse, historyResponse, actionsResponse] = await Promise.all([ - authFetch(`${baseUrl}/requests/${params.id}/snapshot`), - authFetch(`${baseUrl}/requests/${params.id}/history?limit=5`), - authFetch(`${baseUrl}/requests/${params.id}/actions?limit=5`), + authFetch(`${baseUrl}/requests/${requestId}/snapshot`), + authFetch(`${baseUrl}/requests/${requestId}/history?limit=5`), + authFetch(`${baseUrl}/requests/${requestId}/actions?limit=5`), ]) if (snapshotResponse.status === 401) { @@ -252,10 +257,10 @@ export default function RequestTimelinePage({ params }: { params: { id: string } } load() - }, [params.id, router]) + }, [requestId, router]) useEffect(() => { - if (!getToken()) { + if (!getToken() || !requestId) { return } const baseUrl = getApiBase() @@ -267,7 +272,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string } const streamToken = await getEventStreamToken() if (closed) return const streamUrl = `${baseUrl}/events/requests/${encodeURIComponent( - params.id + requestId )}/stream?stream_token=${encodeURIComponent(streamToken)}` source = new EventSource(streamUrl) @@ -278,7 +283,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string } if (!payload || typeof payload !== 'object' || payload.type !== 'request_live') { return } - if (String(payload.request_id ?? '') !== String(params.id)) { + if (String(payload.request_id ?? '') !== String(requestId)) { return } if (payload.snapshot && typeof payload.snapshot === 'object') { @@ -310,7 +315,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string } closed = true source?.close() } - }, [params.id]) + }, [requestId]) if (loading) { return ( @@ -337,7 +342,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string } const arrStageLabel = snapshot.state === 'NEEDS_ADD' ? 'Push to Sonarr/Radarr' : 'Library queue' const pipelineSteps = [ - { key: 'Jellyseerr', label: 'Jellyseerr' }, + { key: 'Seerr', label: 'Seerr' }, { key: 'Sonarr/Radarr', label: arrStageLabel }, { key: 'Prowlarr', label: 'Search' }, { key: 'qBittorrent', label: 'Download' }, diff --git a/frontend/app/ui/AdminSidebar.tsx b/frontend/app/ui/AdminSidebar.tsx index 1547805..40c9608 100644 --- a/frontend/app/ui/AdminSidebar.tsx +++ b/frontend/app/ui/AdminSidebar.tsx @@ -7,7 +7,7 @@ const NAV_GROUPS = [ title: 'Services', items: [ { href: '/admin/general', label: 'General' }, - { href: '/admin/jellyseerr', label: 'Jellyseerr' }, + { href: '/admin/seerr', label: 'Seerr' }, { href: '/admin/jellyfin', label: 'Jellyfin' }, { href: '/admin/sonarr', label: 'Sonarr' }, { href: '/admin/radarr', label: 'Radarr' }, diff --git a/frontend/app/users/[id]/page.tsx b/frontend/app/users/[id]/page.tsx index 2758e78..cd17aaf 100644 --- a/frontend/app/users/[id]/page.tsx +++ b/frontend/app/users/[id]/page.tsx @@ -460,7 +460,7 @@ export default function UserDetailPage() {
    - Jellyseerr ID + Seerr ID {user.jellyseerr_user_id ?? user.id ?? 'Unknown'}
    diff --git a/frontend/app/users/page.tsx b/frontend/app/users/page.tsx index 6cd9357..df2fd2a 100644 --- a/frontend/app/users/page.tsx +++ b/frontend/app/users/page.tsx @@ -155,7 +155,7 @@ export default function UsersPage() { await loadUsers() } catch (err) { console.error(err) - setJellyseerrSyncStatus('Could not sync Jellyseerr users.') + setJellyseerrSyncStatus('Could not sync Seerr users.') } finally { setJellyseerrSyncBusy(false) } @@ -163,7 +163,7 @@ export default function UsersPage() { const resyncJellyseerrUsers = async () => { const confirmed = window.confirm( - 'This will remove all non-admin users and re-import from Jellyseerr. Continue?' + 'This will remove all non-admin users and re-import from Seerr. Continue?' ) if (!confirmed) return setJellyseerrSyncStatus(null) @@ -184,7 +184,7 @@ export default function UsersPage() { await loadUsers() } catch (err) { console.error(err) - setJellyseerrSyncStatus('Could not resync Jellyseerr users.') + setJellyseerrSyncStatus('Could not resync Seerr users.') } finally { setJellyseerrResyncBusy(false) } @@ -322,17 +322,17 @@ export default function UsersPage() {
    - Jellyseerr sync + Seerr sync
    diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..2ff9fdb --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,976 @@ +{ + "name": "magent-frontend", + "version": "2802262051", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "magent-frontend", + "version": "2802262051", + "dependencies": { + "next": "16.1.6", + "react": "19.2.4", + "react-dom": "19.2.4" + }, + "devDependencies": { + "@types/node": "24.11.0", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "typescript": "5.9.3" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", + "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", + "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", + "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", + "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", + "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", + "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", + "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", + "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", + "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "24.11.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.11.0.tgz", + "integrity": "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", + "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.1.6", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.1.6", + "@next/swc-darwin-x64": "16.1.6", + "@next/swc-linux-arm64-gnu": "16.1.6", + "@next/swc-linux-arm64-musl": "16.1.6", + "@next/swc-linux-x64-gnu": "16.1.6", + "@next/swc-linux-x64-musl": "16.1.6", + "@next/swc-win32-arm64-msvc": "16.1.6", + "@next/swc-win32-x64-msvc": "16.1.6", + "sharp": "^0.34.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/frontend/package.json b/frontend/package.json index 3ac8fe9..a01a3fd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "magent-frontend", "private": true, - "version": "2702261314", + "version": "2802262051", "scripts": { "dev": "next dev", "build": "next build", @@ -9,14 +9,14 @@ "lint": "next lint" }, "dependencies": { - "next": "14.2.5", - "react": "18.3.1", - "react-dom": "18.3.1" + "next": "16.1.6", + "react": "19.2.4", + "react-dom": "19.2.4" }, "devDependencies": { - "typescript": "5.5.4", - "@types/node": "20.14.10", - "@types/react": "18.3.3", - "@types/react-dom": "18.3.0" + "typescript": "5.9.3", + "@types/node": "24.11.0", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3" } } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index d15de4f..3a213e9 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -11,9 +11,20 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", - "incremental": true + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], "exclude": ["node_modules"] }