Refactor indexer management and improve search logic

Refactors admin settings to use a new IndexersTab and card-based indexer management UI, supporting category selection and improved configuration. Updates backend and API routes to handle indexer categories, propagate ASIN for better search scoring, and group indexers by categories to optimize Prowlarr searches. Enhances documentation to clarify non-terminal request matching and auto-completion behavior. Adds new reusable components for indexer management and category selection.
This commit is contained in:
kikootwo
2026-01-13 21:32:54 -05:00
parent e346f88f42
commit 307b63fab4
30 changed files with 1787 additions and 671 deletions
@@ -0,0 +1,285 @@
/**
* Component: Indexer Management Container
* Documentation: documentation/frontend/components.md
*/
'use client';
import React, { useState, useEffect } from 'react';
import { Button } from '@/components/ui/Button';
import { IndexerCard } from './IndexerCard';
import { IndexerConfigModal } from './IndexerConfigModal';
import { AvailableIndexerRow } from './AvailableIndexerRow';
import { DeleteConfirmModal } from './DeleteConfirmModal';
import { fetchWithAuth } from '@/lib/utils/api';
interface ProwlarrIndexer {
id: number;
name: string;
protocol: string;
supportsRss: boolean;
}
interface SavedIndexerConfig {
id: number;
name: string;
priority: number;
seedingTimeMinutes: number;
rssEnabled: boolean;
categories: number[];
}
interface IndexerManagementProps {
prowlarrUrl: string;
prowlarrApiKey: string;
mode: 'wizard' | 'settings';
initialIndexers?: SavedIndexerConfig[];
onIndexersChange?: (indexers: SavedIndexerConfig[]) => void;
}
export function IndexerManagement({
prowlarrUrl,
prowlarrApiKey,
mode,
initialIndexers = [],
onIndexersChange,
}: IndexerManagementProps) {
const [fetchedIndexers, setFetchedIndexers] = useState<ProwlarrIndexer[]>([]);
const [configuredIndexers, setConfiguredIndexers] = useState<SavedIndexerConfig[]>(initialIndexers);
const [modalState, setModalState] = useState<{
isOpen: boolean;
mode: 'add' | 'edit';
indexer?: ProwlarrIndexer;
currentConfig?: SavedIndexerConfig;
}>({ isOpen: false, mode: 'add' });
const [deleteModalState, setDeleteModalState] = useState<{
isOpen: boolean;
indexerId?: number;
indexerName?: string;
}>({ isOpen: false });
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Sync with parent when configuredIndexers changes
useEffect(() => {
if (onIndexersChange) {
onIndexersChange(configuredIndexers);
}
}, [configuredIndexers, onIndexersChange]);
// Sync with initialIndexers prop changes
useEffect(() => {
setConfiguredIndexers(initialIndexers);
}, [initialIndexers]);
const fetchIndexers = async () => {
setLoading(true);
setError(null);
try {
const endpoint = mode === 'wizard'
? '/api/setup/test-prowlarr'
: '/api/admin/settings/test-prowlarr';
// Use fetchWithAuth for settings mode (requires authentication)
// Use plain fetch for wizard mode (no auth required)
const response = mode === 'settings'
? await fetchWithAuth(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: prowlarrUrl,
apiKey: prowlarrApiKey,
}),
})
: await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: prowlarrUrl,
apiKey: prowlarrApiKey,
}),
});
const data = await response.json();
if (!response.ok || !data.success) {
throw new Error(data.error || 'Failed to fetch indexers');
}
setFetchedIndexers(data.indexers || []);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch indexers');
setFetchedIndexers([]);
} finally {
setLoading(false);
}
};
const openAddModal = (indexer: ProwlarrIndexer) => {
setModalState({
isOpen: true,
mode: 'add',
indexer,
});
};
const openEditModal = (config: SavedIndexerConfig) => {
// Find the full indexer info from fetched list
const indexer = fetchedIndexers.find((idx) => idx.id === config.id);
setModalState({
isOpen: true,
mode: 'edit',
indexer: indexer || {
id: config.id,
name: config.name,
protocol: 'torrent', // Default fallback
supportsRss: config.rssEnabled,
},
currentConfig: config,
});
};
const closeModal = () => {
setModalState({ isOpen: false, mode: 'add' });
};
const handleSave = (config: SavedIndexerConfig) => {
if (modalState.mode === 'add') {
// Add new indexer
setConfiguredIndexers([...configuredIndexers, config]);
} else {
// Update existing indexer
setConfiguredIndexers(
configuredIndexers.map((idx) =>
idx.id === config.id ? config : idx
)
);
}
};
const handleDelete = (id: number) => {
const indexer = configuredIndexers.find((idx) => idx.id === id);
if (!indexer) return;
setDeleteModalState({
isOpen: true,
indexerId: id,
indexerName: indexer.name,
});
};
const confirmDelete = () => {
if (deleteModalState.indexerId) {
setConfiguredIndexers(
configuredIndexers.filter((idx) => idx.id !== deleteModalState.indexerId)
);
}
};
const isIndexerAdded = (id: number) => {
return configuredIndexers.some((idx) => idx.id === id);
};
return (
<div className="space-y-6">
{/* Section 1: Available Indexers */}
<div className="border-b border-gray-200 dark:border-gray-700 pb-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
Available Indexers
</h3>
<Button
onClick={fetchIndexers}
loading={loading}
variant="outline"
disabled={!prowlarrUrl || !prowlarrApiKey}
>
{configuredIndexers.length > 0 || fetchedIndexers.length > 0
? 'Refresh Indexers'
: 'Fetch Indexers'}
</Button>
</div>
{error && (
<div className="mb-4 p-3 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-sm text-red-800 dark:text-red-200">
{error}
</div>
)}
{fetchedIndexers.length > 0 && (
<div className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden max-h-64 overflow-y-auto">
{fetchedIndexers.map((indexer) => (
<AvailableIndexerRow
key={indexer.id}
indexer={indexer}
isAdded={isIndexerAdded(indexer.id)}
onAdd={() => openAddModal(indexer)}
/>
))}
</div>
)}
{!loading && fetchedIndexers.length === 0 && !error && (
<div className="text-sm text-gray-500 dark:text-gray-400 py-6 text-center border border-dashed border-gray-300 dark:border-gray-600 rounded-lg">
{prowlarrUrl && prowlarrApiKey
? 'Click "Fetch Indexers" to load available indexers from Prowlarr.'
: 'Enter Prowlarr URL and API key above, then fetch indexers.'}
</div>
)}
</div>
{/* Section 2: Configured Indexers */}
<div>
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">
Configured Indexers ({configuredIndexers.length})
</h3>
{configuredIndexers.length === 0 ? (
<div className="text-sm text-gray-500 dark:text-gray-400 py-8 text-center border border-dashed border-gray-300 dark:border-gray-600 rounded-lg">
<p className="mb-2">No indexers configured yet</p>
<p className="text-xs">
Fetch indexers from Prowlarr and click "Add" to configure them.
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{configuredIndexers.map((config) => (
<IndexerCard
key={config.id}
indexer={{
id: config.id,
name: config.name,
protocol: 'torrent', // Will be populated correctly from fetched data
}}
onEdit={() => openEditModal(config)}
onDelete={() => handleDelete(config.id)}
/>
))}
</div>
)}
</div>
{/* Config Modal */}
{modalState.isOpen && modalState.indexer && (
<IndexerConfigModal
isOpen={modalState.isOpen}
onClose={closeModal}
mode={modalState.mode}
indexer={modalState.indexer}
initialConfig={modalState.currentConfig}
onSave={handleSave}
/>
)}
{/* Delete Confirmation Modal */}
<DeleteConfirmModal
isOpen={deleteModalState.isOpen}
onClose={() => setDeleteModalState({ isOpen: false })}
onConfirm={confirmDelete}
indexerName={deleteModalState.indexerName || ''}
/>
</div>
);
}