From 96333c0d850a616e38c0697e0ff9ff0772f9daea Mon Sep 17 00:00:00 2001 From: Rephl3x Date: Tue, 3 Mar 2026 16:01:36 +1300 Subject: [PATCH] Fix shared request access and Jellyfin-ready pipeline status --- .build_number | 2 +- backend/app/build_info.py | 4 +- backend/app/db.py | 3 +- backend/app/routers/requests.py | 133 ++++++++++++++++------------ backend/app/services/snapshot.py | 131 ++++++++++++++++++++++++--- frontend/app/page.tsx | 14 ++- frontend/app/requests/[id]/page.tsx | 11 ++- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- 9 files changed, 219 insertions(+), 85 deletions(-) diff --git a/.build_number b/.build_number index b8b4006..75f4027 100644 --- a/.build_number +++ b/.build_number @@ -1 +1 @@ -0303261507 +0303261601 diff --git a/backend/app/build_info.py b/backend/app/build_info.py index 529aa12..7d5c261 100644 --- a/backend/app/build_info.py +++ b/backend/app/build_info.py @@ -1,2 +1,2 @@ -BUILD_NUMBER = "0303261507" -CHANGELOG = '2026-03-03|Improve SQLite batching and diagnostics visibility\n2026-03-03|Add login page visibility controls\n2026-03-03|Hotfix: expand landing-page search to all requests\n2026-03-02|Hotfix: add logged-out password reset flow\n2026-03-02|Process 1 build 0203261953\n2026-03-02|Process 1 build 0203261610\n2026-03-02|Process 1 build 0203261608\n2026-03-02|Add dedicated profile invites page and fix mobile admin layout\n2026-03-01|Persist Seerr media failure suppression and reduce sync error noise\n2026-03-01|Add repository line ending policy\n2026-03-01|Finalize diagnostics, logging controls, and email test support\n2026-03-01|Add invite email templates and delivery workflow\n2026-02-28|Finalize dev-1.3 upgrades and Seerr updates\n2026-02-27|admin docs and layout refresh, build 2702261314\n2026-02-27|Build 2702261153: fix jellyfin sync user visibility\n2026-02-26|Build 2602262241: live request page updates\n2026-02-26|Build 2602262204\n2026-02-26|Build 2602262159: restore jellyfin-first user source\n2026-02-26|Build 2602262049: split magent settings and harden local login\n2026-02-26|Build 2602262030: add magent settings and hardening\n2026-02-26|Build 2602261731: fix user resync after nuclear wipe\n2026-02-26|Build 2602261717: master invite policy and self-service invite controls\n2026-02-26|Build 2602261636: self-service invites and count fixes\n2026-02-26|Build 2602261605: invite trace and cross-system user lifecycle\n2026-02-26|Build 2602261536: refine invite layouts and tighten UI\n2026-02-26|Build 2602261523: live updates, invite cleanup and nuclear resync\n2026-02-26|Build 2602261442: tidy users and invite layouts\n2026-02-26|Build 2602261409: unify invite management controls\n2026-02-26|Build 2602260214: invites profiles and expiry admin controls\n2026-02-26|Build 2602260022: enterprise UI refresh and users bulk auto-search\n2026-02-25|Build 2502262321: fix auto-search quality and per-user toggle\n2026-02-02|Build 0202261541: allow FQDN service URLs\n2026-01-30|Build 3001262148: single container\n2026-01-29|Build 2901262244: format changelog\n2026-01-29|Build 2901262240: cache users\n2026-01-29|Tidy full changelog\n2026-01-29|Update full changelog\n2026-01-29|Bake build number and changelog\n2026-01-29|Hardcode build number in backend\n2026-01-29|release: 2901262102\n2026-01-29|release: 2901262044\n2026-01-29|release: 2901262036\n2026-01-27|Hydrate missing artwork from Jellyseerr (build 271261539)\n2026-01-27|Fallback to TMDB when artwork cache fails (build 271261524)\n2026-01-27|Add service test buttons (build 271261335)\n2026-01-27|Bump build number (process 2) 271261322\n2026-01-27|Add cache load spinner (build 271261238)\n2026-01-27|Fix snapshot title fallback (build 271261228)\n2026-01-27|Fix request titles in snapshots (build 271261219)\n2026-01-27|Bump build number to 271261202\n2026-01-27|Clarify request sync settings (build 271261159)\n2026-01-27|Fix backend cache stats import (build 271261149)\n2026-01-27|Improve cache stats performance (build 271261145)\n2026-01-27|Add cache control artwork stats\n2026-01-26|Fix sync progress bar animation\n2026-01-26|Fix cache title hydration\n2026-01-25|Build 2501262041\n2026-01-25|Harden request cache titles and cache-only reads\n2026-01-25|Serve bundled branding assets by default\n2026-01-25|Seed branding logo from bundled assets\n2026-01-25|Tidy request sync controls\n2026-01-25|Add Jellyfin login cache and admin-only stats\n2026-01-25|Add user stats and activity tracking\n2026-01-25|Move account actions into avatar menu\n2026-01-25|Improve mobile header layout\n2026-01-25|Automate build number tagging and sync\n2026-01-25|Add site banner, build number, and changelog\n2026-01-24|Improve request handling and qBittorrent categories\n2026-01-24|Map Prowlarr releases to Arr indexers for manual grab\n2026-01-24|Clarify how-it-works steps and fixes\n2026-01-24|Document fix buttons in how-it-works\n2026-01-24|Route grabs through Sonarr/Radarr only\n2026-01-23|Use backend branding assets for logo and favicon\n2026-01-23|Copy public assets into frontend image\n2026-01-23|Fix backend Dockerfile paths for root context\n2026-01-23|Add Docker Hub compose override\n2026-01-23|Remove password fields from users page\n2026-01-23|Use bundled branding assets\n2026-01-23|Add default branding assets when missing\n2026-01-23|Show available status on landing when in Jellyfin\n2026-01-23|Fix cache titles and move feedback link\n2026-01-23|Add feedback form and webhook\n2026-01-23|Hide header actions when signed out\n2026-01-23|Fallback manual grab to qBittorrent\n2026-01-23|Split search actions and improve download options\n2026-01-23|Fix cache titles via Jellyseerr media lookup\n2026-01-22|Update README with Docker-first guide\n2026-01-22|Update README\n2026-01-22|Ignore build artifacts\n2026-01-22|Initial commit' +BUILD_NUMBER = "0303261601" +CHANGELOG = '2026-03-03|Process 1 build 0303261507\n2026-03-03|Improve SQLite batching and diagnostics visibility\n2026-03-03|Add login page visibility controls\n2026-03-03|Hotfix: expand landing-page search to all requests\n2026-03-02|Hotfix: add logged-out password reset flow\n2026-03-02|Process 1 build 0203261953\n2026-03-02|Process 1 build 0203261610\n2026-03-02|Process 1 build 0203261608\n2026-03-02|Add dedicated profile invites page and fix mobile admin layout\n2026-03-01|Persist Seerr media failure suppression and reduce sync error noise\n2026-03-01|Add repository line ending policy\n2026-03-01|Finalize diagnostics, logging controls, and email test support\n2026-03-01|Add invite email templates and delivery workflow\n2026-02-28|Finalize dev-1.3 upgrades and Seerr updates\n2026-02-27|admin docs and layout refresh, build 2702261314\n2026-02-27|Build 2702261153: fix jellyfin sync user visibility\n2026-02-26|Build 2602262241: live request page updates\n2026-02-26|Build 2602262204\n2026-02-26|Build 2602262159: restore jellyfin-first user source\n2026-02-26|Build 2602262049: split magent settings and harden local login\n2026-02-26|Build 2602262030: add magent settings and hardening\n2026-02-26|Build 2602261731: fix user resync after nuclear wipe\n2026-02-26|Build 2602261717: master invite policy and self-service invite controls\n2026-02-26|Build 2602261636: self-service invites and count fixes\n2026-02-26|Build 2602261605: invite trace and cross-system user lifecycle\n2026-02-26|Build 2602261536: refine invite layouts and tighten UI\n2026-02-26|Build 2602261523: live updates, invite cleanup and nuclear resync\n2026-02-26|Build 2602261442: tidy users and invite layouts\n2026-02-26|Build 2602261409: unify invite management controls\n2026-02-26|Build 2602260214: invites profiles and expiry admin controls\n2026-02-26|Build 2602260022: enterprise UI refresh and users bulk auto-search\n2026-02-25|Build 2502262321: fix auto-search quality and per-user toggle\n2026-02-02|Build 0202261541: allow FQDN service URLs\n2026-01-30|Build 3001262148: single container\n2026-01-29|Build 2901262244: format changelog\n2026-01-29|Build 2901262240: cache users\n2026-01-29|Tidy full changelog\n2026-01-29|Update full changelog\n2026-01-29|Bake build number and changelog\n2026-01-29|Hardcode build number in backend\n2026-01-29|release: 2901262102\n2026-01-29|release: 2901262044\n2026-01-29|release: 2901262036\n2026-01-27|Hydrate missing artwork from Jellyseerr (build 271261539)\n2026-01-27|Fallback to TMDB when artwork cache fails (build 271261524)\n2026-01-27|Add service test buttons (build 271261335)\n2026-01-27|Bump build number (process 2) 271261322\n2026-01-27|Add cache load spinner (build 271261238)\n2026-01-27|Fix snapshot title fallback (build 271261228)\n2026-01-27|Fix request titles in snapshots (build 271261219)\n2026-01-27|Bump build number to 271261202\n2026-01-27|Clarify request sync settings (build 271261159)\n2026-01-27|Fix backend cache stats import (build 271261149)\n2026-01-27|Improve cache stats performance (build 271261145)\n2026-01-27|Add cache control artwork stats\n2026-01-26|Fix sync progress bar animation\n2026-01-26|Fix cache title hydration\n2026-01-25|Build 2501262041\n2026-01-25|Harden request cache titles and cache-only reads\n2026-01-25|Serve bundled branding assets by default\n2026-01-25|Seed branding logo from bundled assets\n2026-01-25|Tidy request sync controls\n2026-01-25|Add Jellyfin login cache and admin-only stats\n2026-01-25|Add user stats and activity tracking\n2026-01-25|Move account actions into avatar menu\n2026-01-25|Improve mobile header layout\n2026-01-25|Automate build number tagging and sync\n2026-01-25|Add site banner, build number, and changelog\n2026-01-24|Improve request handling and qBittorrent categories\n2026-01-24|Map Prowlarr releases to Arr indexers for manual grab\n2026-01-24|Clarify how-it-works steps and fixes\n2026-01-24|Document fix buttons in how-it-works\n2026-01-24|Route grabs through Sonarr/Radarr only\n2026-01-23|Use backend branding assets for logo and favicon\n2026-01-23|Copy public assets into frontend image\n2026-01-23|Fix backend Dockerfile paths for root context\n2026-01-23|Add Docker Hub compose override\n2026-01-23|Remove password fields from users page\n2026-01-23|Use bundled branding assets\n2026-01-23|Add default branding assets when missing\n2026-01-23|Show available status on landing when in Jellyfin\n2026-01-23|Fix cache titles and move feedback link\n2026-01-23|Add feedback form and webhook\n2026-01-23|Hide header actions when signed out\n2026-01-23|Fallback manual grab to qBittorrent\n2026-01-23|Split search actions and improve download options\n2026-01-23|Fix cache titles via Jellyseerr media lookup\n2026-01-22|Update README with Docker-first guide\n2026-01-22|Update README\n2026-01-22|Ignore build artifacts\n2026-01-22|Initial commit' diff --git a/backend/app/db.py b/backend/app/db.py index 49909ae..2aab0e9 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -25,7 +25,8 @@ SQLITE_MMAP_SIZE_BYTES = 256 * 1024 * 1024 def _db_path() -> str: path = settings.sqlite_path or "data/magent.db" if not os.path.isabs(path): - path = os.path.join(os.getcwd(), path) + app_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + path = os.path.join(app_root, path) os.makedirs(os.path.dirname(path), exist_ok=True) return path diff --git a/backend/app/routers/requests.py b/backend/app/routers/requests.py index 2fc750a..2f7ad1e 100644 --- a/backend/app/routers/requests.py +++ b/backend/app/routers/requests.py @@ -49,7 +49,7 @@ from ..db import ( clear_seerr_media_failure, ) from ..models import Snapshot, TriageResult, RequestType -from ..services.snapshot import build_snapshot +from ..services.snapshot import build_snapshot, jellyfin_item_matches_request router = APIRouter(prefix="/requests", tags=["requests"], dependencies=[Depends(get_current_user)]) @@ -118,6 +118,57 @@ def _cache_get(key: str) -> Optional[Dict[str, Any]]: def _cache_set(key: str, payload: Dict[str, Any]) -> None: _detail_cache[key] = (time.time() + CACHE_TTL_SECONDS, payload) + + +def _status_label_with_jellyfin(current_status: Any, jellyfin_available: bool) -> str: + if not jellyfin_available: + return _status_label(current_status) + try: + status_code = int(current_status) + except (TypeError, ValueError): + status_code = None + if status_code == 6: + return STATUS_LABELS[6] + return STATUS_LABELS[4] + + +async def _request_is_available_in_jellyfin( + jellyfin: JellyfinClient, + title: Optional[str], + year: Optional[int], + media_type: Optional[str], + request_payload: Optional[Dict[str, Any]], + availability_cache: Dict[str, bool], +) -> bool: + if not jellyfin.configured() or not title: + return False + cache_key = f"{media_type or ''}:{title.lower()}:{year or ''}:{request_payload.get('id') if isinstance(request_payload, dict) else ''}" + cached_value = availability_cache.get(cache_key) + if cached_value is not None: + return cached_value + types = ["Movie"] if media_type == "movie" else ["Series"] + try: + search = await jellyfin.search_items(title, types, limit=50) + except Exception: + availability_cache[cache_key] = False + return False + if isinstance(search, dict): + items = search.get("Items") or search.get("items") or [] + request_type = RequestType.movie if media_type == "movie" else RequestType.tv + for item in items: + if not isinstance(item, dict): + continue + if jellyfin_item_matches_request( + item, + title=title, + year=year, + request_type=request_type, + request_payload=request_payload, + ): + availability_cache[cache_key] = True + return True + availability_cache[cache_key] = False + return False _failed_detail_cache.pop(key, None) @@ -1295,23 +1346,9 @@ def get_requests_sync_state() -> Dict[str, Any]: async def _ensure_request_access( client: JellyseerrClient, request_id: int, user: Dict[str, str] ) -> None: - if user.get("role") == "admin": + if user.get("role") == "admin" or user.get("username"): return - runtime = get_runtime_settings() - mode = (runtime.requests_data_source or "prefer_cache").lower() - cached = get_request_cache_payload(request_id) - if mode != "always_js": - if cached is None: - logger.debug("access cache miss: request_id=%s mode=%s", request_id, mode) - raise HTTPException(status_code=404, detail="Request not found in cache") - logger.debug("access cache hit: request_id=%s mode=%s", request_id, mode) - if _request_matches_user(cached, user.get("username", "")): - return - raise HTTPException(status_code=403, detail="Request not accessible for this user") - logger.debug("access cache miss: request_id=%s mode=%s", request_id, mode) - details = await _get_request_details(client, request_id) - if details is None or not _request_matches_user(details, user.get("username", "")): - raise HTTPException(status_code=403, detail="Request not accessible for this user") + raise HTTPException(status_code=403, detail="Request not accessible for this user") def _build_recent_map(response: Dict[str, Any]) -> Dict[int, Dict[str, Any]]: @@ -1619,36 +1656,6 @@ async def recent_requests( allow_artwork_hydrate = client.configured() jellyfin = JellyfinClient(runtime.jellyfin_base_url, runtime.jellyfin_api_key) jellyfin_cache: Dict[str, bool] = {} - - async def _jellyfin_available( - title_value: Optional[str], year_value: Optional[int], media_type_value: Optional[str] - ) -> bool: - if not jellyfin.configured() or not title_value: - return False - cache_key = f"{media_type_value or ''}:{title_value.lower()}:{year_value or ''}" - cached_value = jellyfin_cache.get(cache_key) - if cached_value is not None: - return cached_value - types = ["Movie"] if media_type_value == "movie" else ["Series"] - try: - search = await jellyfin.search_items(title_value, types) - except Exception: - jellyfin_cache[cache_key] = False - return False - if isinstance(search, dict): - items = search.get("Items") or search.get("items") or [] - for item in items: - if not isinstance(item, dict): - continue - name = item.get("Name") or item.get("title") - year = item.get("ProductionYear") or item.get("Year") - if name and name.strip().lower() == title_value.strip().lower(): - if year_value and year and int(year) != int(year_value): - continue - jellyfin_cache[cache_key] = True - return True - jellyfin_cache[cache_key] = False - return False results = [] for row in rows: status = row.get("status") @@ -1743,10 +1750,16 @@ async def recent_requests( payload_json=json.dumps(details, ensure_ascii=True), ) status_label = _status_label(status) - if status_label == "Working on it": - is_available = await _jellyfin_available(title, year, row.get("media_type")) - if is_available: - status_label = "Available" + if status_label in {"Working on it", "Ready to watch", "Partially ready"}: + is_available = await _request_is_available_in_jellyfin( + jellyfin, + title, + year, + row.get("media_type"), + details if isinstance(details, dict) else None, + jellyfin_cache, + ) + status_label = _status_label_with_jellyfin(status, is_available) results.append( { "id": row.get("request_id"), @@ -1790,6 +1803,8 @@ async def search_requests( pass results = [] + jellyfin = JellyfinClient(runtime.jellyfin_base_url, runtime.jellyfin_api_key) + jellyfin_cache: Dict[str, bool] = {} for item in response.get("results", []): media_type = item.get("mediaType") title = item.get("title") or item.get("name") @@ -1824,11 +1839,19 @@ async def search_requests( details = get_request_cache_payload(request_id) if not isinstance(details, dict): details = await _get_request_details(client, request_id) - requested_by = _request_display_name(details) if user.get("role") == "admin": - accessible = True - elif isinstance(details, dict): - accessible = _request_matches_user(details, user.get("username", "")) + requested_by = _request_display_name(details) + accessible = True + if status is not None: + is_available = await _request_is_available_in_jellyfin( + jellyfin, + title, + year, + media_type, + details if isinstance(details, dict) else None, + jellyfin_cache, + ) + status_label = _status_label_with_jellyfin(status, is_available) results.append( { diff --git a/backend/app/services/snapshot.py b/backend/app/services/snapshot.py index 2ae839e..9b31230 100644 --- a/backend/app/services/snapshot.py +++ b/backend/app/services/snapshot.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional import asyncio import logging +import re from datetime import datetime, timezone from urllib.parse import quote import httpx @@ -57,6 +58,100 @@ def _pick_first(value: Any) -> Optional[Dict[str, Any]]: return None +def _normalize_media_title(value: Any) -> Optional[str]: + if not isinstance(value, str): + return None + normalized = re.sub(r"[^a-z0-9]+", " ", value.lower()).strip() + return normalized or None + + +def _canonical_provider_key(value: str) -> str: + normalized = value.strip().lower() + if normalized.endswith("id"): + normalized = normalized[:-2] + return normalized + + +def extract_request_provider_ids(payload: Any) -> Dict[str, str]: + provider_ids: Dict[str, str] = {} + candidates: List[Any] = [] + if isinstance(payload, dict): + candidates.append(payload) + media = payload.get("media") + if isinstance(media, dict): + candidates.append(media) + for candidate in candidates: + if not isinstance(candidate, dict): + continue + embedded = candidate.get("ProviderIds") or candidate.get("providerIds") + if isinstance(embedded, dict): + for key, value in embedded.items(): + if value is None: + continue + text = str(value).strip() + if text: + provider_ids[_canonical_provider_key(str(key))] = text + for key in ("tmdbId", "tvdbId", "imdbId", "tmdb_id", "tvdb_id", "imdb_id"): + value = candidate.get(key) + if value is None: + continue + text = str(value).strip() + if text: + provider_ids[_canonical_provider_key(key)] = text + return provider_ids + + +def jellyfin_item_matches_request( + item: Dict[str, Any], + *, + title: Optional[str], + year: Optional[int], + request_type: RequestType, + request_payload: Optional[Dict[str, Any]] = None, +) -> bool: + request_provider_ids = extract_request_provider_ids(request_payload or {}) + item_provider_ids = extract_request_provider_ids(item) + + provider_priority = ("tmdb", "tvdb", "imdb") + for key in provider_priority: + request_id = request_provider_ids.get(key) + item_id = item_provider_ids.get(key) + if request_id and item_id and request_id == item_id: + return True + + request_title = _normalize_media_title(title) + if not request_title: + return False + + item_titles = [ + _normalize_media_title(item.get("Name")), + _normalize_media_title(item.get("OriginalTitle")), + _normalize_media_title(item.get("SortName")), + _normalize_media_title(item.get("SeriesName")), + _normalize_media_title(item.get("title")), + ] + item_titles = [candidate for candidate in item_titles if candidate] + + item_year = item.get("ProductionYear") or item.get("Year") + try: + item_year_value = int(item_year) if item_year is not None else None + except (TypeError, ValueError): + item_year_value = None + + if year and item_year_value and int(year) != item_year_value: + return False + + if request_title in item_titles: + return True + + if request_type == RequestType.tv: + for candidate in item_titles: + if candidate and (candidate.startswith(request_title) or request_title.startswith(candidate)): + return True + + return False + + def _extract_http_error_message(exc: httpx.HTTPStatusError) -> Optional[str]: response = exc.response if response is None: @@ -513,7 +608,7 @@ async def build_snapshot(request_id: str) -> Snapshot: if jellyfin.configured() and snapshot.title: types = ["Movie"] if snapshot.request_type == RequestType.movie else ["Series"] try: - search = await jellyfin.search_items(snapshot.title, types) + search = await jellyfin.search_items(snapshot.title, types, limit=50) except Exception: search = None if isinstance(search, dict): @@ -521,11 +616,13 @@ async def build_snapshot(request_id: str) -> Snapshot: for item in items: if not isinstance(item, dict): continue - name = item.get("Name") or item.get("title") - year = item.get("ProductionYear") or item.get("Year") - if name and name.strip().lower() == (snapshot.title or "").strip().lower(): - if snapshot.year and year and int(year) != int(snapshot.year): - continue + if jellyfin_item_matches_request( + item, + title=snapshot.title, + year=snapshot.year, + request_type=snapshot.request_type, + request_payload=jelly_request, + ): jellyfin_available = True jellyfin_item = item break @@ -646,12 +743,22 @@ async def build_snapshot(request_id: str) -> Snapshot: snapshot.state = NormalizedState.added_to_arr snapshot.state_reason = "Item is present in Sonarr/Radarr" - if jellyfin_available and snapshot.state not in { - NormalizedState.downloading, - NormalizedState.importing, - }: - snapshot.state = NormalizedState.completed - snapshot.state_reason = "Ready to watch in Jellyfin." + if jellyfin_available: + missing_episodes = arr_details.get("missingEpisodes") + if snapshot.request_type == RequestType.tv and isinstance(missing_episodes, dict) and missing_episodes: + snapshot.state = NormalizedState.importing + snapshot.state_reason = "Some episodes are available in Jellyfin, but the request is still incomplete." + for hop in timeline: + if hop.service == "Seerr": + hop.status = "Partially ready" + else: + snapshot.state = NormalizedState.completed + snapshot.state_reason = "Ready to watch in Jellyfin." + for hop in timeline: + if hop.service == "Seerr": + hop.status = "Available" + elif hop.service == "Sonarr/Radarr" and hop.status not in {"error"}: + hop.status = "available" snapshot.timeline = timeline actions: List[ActionOption] = [] diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index fe7a273..ea1117e 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -567,21 +567,17 @@ export default function HomePage() { )) )} diff --git a/frontend/app/requests/[id]/page.tsx b/frontend/app/requests/[id]/page.tsx index c5e1026..b19c792 100644 --- a/frontend/app/requests/[id]/page.tsx +++ b/frontend/app/requests/[id]/page.tsx @@ -368,7 +368,14 @@ export default function RequestTimelinePage() { const jellyfinLink = snapshot.raw?.jellyfin?.link const posterUrl = snapshot.artwork?.poster_url const resolvedPoster = - posterUrl && posterUrl.startsWith('http') ? posterUrl : posterUrl ? `${getApiBase()}${posterUrl}` : null + posterUrl && posterUrl.startsWith('http') ? posterUrl : posterUrl ? `${getApiBase()}${posterUrl}` : null + const hasPartialReadyTimeline = snapshot.timeline.some( + (hop) => hop.service === 'Seerr' && hop.status === 'Partially ready' + ) + const currentStatusText = + snapshot.state === 'IMPORTING' && hasPartialReadyTimeline + ? 'Partially ready' + : friendlyState(snapshot.state) return (
@@ -400,7 +407,7 @@ export default function RequestTimelinePage() {

Status

-

{friendlyState(snapshot.state)}

+

{currentStatusText}

What this means

diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4dbfb1c..b1800f6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "magent-frontend", - "version": "0303261507", + "version": "0303261601", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "magent-frontend", - "version": "0303261507", + "version": "0303261601", "dependencies": { "next": "16.1.6", "react": "19.2.4", diff --git a/frontend/package.json b/frontend/package.json index d3658c5..67cf067 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "magent-frontend", "private": true, - "version": "0303261507", + "version": "0303261601", "scripts": { "dev": "next dev", "build": "next build",