diff --git a/.build_number b/.build_number index 4004d24..592acce 100644 --- a/.build_number +++ b/.build_number @@ -1 +1 @@ -261261420 +271261125 \ No newline at end of file diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py index c5e9abf..65f8943 100644 --- a/backend/app/routers/admin.py +++ b/backend/app/routers/admin.py @@ -10,6 +10,7 @@ from ..db import ( get_all_users, get_request_cache_overview, get_request_cache_missing_titles, + get_request_cache_count, get_settings_overrides, get_user_by_username, set_setting, @@ -38,6 +39,22 @@ from ..routers.branding import save_branding_image router = APIRouter(prefix="/admin", tags=["admin"], dependencies=[Depends(require_admin)]) logger = logging.getLogger(__name__) +def _get_artwork_cache_stats() -> Dict[str, int]: + cache_root = os.path.join(os.getcwd(), "data", "artwork") + total_bytes = 0 + total_files = 0 + if not os.path.isdir(cache_root): + return {"cache_bytes": 0, "cache_files": 0} + for root, _, files in os.walk(cache_root): + for name in files: + path = os.path.join(root, name) + try: + total_bytes += os.path.getsize(path) + total_files += 1 + except OSError: + continue + return {"cache_bytes": total_bytes, "cache_files": total_files} + SENSITIVE_KEYS = { "jellyseerr_api_key", "jellyfin_api_key", @@ -277,10 +294,12 @@ async def requests_sync_delta() -> Dict[str, Any]: @router.post("/requests/artwork/prefetch") -async def requests_artwork_prefetch() -> Dict[str, Any]: +async def requests_artwork_prefetch(only_missing: bool = False) -> Dict[str, Any]: runtime = get_runtime_settings() state = await requests_router.start_artwork_prefetch( - runtime.jellyseerr_base_url, runtime.jellyseerr_api_key + runtime.jellyseerr_base_url, + runtime.jellyseerr_api_key, + only_missing=only_missing, ) logger.info("Admin triggered artwork prefetch: status=%s", state.get("status")) return {"status": "ok", "prefetch": state} @@ -290,6 +309,17 @@ async def requests_artwork_prefetch() -> Dict[str, Any]: async def requests_artwork_status() -> Dict[str, Any]: return {"status": "ok", "prefetch": requests_router.get_artwork_prefetch_state()} +@router.get("/requests/artwork/summary") +async def requests_artwork_summary() -> Dict[str, Any]: + runtime = get_runtime_settings() + summary = { + "total_requests": get_request_cache_count(), + "missing_artwork": requests_router.get_artwork_cache_missing_count(), + "cache_mode": (runtime.artwork_cache_mode or "remote").lower(), + } + summary.update(_get_artwork_cache_stats()) + return {"status": "ok", "summary": summary} + @router.get("/requests/sync/status") async def requests_sync_status() -> Dict[str, Any]: diff --git a/backend/app/routers/images.py b/backend/app/routers/images.py index 4d40fd3..83e4480 100644 --- a/backend/app/routers/images.py +++ b/backend/app/routers/images.py @@ -1,6 +1,7 @@ import os import re import mimetypes +from typing import Optional from fastapi import APIRouter, HTTPException, Response from fastapi.responses import FileResponse, RedirectResponse import httpx @@ -19,13 +20,24 @@ def _safe_filename(path: str) -> str: safe = re.sub(r"[^A-Za-z0-9_.-]", "_", trimmed) return safe or "image" - -async def cache_tmdb_image(path: str, size: str = "w342") -> bool: +def tmdb_cache_path(path: str, size: str) -> Optional[str]: if not path or "://" in path or ".." in path: - return False + return None if not path.startswith("/"): path = f"/{path}" if size not in _ALLOWED_SIZES: + return None + cache_dir = os.path.join(os.getcwd(), "data", "artwork", "tmdb", size) + return os.path.join(cache_dir, _safe_filename(path)) + + +def is_tmdb_cached(path: str, size: str) -> bool: + file_path = tmdb_cache_path(path, size) + return bool(file_path and os.path.exists(file_path)) + + +async def cache_tmdb_image(path: str, size: str = "w342") -> bool: + if not path or "://" in path or ".." in path: return False runtime = get_runtime_settings() @@ -33,9 +45,10 @@ async def cache_tmdb_image(path: str, size: str = "w342") -> bool: if cache_mode != "cache": return False - cache_dir = os.path.join(os.getcwd(), "data", "artwork", "tmdb", size) - os.makedirs(cache_dir, exist_ok=True) - file_path = os.path.join(cache_dir, _safe_filename(path)) + file_path = tmdb_cache_path(path, size) + if not file_path: + return False + os.makedirs(os.path.dirname(file_path), exist_ok=True) if os.path.exists(file_path): return True @@ -64,9 +77,10 @@ async def tmdb_image(path: str, size: str = "w342"): if cache_mode != "cache": return RedirectResponse(url=url) - cache_dir = os.path.join(os.getcwd(), "data", "artwork", "tmdb", size) - os.makedirs(cache_dir, exist_ok=True) - file_path = os.path.join(cache_dir, _safe_filename(path)) + file_path = tmdb_cache_path(path, size) + if not file_path: + raise HTTPException(status_code=400, detail="Invalid image path") + os.makedirs(os.path.dirname(file_path), exist_ok=True) headers = {"Cache-Control": "public, max-age=86400"} if os.path.exists(file_path): media_type = mimetypes.guess_type(file_path)[0] or "image/jpeg" diff --git a/backend/app/routers/requests.py b/backend/app/routers/requests.py index df07490..deaa329 100644 --- a/backend/app/routers/requests.py +++ b/backend/app/routers/requests.py @@ -17,7 +17,7 @@ from ..clients.prowlarr import ProwlarrClient from ..ai.triage import triage_snapshot from ..auth import get_current_user from ..runtime import get_runtime_settings -from .images import cache_tmdb_image +from .images import cache_tmdb_image, is_tmdb_cached from ..db import ( save_action, get_recent_actions, @@ -65,6 +65,7 @@ _artwork_prefetch_state: Dict[str, Any] = { "processed": 0, "total": 0, "message": "", + "only_missing": False, "started_at": None, "finished_at": None, } @@ -227,6 +228,61 @@ def _extract_artwork_paths(item: Dict[str, Any]) -> tuple[Optional[str], Optiona backdrop_path = item.get("backdropPath") or item.get("backdrop_path") return poster_path, backdrop_path +def _extract_tmdb_lookup(payload: Dict[str, Any]) -> tuple[Optional[int], Optional[str]]: + media = payload.get("media") or {} + if not isinstance(media, dict): + media = {} + tmdb_id = media.get("tmdbId") or payload.get("tmdbId") + media_type = ( + media.get("mediaType") + or payload.get("mediaType") + or payload.get("type") + ) + try: + tmdb_id = int(tmdb_id) if tmdb_id is not None else None + except (TypeError, ValueError): + tmdb_id = None + if isinstance(media_type, str): + media_type = media_type.strip().lower() or None + else: + media_type = None + return tmdb_id, media_type + + +def _artwork_missing_for_payload(payload: Dict[str, Any]) -> bool: + poster_path, backdrop_path = _extract_artwork_paths(payload) + tmdb_id, media_type = _extract_tmdb_lookup(payload) + can_hydrate = bool(tmdb_id and media_type) + if poster_path: + if not is_tmdb_cached(poster_path, "w185") or not is_tmdb_cached(poster_path, "w342"): + return True + elif can_hydrate: + return True + if backdrop_path: + if not is_tmdb_cached(backdrop_path, "w780"): + return True + elif can_hydrate: + return True + return False + + +def get_artwork_cache_missing_count() -> int: + limit = 400 + offset = 0 + missing = 0 + while True: + batch = get_request_cache_payloads(limit=limit, offset=offset) + if not batch: + break + for row in batch: + payload = row.get("payload") + if not isinstance(payload, dict): + continue + if _artwork_missing_for_payload(payload): + missing += 1 + offset += limit + return missing + async def _get_request_details(client: JellyseerrClient, request_id: int) -> Optional[Dict[str, Any]]: cache_key = f"request:{request_id}" @@ -632,7 +688,9 @@ async def _sync_delta_requests(client: JellyseerrClient) -> int: return stored -async def _prefetch_artwork_cache(client: JellyseerrClient) -> None: +async def _prefetch_artwork_cache( + client: JellyseerrClient, only_missing: bool = False, total: Optional[int] = None +) -> None: runtime = get_runtime_settings() cache_mode = (runtime.artwork_cache_mode or "remote").lower() if cache_mode != "cache": @@ -645,17 +703,30 @@ async def _prefetch_artwork_cache(client: JellyseerrClient) -> None: ) return - total = get_request_cache_count() + total = total if total is not None else get_request_cache_count() _artwork_prefetch_state.update( { "status": "running", "processed": 0, "total": total, - "message": "Starting artwork prefetch", + "message": "Starting missing artwork prefetch" + if only_missing + else "Starting artwork prefetch", + "only_missing": only_missing, "started_at": datetime.now(timezone.utc).isoformat(), "finished_at": None, } ) + if only_missing and total == 0: + _artwork_prefetch_state.update( + { + "status": "completed", + "processed": 0, + "message": "No missing artwork to cache.", + "finished_at": datetime.now(timezone.utc).isoformat(), + } + ) + return offset = 0 limit = 200 processed = 0 @@ -666,42 +737,43 @@ async def _prefetch_artwork_cache(client: JellyseerrClient) -> None: for row in batch: payload = row.get("payload") if not isinstance(payload, dict): - processed += 1 + if not only_missing: + processed += 1 + continue + if only_missing and not _artwork_missing_for_payload(payload): continue poster_path, backdrop_path = _extract_artwork_paths(payload) - if not (poster_path or backdrop_path) and client.configured(): + tmdb_id, media_type = _extract_tmdb_lookup(payload) + if (not poster_path or not backdrop_path) and client.configured() and tmdb_id and media_type: media = payload.get("media") or {} - tmdb_id = media.get("tmdbId") or payload.get("tmdbId") - media_type = media.get("mediaType") or payload.get("type") - if tmdb_id and media_type: - hydrated_poster, hydrated_backdrop = await _hydrate_artwork_from_tmdb( - client, media_type, tmdb_id - ) - poster_path = poster_path or hydrated_poster - backdrop_path = backdrop_path or hydrated_backdrop - if hydrated_poster or hydrated_backdrop: - media = dict(media) if isinstance(media, dict) else {} - if hydrated_poster: - media["posterPath"] = hydrated_poster - if hydrated_backdrop: - media["backdropPath"] = hydrated_backdrop - payload["media"] = media - parsed = _parse_request_payload(payload) - request_id = parsed.get("request_id") - if isinstance(request_id, int): - upsert_request_cache( - request_id=request_id, - media_id=parsed.get("media_id"), - media_type=parsed.get("media_type"), - status=parsed.get("status"), - title=parsed.get("title"), - year=parsed.get("year"), - requested_by=parsed.get("requested_by"), - requested_by_norm=parsed.get("requested_by_norm"), - created_at=parsed.get("created_at"), - updated_at=parsed.get("updated_at"), - payload_json=json.dumps(payload, ensure_ascii=True), - ) + hydrated_poster, hydrated_backdrop = await _hydrate_artwork_from_tmdb( + client, media_type, tmdb_id + ) + poster_path = poster_path or hydrated_poster + backdrop_path = backdrop_path or hydrated_backdrop + if hydrated_poster or hydrated_backdrop: + media = dict(media) if isinstance(media, dict) else {} + if hydrated_poster: + media["posterPath"] = hydrated_poster + if hydrated_backdrop: + media["backdropPath"] = hydrated_backdrop + payload["media"] = media + parsed = _parse_request_payload(payload) + request_id = parsed.get("request_id") + if isinstance(request_id, int): + upsert_request_cache( + request_id=request_id, + media_id=parsed.get("media_id"), + media_type=parsed.get("media_type"), + status=parsed.get("status"), + title=parsed.get("title"), + year=parsed.get("year"), + requested_by=parsed.get("requested_by"), + requested_by_norm=parsed.get("requested_by_norm"), + created_at=parsed.get("created_at"), + updated_at=parsed.get("updated_at"), + payload_json=json.dumps(payload, ensure_ascii=True), + ) if poster_path: try: await cache_tmdb_image(poster_path, "w185") @@ -730,25 +802,43 @@ async def _prefetch_artwork_cache(client: JellyseerrClient) -> None: ) -async def start_artwork_prefetch(base_url: Optional[str], api_key: Optional[str]) -> Dict[str, Any]: +async def start_artwork_prefetch( + base_url: Optional[str], api_key: Optional[str], only_missing: bool = False +) -> Dict[str, Any]: global _artwork_prefetch_task if _artwork_prefetch_task and not _artwork_prefetch_task.done(): return dict(_artwork_prefetch_state) client = JellyseerrClient(base_url, api_key) + total = get_request_cache_count() + if only_missing: + total = get_artwork_cache_missing_count() _artwork_prefetch_state.update( { "status": "running", "processed": 0, - "total": get_request_cache_count(), - "message": "Starting artwork prefetch", + "total": total, + "message": "Starting missing artwork prefetch" + if only_missing + else "Starting artwork prefetch", + "only_missing": only_missing, "started_at": datetime.now(timezone.utc).isoformat(), "finished_at": None, } ) + if only_missing and total == 0: + _artwork_prefetch_state.update( + { + "status": "completed", + "processed": 0, + "message": "No missing artwork to cache.", + "finished_at": datetime.now(timezone.utc).isoformat(), + } + ) + return dict(_artwork_prefetch_state) async def _runner() -> None: try: - await _prefetch_artwork_cache(client) + await _prefetch_artwork_cache(client, only_missing=only_missing, total=total) except Exception: logger.exception("Artwork prefetch failed") _artwork_prefetch_state.update( diff --git a/frontend/app/admin/SettingsPage.tsx b/frontend/app/admin/SettingsPage.tsx index 5dad47c..f7e536b 100644 --- a/frontend/app/admin/SettingsPage.tsx +++ b/frontend/app/admin/SettingsPage.tsx @@ -21,8 +21,8 @@ type ServiceOptions = { const SECTION_LABELS: Record = { jellyseerr: 'Jellyseerr', jellyfin: 'Jellyfin', - artwork: 'Artwork', - cache: 'Cache', + artwork: 'Artwork cache', + cache: 'Cache Control', sonarr: 'Sonarr', radarr: 'Radarr', prowlarr: 'Prowlarr', @@ -39,8 +39,8 @@ const BANNER_TONES = ['info', 'warning', 'error', 'maintenance'] const SECTION_DESCRIPTIONS: Record = { jellyseerr: 'Connect the request system where users submit content.', jellyfin: 'Control Jellyfin login and availability checks.', - artwork: 'Configure how posters and artwork are loaded.', - cache: 'Manage saved request data and offline artwork.', + artwork: 'Cache posters/backdrops and review artwork coverage.', + cache: 'Manage saved requests cache and refresh behavior.', sonarr: 'TV automation settings.', radarr: 'Movie automation settings.', prowlarr: 'Indexer search settings.', @@ -53,7 +53,7 @@ const SECTION_DESCRIPTIONS: Record = { const SETTINGS_SECTION_MAP: Record = { jellyseerr: 'jellyseerr', jellyfin: 'jellyfin', - artwork: 'artwork', + artwork: null, sonarr: 'sonarr', radarr: 'radarr', prowlarr: 'prowlarr', @@ -89,6 +89,19 @@ const labelFromKey = (key: string) => .replace('site banner tone', 'Sitewide banner tone') .replace('site changelog', 'Changelog text') +const formatBytes = (value?: number | null) => { + if (!value || value <= 0) return '0 B' + const units = ['B', 'KB', 'MB', 'GB', 'TB'] + let size = value + let unitIndex = 0 + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024 + unitIndex += 1 + } + const decimals = unitIndex === 0 || size >= 10 ? 0 : 1 + return `${size.toFixed(decimals)} ${units[unitIndex]}` +} + type SettingsPageProps = { section: string } @@ -114,6 +127,8 @@ export default function SettingsPage({ section }: SettingsPageProps) { const [cacheStatus, setCacheStatus] = useState(null) const [requestsSync, setRequestsSync] = useState(null) const [artworkPrefetch, setArtworkPrefetch] = useState(null) + const [artworkSummary, setArtworkSummary] = useState(null) + const [artworkSummaryStatus, setArtworkSummaryStatus] = useState(null) const [maintenanceStatus, setMaintenanceStatus] = useState(null) const [maintenanceBusy, setMaintenanceBusy] = useState(false) @@ -165,6 +180,27 @@ export default function SettingsPage({ section }: SettingsPageProps) { } }, []) + const loadArtworkSummary = useCallback(async () => { + setArtworkSummaryStatus(null) + try { + const baseUrl = getApiBase() + const response = await authFetch(`${baseUrl}/admin/requests/artwork/summary`) + if (!response.ok) { + const text = await response.text() + throw new Error(text || 'Artwork summary fetch failed') + } + const data = await response.json() + setArtworkSummary(data?.summary ?? null) + } catch (err) { + console.error(err) + const message = + err instanceof Error && err.message + ? err.message.replace(/^\\{\"detail\":\"|\"\\}$/g, '') + : 'Could not load artwork stats.' + setArtworkSummaryStatus(message) + } + }, []) + const loadOptions = useCallback(async (service: 'sonarr' | 'radarr') => { try { const baseUrl = getApiBase() @@ -204,8 +240,9 @@ export default function SettingsPage({ section }: SettingsPageProps) { } try { await loadSettings() - if (section === 'artwork') { + if (section === 'cache' || section === 'artwork') { await loadArtworkPrefetchStatus() + await loadArtworkSummary() } } catch (err) { console.error(err) @@ -222,7 +259,7 @@ export default function SettingsPage({ section }: SettingsPageProps) { if (section === 'radarr') { void loadOptions('radarr') } - }, [loadArtworkPrefetchStatus, loadOptions, loadSettings, router, section]) + }, [loadArtworkPrefetchStatus, loadArtworkSummary, loadOptions, loadSettings, router, section]) const groupedSettings = useMemo(() => { const groups: Record = {} @@ -237,28 +274,30 @@ export default function SettingsPage({ section }: SettingsPageProps) { const settingsSection = SETTINGS_SECTION_MAP[section] ?? null const visibleSections = settingsSection ? [settingsSection] : [] const isCacheSection = section === 'cache' - const cacheSettingKeys = new Set([ - 'requests_sync_ttl_minutes', - 'requests_data_source', - 'artwork_cache_mode', - ]) + const cacheSettingKeys = new Set(['requests_sync_ttl_minutes', 'requests_data_source']) + const artworkSettingKeys = new Set(['artwork_cache_mode']) + const hiddenSettingKeys = new Set([...cacheSettingKeys, ...artworkSettingKeys]) const cacheSettings = settings.filter((setting) => cacheSettingKeys.has(setting.key)) + const artworkSettings = settings.filter((setting) => artworkSettingKeys.has(setting.key)) const settingsSections = isCacheSection - ? [{ key: 'cache', title: 'Cache settings', items: cacheSettings }] + ? [ + { key: 'cache', title: 'Cache control', items: cacheSettings }, + { key: 'artwork', title: 'Artwork cache', items: artworkSettings }, + ] : visibleSections.map((sectionKey) => ({ key: sectionKey, title: SECTION_LABELS[sectionKey] ?? sectionKey, items: sectionKey === 'requests' || sectionKey === 'artwork' ? (groupedSettings[sectionKey] ?? []).filter( - (setting) => !cacheSettingKeys.has(setting.key) + (setting) => !hiddenSettingKeys.has(setting.key) ) : groupedSettings[sectionKey] ?? [], })) const showLogs = section === 'logs' const showMaintenance = section === 'maintenance' const showRequestsExtras = section === 'requests' - const showArtworkExtras = section === 'artwork' + const showArtworkExtras = section === 'cache' const showCacheExtras = section === 'cache' const shouldRenderSection = (sectionGroup: { key: string; items?: AdminSetting[] }) => { if (sectionGroup.items && sectionGroup.items.length > 0) return true @@ -292,8 +331,10 @@ export default function SettingsPage({ section }: SettingsPageProps) { qbittorrent_username: 'qBittorrent login username.', qbittorrent_password: 'qBittorrent login password.', requests_sync_ttl_minutes: 'How long saved requests stay fresh before a refresh is needed.', - requests_poll_interval_seconds: 'How often the background checker runs.', - requests_delta_sync_interval_minutes: 'How often we check for new or updated requests.', + requests_poll_interval_seconds: + 'How often Magent wakes up to check if the cache is stale and needs a full refresh.', + requests_delta_sync_interval_minutes: + 'How often we actively poll for new or updated requests (delta sync).', requests_full_sync_time: 'Daily time to refresh the full request list.', requests_cleanup_time: 'Daily time to trim old history.', requests_cleanup_days: 'History older than this is removed during cleanup.', @@ -463,6 +504,31 @@ export default function SettingsPage({ section }: SettingsPageProps) { } } + const prefetchArtworkMissing = async () => { + setArtworkPrefetchStatus(null) + try { + const baseUrl = getApiBase() + const response = await authFetch( + `${baseUrl}/admin/requests/artwork/prefetch?only_missing=1`, + { method: 'POST' } + ) + if (!response.ok) { + const text = await response.text() + throw new Error(text || 'Missing artwork prefetch failed') + } + const data = await response.json() + setArtworkPrefetch(data?.prefetch ?? null) + setArtworkPrefetchStatus('Missing artwork caching started.') + } catch (err) { + console.error(err) + const message = + err instanceof Error && err.message + ? err.message.replace(/^\\{\"detail\":\"|\"\\}$/g, '') + : 'Could not cache missing artwork.' + setArtworkPrefetchStatus(message) + } + } + useEffect(() => { if (!artworkPrefetch || artworkPrefetch.status !== 'running') { return @@ -480,6 +546,7 @@ export default function SettingsPage({ section }: SettingsPageProps) { setArtworkPrefetch(data?.prefetch ?? null) if (data?.prefetch?.status && data.prefetch.status !== 'running') { setArtworkPrefetchStatus(data.prefetch.message || 'Artwork caching complete.') + void loadArtworkSummary() } } catch (err) { console.error(err) @@ -489,7 +556,7 @@ export default function SettingsPage({ section }: SettingsPageProps) { active = false clearInterval(timer) } - }, [artworkPrefetch]) + }, [artworkPrefetch, loadArtworkSummary]) useEffect(() => { if (!artworkPrefetch || artworkPrefetch.status === 'running') { @@ -731,11 +798,19 @@ export default function SettingsPage({ section }: SettingsPageProps) { Import Jellyfin users )} - {(showArtworkExtras && sectionGroup.key === 'artwork') || - (showCacheExtras && sectionGroup.key === 'cache') ? ( - + {showArtworkExtras && sectionGroup.key === 'artwork' ? ( +
+ + +
) : null} {showRequestsExtras && sectionGroup.key === 'requests' && (
@@ -765,17 +840,48 @@ export default function SettingsPage({ section }: SettingsPageProps) { {sectionGroup.key === 'jellyfin' && jellyfinSyncStatus && (
{jellyfinSyncStatus}
)} - {((showArtworkExtras && sectionGroup.key === 'artwork') || - (showCacheExtras && sectionGroup.key === 'cache')) && - artworkPrefetchStatus && ( + {showArtworkExtras && sectionGroup.key === 'artwork' && artworkPrefetchStatus && (
{artworkPrefetchStatus}
)} + {showArtworkExtras && sectionGroup.key === 'artwork' && artworkSummaryStatus && ( +
{artworkSummaryStatus}
+ )} + {showArtworkExtras && sectionGroup.key === 'artwork' && ( +
+
+ Missing artwork +

{artworkSummary?.missing_artwork ?? '--'}

+
Requests missing poster/backdrop or cache files.
+
+
+ Artwork cache size +

{formatBytes(artworkSummary?.cache_bytes)}

+
+ {artworkSummary?.cache_files ?? '--'} cached files +
+
+
+ Total requests +

{artworkSummary?.total_requests ?? '--'}

+
Requests currently tracked in cache.
+
+
+ Cache mode +

{artworkSummary?.cache_mode ?? '--'}

+
Artwork setting applied to posters/backdrops.
+
+
+ )} {showRequestsExtras && sectionGroup.key === 'requests' && requestsSyncStatus && (
{requestsSyncStatus}
)} - {((showArtworkExtras && sectionGroup.key === 'artwork') || - (showCacheExtras && sectionGroup.key === 'cache')) && - artworkPrefetch && ( + {showRequestsExtras && sectionGroup.key === 'requests' && ( +
+ Background refresh checks only decide when to run a full refresh. The delta sync + interval actively polls for new or updated requests. +
+ )} + {showArtworkExtras && sectionGroup.key === 'artwork' && artworkPrefetch && (
Status: {artworkPrefetch.status} @@ -1202,7 +1308,7 @@ export default function SettingsPage({ section }: SettingsPageProps) { ) : (
- No settings to show here yet. Try the Cache page for artwork and saved-request controls. + No settings to show here yet. Try the Cache Control page for artwork and saved-request controls.
)} {showLogs && ( diff --git a/frontend/app/ui/AdminSidebar.tsx b/frontend/app/ui/AdminSidebar.tsx index 6d558aa..d877f0a 100644 --- a/frontend/app/ui/AdminSidebar.tsx +++ b/frontend/app/ui/AdminSidebar.tsx @@ -18,8 +18,7 @@ const NAV_GROUPS = [ title: 'Requests', items: [ { href: '/admin/requests', label: 'Request syncing' }, - { href: '/admin/artwork', label: 'Artwork' }, - { href: '/admin/cache', label: 'Cache' }, + { href: '/admin/cache', label: 'Cache Control' }, ], }, {