Build 2602261523: live updates, invite cleanup and nuclear resync
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { authFetch, clearToken, getApiBase, getToken } from '../lib/auth'
|
||||
import AdminShell from '../ui/AdminShell'
|
||||
@@ -141,6 +141,9 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
const [artworkSummaryStatus, setArtworkSummaryStatus] = useState<string | null>(null)
|
||||
const [maintenanceStatus, setMaintenanceStatus] = useState<string | null>(null)
|
||||
const [maintenanceBusy, setMaintenanceBusy] = useState(false)
|
||||
const [liveStreamConnected, setLiveStreamConnected] = useState(false)
|
||||
const requestsSyncRef = useRef<any | null>(null)
|
||||
const artworkPrefetchRef = useRef<any | null>(null)
|
||||
|
||||
const loadSettings = useCallback(async () => {
|
||||
const baseUrl = getApiBase()
|
||||
@@ -338,6 +341,14 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
return false
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
requestsSyncRef.current = requestsSync
|
||||
}, [requestsSync])
|
||||
|
||||
useEffect(() => {
|
||||
artworkPrefetchRef.current = artworkPrefetch
|
||||
}, [artworkPrefetch])
|
||||
|
||||
const settingDescriptions: Record<string, string> = {
|
||||
jellyseerr_base_url:
|
||||
'Base URL for your Jellyseerr server (FQDN or IP). Scheme is optional.',
|
||||
@@ -576,7 +587,100 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!artworkPrefetch || artworkPrefetch.status !== 'running') {
|
||||
const shouldSubscribe = showRequestsExtras || showArtworkExtras || showLogs
|
||||
if (!shouldSubscribe) {
|
||||
setLiveStreamConnected(false)
|
||||
return
|
||||
}
|
||||
const token = getToken()
|
||||
if (!token) {
|
||||
setLiveStreamConnected(false)
|
||||
return
|
||||
}
|
||||
|
||||
const baseUrl = getApiBase()
|
||||
const params = new URLSearchParams()
|
||||
params.set('access_token', token)
|
||||
if (showLogs) {
|
||||
params.set('include_logs', '1')
|
||||
params.set('log_lines', String(logsCount))
|
||||
}
|
||||
const streamUrl = `${baseUrl}/admin/events/stream?${params.toString()}`
|
||||
let closed = false
|
||||
const source = new EventSource(streamUrl)
|
||||
|
||||
source.onopen = () => {
|
||||
if (closed) return
|
||||
setLiveStreamConnected(true)
|
||||
}
|
||||
|
||||
source.onmessage = (event) => {
|
||||
if (closed) return
|
||||
setLiveStreamConnected(true)
|
||||
try {
|
||||
const payload = JSON.parse(event.data)
|
||||
if (!payload || payload.type !== 'admin_live_state') {
|
||||
return
|
||||
}
|
||||
|
||||
const rawSync =
|
||||
payload.requestsSync && typeof payload.requestsSync === 'object'
|
||||
? payload.requestsSync
|
||||
: null
|
||||
const nextSync = rawSync?.status === 'idle' ? null : rawSync
|
||||
const prevSync = requestsSyncRef.current
|
||||
requestsSyncRef.current = nextSync
|
||||
setRequestsSync(nextSync)
|
||||
if (prevSync?.status === 'running' && nextSync?.status && nextSync.status !== 'running') {
|
||||
setRequestsSyncStatus(nextSync.message || 'Sync complete.')
|
||||
}
|
||||
|
||||
const rawArtwork =
|
||||
payload.artworkPrefetch && typeof payload.artworkPrefetch === 'object'
|
||||
? payload.artworkPrefetch
|
||||
: null
|
||||
const nextArtwork = rawArtwork?.status === 'idle' ? null : rawArtwork
|
||||
const prevArtwork = artworkPrefetchRef.current
|
||||
artworkPrefetchRef.current = nextArtwork
|
||||
setArtworkPrefetch(nextArtwork)
|
||||
if (
|
||||
prevArtwork?.status === 'running' &&
|
||||
nextArtwork?.status &&
|
||||
nextArtwork.status !== 'running'
|
||||
) {
|
||||
setArtworkPrefetchStatus(nextArtwork.message || 'Artwork caching complete.')
|
||||
if (showArtworkExtras) {
|
||||
void loadArtworkSummary()
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.logs && typeof payload.logs === 'object') {
|
||||
if (Array.isArray(payload.logs.lines)) {
|
||||
setLogsLines(payload.logs.lines)
|
||||
setLogsStatus(null)
|
||||
} else if (typeof payload.logs.error === 'string' && payload.logs.error.trim()) {
|
||||
setLogsStatus(payload.logs.error)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
source.onerror = () => {
|
||||
if (closed) return
|
||||
setLiveStreamConnected(false)
|
||||
}
|
||||
|
||||
return () => {
|
||||
closed = true
|
||||
setLiveStreamConnected(false)
|
||||
source.close()
|
||||
}
|
||||
}, [loadArtworkSummary, logsCount, showArtworkExtras, showLogs, showRequestsExtras])
|
||||
|
||||
useEffect(() => {
|
||||
if (liveStreamConnected || !artworkPrefetch || artworkPrefetch.status !== 'running') {
|
||||
return
|
||||
}
|
||||
let active = true
|
||||
@@ -602,7 +706,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
active = false
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, [artworkPrefetch, loadArtworkSummary])
|
||||
}, [artworkPrefetch, liveStreamConnected, loadArtworkSummary])
|
||||
|
||||
useEffect(() => {
|
||||
if (!artworkPrefetch || artworkPrefetch.status === 'running') {
|
||||
@@ -615,7 +719,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
}, [artworkPrefetch])
|
||||
|
||||
useEffect(() => {
|
||||
if (!requestsSync || requestsSync.status !== 'running') {
|
||||
if (liveStreamConnected || !requestsSync || requestsSync.status !== 'running') {
|
||||
return
|
||||
}
|
||||
let active = true
|
||||
@@ -640,7 +744,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
active = false
|
||||
clearInterval(timer)
|
||||
}
|
||||
}, [requestsSync])
|
||||
}, [liveStreamConnected, requestsSync])
|
||||
|
||||
useEffect(() => {
|
||||
if (!requestsSync || requestsSync.status === 'running') {
|
||||
@@ -683,12 +787,15 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
if (!showLogs) {
|
||||
return
|
||||
}
|
||||
if (liveStreamConnected) {
|
||||
return
|
||||
}
|
||||
void loadLogs()
|
||||
const timer = setInterval(() => {
|
||||
void loadLogs()
|
||||
}, 5000)
|
||||
return () => clearInterval(timer)
|
||||
}, [loadLogs, showLogs])
|
||||
}, [liveStreamConnected, loadLogs, showLogs])
|
||||
|
||||
const loadCache = async () => {
|
||||
setCacheStatus(null)
|
||||
@@ -763,7 +870,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
setMaintenanceBusy(true)
|
||||
if (typeof window !== 'undefined') {
|
||||
const ok = window.confirm(
|
||||
'This will clear cached requests and history, then re-sync 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 Jellyseerr. Continue?'
|
||||
)
|
||||
if (!ok) {
|
||||
setMaintenanceBusy(false)
|
||||
@@ -772,7 +879,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
}
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
setMaintenanceStatus('Flushing database...')
|
||||
setMaintenanceStatus('Running nuclear flush...')
|
||||
const flushResponse = await authFetch(`${baseUrl}/admin/maintenance/flush`, {
|
||||
method: 'POST',
|
||||
})
|
||||
@@ -780,12 +887,25 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
const text = await flushResponse.text()
|
||||
throw new Error(text || 'Flush failed')
|
||||
}
|
||||
setMaintenanceStatus('Database flushed. Starting re-sync...')
|
||||
const flushData = await flushResponse.json()
|
||||
const usersCleared = Number(flushData?.userObjectsCleared?.users ?? 0)
|
||||
setMaintenanceStatus(`Nuclear flush complete. Cleared ${usersCleared} non-admin users. Re-syncing users...`)
|
||||
const usersResyncResponse = await authFetch(`${baseUrl}/admin/jellyseerr/users/resync`, {
|
||||
method: 'POST',
|
||||
})
|
||||
if (!usersResyncResponse.ok) {
|
||||
const text = await usersResyncResponse.text()
|
||||
throw new Error(text || 'User resync failed')
|
||||
}
|
||||
const usersResyncData = await usersResyncResponse.json()
|
||||
setMaintenanceStatus(
|
||||
`Users re-synced (${usersResyncData?.imported ?? 0} imported). Starting request re-sync...`
|
||||
)
|
||||
await syncRequests()
|
||||
setMaintenanceStatus('Database flushed. Re-sync running now.')
|
||||
setMaintenanceStatus('Nuclear flush complete. User and request re-sync running now.')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setMaintenanceStatus('Flush + resync failed.')
|
||||
setMaintenanceStatus('Nuclear flush + resync failed.')
|
||||
} finally {
|
||||
setMaintenanceBusy(false)
|
||||
}
|
||||
@@ -1452,7 +1572,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
<h2>Maintenance</h2>
|
||||
</div>
|
||||
<div className="status-banner">
|
||||
Emergency tools. Use with care: flush will clear saved requests and history.
|
||||
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.
|
||||
</div>
|
||||
{maintenanceStatus && <div className="status-banner">{maintenanceStatus}</div>}
|
||||
<div className="maintenance-grid">
|
||||
@@ -1471,7 +1591,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
|
||||
onClick={runFlushAndResync}
|
||||
disabled={maintenanceBusy}
|
||||
>
|
||||
Flush database + resync
|
||||
Nuclear flush + resync
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user