Finalize dev-1.3 upgrades and Seerr updates

This commit is contained in:
2026-02-28 21:41:16 +13:00
parent 05a3d1e3b0
commit c205df4367
27 changed files with 1201 additions and 214 deletions

View File

@@ -1,4 +1,4 @@
FROM node:20-slim AS frontend-builder FROM node:24-slim AS frontend-builder
WORKDIR /frontend WORKDIR /frontend
@@ -6,8 +6,8 @@ ENV NODE_ENV=production \
BACKEND_INTERNAL_URL=http://127.0.0.1:8000 \ BACKEND_INTERNAL_URL=http://127.0.0.1:8000 \
NEXT_PUBLIC_API_BASE=/api NEXT_PUBLIC_API_BASE=/api
COPY frontend/package.json ./ COPY frontend/package.json frontend/package-lock.json ./
RUN npm install RUN npm ci --include=dev
COPY frontend/app ./app COPY frontend/app ./app
COPY frontend/public ./public COPY frontend/public ./public
@@ -17,7 +17,7 @@ COPY frontend/tsconfig.json ./tsconfig.json
RUN npm run build RUN npm run build
FROM python:3.12-slim FROM python:3.14-slim
WORKDIR /app WORKDIR /app
@@ -27,7 +27,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends curl gnupg supervisor \ && 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 install -y --no-install-recommends nodejs \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

View File

@@ -1,10 +1,10 @@
# Magent # 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 ## 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. 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. 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. 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. - Request search by title/year or request ID.
- Recent requests list with posters and status. - 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. - Central status box with clear reason + next steps.
- Safe action buttons (search, resume, re-add, etc.). - Safe action buttons (search, resume, re-add, etc.).
- Admin settings for service URLs, API keys, profiles, and root folders. - 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 ### No recent requests
- Confirm Jellyseerr credentials in Settings. - Confirm Seerr credentials in Settings.
- Run a full sync from Settings -> Requests. - Run a full sync from Settings -> Requests.
### Docker images not updating ### Docker images not updating

View File

@@ -9,12 +9,12 @@ def triage_snapshot(snapshot: Snapshot) -> TriageResult:
if snapshot.state == NormalizedState.requested: if snapshot.state == NormalizedState.requested:
root_cause = "approval" root_cause = "approval"
summary = "The request is waiting for approval in Jellyseerr." summary = "The request is waiting for approval in Seerr."
recommendations.append( recommendations.append(
TriageRecommendation( TriageRecommendation(
action_id="wait_for_approval", action_id="wait_for_approval",
title="Ask an admin to approve the request", 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", risk="low",
) )
) )

View File

@@ -1,4 +1,4 @@
BUILD_NUMBER = "2702261314" 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 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' 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'

View File

@@ -1,4 +1,5 @@
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import httpx
from .base import ApiClient 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]]: async def get_movie(self, tmdb_id: int) -> Optional[Dict[str, Any]]:
return await self.get(f"/api/v1/movie/{tmdb_id}") 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]]: async def delete_user(self, user_id: int) -> Optional[Dict[str, Any]]:
return await self.delete(f"/api/v1/user/{user_id}") 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

View File

@@ -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 # 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. # from admin/user-management views to avoid duplicate accounts in the UI.
def _provider_rank(user: Dict[str, Any]) -> int: def _provider_rank(user: Dict[str, Any]) -> int:
provider = str(user.get("auth_provider") or "local").strip().lower() provider = str(user.get("auth_provider") or "local").strip().lower()

View File

@@ -696,12 +696,13 @@ async def _fetch_all_jellyseerr_users(
return save_jellyseerr_users_cache(users) return save_jellyseerr_users_cache(users)
return users return users
@router.post("/seerr/users/sync")
@router.post("/jellyseerr/users/sync") @router.post("/jellyseerr/users/sync")
async def jellyseerr_users_sync() -> Dict[str, Any]: async def jellyseerr_users_sync() -> Dict[str, Any]:
runtime = get_runtime_settings() runtime = get_runtime_settings()
client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
if not client.configured(): 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) jellyseerr_users = await _fetch_all_jellyseerr_users(client, use_cache=False)
if not jellyseerr_users: if not jellyseerr_users:
return {"status": "ok", "matched": 0, "skipped": 0, "total": 0} 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 return None
@router.post("/seerr/users/resync")
@router.post("/jellyseerr/users/resync") @router.post("/jellyseerr/users/resync")
async def jellyseerr_users_resync() -> Dict[str, Any]: async def jellyseerr_users_resync() -> Dict[str, Any]:
runtime = get_runtime_settings() runtime = get_runtime_settings()
client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
if not client.configured(): 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) jellyseerr_users = await _fetch_all_jellyseerr_users(client, use_cache=False)
if not jellyseerr_users: if not jellyseerr_users:
return {"status": "ok", "imported": 0, "cleared": 0} return {"status": "ok", "imported": 0, "cleared": 0}
@@ -772,7 +774,7 @@ async def requests_sync() -> Dict[str, Any]:
runtime = get_runtime_settings() runtime = get_runtime_settings()
client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
if not client.configured(): 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( state = await requests_router.start_requests_sync(
runtime.jellyseerr_base_url, runtime.jellyseerr_api_key runtime.jellyseerr_base_url, runtime.jellyseerr_api_key
) )
@@ -785,7 +787,7 @@ async def requests_sync_delta() -> Dict[str, Any]:
runtime = get_runtime_settings() runtime = get_runtime_settings()
client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
if not client.configured(): 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( state = await requests_router.start_requests_delta_sync(
runtime.jellyseerr_base_url, runtime.jellyseerr_api_key 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) logger.info("Requests cache titles repaired via settings view: %s", repaired)
hydrated = await _hydrate_cache_titles_from_jellyseerr(limit) hydrated = await _hydrate_cache_titles_from_jellyseerr(limit)
if hydrated: 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) rows = get_request_cache_overview(limit)
return {"rows": rows} return {"rows": rows}
@@ -1073,7 +1075,7 @@ async def user_system_action(username: str, payload: Dict[str, Any]) -> Dict[str
"username": user.get("username"), "username": user.get("username"),
"local": {"status": "pending"}, "local": {"status": "pending"},
"jellyfin": {"status": "skipped", "detail": "Jellyfin not configured"}, "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}, "invites": {"status": "pending", "disabled": 0},
} }

View File

@@ -566,21 +566,21 @@ async def jellyfin_login(request: Request, form_data: OAuth2PasswordRequestForm
} }
@router.post("/seerr/login")
@router.post("/jellyseerr/login") @router.post("/jellyseerr/login")
async def jellyseerr_login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()) -> dict: async def jellyseerr_login(request: Request, form_data: OAuth2PasswordRequestForm = Depends()) -> dict:
_enforce_login_rate_limit(request, form_data.username) _enforce_login_rate_limit(request, form_data.username)
runtime = get_runtime_settings() runtime = get_runtime_settings()
client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
if not client.configured(): if not client.configured():
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Jellyseerr not configured") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Seerr not configured")
payload = {"email": form_data.username, "password": form_data.password}
try: 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: except Exception as exc:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
if not isinstance(response, dict): if not isinstance(response, dict):
_record_login_failure(request, form_data.username) _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) jellyseerr_user_id = _extract_jellyseerr_user_id(response)
ci_matches = get_users_by_username_ci(form_data.username) ci_matches = get_users_by_username_ci(form_data.username)
preferred_match = _pick_preferred_ci_user_match(ci_matches, form_data.username) preferred_match = _pick_preferred_ci_user_match(ci_matches, form_data.username)

View File

@@ -76,7 +76,6 @@ _artwork_prefetch_state: Dict[str, Any] = {
"finished_at": None, "finished_at": None,
} }
_artwork_prefetch_task: Optional[asyncio.Task] = None _artwork_prefetch_task: Optional[asyncio.Task] = None
_media_endpoint_supported: Optional[bool] = None
STATUS_LABELS = { STATUS_LABELS = {
1: "Waiting for approval", 1: "Waiting for approval",
@@ -419,23 +418,6 @@ async def _hydrate_title_from_tmdb(
return None, None 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( async def _hydrate_artwork_from_tmdb(
client: JellyseerrClient, media_type: Optional[str], tmdb_id: Optional[int] client: JellyseerrClient, media_type: Optional[str], tmdb_id: Optional[int]
) -> tuple[Optional[str], Optional[str]]: ) -> tuple[Optional[str], Optional[str]]:
@@ -511,7 +493,7 @@ async def _sync_all_requests(client: JellyseerrClient) -> int:
skip = 0 skip = 0
stored = 0 stored = 0
cache_mode = (get_runtime_settings().artwork_cache_mode or "remote").lower() 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( _sync_state.update(
{ {
"status": "running", "status": "running",
@@ -527,11 +509,11 @@ async def _sync_all_requests(client: JellyseerrClient) -> int:
try: try:
response = await client.get_recent_requests(take=take, skip=skip) response = await client.get_recent_requests(take=take, skip=skip)
except httpx.HTTPError as exc: 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}"}) _sync_state.update({"status": "failed", "message": f"Sync failed: {exc}"})
break break
if not isinstance(response, dict): 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"}) _sync_state.update({"status": "failed", "message": "Invalid response"})
break break
if _sync_state["total"] is None: if _sync_state["total"] is None:
@@ -546,7 +528,7 @@ async def _sync_all_requests(client: JellyseerrClient) -> int:
_sync_state["total"] = total _sync_state["total"] = total
items = response.get("results") or [] items = response.get("results") or []
if not isinstance(items, list) or not items: 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 break
for item in items: for item in items:
if not isinstance(item, dict): 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) cached = get_request_cache_by_id(request_id)
if cached and cached.get("title"): if cached and cached.get("title"):
cached_title = cached.get("title") cached_title = cached.get("title")
if not payload.get("title") or not payload.get("media_id"): needs_details = (
logger.debug("Jellyseerr sync hydrate request_id=%s", request_id) 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) details = await _get_request_details(client, request_id)
if isinstance(details, dict): if isinstance(details, dict):
payload = _parse_request_payload(details) payload = _parse_request_payload(details)
item = 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) poster_path, backdrop_path = _extract_artwork_paths(item)
if cache_mode == "cache" and not (poster_path or backdrop_path): if cache_mode == "cache" and not (poster_path or backdrop_path):
details = await _get_request_details(client, request_id) details = await _get_request_details(client, request_id)
@@ -629,12 +591,12 @@ async def _sync_all_requests(client: JellyseerrClient) -> int:
stored += 1 stored += 1
_sync_state["stored"] = stored _sync_state["stored"] = stored
if len(items) < take: if len(items) < take:
logger.info("Jellyseerr sync completed: stored=%s", stored) logger.info("Seerr sync completed: stored=%s", stored)
break break
skip += take skip += take
_sync_state["skip"] = skip _sync_state["skip"] = skip
_sync_state["message"] = f"Synced {stored} requests" _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( _sync_state.update(
{ {
"status": "completed", "status": "completed",
@@ -659,7 +621,7 @@ async def _sync_delta_requests(client: JellyseerrClient) -> int:
stored = 0 stored = 0
unchanged_pages = 0 unchanged_pages = 0
cache_mode = (get_runtime_settings().artwork_cache_mode or "remote").lower() 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( _sync_state.update(
{ {
"status": "running", "status": "running",
@@ -675,16 +637,16 @@ async def _sync_delta_requests(client: JellyseerrClient) -> int:
try: try:
response = await client.get_recent_requests(take=take, skip=skip) response = await client.get_recent_requests(take=take, skip=skip)
except httpx.HTTPError as exc: 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}"}) _sync_state.update({"status": "failed", "message": f"Delta sync failed: {exc}"})
break break
if not isinstance(response, dict): 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"}) _sync_state.update({"status": "failed", "message": "Invalid response"})
break break
items = response.get("results") or [] items = response.get("results") or []
if not isinstance(items, list) or not items: 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 break
page_changed = False page_changed = False
for item in items: 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 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"): if cached and incoming_updated and cached.get("updated_at") == incoming_updated and cached.get("title"):
continue 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) details = await _get_request_details(client, request_id)
if isinstance(details, dict): if isinstance(details, dict):
payload = _parse_request_payload(details) payload = _parse_request_payload(details)
item = 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) poster_path, backdrop_path = _extract_artwork_paths(item)
if cache_mode == "cache" and not (poster_path or backdrop_path): if cache_mode == "cache" and not (poster_path or backdrop_path):
details = await _get_request_details(client, request_id) details = await _get_request_details(client, request_id)
@@ -772,15 +714,15 @@ async def _sync_delta_requests(client: JellyseerrClient) -> int:
else: else:
unchanged_pages = 0 unchanged_pages = 0
if len(items) < take or unchanged_pages >= 2: 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 break
skip += take skip += take
_sync_state["skip"] = skip _sync_state["skip"] = skip
_sync_state["message"] = f"Delta synced {stored} requests" _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() deduped = prune_duplicate_requests_cache()
if deduped: 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( _sync_state.update(
{ {
"status": "completed", "status": "completed",
@@ -1118,7 +1060,7 @@ async def run_daily_requests_full_sync() -> None:
runtime = get_runtime_settings() runtime = get_runtime_settings()
client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
if not client.configured(): if not client.configured():
logger.info("Daily full sync skipped: Jellyseerr not configured.") logger.info("Daily full sync skipped: Seerr not configured.")
continue continue
if _sync_task and not _sync_task.done(): if _sync_task and not _sync_task.done():
logger.info("Daily full sync skipped: another sync is running.") 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(): if _sync_task and not _sync_task.done():
return dict(_sync_state) return dict(_sync_state)
if not base_url: 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) return dict(_sync_state)
client = JellyseerrClient(base_url, api_key) client = JellyseerrClient(base_url, api_key)
_sync_state.update( _sync_state.update(
@@ -1163,7 +1105,7 @@ async def start_requests_sync(base_url: Optional[str], api_key: Optional[str]) -
try: try:
await _sync_all_requests(client) await _sync_all_requests(client)
except Exception as exc: except Exception as exc:
logger.exception("Jellyseerr sync failed") logger.exception("Seerr sync failed")
_sync_state.update( _sync_state.update(
{ {
"status": "failed", "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(): if _sync_task and not _sync_task.done():
return dict(_sync_state) return dict(_sync_state)
if not base_url: 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) return dict(_sync_state)
client = JellyseerrClient(base_url, api_key) client = JellyseerrClient(base_url, api_key)
_sync_state.update( _sync_state.update(
@@ -1200,7 +1142,7 @@ async def start_requests_delta_sync(base_url: Optional[str], api_key: Optional[s
try: try:
await _sync_delta_requests(client) await _sync_delta_requests(client)
except Exception as exc: except Exception as exc:
logger.exception("Jellyseerr delta sync failed") logger.exception("Seerr delta sync failed")
_sync_state.update( _sync_state.update(
{ {
"status": "failed", "status": "failed",
@@ -1514,7 +1456,7 @@ async def recent_requests(
allow_remote = mode == "always_js" allow_remote = mode == "always_js"
if allow_remote: if allow_remote:
if not client.configured(): if not client.configured():
raise HTTPException(status_code=400, detail="Jellyseerr not configured") raise HTTPException(status_code=400, detail="Seerr not configured")
try: try:
await _ensure_requests_cache(client) await _ensure_requests_cache(client)
except httpx.HTTPStatusError as exc: except httpx.HTTPStatusError as exc:
@@ -1690,7 +1632,7 @@ async def search_requests(
runtime = get_runtime_settings() runtime = get_runtime_settings()
client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key) client = JellyseerrClient(runtime.jellyseerr_base_url, runtime.jellyseerr_api_key)
if not client.configured(): if not client.configured():
raise HTTPException(status_code=400, detail="Jellyseerr not configured") raise HTTPException(status_code=400, detail="Seerr not configured")
try: try:
response = await client.search(query=query, page=page) response = await client.search(query=query, page=page)

View File

@@ -41,7 +41,7 @@ async def services_status() -> Dict[str, Any]:
services = [] services = []
services.append( services.append(
await _check( await _check(
"Jellyseerr", "Seerr",
jellyseerr.configured(), jellyseerr.configured(),
lambda: jellyseerr.get_recent_requests(take=1, skip=0), 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() service_key = service.strip().lower()
checks = { checks = {
"seerr": (
"Seerr",
jellyseerr.configured(),
lambda: jellyseerr.get_recent_requests(take=1, skip=0),
),
"jellyseerr": ( "jellyseerr": (
"Jellyseerr", "Seerr",
jellyseerr.configured(), jellyseerr.configured(),
lambda: jellyseerr.get_recent_requests(take=1, skip=0), lambda: jellyseerr.get_recent_requests(take=1, skip=0),
), ),

View File

@@ -29,7 +29,7 @@ async def sync_jellyfin_users() -> int:
if not isinstance(users, list): if not isinstance(users, list):
return 0 return 0
save_jellyfin_users_cache(users) 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. # matched as enrichment when possible.
jellyseerr_users = get_cached_jellyseerr_users() jellyseerr_users = get_cached_jellyseerr_users()
candidate_map = build_jellyseerr_candidate_map(jellyseerr_users or []) candidate_map = build_jellyseerr_candidate_map(jellyseerr_users or [])

View File

@@ -242,14 +242,14 @@ async def build_snapshot(request_id: str) -> Snapshot:
allow_remote = mode == "always_js" and jellyseerr.configured() allow_remote = mode == "always_js" and jellyseerr.configured()
if not jellyseerr.configured() and not cached_request: 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="Sonarr/Radarr", status="not_configured"))
timeline.append(TimelineHop(service="Prowlarr", status="not_configured")) timeline.append(TimelineHop(service="Prowlarr", status="not_configured"))
timeline.append(TimelineHop(service="qBittorrent", status="not_configured")) timeline.append(TimelineHop(service="qBittorrent", status="not_configured"))
snapshot.timeline = timeline snapshot.timeline = timeline
return snapshot return snapshot
if cached_request is None and not allow_remote: 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.timeline = timeline
snapshot.state = NormalizedState.unknown snapshot.state = NormalizedState.unknown
snapshot.state_reason = "Request not found in cache" snapshot.state_reason = "Request not found in cache"
@@ -260,20 +260,20 @@ async def build_snapshot(request_id: str) -> Snapshot:
try: try:
jelly_request = await jellyseerr.get_request(request_id) jelly_request = await jellyseerr.get_request(request_id)
logging.getLogger(__name__).debug( 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: 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.timeline = timeline
snapshot.state = NormalizedState.failed snapshot.state = NormalizedState.failed
snapshot.state_reason = "Failed to reach Jellyseerr" snapshot.state_reason = "Failed to reach Seerr"
return snapshot return snapshot
if not jelly_request: if not jelly_request:
timeline.append(TimelineHop(service="Jellyseerr", status="not_found")) timeline.append(TimelineHop(service="Seerr", status="not_found"))
snapshot.timeline = timeline snapshot.timeline = timeline
snapshot.state = NormalizedState.unknown snapshot.state = NormalizedState.unknown
snapshot.state_reason = "Request not found in Jellyseerr" snapshot.state_reason = "Request not found in Seerr"
return snapshot return snapshot
jelly_status = jelly_request.get("status", "unknown") jelly_status = jelly_request.get("status", "unknown")
@@ -338,7 +338,7 @@ async def build_snapshot(request_id: str) -> Snapshot:
timeline.append( timeline.append(
TimelineHop( TimelineHop(
service="Jellyseerr", service="Seerr",
status=jelly_status_label, status=jelly_status_label,
details={ details={
"requestedBy": jelly_request.get("requestedBy", {}).get("displayName") "requestedBy": jelly_request.get("requestedBy", {}).get("displayName")

View File

@@ -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) _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 return normalized

View File

@@ -1,9 +1,9 @@
fastapi==0.115.0 fastapi==0.134.0
uvicorn==0.30.6 uvicorn==0.41.0
httpx==0.27.2 httpx==0.28.1
pydantic==2.9.2 pydantic==2.12.5
pydantic-settings==2.5.2 pydantic-settings==2.13.1
python-jose[cryptography]==3.3.0 python-jose[cryptography]==3.5.0
passlib==1.7.4 passlib==1.7.4
python-multipart==0.0.9 python-multipart==0.0.22
Pillow==10.4.0 Pillow==12.1.1

View File

@@ -22,7 +22,8 @@ const SECTION_LABELS: Record<string, string> = {
magent: 'Magent', magent: 'Magent',
general: 'General', general: 'General',
notifications: 'Notifications', notifications: 'Notifications',
jellyseerr: 'Jellyseerr', seerr: 'Seerr',
jellyseerr: 'Seerr',
jellyfin: 'Jellyfin', jellyfin: 'Jellyfin',
artwork: 'Artwork cache', artwork: 'Artwork cache',
cache: 'Cache Control', cache: 'Cache Control',
@@ -89,7 +90,8 @@ const SECTION_DESCRIPTIONS: Record<string, string> = {
'Application runtime, binding, reverse proxy, and manual SSL settings for the Magent UI/API.', 'Application runtime, binding, reverse proxy, and manual SSL settings for the Magent UI/API.',
notifications: notifications:
'Notification providers and delivery channel settings used by Magent messaging features.', '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.', jellyfin: 'Control Jellyfin login and availability checks.',
artwork: 'Cache posters/backdrops and review artwork coverage.', artwork: 'Cache posters/backdrops and review artwork coverage.',
cache: 'Manage saved requests cache and refresh behavior.', cache: 'Manage saved requests cache and refresh behavior.',
@@ -106,6 +108,7 @@ const SETTINGS_SECTION_MAP: Record<string, string | null> = {
magent: 'magent', magent: 'magent',
general: 'magent', general: 'magent',
notifications: 'magent', notifications: 'magent',
seerr: 'jellyseerr',
jellyseerr: 'jellyseerr', jellyseerr: 'jellyseerr',
jellyfin: 'jellyfin', jellyfin: 'jellyfin',
artwork: null, artwork: null,
@@ -234,6 +237,8 @@ const MAGENT_GROUPS_BY_SECTION: Record<string, Set<string>> = {
} }
const SETTING_LABEL_OVERRIDES: Record<string, string> = { const SETTING_LABEL_OVERRIDES: Record<string, string> = {
jellyseerr_base_url: 'Seerr base URL',
jellyseerr_api_key: 'Seerr API key',
magent_application_url: 'Application URL', magent_application_url: 'Application URL',
magent_application_port: 'Application port', magent_application_port: 'Application port',
magent_api_url: 'API URL', magent_api_url: 'API URL',
@@ -278,6 +283,7 @@ const labelFromKey = (key: string) =>
SETTING_LABEL_OVERRIDES[key] ?? SETTING_LABEL_OVERRIDES[key] ??
key key
.replaceAll('_', ' ') .replaceAll('_', ' ')
.replace('jellyseerr', 'Seerr')
.replace('base url', 'URL') .replace('base url', 'URL')
.replace('api key', 'API key') .replace('api key', 'API key')
.replace('quality profile id', 'Quality profile ID') .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 full sync time', 'Daily full refresh time (24h)')
.replace('requests cleanup time', 'Daily history cleanup time (24h)') .replace('requests cleanup time', 'Daily history cleanup time (24h)')
.replace('requests cleanup days', 'History retention window (days)') .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 public url', 'Jellyfin public URL')
.replace('jellyfin sync to arr', 'Sync Jellyfin to Sonarr/Radarr') .replace('jellyfin sync to arr', 'Sync Jellyfin to Sonarr/Radarr')
.replace('artwork cache mode', 'Artwork cache mode') .replace('artwork cache mode', 'Artwork cache mode')
@@ -352,6 +358,21 @@ export default function SettingsPage({ section }: SettingsPageProps) {
const [liveStreamConnected, setLiveStreamConnected] = useState(false) const [liveStreamConnected, setLiveStreamConnected] = useState(false)
const requestsSyncRef = useRef<any | null>(null) const requestsSyncRef = useRef<any | null>(null)
const artworkPrefetchRef = useRef<any | null>(null) const artworkPrefetchRef = useRef<any | null>(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 loadSettings = useCallback(async () => {
const baseUrl = getApiBase() const baseUrl = getApiBase()
@@ -642,7 +663,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
magent_notify_webhook_url: magent_notify_webhook_url:
'Generic webhook endpoint for custom integrations or automation flows.', 'Generic webhook endpoint for custom integrations or automation flows.',
jellyseerr_base_url: 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.', jellyseerr_api_key: 'API key used to read requests and status.',
jellyfin_base_url: jellyfin_base_url:
'Jellyfin server URL for logins and lookups (FQDN or IP). Scheme is optional.', '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_time: 'Daily time to trim old request history.',
requests_cleanup_days: 'History older than this is removed during cleanup.', requests_cleanup_days: 'History older than this is removed during cleanup.',
requests_data_source: 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_level: 'How much detail is written to the activity log.',
log_file: 'Where the activity log is stored.', log_file: 'Where the activity log is stored.',
site_build_number: 'Build number shown in the account menu (auto-set from releases).', 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 () => { const syncRequests = async () => {
setRequestsSyncStatus(null) setRequestsSyncStatus(null)
setRequestsSync({
status: 'running',
stored: 0,
total: 0,
skip: 0,
message: 'Starting sync',
})
try { try {
const baseUrl = getApiBase() const baseUrl = getApiBase()
const response = await authFetch(`${baseUrl}/admin/requests/sync`, { const response = await authFetch(`${baseUrl}/admin/requests/sync`, {
@@ -829,6 +857,13 @@ export default function SettingsPage({ section }: SettingsPageProps) {
const syncRequestsDelta = async () => { const syncRequestsDelta = async () => {
setRequestsSyncStatus(null) setRequestsSyncStatus(null)
setRequestsSync({
status: 'running',
stored: 0,
total: 0,
skip: 0,
message: 'Starting delta sync',
})
try { try {
const baseUrl = getApiBase() const baseUrl = getApiBase()
const response = await authFetch(`${baseUrl}/admin/requests/sync/delta`, { const response = await authFetch(`${baseUrl}/admin/requests/sync/delta`, {
@@ -853,6 +888,12 @@ export default function SettingsPage({ section }: SettingsPageProps) {
const prefetchArtwork = async () => { const prefetchArtwork = async () => {
setArtworkPrefetchStatus(null) setArtworkPrefetchStatus(null)
setArtworkPrefetch({
status: 'running',
processed: 0,
total: 0,
message: 'Starting artwork caching',
})
try { try {
const baseUrl = getApiBase() const baseUrl = getApiBase()
const response = await authFetch(`${baseUrl}/admin/requests/artwork/prefetch`, { const response = await authFetch(`${baseUrl}/admin/requests/artwork/prefetch`, {
@@ -877,6 +918,12 @@ export default function SettingsPage({ section }: SettingsPageProps) {
const prefetchArtworkMissing = async () => { const prefetchArtworkMissing = async () => {
setArtworkPrefetchStatus(null) setArtworkPrefetchStatus(null)
setArtworkPrefetch({
status: 'running',
processed: 0,
total: 0,
message: 'Starting missing artwork caching',
})
try { try {
const baseUrl = getApiBase() const baseUrl = getApiBase()
const response = await authFetch( const response = await authFetch(
@@ -1202,7 +1249,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
setMaintenanceBusy(true) setMaintenanceBusy(true)
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const ok = window.confirm( 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) { if (!ok) {
setMaintenanceBusy(false) setMaintenanceBusy(false)
@@ -1264,7 +1311,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
const cacheSourceLabel = const cacheSourceLabel =
formValues.requests_data_source === 'always_js' formValues.requests_data_source === 'always_js'
? 'Jellyseerr direct' ? 'Seerr direct'
: formValues.requests_data_source === 'prefer_cache' : formValues.requests_data_source === 'prefer_cache'
? 'Saved requests only' ? 'Saved requests only'
: 'Saved requests only' : 'Saved requests only'
@@ -1485,22 +1532,16 @@ export default function SettingsPage({ section }: SettingsPageProps) {
</span> </span>
</div> </div>
<div <div
className={`progress ${artworkPrefetch.total ? '' : 'progress-indeterminate'} ${ className={`progress ${artworkPrefetch.status === 'completed' ? 'progress-complete' : ''}`}
artworkPrefetch.status === 'completed' ? 'progress-complete' : ''
}`}
> >
<div <div
className="progress-fill" className="progress-fill"
style={{ style={{
width: width: `${computeProgressPercent(
artworkPrefetch.status === 'completed' artworkPrefetch.processed,
? '100%' artworkPrefetch.total,
: artworkPrefetch.total artworkPrefetch.status
? `${Math.min( )}%`,
100,
Math.round((artworkPrefetch.processed / artworkPrefetch.total) * 100)
)}%`
: '30%',
}} }}
/> />
</div> </div>
@@ -1517,22 +1558,16 @@ export default function SettingsPage({ section }: SettingsPageProps) {
</span> </span>
</div> </div>
<div <div
className={`progress ${requestsSync.total ? '' : 'progress-indeterminate'} ${ className={`progress ${requestsSync.status === 'completed' ? 'progress-complete' : ''}`}
requestsSync.status === 'completed' ? 'progress-complete' : ''
}`}
> >
<div <div
className="progress-fill" className="progress-fill"
style={{ style={{
width: width: `${computeProgressPercent(
requestsSync.status === 'completed' requestsSync.stored,
? '100%' requestsSync.total,
: requestsSync.total requestsSync.status
? `${Math.min( )}%`,
100,
Math.round((requestsSync.stored / requestsSync.total) * 100)
)}%`
: '30%',
}} }}
/> />
</div> </div>
@@ -1860,7 +1895,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
})) }))
} }
> >
<option value="always_js">Always use Jellyseerr (slower)</option> <option value="always_js">Always use Seerr (slower)</option>
<option value="prefer_cache"> <option value="prefer_cache">
Use saved requests only (fastest) Use saved requests only (fastest)
</option> </option>
@@ -2005,7 +2040,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
<h2>Maintenance</h2> <h2>Maintenance</h2>
</div> </div>
<div className="status-banner"> <div className="status-banner">
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.
</div> </div>
{maintenanceStatus && <div className="status-banner">{maintenanceStatus}</div>} {maintenanceStatus && <div className="status-banner">{maintenanceStatus}</div>}
<div className="maintenance-grid"> <div className="maintenance-grid">

View File

@@ -2,6 +2,7 @@ import { notFound } from 'next/navigation'
import SettingsPage from '../SettingsPage' import SettingsPage from '../SettingsPage'
const ALLOWED_SECTIONS = new Set([ const ALLOWED_SECTIONS = new Set([
'seerr',
'jellyseerr', 'jellyseerr',
'jellyfin', 'jellyfin',
'artwork', 'artwork',
@@ -20,12 +21,13 @@ const ALLOWED_SECTIONS = new Set([
]) ])
type PageProps = { type PageProps = {
params: { section: string } params: Promise<{ section: string }>
} }
export default function AdminSectionPage({ params }: PageProps) { export default async function AdminSectionPage({ params }: PageProps) {
if (!ALLOWED_SECTIONS.has(params.section)) { const { section } = await params
if (!ALLOWED_SECTIONS.has(section)) {
notFound() notFound()
} }
return <SettingsPage section={params.section} /> return <SettingsPage section={section} />
} }

View File

@@ -21,7 +21,7 @@ const REQUEST_FLOW: FlowStage[] = [
}, },
{ {
title: 'Request intake', title: 'Request intake',
input: 'Jellyseerr request ID', input: 'Seerr request ID',
action: 'Magent snapshots request + media metadata', action: 'Magent snapshots request + media metadata',
output: 'Unified request state', output: 'Unified request state',
}, },

View File

@@ -2197,7 +2197,7 @@ button span {
pointer-events: none; pointer-events: none;
} }
.step-jellyseerr::before { .step-seerr::before {
background: linear-gradient(135deg, rgba(255, 187, 92, 0.35), transparent 60%); background: linear-gradient(135deg, rgba(255, 187, 92, 0.35), transparent 60%);
} }

View File

@@ -14,7 +14,7 @@ export default function HowItWorksPage() {
<section className="how-grid"> <section className="how-grid">
<article className="how-card"> <article className="how-card">
<h2>Jellyseerr</h2> <h2>Seerr</h2>
<p className="how-title">The request box</p> <p className="how-title">The request box</p>
<p> <p>
This is where you ask for a movie or show. It keeps the request and whether it is 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() {
<h2>The pipeline (request to ready)</h2> <h2>The pipeline (request to ready)</h2>
<ol className="how-steps"> <ol className="how-steps">
<li> <li>
<strong>Request created</strong> in Jellyseerr. <strong>Request created</strong> in Seerr.
</li> </li>
<li> <li>
<strong>Approved</strong> and sent to Sonarr/Radarr. <strong>Approved</strong> and sent to Sonarr/Radarr.
@@ -108,7 +108,7 @@ export default function HowItWorksPage() {
<section className="how-flow"> <section className="how-flow">
<h2>Request actions and when to use them</h2> <h2>Request actions and when to use them</h2>
<div className="how-step-grid"> <div className="how-step-grid">
<article className="how-step-card step-jellyseerr"> <article className="how-step-card step-seerr">
<div className="step-badge">1</div> <div className="step-badge">1</div>
<h3>Re-add to Arr</h3> <h3>Re-add to Arr</h3>
<p className="step-note">Use when a request is approved but never entered the Arr queue.</p> <p className="step-note">Use when a request is approved but never entered the Arr queue.</p>

View File

@@ -352,7 +352,7 @@ export default function HomePage() {
<div className="system-list"> <div className="system-list">
{(() => { {(() => {
const order = [ const order = [
'Jellyseerr', 'Seerr',
'Sonarr', 'Sonarr',
'Radarr', 'Radarr',
'Prowlarr', 'Prowlarr',

View File

@@ -2,7 +2,7 @@
import Image from 'next/image' import Image from 'next/image'
import { useEffect, useState } from 'react' 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' import { authFetch, clearToken, getApiBase, getEventStreamToken, getToken } from '../../lib/auth'
type TimelineHop = { type TimelineHop = {
@@ -140,7 +140,7 @@ const friendlyState = (value: string) => {
} }
const friendlyTimelineStatus = (service: string, status: string) => { const friendlyTimelineStatus = (service: string, status: string) => {
if (service === 'Jellyseerr') { if (service === 'Seerr') {
const map: Record<string, string> = { const map: Record<string, string> = {
Pending: 'Waiting for approval', Pending: 'Waiting for approval',
Approved: 'Approved', Approved: 'Approved',
@@ -195,7 +195,9 @@ const friendlyTimelineStatus = (service: string, status: string) => {
return status 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 router = useRouter()
const [snapshot, setSnapshot] = useState<Snapshot | null>(null) const [snapshot, setSnapshot] = useState<Snapshot | null>(null)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@@ -208,6 +210,9 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
const [historyActions, setHistoryActions] = useState<ActionHistory[]>([]) const [historyActions, setHistoryActions] = useState<ActionHistory[]>([])
useEffect(() => { useEffect(() => {
if (!requestId) {
return
}
const load = async () => { const load = async () => {
try { try {
if (!getToken()) { if (!getToken()) {
@@ -216,9 +221,9 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
} }
const baseUrl = getApiBase() const baseUrl = getApiBase()
const [snapshotResponse, historyResponse, actionsResponse] = await Promise.all([ const [snapshotResponse, historyResponse, actionsResponse] = await Promise.all([
authFetch(`${baseUrl}/requests/${params.id}/snapshot`), authFetch(`${baseUrl}/requests/${requestId}/snapshot`),
authFetch(`${baseUrl}/requests/${params.id}/history?limit=5`), authFetch(`${baseUrl}/requests/${requestId}/history?limit=5`),
authFetch(`${baseUrl}/requests/${params.id}/actions?limit=5`), authFetch(`${baseUrl}/requests/${requestId}/actions?limit=5`),
]) ])
if (snapshotResponse.status === 401) { if (snapshotResponse.status === 401) {
@@ -252,10 +257,10 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
} }
load() load()
}, [params.id, router]) }, [requestId, router])
useEffect(() => { useEffect(() => {
if (!getToken()) { if (!getToken() || !requestId) {
return return
} }
const baseUrl = getApiBase() const baseUrl = getApiBase()
@@ -267,7 +272,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
const streamToken = await getEventStreamToken() const streamToken = await getEventStreamToken()
if (closed) return if (closed) return
const streamUrl = `${baseUrl}/events/requests/${encodeURIComponent( const streamUrl = `${baseUrl}/events/requests/${encodeURIComponent(
params.id requestId
)}/stream?stream_token=${encodeURIComponent(streamToken)}` )}/stream?stream_token=${encodeURIComponent(streamToken)}`
source = new EventSource(streamUrl) 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') { if (!payload || typeof payload !== 'object' || payload.type !== 'request_live') {
return return
} }
if (String(payload.request_id ?? '') !== String(params.id)) { if (String(payload.request_id ?? '') !== String(requestId)) {
return return
} }
if (payload.snapshot && typeof payload.snapshot === 'object') { if (payload.snapshot && typeof payload.snapshot === 'object') {
@@ -310,7 +315,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
closed = true closed = true
source?.close() source?.close()
} }
}, [params.id]) }, [requestId])
if (loading) { if (loading) {
return ( return (
@@ -337,7 +342,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
const arrStageLabel = const arrStageLabel =
snapshot.state === 'NEEDS_ADD' ? 'Push to Sonarr/Radarr' : 'Library queue' snapshot.state === 'NEEDS_ADD' ? 'Push to Sonarr/Radarr' : 'Library queue'
const pipelineSteps = [ const pipelineSteps = [
{ key: 'Jellyseerr', label: 'Jellyseerr' }, { key: 'Seerr', label: 'Seerr' },
{ key: 'Sonarr/Radarr', label: arrStageLabel }, { key: 'Sonarr/Radarr', label: arrStageLabel },
{ key: 'Prowlarr', label: 'Search' }, { key: 'Prowlarr', label: 'Search' },
{ key: 'qBittorrent', label: 'Download' }, { key: 'qBittorrent', label: 'Download' },

View File

@@ -7,7 +7,7 @@ const NAV_GROUPS = [
title: 'Services', title: 'Services',
items: [ items: [
{ href: '/admin/general', label: 'General' }, { href: '/admin/general', label: 'General' },
{ href: '/admin/jellyseerr', label: 'Jellyseerr' }, { href: '/admin/seerr', label: 'Seerr' },
{ href: '/admin/jellyfin', label: 'Jellyfin' }, { href: '/admin/jellyfin', label: 'Jellyfin' },
{ href: '/admin/sonarr', label: 'Sonarr' }, { href: '/admin/sonarr', label: 'Sonarr' },
{ href: '/admin/radarr', label: 'Radarr' }, { href: '/admin/radarr', label: 'Radarr' },

View File

@@ -460,7 +460,7 @@ export default function UserDetailPage() {
</div> </div>
<div className="user-detail-meta-grid"> <div className="user-detail-meta-grid">
<div className="user-detail-meta-item"> <div className="user-detail-meta-item">
<span className="label">Jellyseerr ID</span> <span className="label">Seerr ID</span>
<strong>{user.jellyseerr_user_id ?? user.id ?? 'Unknown'}</strong> <strong>{user.jellyseerr_user_id ?? user.id ?? 'Unknown'}</strong>
</div> </div>
<div className="user-detail-meta-item"> <div className="user-detail-meta-item">

View File

@@ -155,7 +155,7 @@ export default function UsersPage() {
await loadUsers() await loadUsers()
} catch (err) { } catch (err) {
console.error(err) console.error(err)
setJellyseerrSyncStatus('Could not sync Jellyseerr users.') setJellyseerrSyncStatus('Could not sync Seerr users.')
} finally { } finally {
setJellyseerrSyncBusy(false) setJellyseerrSyncBusy(false)
} }
@@ -163,7 +163,7 @@ export default function UsersPage() {
const resyncJellyseerrUsers = async () => { const resyncJellyseerrUsers = async () => {
const confirmed = window.confirm( 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 if (!confirmed) return
setJellyseerrSyncStatus(null) setJellyseerrSyncStatus(null)
@@ -184,7 +184,7 @@ export default function UsersPage() {
await loadUsers() await loadUsers()
} catch (err) { } catch (err) {
console.error(err) console.error(err)
setJellyseerrSyncStatus('Could not resync Jellyseerr users.') setJellyseerrSyncStatus('Could not resync Seerr users.')
} finally { } finally {
setJellyseerrResyncBusy(false) setJellyseerrResyncBusy(false)
} }
@@ -322,17 +322,17 @@ export default function UsersPage() {
</div> </div>
</div> </div>
<div className="users-page-toolbar-group"> <div className="users-page-toolbar-group">
<span className="users-page-toolbar-label">Jellyseerr sync</span> <span className="users-page-toolbar-label">Seerr sync</span>
<div className="users-page-toolbar-actions"> <div className="users-page-toolbar-actions">
<button type="button" onClick={syncJellyseerrUsers} disabled={jellyseerrSyncBusy}> <button type="button" onClick={syncJellyseerrUsers} disabled={jellyseerrSyncBusy}>
{jellyseerrSyncBusy ? 'Syncing Jellyseerr users...' : 'Sync Jellyseerr users'} {jellyseerrSyncBusy ? 'Syncing Seerr users...' : 'Sync Seerr users'}
</button> </button>
<button <button
type="button" type="button"
onClick={resyncJellyseerrUsers} onClick={resyncJellyseerrUsers}
disabled={jellyseerrResyncBusy} disabled={jellyseerrResyncBusy}
> >
{jellyseerrResyncBusy ? 'Resyncing Jellyseerr users...' : 'Resync Jellyseerr users'} {jellyseerrResyncBusy ? 'Resyncing Seerr users...' : 'Resync Seerr users'}
</button> </button>
</div> </div>
</div> </div>

976
frontend/package-lock.json generated Normal file
View File

@@ -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"
}
}
}

View File

@@ -1,7 +1,7 @@
{ {
"name": "magent-frontend", "name": "magent-frontend",
"private": true, "private": true,
"version": "2702261314", "version": "2802262051",
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
@@ -9,14 +9,14 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"next": "14.2.5", "next": "16.1.6",
"react": "18.3.1", "react": "19.2.4",
"react-dom": "18.3.1" "react-dom": "19.2.4"
}, },
"devDependencies": { "devDependencies": {
"typescript": "5.5.4", "typescript": "5.9.3",
"@types/node": "20.14.10", "@types/node": "24.11.0",
"@types/react": "18.3.3", "@types/react": "19.2.14",
"@types/react-dom": "18.3.0" "@types/react-dom": "19.2.3"
} }
} }

View File

@@ -11,9 +11,20 @@
"moduleResolution": "bundler", "moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "react-jsx",
"incremental": true "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"] "exclude": ["node_modules"]
} }