import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { flushSync } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { DatabaseZap } from 'lucide-react'; import { useAuthStore } from '../../store/authStore'; import { useLibraryIndexStore } from '../../store/libraryIndexStore'; import { showToast } from '../../utils/ui/toast'; import SettingsSubSection from '../SettingsSubSection'; import { libraryGetStatus, librarySyncCancel, librarySyncClearSession, subscribeLibrarySyncIdle, subscribeLibrarySyncProgress, type SyncStateDto, } from '../../api/library'; import { bootstrapAllIndexedServers, bootstrapIndexedServer, type BindServerResult, } from '../../utils/library/librarySession'; import { enqueueLibrarySync, waitForLibrarySyncIdle } from '../../utils/library/librarySyncQueue'; import { syncIngestDisplayCount } from '../../utils/library/libraryReady'; import { serverListDisplayLabel } from '../../utils/server/serverDisplayName'; import LibraryIndexServerRow, { type LibraryServerConnection } from './LibraryIndexServerRow'; const STATUS_POLL_MS = 3000; const SYNC_POLL_MS = 2500; const OFFLINE_RETRY_MS = 60_000; export default function LibraryIndexSection() { const { t } = useTranslation(); const servers = useAuthStore(s => s.servers); const activeServerId = useAuthStore(s => s.activeServerId); const masterEnabled = useLibraryIndexStore(s => s.masterEnabled); const syncExcludedByServer = useLibraryIndexStore(s => s.syncExcludedByServer); const setMasterEnabled = useLibraryIndexStore(s => s.setMasterEnabled); const setServerSyncExcluded = useLibraryIndexStore(s => s.setServerSyncExcluded); const autoReconcile = useLibraryIndexStore(s => s.autoReconcileEnabled); const setAutoReconcile = useLibraryIndexStore(s => s.setAutoReconcileEnabled); const indexedIds = useMemo(() => { if (!masterEnabled) return []; return servers.map(s => s.id).filter(id => syncExcludedByServer[id] !== true); }, [masterEnabled, syncExcludedByServer, servers]); const indexedServers = useMemo( () => servers.filter(s => indexedIds.includes(s.id)), [servers, indexedIds], ); const excludedServers = useMemo( () => servers.filter(s => syncExcludedByServer[s.id] === true), [servers, syncExcludedByServer], ); const [statusByServer, setStatusByServer] = useState>({}); const [connectionByServer, setConnectionByServer] = useState>({}); const [progressByServer, setProgressByServer] = useState>({}); const [busyServerId, setBusyServerId] = useState(null); const [excludingServerId, setExcludingServerId] = useState(null); const [includingServerId, setIncludingServerId] = useState(null); const [bootstrapping, setBootstrapping] = useState(false); const pollTimer = useRef | null>(null); const ingestCountRef = useRef>({}); const syncPhaseRef = useRef>({}); const applyConnectionResults = useCallback((results: Record) => { setConnectionByServer(prev => { const next = { ...prev }; for (const [id, result] of Object.entries(results)) { next[id] = result === 'offline' ? 'offline' : result === 'bound' ? 'online' : 'unknown'; } return next; }); }, []); const refreshAllStatuses = useCallback(async () => { if (!masterEnabled || indexedServers.length === 0) return; const entries = await Promise.all( indexedServers.map(async srv => { try { const fresh = await libraryGetStatus(srv.id); syncPhaseRef.current[srv.id] = fresh.syncPhase; if (fresh.syncPhase === 'initial_sync') { const next = Math.max(ingestCountRef.current[srv.id] ?? 0, syncIngestDisplayCount(fresh)); ingestCountRef.current[srv.id] = next; setProgressByServer(p => ({ ...p, [srv.id]: t('settings.libraryIndexProgressIngest', { count: next }), })); } else if (fresh.syncPhase === 'ready' || fresh.syncPhase === 'idle') { ingestCountRef.current[srv.id] = 0; } return [srv.id, fresh] as const; } catch { return [srv.id, null] as const; } }), ); setStatusByServer(Object.fromEntries(entries)); }, [masterEnabled, indexedServers, t]); const runBootstrap = useCallback(async () => { if (!masterEnabled) return; setBootstrapping(true); try { const results = await bootstrapAllIndexedServers(); applyConnectionResults(results); await refreshAllStatuses(); } finally { setBootstrapping(false); } }, [masterEnabled, applyConnectionResults, refreshAllStatuses]); const retryOfflineServers = useCallback(async () => { if (!masterEnabled) return; const offline = indexedServers.filter(s => connectionByServer[s.id] === 'offline'); if (offline.length === 0) return; const results: Record = {}; for (const srv of offline) { results[srv.id] = await bootstrapIndexedServer(srv); } applyConnectionResults(results); void refreshAllStatuses(); }, [masterEnabled, indexedServers, connectionByServer, applyConnectionResults, refreshAllStatuses]); useEffect(() => { if (!masterEnabled) { setStatusByServer({}); setConnectionByServer({}); setProgressByServer({}); setBusyServerId(null); setExcludingServerId(null); setIncludingServerId(null); return; } void runBootstrap(); }, [masterEnabled, indexedIds.join(',')]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { if (!masterEnabled) return; const poll = () => { void refreshAllStatuses(); const anyInitial = indexedServers.some( s => syncPhaseRef.current[s.id] === 'initial_sync', ); pollTimer.current = setTimeout(poll, anyInitial ? SYNC_POLL_MS : STATUS_POLL_MS); }; poll(); return () => { if (pollTimer.current) clearTimeout(pollTimer.current); pollTimer.current = null; }; }, [masterEnabled, indexedServers, refreshAllStatuses]); useEffect(() => { if (!masterEnabled) return; const retryTimer = setInterval(() => { void retryOfflineServers(); }, OFFLINE_RETRY_MS); return () => clearInterval(retryTimer); }, [masterEnabled, retryOfflineServers]); useEffect(() => { if (!masterEnabled) return; const unsubs: Array void>> = [ subscribeLibrarySyncProgress(p => { if (!indexedIds.includes(p.serverId)) return; setBusyServerId(p.serverId); if (p.kind === 'ingest_page') { const next = Math.max(ingestCountRef.current[p.serverId] ?? 0, p.ingestedTotal ?? 0); ingestCountRef.current[p.serverId] = next; setProgressByServer(prev => ({ ...prev, [p.serverId]: t('settings.libraryIndexProgressIngest', { count: next }), })); } else if (p.kind === 'tombstoned') { setProgressByServer(prev => ({ ...prev, [p.serverId]: t('settings.libraryIndexProgressVerify', { checked: p.tombstonesChecked ?? 0, deleted: p.tombstonesDeleted ?? 0, }), })); } else if (p.kind === 'phase_changed' && p.phase) { setProgressByServer(prev => ({ ...prev, [p.serverId]: p.phase ?? null })); } }), subscribeLibrarySyncIdle(p => { if (!indexedIds.includes(p.serverId)) return; setBusyServerId(cur => (cur === p.serverId ? null : cur)); ingestCountRef.current[p.serverId] = 0; setProgressByServer(prev => ({ ...prev, [p.serverId]: null })); void refreshAllStatuses(); if (!p.ok && p.error) { showToast(t('settings.libraryIndexSyncError', { error: p.error }), 5000, 'error'); } }), ]; return () => { unsubs.forEach(u => void u.then(fn => fn())); }; }, [masterEnabled, indexedIds, refreshAllStatuses, t]); const handleMasterToggle = async (enabled: boolean) => { if (enabled) { setMasterEnabled(true); await runBootstrap(); return; } setBootstrapping(true); try { for (const srv of servers) { try { await librarySyncClearSession(srv.id); } catch { /* best-effort */ } } setMasterEnabled(false); setStatusByServer({}); setConnectionByServer({}); setProgressByServer({}); setBusyServerId(null); } finally { setBootstrapping(false); } }; const runServerAction = async ( serverId: string, action: 'full' | 'delta' | 'verify', ) => { setBusyServerId(serverId); try { const kind = action === 'verify' ? 'verify' : action === 'full' ? 'full' : statusByServer[serverId]?.lastFullSyncAt ? 'delta' : 'full'; ingestCountRef.current[serverId] = 0; await enqueueLibrarySync({ serverId, kind }); } catch (e) { setBusyServerId(null); showToast(t('settings.libraryIndexSyncError', { error: String(e) }), 5000, 'error'); } }; const handleIncludeServer = async (serverId: string) => { if (includingServerId || excludingServerId) return; const srv = servers.find(s => s.id === serverId); if (!srv) return; flushSync(() => { setIncludingServerId(serverId); setServerSyncExcluded(serverId, false); }); try { const result = await bootstrapIndexedServer(srv); applyConnectionResults({ [serverId]: result }); if (result === 'error') { setServerSyncExcluded(serverId, true); showToast(t('settings.libraryIndexBindError', { error: t('settings.libraryIndexStatusError') }), 5000, 'error'); return; } try { const fresh = await libraryGetStatus(serverId); syncPhaseRef.current[serverId] = fresh.syncPhase; setStatusByServer(prev => ({ ...prev, [serverId]: fresh })); } catch { /* status poll is best-effort */ } } catch (e) { setServerSyncExcluded(serverId, true); showToast(t('settings.libraryIndexBindError', { error: String(e) }), 5000, 'error'); } finally { setIncludingServerId(null); } }; const handleExcludeServer = async (serverId: string) => { if (excludingServerId || includingServerId) return; flushSync(() => setExcludingServerId(serverId)); try { const syncing = busyServerId === serverId || statusByServer[serverId]?.syncPhase === 'initial_sync' || statusByServer[serverId]?.syncPhase === 'probing'; if (syncing) { try { await librarySyncCancel(); await waitForLibrarySyncIdle(serverId); } catch { /* best-effort — proceed with unbind */ } } await librarySyncClearSession(serverId); setServerSyncExcluded(serverId, true); setStatusByServer(prev => { const next = { ...prev }; delete next[serverId]; return next; }); setConnectionByServer(prev => { const next = { ...prev }; delete next[serverId]; return next; }); setProgressByServer(prev => { const next = { ...prev }; delete next[serverId]; return next; }); if (busyServerId === serverId) { setBusyServerId(null); } } catch (e) { showToast(t('settings.libraryIndexBindError', { error: String(e) }), 5000, 'error'); } finally { setExcludingServerId(null); } }; const handleCancel = async () => { try { await librarySyncCancel(); } catch { /* best-effort */ } }; const globalBusy = bootstrapping || busyServerId != null || excludingServerId != null || includingServerId != null; return ( } >

{t('settings.libraryIndexDesc')}

{t('settings.libraryIndexDeltaHint')}

{t('settings.libraryIndexEnable')}
{servers.length > 0 ? t('settings.libraryIndexEnableAllDesc') : t('settings.libraryIndexNoServer')}
{masterEnabled && ( <>
{t('settings.libraryIndexServerListTitle')}
{indexedServers.length === 0 ? (

{t('settings.libraryIndexAllExcluded')}

) : (
{indexedServers.map(srv => ( void runServerAction(srv.id, 'full')} onDeltaSync={() => void runServerAction(srv.id, 'delta')} onVerify={() => void runServerAction(srv.id, 'verify')} onExclude={() => void handleExcludeServer(srv.id)} /> ))}
)} {excludedServers.length > 0 && ( <>
{t('settings.libraryIndexExcludedTitle')}
{excludedServers.map(srv => (
{serverListDisplayLabel(srv, servers)}
))}
)} {busyServerId && (
)}
{t('settings.libraryIndexAutoReconcile')}
{t('settings.libraryIndexAutoReconcileDesc')}
)}
); }