/** * Component: Admin Settings Page (Refactored Shell) * Documentation: documentation/settings-pages.md * * This is a refactored shell component that orchestrates the modular tab components. * Each tab has been extracted into its own component with dedicated hooks for state management. */ 'use client'; import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/Button'; import Link from 'next/link'; import { fetchWithAuth } from '@/lib/utils/api'; import { IndexerFlagConfig } from '@/lib/utils/ranking-algorithm'; // Tab Components import { LibraryTab } from './tabs/LibraryTab/LibraryTab'; import { AuthTab } from './tabs/AuthTab/AuthTab'; import { IndexersTab } from './tabs/IndexersTab/IndexersTab'; import { DownloadTab } from './tabs/DownloadTab/DownloadTab'; import { PathsTab } from './tabs/PathsTab/PathsTab'; import { EbookTab } from './tabs/EbookTab/EbookTab'; import { BookDateTab } from './tabs/BookDateTab/BookDateTab'; // Types and Helpers import type { Settings, SettingsTab, IndexerConfig, SavedIndexerConfig, Message } from './lib/types'; import { parseArrayToCommaSeparated, saveTabSettings, validateAuthSettings, getTabValidation, getTabs } from './lib/helpers'; export default function AdminSettings() { // Core state const [settings, setSettings] = useState(null); const [originalSettings, setOriginalSettings] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [message, setMessage] = useState(null); const [activeTab, setActiveTab] = useState('library'); // Validation state (tracks if each tab's settings are valid) const [validated, setValidated] = useState({ plex: false, audiobookshelf: false, oidc: false, registration: false, prowlarr: false, download: false, paths: false, }); // Indexer-specific state (used by IndexersTab) const [configuredIndexers, setConfiguredIndexers] = useState([]); const [flagConfigs, setFlagConfigs] = useState([]); // Initial data fetch useEffect(() => { fetchSettings(); }, []); /** * Fetches all settings from the API */ const fetchSettings = async () => { try { const response = await fetchWithAuth('/api/admin/settings'); if (response.ok) { const data = await response.json(); // Convert OIDC allowed lists from JSON arrays to comma-separated strings for display if (data.oidc) { data.oidc.allowedEmails = parseArrayToCommaSeparated(data.oidc.allowedEmails); data.oidc.allowedUsernames = parseArrayToCommaSeparated(data.oidc.allowedUsernames); } setSettings(data); setOriginalSettings(JSON.parse(JSON.stringify(data))); } } catch (error) { console.error('Failed to fetch settings:', error); } finally { setLoading(false); } }; /** * Fetches indexers from Prowlarr (used by IndexersTab) */ const fetchIndexers = async (force = false) => { try { const response = await fetchWithAuth('/api/admin/settings/prowlarr/indexers'); if (response.ok) { const data = await response.json(); setFlagConfigs(data.flagConfigs || []); // Extract configured indexers (enabled ones) const configured = (data.indexers || []) .filter((idx: IndexerConfig) => idx.enabled) .map((idx: IndexerConfig) => ({ id: idx.id, name: idx.name, priority: idx.priority, seedingTimeMinutes: idx.seedingTimeMinutes, rssEnabled: idx.rssEnabled, categories: idx.categories || [3030], })); setConfiguredIndexers(configured); } else { console.error('Failed to fetch indexers:', response.status); if (force) { setMessage({ type: 'error', text: 'Failed to load indexers. Check your Prowlarr settings.' }); } } } catch (error) { console.error('Failed to fetch indexers:', error); if (force) { setMessage({ type: 'error', text: 'Failed to load Prowlarr indexers. Check your Prowlarr URL and API key.' }); } } }; /** * Saves settings for the currently active tab */ const saveSettings = async () => { if (!settings) return; // Validate auth settings before saving if (activeTab === 'auth') { const validation = validateAuthSettings(settings); if (!validation.valid) { setMessage({ type: 'error', text: validation.message! }); return; } } setSaving(true); setMessage(null); try { await saveTabSettings(activeTab, settings, configuredIndexers, flagConfigs); setMessage({ type: 'success', text: 'Settings saved successfully!' }); setOriginalSettings(JSON.parse(JSON.stringify(settings))); setTimeout(() => setMessage(null), 3000); } catch (error) { setMessage({ type: 'error', text: error instanceof Error ? error.message : 'Failed to save settings', }); } finally { setSaving(false); } }; // Loading state if (loading || !settings) { return (
); } // Dynamic tabs, validation, and change detection const tabs = getTabs(settings.backendMode); const currentTabValidation = getTabValidation(activeTab, settings, validated); const hasUnsavedChanges = JSON.stringify(settings) !== JSON.stringify(originalSettings); return (
{/* Header */}

Settings

{/* Tab Navigation */}
{/* Message Display */} {message && (
{message.text}
)} {/* Tab Content */}
{/* Library Tab */} {activeTab === 'library' && ( { setValidated({ ...validated, [section]: isValid }); }} onSuccess={(msg) => setMessage({ type: 'success', text: msg })} onError={(msg) => setMessage({ type: 'error', text: msg })} /> )} {/* Auth Tab (only in Audiobookshelf mode) */} {activeTab === 'auth' && settings?.backendMode === 'audiobookshelf' && ( { setValidated({ ...validated, [section]: isValid }); }} onSuccess={(msg) => setMessage({ type: 'success', text: msg })} onError={(msg) => setMessage({ type: 'error', text: msg })} /> )} {/* Indexers Tab */} {activeTab === 'prowlarr' && ( setValidated({ ...validated, prowlarr: isValid })} onRefreshIndexers={() => fetchIndexers(true)} /> )} {/* Download Client Tab */} {activeTab === 'download' && ( setSettings({ ...settings, downloadClient: dc })} onValidationChange={(isValid) => setValidated({ ...validated, download: isValid })} /> )} {/* Paths Tab */} {activeTab === 'paths' && ( setSettings({ ...settings, paths })} onValidationChange={(isValid) => setValidated({ ...validated, paths: isValid })} /> )} {/* E-book Sidecar Tab */} {activeTab === 'ebook' && ( setSettings({ ...settings, ebook })} onSuccess={(msg) => setMessage({ type: 'success', text: msg })} onError={(msg) => setMessage({ type: 'error', text: msg })} markAsSaved={() => setOriginalSettings(JSON.parse(JSON.stringify(settings)))} /> )} {/* BookDate Tab */} {activeTab === 'bookdate' && ( setMessage({ type: 'success', text: msg })} onError={(msg) => setMessage({ type: 'error', text: msg })} /> )} {/* Save Button (only for tabs that save through main page) */} {activeTab !== 'ebook' && activeTab !== 'bookdate' && (
{!currentTabValidation && hasUnsavedChanges && (

Please test the connection before saving

)}
)}
); }