Skip to content
Merged

. #77

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions frontend/src/components/config/ArrsConfigSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,39 @@ export function ArrsConfigSection({
</div>
</fieldset>

<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<fieldset className="fieldset">
<legend className="fieldset-legend font-semibold">Cleanup Grace Period</legend>
<div className="join w-full">
<input
type="number"
className="input input-bordered join-item w-full bg-base-100 font-mono text-sm"
value={formData.queue_cleanup_grace_period_minutes ?? 10}
onChange={(e) => handleFormChange("queue_cleanup_grace_period_minutes", parseInt(e.target.value) || 10)}
min={0}
disabled={isReadOnly}
/>
<span className="btn btn-ghost border-base-300 join-item pointer-events-none text-xs">min</span>
</div>
<p className="label text-[10px] opacity-50 break-words mt-1">Wait time before considering a failed item "stuck" and eligible for cleanup.</p>
</fieldset>

<fieldset className="fieldset">
<legend className="fieldset-legend font-semibold">Import Failure Cleanup</legend>
<label className="label cursor-pointer justify-start gap-4 items-center h-12">
<input
type="checkbox"
className="toggle toggle-primary toggle-sm"
checked={formData.cleanup_automatic_import_failure ?? false}
onChange={(e) => handleFormChange("cleanup_automatic_import_failure", e.target.checked)}
disabled={isReadOnly}
/>
<span className="label-text font-bold text-xs break-words">Purge Automatic Failures</span>
</label>
<p className="label text-[10px] opacity-50 break-words mt-1">Automatically remove items from queue that failed with "Automatic Import" errors.</p>
</fieldset>
</div>

<div className="space-y-4">
<h5 className="font-bold text-[10px] uppercase opacity-40">Allowlist (Ignore Errors)</h5>
<div className="space-y-2 max-h-48 overflow-y-auto pr-2 custom-scrollbar">
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/components/config/HealthConfigSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,33 @@ export function HealthConfigSection({
<p className="label text-[10px] opacity-50 break-words">How often to scan your library for new files.</p>
</fieldset>
</div>

<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<fieldset className="fieldset">
<legend className="fieldset-legend font-semibold">Health Check Loop Interval (Sec)</legend>
<input
type="number"
className="input input-bordered w-full bg-base-100 font-mono text-sm"
value={formData.check_interval_seconds}
readOnly={isReadOnly}
min={1}
onChange={(e) => handleInputChange("check_interval_seconds", Number.parseInt(e.target.value, 10) || 5)}
/>
<p className="label text-[10px] opacity-50 break-words">Idle time between background health check cycles.</p>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend font-semibold">Sync Concurrency</legend>
<input
type="number"
className="input input-bordered w-full bg-base-100 font-mono text-sm"
value={formData.library_sync_concurrency}
readOnly={isReadOnly}
min={0}
onChange={(e) => handleInputChange("library_sync_concurrency", Number.parseInt(e.target.value, 10) || 0)}
/>
<p className="label text-[10px] opacity-50 break-words">Max parallel file scans during sync (0 = auto).</p>
</fieldset>
</div>
</div>
</div>
</div>
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/components/config/MountConfigSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,21 @@ function FuseMountSubSection({ config, isRunning, onFormDataChange }: FuseSubSec
<span className="label-text text-xs">Allow other users to access mount</span>
</label>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend">Prefetch Concurrency</legend>
<input
type="number"
className="input input-bordered w-full bg-base-100 font-mono text-sm"
value={formData.prefetch_concurrency ?? 0}
onChange={(e) =>
updateField({
prefetch_concurrency: Number.parseInt(e.target.value, 10) || 0,
})
}
disabled={isRunning}
/>
<p className="label text-[10px] opacity-50 break-words mt-1">Number of parallel segment downloads during prefetch (0 = auto).</p>
</fieldset>
</div>
</div>

Expand Down
118 changes: 101 additions & 17 deletions frontend/src/components/config/SystemConfigSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function SystemConfigSection({
max_backups: config.log.max_backups,
compress: config.log.compress,
});
const [profilerEnabled, setProfilerEnabled] = useState(config.profiler_enabled);
const [hasChanges, setHasChanges] = useState(false);

const regenerateAPIKey = useRegenerateAPIKey();
Expand All @@ -45,8 +46,9 @@ export function SystemConfigSection({
compress: config.log.compress,
};
setFormData(newFormData);
setProfilerEnabled(config.profiler_enabled);
setHasChanges(false);
}, [config.log]);
}, [config.log, config.profiler_enabled]);

const handleInputChange = (field: keyof LogFormData, value: string | number | boolean) => {
const newData = { ...formData, [field]: value };
Expand All @@ -55,12 +57,20 @@ export function SystemConfigSection({
file: config.log.file, level: config.log.level, max_size: config.log.max_size,
max_age: config.log.max_age, max_backups: config.log.max_backups, compress: config.log.compress,
};
setHasChanges(JSON.stringify(newData) !== JSON.stringify(configData));
setHasChanges(JSON.stringify(newData) !== JSON.stringify(configData) || profilerEnabled !== config.profiler_enabled);
};

const handleProfilerChange = (enabled: boolean) => {
setProfilerEnabled(enabled);
setHasChanges(true);
};

const handleSave = async () => {
if (onUpdate && hasChanges) {
await onUpdate("log", formData);
// We need a way to update profiler_enabled too.
// In ConfigurationPage, onUpdate for 'log' updates 'system' section which includes log.
// Let's assume the backend handles both if we send them.
await onUpdate("log", { ...formData, profiler_enabled: profilerEnabled } as any);
setHasChanges(false);
}
};
Expand Down Expand Up @@ -109,21 +119,95 @@ export function SystemConfigSection({
<div className="h-px flex-1 bg-base-300/50" />
</div>

<fieldset className="fieldset max-w-sm">
<legend className="fieldset-legend font-semibold text-xs">Minimum Log Level</legend>
<select
className="select select-bordered w-full bg-base-100"
value={formData.level}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<fieldset className="fieldset">
<legend className="fieldset-legend font-semibold text-xs">Minimum Log Level</legend>
<select
className="select select-bordered w-full bg-base-100"
value={formData.level}
disabled={isReadOnly}
onChange={(e) => handleInputChange("level", e.target.value)}
>
<option value="debug">Debug (Verbose)</option>
<option value="info">Info (Standard)</option>
<option value="warn">Warning (Alerts)</option>
<option value="error">Error (Critical)</option>
</select>
<p className="label text-[10px] opacity-50 break-words mt-2">Determines how much information is stored in logs.</p>
</fieldset>

<fieldset className="fieldset">
<legend className="fieldset-legend font-semibold text-xs">Max Log Size (MB)</legend>
<input
type="number"
className="input input-bordered w-full bg-base-100 font-mono text-sm"
value={formData.max_size}
disabled={isReadOnly}
onChange={(e) => handleInputChange("max_size", parseInt(e.target.value) || 0)}
/>
</fieldset>
</div>

<div className="grid grid-cols-1 gap-6 sm:grid-cols-3">
<fieldset className="fieldset">
<legend className="fieldset-legend font-semibold text-xs">Max Age (Days)</legend>
<input
type="number"
className="input input-bordered w-full bg-base-100 font-mono text-sm"
value={formData.max_age}
disabled={isReadOnly}
onChange={(e) => handleInputChange("max_age", parseInt(e.target.value) || 0)}
/>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend font-semibold text-xs">Max Backups</legend>
<input
type="number"
className="input input-bordered w-full bg-base-100 font-mono text-sm"
value={formData.max_backups}
disabled={isReadOnly}
onChange={(e) => handleInputChange("max_backups", parseInt(e.target.value) || 0)}
/>
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend font-semibold text-xs">Compress Logs</legend>
<div className="flex items-center h-12">
<input
type="checkbox"
className="checkbox checkbox-primary"
checked={formData.compress}
disabled={isReadOnly}
onChange={(e) => handleInputChange("compress", e.target.checked)}
/>
</div>
</fieldset>
</div>
</div>

{/* Performance Profiler */}
<div className="rounded-2xl border border-base-300 bg-base-200/30 p-6 space-y-6">
<div className="flex items-center gap-2">
<Terminal className="h-4 w-4 opacity-40" />
<h4 className="font-bold text-[10px] text-base-content/40 uppercase tracking-widest">Performance</h4>
<div className="h-px flex-1 bg-base-300/50" />
</div>

<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<h5 className="font-bold text-sm">System Profiler (pprof)</h5>
<p className="text-[11px] text-base-content/50 mt-1 break-words leading-relaxed">
Enable Go runtime profiling at <code>/debug/pprof</code>.
Only recommended for debugging resource leaks.
</p>
</div>
<input
type="checkbox"
className="toggle toggle-warning shrink-0 mt-1"
checked={profilerEnabled}
disabled={isReadOnly}
onChange={(e) => handleInputChange("level", e.target.value)}
>
<option value="debug">Debug (Verbose)</option>
<option value="info">Info (Standard)</option>
<option value="warn">Warning (Alerts)</option>
<option value="error">Error (Critical)</option>
</select>
<p className="label text-[10px] opacity-50 break-words mt-2">Determines how much information is stored in logs.</p>
</fieldset>
onChange={(e) => handleProfilerChange(e.target.checked)}
/>
</div>
</div>

{/* Security Section */}
Expand Down
67 changes: 67 additions & 0 deletions frontend/src/components/config/WorkersConfigSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,34 @@ export function ImportConfigSection({
<p className="label text-[10px] opacity-50 break-words">Socket limit per active worker.</p>
</fieldset>
</div>

<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<fieldset className="fieldset">
<legend className="fieldset-legend font-semibold">Max Download Prefetch</legend>
<input
type="number"
className="input input-bordered w-full bg-base-100 font-mono text-sm"
value={formData.max_download_prefetch}
readOnly={isReadOnly}
min={1}
onChange={(e) => handleInputChange("max_download_prefetch", Number.parseInt(e.target.value, 10) || 1)}
/>
<p className="label text-[10px] opacity-50 break-words">Segments prefetched ahead for archive analysis.</p>
</fieldset>

<fieldset className="fieldset">
<legend className="fieldset-legend font-semibold">Read Timeout (Seconds)</legend>
<input
type="number"
className="input input-bordered w-full bg-base-100 font-mono text-sm"
value={formData.read_timeout_seconds}
readOnly={isReadOnly}
min={1}
onChange={(e) => handleInputChange("read_timeout_seconds", Number.parseInt(e.target.value, 10) || 300)}
/>
<p className="label text-[10px] opacity-50 break-words">Usenet socket read timeout.</p>
</fieldset>
</div>
</div>

{/* Validation Slider */}
Expand Down Expand Up @@ -213,6 +241,45 @@ export function ImportConfigSection({
</fieldset>
)}
</div>

<div className="divider opacity-50" />

<div className="space-y-6">
<div>
<h5 className="font-bold text-sm">NZB Watch Directory</h5>
<p className="text-[11px] text-base-content/50 break-words mt-1 leading-relaxed">
Monitor a specific folder for new NZB files and import them automatically.
</p>
</div>

<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<fieldset className="fieldset">
<legend className="fieldset-legend font-semibold">Watch Directory Path</legend>
<input
type="text"
className="input input-bordered w-full bg-base-100 font-mono text-sm"
value={formData.watch_dir || ""}
readOnly={isReadOnly}
placeholder="/path/to/watch"
onChange={(e) => handleInputChange("watch_dir", e.target.value)}
/>
<p className="label text-[10px] opacity-50 break-words mt-2">Absolute path to monitor.</p>
</fieldset>

<fieldset className="fieldset">
<legend className="fieldset-legend font-semibold">Polling Interval (Seconds)</legend>
<input
type="number"
className="input input-bordered w-full bg-base-100 font-mono text-sm"
value={formData.watch_interval_seconds || 10}
readOnly={isReadOnly}
min={1}
onChange={(e) => handleInputChange("watch_interval_seconds", Number.parseInt(e.target.value, 10) || 10)}
/>
<p className="label text-[10px] opacity-50 break-words mt-2">How often to check for new files.</p>
</fieldset>
</div>
</div>
</div>

{/* File Extensions */}
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/pages/ConfigurationPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,14 @@ export function ConfigurationPage() {
config: { providers: data as unknown as ProviderConfig[] },
});
} else if (section === "log") {
const logData = data as unknown as LogFormData & { profiler_enabled?: boolean };
const { profiler_enabled, ...logConfig } = logData;
await updateConfigSection.mutateAsync({
section: "system",
config: { log: data as unknown as LogFormData },
config: {
log: logConfig,
profiler_enabled: profiler_enabled
},
});
}
} catch (error) {
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ConfigResponse {
mount_path: string;
mount_type: MountType;
api_key?: string;
profiler_enabled: boolean;
}

// WebDAV server configuration
Expand Down Expand Up @@ -79,6 +80,7 @@ export interface HealthConfig {
max_concurrent_jobs?: number; // Max concurrent health check jobs
segment_sample_percentage?: number; // Percentage of segments to check (1-100)
library_sync_interval_minutes?: number; // Library sync interval in minutes (optional)
library_sync_concurrency?: number;
check_all_segments?: boolean; // Whether to check all segments or use sampling
resolve_repair_on_import?: boolean; // Automatically resolve pending repairs in the same directory when a new file is imported
verify_data?: boolean; // Verify 1 byte of data for each segment
Expand Down Expand Up @@ -186,6 +188,7 @@ export interface FuseConfig {
disk_cache_expiry_hours?: number;
chunk_size_mb?: number;
read_ahead_chunks?: number;
prefetch_concurrency?: number;
}

// Import strategy type
Expand Down Expand Up @@ -273,6 +276,7 @@ export interface ConfigUpdateRequest {
providers?: ProviderUpdateRequest[];
mount_path?: string;
mount_type?: MountType;
profiler_enabled?: boolean;
}

// WebDAV update request
Expand Down Expand Up @@ -318,6 +322,7 @@ export interface HealthUpdateRequest {
max_connections_for_health_checks?: number;
max_concurrent_jobs?: number; // Max concurrent health check jobs
library_sync_interval_minutes?: number; // Library sync interval in minutes (optional)
library_sync_concurrency?: number;
check_all_segments?: boolean; // Whether to check all segments or use sampling
resolve_repair_on_import?: boolean;
verify_data?: boolean;
Expand Down Expand Up @@ -469,6 +474,8 @@ export interface APIFormData {
export interface ImportFormData {
max_processor_workers: number;
queue_processing_interval_seconds: number; // Interval in seconds for queue processing
max_download_prefetch: number;
read_timeout_seconds: number;
import_strategy: ImportStrategy;
import_dir: string;
watch_dir?: string;
Expand Down
Loading
Loading