Add backend unit test framework and modularize settings UI

Introduced a Vitest-based backend unit testing framework with supporting scripts, helpers, and GitHub Actions integration. Refactored the admin settings page to a modular architecture, splitting monolithic logic into feature-specific tabs and hooks for improved maintainability and testability. Updated documentation to reflect the new testing setup and settings architecture, and added new dependencies for testing utilities.
This commit is contained in:
kikootwo
2026-01-15 16:49:59 -05:00
parent b3f89d67bb
commit 94dbaf073b
127 changed files with 23549 additions and 2868 deletions
@@ -0,0 +1,183 @@
/**
* Component: Indexers Settings Tab
* Documentation: documentation/settings-pages.md
*/
'use client';
import React, { useEffect } from 'react';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { IndexerManagement } from '@/components/admin/indexers/IndexerManagement';
import { FlagConfigRow } from '@/components/admin/FlagConfigRow';
import { IndexerFlagConfig } from '@/lib/utils/ranking-algorithm';
import { useIndexersSettings } from './useIndexersSettings';
import type { Settings, SavedIndexerConfig } from '../../lib/types';
interface IndexersTabProps {
settings: Settings;
indexers: SavedIndexerConfig[];
flagConfigs: IndexerFlagConfig[];
onChange: (settings: Settings) => void;
onIndexersChange: (indexers: SavedIndexerConfig[]) => void;
onFlagConfigsChange: (configs: IndexerFlagConfig[]) => void;
onValidationChange: (isValid: boolean) => void;
onRefreshIndexers?: () => Promise<void>;
}
export function IndexersTab({
settings,
indexers,
flagConfigs,
onChange,
onIndexersChange,
onFlagConfigsChange,
onValidationChange,
onRefreshIndexers,
}: IndexersTabProps) {
const { testing, testResult, testConnection } = useIndexersSettings({
prowlarrUrl: settings.prowlarr.url,
prowlarrApiKey: settings.prowlarr.apiKey,
onValidationChange,
onRefreshIndexers,
});
// Auto-load indexers when component mounts if prowlarr is configured
useEffect(() => {
if (settings.prowlarr.url && settings.prowlarr.apiKey && onRefreshIndexers) {
onRefreshIndexers();
}
// Only run on mount, not when settings change
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="space-y-6 max-w-4xl">
<div>
<h2 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">
Indexer Configuration
</h2>
<p className="text-gray-600 dark:text-gray-400 mb-6">
Configure your Prowlarr connection and manage which indexers to use with priority and seeding time.
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Prowlarr Server URL
</label>
<Input
type="url"
value={settings.prowlarr.url}
onChange={(e) => {
onChange({
...settings,
prowlarr: { ...settings.prowlarr, url: e.target.value },
});
onValidationChange(false);
}}
placeholder="http://localhost:9696"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Prowlarr API Key
</label>
<Input
type="password"
value={settings.prowlarr.apiKey}
onChange={(e) => {
onChange({
...settings,
prowlarr: { ...settings.prowlarr, apiKey: e.target.value },
});
onValidationChange(false);
}}
placeholder="Enter API key"
/>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Found in Prowlarr Settings General Security API Key
</p>
</div>
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
<Button
onClick={testConnection}
loading={testing}
disabled={!settings.prowlarr.url || !settings.prowlarr.apiKey}
variant="outline"
className="w-full"
>
Test Connection
</Button>
{testResult && (
<div className={`mt-3 p-3 rounded-lg text-sm ${
testResult.success
? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 text-green-800 dark:text-green-200'
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-800 dark:text-red-200'
}`}>
{testResult.message}
</div>
)}
</div>
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
<IndexerManagement
prowlarrUrl={settings.prowlarr.url}
prowlarrApiKey={settings.prowlarr.apiKey}
mode="settings"
initialIndexers={indexers}
onIndexersChange={onIndexersChange}
/>
</div>
{/* Flag Configuration Section */}
<div className="border-t border-gray-200 dark:border-gray-700 pt-6">
<div className="mb-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
Indexer Flag Configuration (Optional)
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Configure score bonuses or penalties for indexer flags like "Freeleech".
These modifiers apply universally across all indexers and affect final torrent ranking.
</p>
</div>
{flagConfigs.length > 0 && (
<div className="space-y-3 mb-4">
{flagConfigs.map((config, index) => (
<FlagConfigRow
key={index}
config={config}
onChange={(updated) => {
const newConfigs = [...flagConfigs];
newConfigs[index] = updated;
onFlagConfigsChange(newConfigs);
}}
onRemove={() => {
onFlagConfigsChange(flagConfigs.filter((_, i) => i !== index));
}}
/>
))}
</div>
)}
<Button
onClick={() => {
onFlagConfigsChange([...flagConfigs, { name: '', modifier: 0 }]);
}}
variant="outline"
size="sm"
>
+ Add Flag Rule
</Button>
{flagConfigs.length === 0 && (
<p className="text-sm text-gray-500 dark:text-gray-400 mt-3 italic">
No flag rules configured. Flag bonuses/penalties are optional.
</p>
)}
</div>
</div>
);
}