Fix shared request access and Jellyfin-ready pipeline status

This commit is contained in:
2026-03-03 16:01:36 +13:00
parent bac96c7db3
commit 96333c0d85
9 changed files with 219 additions and 85 deletions

View File

@@ -1 +1 @@
0303261507
0303261601

View File

@@ -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'

View File

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

View File

@@ -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(
{

View File

@@ -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] = []

View File

@@ -567,21 +567,17 @@ export default function HomePage() {
<button
key={`${item.title || 'Untitled'}-${index}`}
type="button"
disabled={!item.requestId || !item.accessible}
disabled={!item.requestId}
onClick={() =>
item.requestId && item.accessible && router.push(`/requests/${item.requestId}`)
item.requestId && router.push(`/requests/${item.requestId}`)
}
>
{item.title || 'Untitled'} {item.year ? `(${item.year})` : ''}{' '}
{!item.requestId
? '- not requested'
: !item.accessible
? `- ${item.statusLabel || 'Requested'} · requested by ${
item.requestedBy || 'another user'
}`
: item.statusLabel
? `- ${item.statusLabel}`
: ''}
: item.statusLabel
? `- ${item.statusLabel}`
: '- already requested'}
</button>
))
)}

View File

@@ -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 (
<main className="card">
@@ -400,7 +407,7 @@ export default function RequestTimelinePage() {
<section className="status-box">
<div>
<h2>Status</h2>
<p className="status-text">{friendlyState(snapshot.state)}</p>
<p className="status-text">{currentStatusText}</p>
</div>
<div>
<h2>What this means</h2>

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "magent-frontend",
"private": true,
"version": "0303261507",
"version": "0303261601",
"scripts": {
"dev": "next dev",
"build": "next build",