Finalize dev-1.3 upgrades and Seerr updates
This commit is contained in:
@@ -22,7 +22,8 @@ const SECTION_LABELS: Record<string, string> = {
|
||||
magent: 'Magent',
|
||||
general: 'General',
|
||||
notifications: 'Notifications',
|
||||
jellyseerr: 'Jellyseerr',
|
||||
seerr: 'Seerr',
|
||||
jellyseerr: 'Seerr',
|
||||
jellyfin: 'Jellyfin',
|
||||
artwork: 'Artwork cache',
|
||||
cache: 'Cache Control',
|
||||
@@ -89,7 +90,8 @@ const SECTION_DESCRIPTIONS: Record<string, string> = {
|
||||
'Application runtime, binding, reverse proxy, and manual SSL settings for the Magent UI/API.',
|
||||
notifications:
|
||||
'Notification providers and delivery channel settings used by Magent messaging features.',
|
||||
jellyseerr: 'Connect the request system where users submit content.',
|
||||
seerr: 'Connect Seerr where users submit content requests.',
|
||||
jellyseerr: 'Connect Seerr where users submit content requests.',
|
||||
jellyfin: 'Control Jellyfin login and availability checks.',
|
||||
artwork: 'Cache posters/backdrops and review artwork coverage.',
|
||||
cache: 'Manage saved requests cache and refresh behavior.',
|
||||
@@ -106,6 +108,7 @@ const SETTINGS_SECTION_MAP: Record<string, string | null> = {
|
||||
magent: 'magent',
|
||||
general: 'magent',
|
||||
notifications: 'magent',
|
||||
seerr: 'jellyseerr',
|
||||
jellyseerr: 'jellyseerr',
|
||||
jellyfin: 'jellyfin',
|
||||
artwork: null,
|
||||
@@ -234,6 +237,8 @@ const MAGENT_GROUPS_BY_SECTION: Record<string, Set<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_port: 'Application port',
|
||||
magent_api_url: 'API URL',
|
||||
@@ -278,6 +283,7 @@ const labelFromKey = (key: string) =>
|
||||
SETTING_LABEL_OVERRIDES[key] ??
|
||||
key
|
||||
.replaceAll('_', ' ')
|
||||
.replace('jellyseerr', 'Seerr')
|
||||
.replace('base url', 'URL')
|
||||
.replace('api key', 'API key')
|
||||
.replace('quality profile id', 'Quality profile ID')
|
||||
@@ -289,7 +295,7 @@ const labelFromKey = (key: string) =>
|
||||
.replace('requests full sync time', 'Daily full refresh time (24h)')
|
||||
.replace('requests cleanup time', 'Daily history cleanup time (24h)')
|
||||
.replace('requests cleanup days', 'History retention window (days)')
|
||||
.replace('requests data source', 'Request source (cache vs Jellyseerr)')
|
||||
.replace('requests data source', 'Request source (cache vs Seerr)')
|
||||
.replace('jellyfin public url', 'Jellyfin public URL')
|
||||
.replace('jellyfin sync to arr', 'Sync Jellyfin to Sonarr/Radarr')
|
||||
.replace('artwork cache mode', 'Artwork cache mode')
|
||||
@@ -352,6 +358,21 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
const [liveStreamConnected, setLiveStreamConnected] = useState(false)
|
||||
const requestsSyncRef = useRef<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 baseUrl = getApiBase()
|
||||
@@ -642,7 +663,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
magent_notify_webhook_url:
|
||||
'Generic webhook endpoint for custom integrations or automation flows.',
|
||||
jellyseerr_base_url:
|
||||
'Base URL for your Jellyseerr server (FQDN or IP). Scheme is optional.',
|
||||
'Base URL for your Seerr server (FQDN or IP). Scheme is optional.',
|
||||
jellyseerr_api_key: 'API key used to read requests and status.',
|
||||
jellyfin_base_url:
|
||||
'Jellyfin server URL for logins and lookups (FQDN or IP). Scheme is optional.',
|
||||
@@ -677,7 +698,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
requests_cleanup_time: 'Daily time to trim old request history.',
|
||||
requests_cleanup_days: 'History older than this is removed during cleanup.',
|
||||
requests_data_source:
|
||||
'Pick where Magent should read requests from. Cache-only avoids Jellyseerr lookups on reads.',
|
||||
'Pick where Magent should read requests from. Cache-only avoids Seerr lookups on reads.',
|
||||
log_level: 'How much detail is written to the activity log.',
|
||||
log_file: 'Where the activity log is stored.',
|
||||
site_build_number: 'Build number shown in the account menu (auto-set from releases).',
|
||||
@@ -805,6 +826,13 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
|
||||
const syncRequests = async () => {
|
||||
setRequestsSyncStatus(null)
|
||||
setRequestsSync({
|
||||
status: 'running',
|
||||
stored: 0,
|
||||
total: 0,
|
||||
skip: 0,
|
||||
message: 'Starting sync',
|
||||
})
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(`${baseUrl}/admin/requests/sync`, {
|
||||
@@ -829,6 +857,13 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
|
||||
const syncRequestsDelta = async () => {
|
||||
setRequestsSyncStatus(null)
|
||||
setRequestsSync({
|
||||
status: 'running',
|
||||
stored: 0,
|
||||
total: 0,
|
||||
skip: 0,
|
||||
message: 'Starting delta sync',
|
||||
})
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(`${baseUrl}/admin/requests/sync/delta`, {
|
||||
@@ -853,6 +888,12 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
|
||||
const prefetchArtwork = async () => {
|
||||
setArtworkPrefetchStatus(null)
|
||||
setArtworkPrefetch({
|
||||
status: 'running',
|
||||
processed: 0,
|
||||
total: 0,
|
||||
message: 'Starting artwork caching',
|
||||
})
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(`${baseUrl}/admin/requests/artwork/prefetch`, {
|
||||
@@ -877,6 +918,12 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
|
||||
const prefetchArtworkMissing = async () => {
|
||||
setArtworkPrefetchStatus(null)
|
||||
setArtworkPrefetch({
|
||||
status: 'running',
|
||||
processed: 0,
|
||||
total: 0,
|
||||
message: 'Starting missing artwork caching',
|
||||
})
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
const response = await authFetch(
|
||||
@@ -1202,7 +1249,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
setMaintenanceBusy(true)
|
||||
if (typeof window !== 'undefined') {
|
||||
const ok = window.confirm(
|
||||
'This will perform a nuclear reset: clear cached requests/history, wipe non-admin users, invites, and profiles, then re-sync users and requests from Jellyseerr. Continue?'
|
||||
'This will perform a nuclear reset: clear cached requests/history, wipe non-admin users, invites, and profiles, then re-sync users and requests from Seerr. Continue?'
|
||||
)
|
||||
if (!ok) {
|
||||
setMaintenanceBusy(false)
|
||||
@@ -1264,7 +1311,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
|
||||
const cacheSourceLabel =
|
||||
formValues.requests_data_source === 'always_js'
|
||||
? 'Jellyseerr direct'
|
||||
? 'Seerr direct'
|
||||
: formValues.requests_data_source === 'prefer_cache'
|
||||
? 'Saved requests only'
|
||||
: 'Saved requests only'
|
||||
@@ -1485,22 +1532,16 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`progress ${artworkPrefetch.total ? '' : 'progress-indeterminate'} ${
|
||||
artworkPrefetch.status === 'completed' ? 'progress-complete' : ''
|
||||
}`}
|
||||
className={`progress ${artworkPrefetch.status === 'completed' ? 'progress-complete' : ''}`}
|
||||
>
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{
|
||||
width:
|
||||
artworkPrefetch.status === 'completed'
|
||||
? '100%'
|
||||
: artworkPrefetch.total
|
||||
? `${Math.min(
|
||||
100,
|
||||
Math.round((artworkPrefetch.processed / artworkPrefetch.total) * 100)
|
||||
)}%`
|
||||
: '30%',
|
||||
width: `${computeProgressPercent(
|
||||
artworkPrefetch.processed,
|
||||
artworkPrefetch.total,
|
||||
artworkPrefetch.status
|
||||
)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -1517,22 +1558,16 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={`progress ${requestsSync.total ? '' : 'progress-indeterminate'} ${
|
||||
requestsSync.status === 'completed' ? 'progress-complete' : ''
|
||||
}`}
|
||||
className={`progress ${requestsSync.status === 'completed' ? 'progress-complete' : ''}`}
|
||||
>
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{
|
||||
width:
|
||||
requestsSync.status === 'completed'
|
||||
? '100%'
|
||||
: requestsSync.total
|
||||
? `${Math.min(
|
||||
100,
|
||||
Math.round((requestsSync.stored / requestsSync.total) * 100)
|
||||
)}%`
|
||||
: '30%',
|
||||
width: `${computeProgressPercent(
|
||||
requestsSync.stored,
|
||||
requestsSync.total,
|
||||
requestsSync.status
|
||||
)}%`,
|
||||
}}
|
||||
/>
|
||||
</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">
|
||||
Use saved requests only (fastest)
|
||||
</option>
|
||||
@@ -2005,7 +2040,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
<h2>Maintenance</h2>
|
||||
</div>
|
||||
<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>
|
||||
{maintenanceStatus && <div className="status-banner">{maintenanceStatus}</div>}
|
||||
<div className="maintenance-grid">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { notFound } from 'next/navigation'
|
||||
import SettingsPage from '../SettingsPage'
|
||||
|
||||
const ALLOWED_SECTIONS = new Set([
|
||||
'seerr',
|
||||
'jellyseerr',
|
||||
'jellyfin',
|
||||
'artwork',
|
||||
@@ -20,12 +21,13 @@ const ALLOWED_SECTIONS = new Set([
|
||||
])
|
||||
|
||||
type PageProps = {
|
||||
params: { section: string }
|
||||
params: Promise<{ section: string }>
|
||||
}
|
||||
|
||||
export default function AdminSectionPage({ params }: PageProps) {
|
||||
if (!ALLOWED_SECTIONS.has(params.section)) {
|
||||
export default async function AdminSectionPage({ params }: PageProps) {
|
||||
const { section } = await params
|
||||
if (!ALLOWED_SECTIONS.has(section)) {
|
||||
notFound()
|
||||
}
|
||||
return <SettingsPage section={params.section} />
|
||||
return <SettingsPage section={section} />
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const REQUEST_FLOW: FlowStage[] = [
|
||||
},
|
||||
{
|
||||
title: 'Request intake',
|
||||
input: 'Jellyseerr request ID',
|
||||
input: 'Seerr request ID',
|
||||
action: 'Magent snapshots request + media metadata',
|
||||
output: 'Unified request state',
|
||||
},
|
||||
|
||||
@@ -2197,7 +2197,7 @@ button span {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.step-jellyseerr::before {
|
||||
.step-seerr::before {
|
||||
background: linear-gradient(135deg, rgba(255, 187, 92, 0.35), transparent 60%);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function HowItWorksPage() {
|
||||
|
||||
<section className="how-grid">
|
||||
<article className="how-card">
|
||||
<h2>Jellyseerr</h2>
|
||||
<h2>Seerr</h2>
|
||||
<p className="how-title">The request box</p>
|
||||
<p>
|
||||
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>
|
||||
<ol className="how-steps">
|
||||
<li>
|
||||
<strong>Request created</strong> in Jellyseerr.
|
||||
<strong>Request created</strong> in Seerr.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Approved</strong> and sent to Sonarr/Radarr.
|
||||
@@ -108,7 +108,7 @@ export default function HowItWorksPage() {
|
||||
<section className="how-flow">
|
||||
<h2>Request actions and when to use them</h2>
|
||||
<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>
|
||||
<h3>Re-add to Arr</h3>
|
||||
<p className="step-note">Use when a request is approved but never entered the Arr queue.</p>
|
||||
|
||||
@@ -352,7 +352,7 @@ export default function HomePage() {
|
||||
<div className="system-list">
|
||||
{(() => {
|
||||
const order = [
|
||||
'Jellyseerr',
|
||||
'Seerr',
|
||||
'Sonarr',
|
||||
'Radarr',
|
||||
'Prowlarr',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Image from 'next/image'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useParams, useRouter } from 'next/navigation'
|
||||
import { authFetch, clearToken, getApiBase, getEventStreamToken, getToken } from '../../lib/auth'
|
||||
|
||||
type TimelineHop = {
|
||||
@@ -140,7 +140,7 @@ const friendlyState = (value: string) => {
|
||||
}
|
||||
|
||||
const friendlyTimelineStatus = (service: string, status: string) => {
|
||||
if (service === 'Jellyseerr') {
|
||||
if (service === 'Seerr') {
|
||||
const map: Record<string, string> = {
|
||||
Pending: 'Waiting for approval',
|
||||
Approved: 'Approved',
|
||||
@@ -195,7 +195,9 @@ const friendlyTimelineStatus = (service: string, status: string) => {
|
||||
return status
|
||||
}
|
||||
|
||||
export default function RequestTimelinePage({ params }: { params: { id: string } }) {
|
||||
export default function RequestTimelinePage() {
|
||||
const params = useParams<{ id: string | string[] }>()
|
||||
const requestId = Array.isArray(params?.id) ? params.id[0] : params?.id
|
||||
const router = useRouter()
|
||||
const [snapshot, setSnapshot] = useState<Snapshot | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -208,6 +210,9 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
|
||||
const [historyActions, setHistoryActions] = useState<ActionHistory[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!requestId) {
|
||||
return
|
||||
}
|
||||
const load = async () => {
|
||||
try {
|
||||
if (!getToken()) {
|
||||
@@ -216,9 +221,9 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
|
||||
}
|
||||
const baseUrl = getApiBase()
|
||||
const [snapshotResponse, historyResponse, actionsResponse] = await Promise.all([
|
||||
authFetch(`${baseUrl}/requests/${params.id}/snapshot`),
|
||||
authFetch(`${baseUrl}/requests/${params.id}/history?limit=5`),
|
||||
authFetch(`${baseUrl}/requests/${params.id}/actions?limit=5`),
|
||||
authFetch(`${baseUrl}/requests/${requestId}/snapshot`),
|
||||
authFetch(`${baseUrl}/requests/${requestId}/history?limit=5`),
|
||||
authFetch(`${baseUrl}/requests/${requestId}/actions?limit=5`),
|
||||
])
|
||||
|
||||
if (snapshotResponse.status === 401) {
|
||||
@@ -252,10 +257,10 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
|
||||
}
|
||||
|
||||
load()
|
||||
}, [params.id, router])
|
||||
}, [requestId, router])
|
||||
|
||||
useEffect(() => {
|
||||
if (!getToken()) {
|
||||
if (!getToken() || !requestId) {
|
||||
return
|
||||
}
|
||||
const baseUrl = getApiBase()
|
||||
@@ -267,7 +272,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
|
||||
const streamToken = await getEventStreamToken()
|
||||
if (closed) return
|
||||
const streamUrl = `${baseUrl}/events/requests/${encodeURIComponent(
|
||||
params.id
|
||||
requestId
|
||||
)}/stream?stream_token=${encodeURIComponent(streamToken)}`
|
||||
source = new EventSource(streamUrl)
|
||||
|
||||
@@ -278,7 +283,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
|
||||
if (!payload || typeof payload !== 'object' || payload.type !== 'request_live') {
|
||||
return
|
||||
}
|
||||
if (String(payload.request_id ?? '') !== String(params.id)) {
|
||||
if (String(payload.request_id ?? '') !== String(requestId)) {
|
||||
return
|
||||
}
|
||||
if (payload.snapshot && typeof payload.snapshot === 'object') {
|
||||
@@ -310,7 +315,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
|
||||
closed = true
|
||||
source?.close()
|
||||
}
|
||||
}, [params.id])
|
||||
}, [requestId])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -337,7 +342,7 @@ export default function RequestTimelinePage({ params }: { params: { id: string }
|
||||
const arrStageLabel =
|
||||
snapshot.state === 'NEEDS_ADD' ? 'Push to Sonarr/Radarr' : 'Library queue'
|
||||
const pipelineSteps = [
|
||||
{ key: 'Jellyseerr', label: 'Jellyseerr' },
|
||||
{ key: 'Seerr', label: 'Seerr' },
|
||||
{ key: 'Sonarr/Radarr', label: arrStageLabel },
|
||||
{ key: 'Prowlarr', label: 'Search' },
|
||||
{ key: 'qBittorrent', label: 'Download' },
|
||||
|
||||
@@ -7,7 +7,7 @@ const NAV_GROUPS = [
|
||||
title: 'Services',
|
||||
items: [
|
||||
{ href: '/admin/general', label: 'General' },
|
||||
{ href: '/admin/jellyseerr', label: 'Jellyseerr' },
|
||||
{ href: '/admin/seerr', label: 'Seerr' },
|
||||
{ href: '/admin/jellyfin', label: 'Jellyfin' },
|
||||
{ href: '/admin/sonarr', label: 'Sonarr' },
|
||||
{ href: '/admin/radarr', label: 'Radarr' },
|
||||
|
||||
@@ -460,7 +460,7 @@ export default function UserDetailPage() {
|
||||
</div>
|
||||
<div className="user-detail-meta-grid">
|
||||
<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>
|
||||
</div>
|
||||
<div className="user-detail-meta-item">
|
||||
|
||||
@@ -155,7 +155,7 @@ export default function UsersPage() {
|
||||
await loadUsers()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setJellyseerrSyncStatus('Could not sync Jellyseerr users.')
|
||||
setJellyseerrSyncStatus('Could not sync Seerr users.')
|
||||
} finally {
|
||||
setJellyseerrSyncBusy(false)
|
||||
}
|
||||
@@ -163,7 +163,7 @@ export default function UsersPage() {
|
||||
|
||||
const resyncJellyseerrUsers = async () => {
|
||||
const confirmed = window.confirm(
|
||||
'This will remove all non-admin users and re-import from Jellyseerr. Continue?'
|
||||
'This will remove all non-admin users and re-import from Seerr. Continue?'
|
||||
)
|
||||
if (!confirmed) return
|
||||
setJellyseerrSyncStatus(null)
|
||||
@@ -184,7 +184,7 @@ export default function UsersPage() {
|
||||
await loadUsers()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setJellyseerrSyncStatus('Could not resync Jellyseerr users.')
|
||||
setJellyseerrSyncStatus('Could not resync Seerr users.')
|
||||
} finally {
|
||||
setJellyseerrResyncBusy(false)
|
||||
}
|
||||
@@ -322,17 +322,17 @@ export default function UsersPage() {
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<button type="button" onClick={syncJellyseerrUsers} disabled={jellyseerrSyncBusy}>
|
||||
{jellyseerrSyncBusy ? 'Syncing Jellyseerr users...' : 'Sync Jellyseerr users'}
|
||||
{jellyseerrSyncBusy ? 'Syncing Seerr users...' : 'Sync Seerr users'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={resyncJellyseerrUsers}
|
||||
disabled={jellyseerrResyncBusy}
|
||||
>
|
||||
{jellyseerrResyncBusy ? 'Resyncing Jellyseerr users...' : 'Resync Jellyseerr users'}
|
||||
{jellyseerrResyncBusy ? 'Resyncing Seerr users...' : 'Resync Seerr users'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user