'use client' import { useRouter } from 'next/navigation' import { useEffect, useState } from 'react' import { authFetch, getApiBase, getToken, clearToken } from './lib/auth' const normalizeRecentResults = (items: any[]) => items .filter((item: any) => item?.id) .map((item: any) => { const id = item.id const rawTitle = item.title const placeholder = typeof rawTitle === 'string' && rawTitle.trim().toLowerCase() === `request ${id}` return { id, title: !rawTitle || placeholder ? `Request #${id}` : rawTitle, year: item.year, statusLabel: item.statusLabel, artwork: item.artwork, createdAt: item.createdAt ?? null, } }) export default function HomePage() { const router = useRouter() const [query, setQuery] = useState('') const [recent, setRecent] = useState< { id: number title: string year?: number statusLabel?: string artwork?: { poster_url?: string } createdAt?: string | null }[] >([]) const [recentError, setRecentError] = useState(null) const [recentLoading, setRecentLoading] = useState(false) const [searchResults, setSearchResults] = useState< { title: string; year?: number; type?: string; requestId?: number; statusLabel?: string }[] >([]) const [searchError, setSearchError] = useState(null) const [role, setRole] = useState(null) const [recentDays, setRecentDays] = useState(90) const [authReady, setAuthReady] = useState(false) const [servicesStatus, setServicesStatus] = useState< { overall: string; services: { name: string; status: string; message?: string }[] } | null >(null) const [servicesLoading, setServicesLoading] = useState(false) const [servicesError, setServicesError] = useState(null) const [serviceTesting, setServiceTesting] = useState>({}) const [serviceTestResults, setServiceTestResults] = useState>({}) const [liveStreamConnected, setLiveStreamConnected] = useState(false) const submit = (event: React.FormEvent) => { event.preventDefault() const trimmed = query.trim() if (!trimmed) return if (/^\d+$/.test(trimmed)) { router.push(`/requests/${encodeURIComponent(trimmed)}`) return } void runSearch(trimmed) } const toServiceSlug = (name: string) => name.toLowerCase().replace(/[^a-z0-9]/g, '') const updateServiceStatus = (name: string, status: string, message?: string) => { setServicesStatus((prev) => { if (!prev) return prev return { ...prev, services: prev.services.map((service) => service.name === name ? { ...service, status, message } : service ), } }) } const testService = async (name: string) => { const slug = toServiceSlug(name) setServiceTesting((prev) => ({ ...prev, [name]: true })) setServiceTestResults((prev) => ({ ...prev, [name]: null })) try { const baseUrl = getApiBase() const response = await authFetch(`${baseUrl}/status/services/${slug}/test`, { method: 'POST', }) if (!response.ok) { if (response.status === 401) { clearToken() router.push('/login') return } const text = await response.text() throw new Error(text || `Service test failed: ${response.status}`) } const data = await response.json() const status = data?.status ?? 'unknown' const message = data?.message || (status === 'up' ? 'API OK' : status === 'down' ? 'API unreachable' : status === 'degraded' ? 'Health warnings' : status === 'not_configured' ? 'Not configured' : 'Unknown') setServiceTestResults((prev) => ({ ...prev, [name]: message })) updateServiceStatus(name, status, data?.message) } catch (error) { console.error(error) setServiceTestResults((prev) => ({ ...prev, [name]: 'Test failed' })) } finally { setServiceTesting((prev) => ({ ...prev, [name]: false })) } } useEffect(() => { if (!getToken()) { router.push('/login') return } const load = async () => { setRecentLoading(true) setRecentError(null) try { const baseUrl = getApiBase() const meResponse = await authFetch(`${baseUrl}/auth/me`) if (!meResponse.ok) { if (meResponse.status === 401) { clearToken() router.push('/login') return } throw new Error(`Auth failed: ${meResponse.status}`) } const me = await meResponse.json() const userRole = me?.role ?? null setRole(userRole) setAuthReady(true) const take = userRole === 'admin' ? 50 : 6 const response = await authFetch( `${baseUrl}/requests/recent?take=${take}&days=${recentDays}` ) if (!response.ok) { if (response.status === 401) { clearToken() router.push('/login') return } throw new Error(`Recent requests failed: ${response.status}`) } const data = await response.json() if (Array.isArray(data?.results)) { setRecent(normalizeRecentResults(data.results)) } } catch (error) { console.error(error) setRecentError('Recent requests are not available right now.') } finally { setRecentLoading(false) } } load() }, [recentDays]) useEffect(() => { if (!authReady) { return } const load = async () => { setServicesLoading(true) setServicesError(null) try { const baseUrl = getApiBase() const response = await authFetch(`${baseUrl}/status/services`) if (!response.ok) { if (response.status === 401) { clearToken() router.push('/login') return } throw new Error(`Service status failed: ${response.status}`) } const data = await response.json() setServicesStatus(data) } catch (error) { console.error(error) setServicesError('Service status is not available right now.') } finally { setServicesLoading(false) } } void load() if (liveStreamConnected) { return } const timer = setInterval(load, 30000) return () => clearInterval(timer) }, [authReady, liveStreamConnected, router]) useEffect(() => { if (!authReady) { setLiveStreamConnected(false) return } const token = getToken() if (!token) { setLiveStreamConnected(false) return } const baseUrl = getApiBase() const streamUrl = `${baseUrl}/events/stream?access_token=${encodeURIComponent(token)}&recent_days=${encodeURIComponent(String(recentDays))}` 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 || typeof payload !== 'object') { return } if (payload.type === 'home_recent') { if (Array.isArray(payload.results)) { setRecent(normalizeRecentResults(payload.results)) setRecentError(null) setRecentLoading(false) } else if (typeof payload.error === 'string' && payload.error.trim()) { setRecentError('Recent requests are not available right now.') setRecentLoading(false) } return } if (payload.type === 'home_services') { if (payload.status && typeof payload.status === 'object') { setServicesStatus(payload.status) setServicesError(null) setServicesLoading(false) } else if (typeof payload.error === 'string' && payload.error.trim()) { setServicesError('Service status is not available right now.') setServicesLoading(false) } } } catch (error) { console.error(error) } } source.onerror = () => { if (closed) return setLiveStreamConnected(false) } return () => { closed = true setLiveStreamConnected(false) source.close() } }, [authReady, recentDays]) const runSearch = async (term: string) => { try { const baseUrl = getApiBase() const response = await authFetch(`${baseUrl}/requests/search?query=${encodeURIComponent(term)}`) if (!response.ok) { if (response.status === 401) { clearToken() router.push('/login') return } throw new Error(`Search failed: ${response.status}`) } const data = await response.json() if (Array.isArray(data?.results)) { setSearchResults( data.results.map((item: any) => ({ title: item.title, year: item.year, type: item.type, requestId: item.requestId, statusLabel: item.statusLabel, })) ) setSearchError(null) } } catch (error) { console.error(error) setSearchError('Search failed. Try a request ID instead.') setSearchResults([]) } } const resolveArtworkUrl = (url?: string | null) => { if (!url) return null return url.startsWith('http') ? url : `${getApiBase()}${url}` } const formatRequestTime = (value?: string | null) => { if (!value) return null const date = new Date(value) if (Number.isNaN(date.valueOf())) return value return date.toLocaleString() } return (

System status

{servicesLoading ? 'Checking services...' : servicesError ? 'Status not available yet' : servicesStatus?.overall === 'up' ? 'Services are up and running' : servicesStatus?.overall === 'down' ? 'Something is down' : 'Some services need attention'}
{(() => { const order = [ 'Jellyseerr', 'Sonarr', 'Radarr', 'Prowlarr', 'qBittorrent', 'Jellyfin', ] const items = servicesStatus?.services ?? [] return order.map((name) => { const item = items.find((entry) => entry.name === name) const status = item?.status ?? 'unknown' const testing = serviceTesting[name] ?? false return (
{name} {serviceTestResults[name] && ( {serviceTestResults[name]} )}
{status === 'up' ? 'Up' : status === 'down' ? 'Down' : status === 'degraded' ? 'Needs attention' : status === 'not_configured' ? 'Not configured' : 'Unknown'}
) }) })()}

{role === 'admin' ? 'All requests' : 'My recent requests'}

{authReady && ( )}
{recentLoading ? (
) : recentError ? ( ) : recent.length === 0 ? ( ) : ( recent.map((item) => ( )) )}
) }