import { useState } from 'react'; import { useNavigate } from 'react-router'; import { useTranslation } from 'react-i18next'; import { Window, Card, Button, SegmentedControl, Callout } from '@olly/modern-sk'; import { TrackRow } from '../../components/track/TrackRow'; import { LoadingSkeleton } from '../../components/common/LoadingSkeleton'; import { EmptyState } from '../../components/common/EmptyState'; import { ErrorState } from '../../components/common/ErrorState'; import { usePermissions } from '../../hooks/usePermissions'; import { useIsOffline } from '../../hooks/useConnectionStatus'; import { useGetDuplicatesQuery, useGetBrokenFilesQuery, useGetMissingMetadataQuery, useRunCleanupMutation, } from '../../api/endpoints/storage'; const PAGE_SIZE = 50; type Tab = 'duplicates' | 'broken' | 'missing'; /** `/storage/maintenance` — A6 maintenance: duplicates, broken files, missing * metadata, and orphan cleanup. */ export function StorageMaintenancePage() { const { t } = useTranslation(); const offline = useIsOffline(); const [tab, setTab] = useState('duplicates'); return (

{t('storage.maintenance.subtitle')}

{offline && ( {t('common.offlineBanner')} )}
setTab(v as Tab)} items={[ { value: 'duplicates', label: t('storage.maintenance.tabs.duplicates') }, { value: 'broken', label: t('storage.maintenance.tabs.broken') }, { value: 'missing', label: t('storage.maintenance.tabs.missing') }, ]} />
{/* Only the active tab is mounted, so only its query fires. */} {tab === 'duplicates' && } {tab === 'broken' && ( )} {tab === 'missing' && ( )}
); } /** Orphan-file cleanup — admin only (enqueues a worker job). */ function CleanupSection() { const { t } = useTranslation(); const { isAdmin } = usePermissions(); const [runCleanup, { isLoading }] = useRunCleanupMutation(); const [done, setDone] = useState(false); if (!isAdmin) return null; const handle = async () => { setDone(false); await runCleanup().unwrap(); setDone(true); }; return (
{t('storage.maintenance.cleanup.title')}
{t('storage.maintenance.cleanup.description')}
{done && ( {t('storage.maintenance.cleanup.enqueued')} )}
); } /** Duplicate groups (tracks sharing an acoustic fingerprint). */ function DuplicatesTab() { const { t } = useTranslation(); const navigate = useNavigate(); const { data, isLoading, isError, refetch } = useGetDuplicatesQuery(); if (isLoading) return ; if (isError) return ; if (!data || data.length === 0) { return ( ); } return (
{data.map((group) => (
{t('storage.maintenance.duplicates.groupTitle', { count: group.tracks.length, })}
{group.tracks.map((track) => ( void navigate(`/tracks/${tr.id}/metadata`)} /> ))}
))}
); } /** A paginated track list backed by a maintenance query (broken / missing). */ function TrackListTab({ useQuery, emptyKey, }: { useQuery: | typeof useGetBrokenFilesQuery | typeof useGetMissingMetadataQuery; emptyKey: 'broken' | 'missing'; }) { const { t } = useTranslation(); const navigate = useNavigate(); const [offset, setOffset] = useState(0); const { data, isLoading, isError, refetch } = useQuery({ limit: PAGE_SIZE, offset, }); if (isLoading) return ; if (isError) return ; if (!data || data.items.length === 0) { return ( ); } const from = offset + 1; const to = offset + data.items.length; // Broken files map to the `failed` metadata bucket; missing → `pending`. const batchStatus = emptyKey === 'broken' ? 'failed' : 'pending'; return (
{data.items.map((track) => ( void navigate(`/tracks/${tr.id}/metadata`)} /> ))} {(offset > 0 || data.hasMore) && (
{t('storage.maintenance.pagination', { from, to, total: data.total, })}
)}
); }