"use client"; import { useState } from "react"; import { useProviderConfigs, useCreateProvider, useUpdateProvider, useDeleteProvider, useTestProvider, } from "@/hooks/use-admin-providers"; import { useConfig } from "@/hooks/use-config"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { CheckCircle, XCircle, Loader2, Plus, Trash2 } from "lucide-react"; import { ApiRequestError } from "@/lib/api"; import type { ProviderConfig } from "@/lib/types"; const PROVIDER_FIELDS: Record< string, Array<{ key: string; label: string; type?: string; required?: boolean }> > = { jellyfin: [ { key: "base_url", label: "Base URL", required: true }, { key: "api_key", label: "API Key", type: "password", required: true }, { key: "user_id", label: "User ID", required: true }, ], local_files: [ { key: "files_dir", label: "Files Directory", required: true }, { key: "transcode_dir", label: "Transcode Directory" }, { key: "cleanup_ttl_hours", label: "Cleanup TTL Hours" }, ], }; function isValidInstanceId(id: string): boolean { return id.length >= 1 && id.length <= 40 && /^[a-zA-Z0-9-]+$/.test(id); } // --------------------------------------------------------------------------- // Existing instance card // --------------------------------------------------------------------------- interface ProviderCardProps { config: ProviderConfig; existingIds: string[]; } function ProviderCard({ config }: ProviderCardProps) { const fields = PROVIDER_FIELDS[config.provider_type] ?? []; const [formValues, setFormValues] = useState>( () => config.config_json ?? {}, ); const [enabled, setEnabled] = useState(config.enabled); const [conflictError, setConflictError] = useState(false); const [testResult, setTestResult] = useState<{ ok: boolean; message: string } | null>(null); const updateProvider = useUpdateProvider(); const deleteProvider = useDeleteProvider(); const testProvider = useTestProvider(); const handleSave = async () => { setConflictError(false); try { await updateProvider.mutateAsync({ id: config.id, payload: { config_json: formValues, enabled }, }); } catch (e: unknown) { if (e instanceof ApiRequestError && e.status === 409) { setConflictError(true); } } }; const handleTest = async () => { setTestResult(null); const result = await testProvider.mutateAsync({ provider_type: config.provider_type, config_json: formValues, }); setTestResult(result); }; const handleDelete = async () => { if (!confirm(`Delete provider instance "${config.id}"?`)) return; await deleteProvider.mutateAsync(config.id); }; return (
{config.id} {config.provider_type.replace("_", " ")}
Enabled
{conflictError && (
UI config disabled — set CONFIG_SOURCE=db on the server
)} {fields.map((field) => (
setFormValues((prev) => ({ ...prev, [field.key]: e.target.value })) } placeholder={ field.type === "password" ? "••••••••" : `Enter ${field.label.toLowerCase()}` } className="h-8 border-zinc-700 bg-zinc-800 text-xs text-zinc-100" />
))} {testResult && (
{testResult.ok ? ( ) : ( )} {testResult.message}
)}
); } // --------------------------------------------------------------------------- // Add Instance dialog // --------------------------------------------------------------------------- interface AddInstanceDialogProps { open: boolean; onClose: () => void; availableTypes: string[]; existingIds: string[]; } function AddInstanceDialog({ open, onClose, availableTypes, existingIds }: AddInstanceDialogProps) { const [instanceId, setInstanceId] = useState(""); const [providerType, setProviderType] = useState(availableTypes[0] ?? ""); const [formValues, setFormValues] = useState>({}); const [idError, setIdError] = useState(null); const [apiError, setApiError] = useState(null); const createProvider = useCreateProvider(); const testProvider = useTestProvider(); const [testResult, setTestResult] = useState<{ ok: boolean; message: string } | null>(null); const fields = PROVIDER_FIELDS[providerType] ?? []; const handleTypeChange = (t: string) => { setProviderType(t); setFormValues({}); setTestResult(null); }; const validateId = (id: string): string | null => { if (!id) return "ID is required"; if (!isValidInstanceId(id)) return "Only alphanumeric characters and hyphens, 1–40 chars"; if (existingIds.includes(id)) return "An instance with this ID already exists"; return null; }; const handleCreate = async () => { const err = validateId(instanceId); if (err) { setIdError(err); return; } setIdError(null); setApiError(null); try { await createProvider.mutateAsync({ id: instanceId, provider_type: providerType, config_json: formValues, enabled: true, }); onClose(); setInstanceId(""); setFormValues({}); setTestResult(null); } catch (e: unknown) { if (e instanceof ApiRequestError && e.status === 409) { setIdError("An instance with this ID already exists"); } else if (e instanceof ApiRequestError) { setApiError(e.message); } } }; const handleTest = async () => { setTestResult(null); const result = await testProvider.mutateAsync({ provider_type: providerType, config_json: formValues, }); setTestResult(result); }; return ( { if (!v) onClose(); }}> Add Provider Instance
{ setInstanceId(e.target.value); setIdError(null); }} placeholder="e.g. jellyfin-main" className="h-8 border-zinc-700 bg-zinc-800 text-xs text-zinc-100 font-mono" /> {idError &&

{idError}

}

Alphanumeric + hyphens, 1–40 chars

{fields.map((field) => (
setFormValues((prev) => ({ ...prev, [field.key]: e.target.value })) } placeholder={ field.type === "password" ? "••••••••" : `Enter ${field.label.toLowerCase()}` } className="h-8 border-zinc-700 bg-zinc-800 text-xs text-zinc-100" />
))} {testResult && (
{testResult.ok ? ( ) : ( )} {testResult.message}
)} {apiError && (

{apiError}

)}
); } // --------------------------------------------------------------------------- // Panel // --------------------------------------------------------------------------- export function ProviderSettingsPanel() { const { data: config } = useConfig(); const { data: providerConfigs = [] } = useProviderConfigs(); const [addOpen, setAddOpen] = useState(false); const availableTypes = config?.available_provider_types ?? []; const existingIds = providerConfigs.map((c) => c.id); return (

Provider Instances

Manage media provider instances. Requires CONFIG_SOURCE=db on the server.

{availableTypes.length > 0 && ( )}
{availableTypes.length === 0 ? (

No providers available in this build.

) : providerConfigs.length === 0 ? (

No provider instances configured. Click "Add Instance" to get started.

) : (
{providerConfigs.map((c) => ( ))}
)} setAddOpen(false)} availableTypes={availableTypes} existingIds={existingIds} />
); }