import { useState, useEffect } from 'react' import DirectoryBrowser from './DirectoryBrowser' import { useConfig, useSaveConfig } from '../hooks/useConfig' import { apiFetch } from '../lib/api' function fmtSeconds(s) { if (s == null) return '' const m = Math.round(parseFloat(s) / 60) return String(m) } export default function ConfigModal({ onClose }) { const { data: config, isLoading } = useConfig() const { mutate: save, isPending } = useSaveConfig() const [form, setForm] = useState({}) const [browser, setBrowser] = useState(null) // 'target_dir' | 'results_dir' | null const [scanResult, setScanResult] = useState(null) // { testCount, completedCount } | null const [saveError, setSaveError] = useState(null) const [isRescanning, setIsRescanning] = useState(false) const [isSavingTimes, setIsSavingTimes] = useState(false) useEffect(() => { if (config) { setForm({ target_dir: config.target_dir ?? '', results_dir: config.results_dir ?? '', results_dir_ref: config.results_dir_ref ?? '', avg_time_coe: config.avg_time_coe ? fmtSeconds(config.avg_time_coe) : '', avg_time_p2p: config.avg_time_p2p ? fmtSeconds(config.avg_time_p2p) : '', avg_time_p3p: config.avg_time_p3p ? fmtSeconds(config.avg_time_p3p) : '', smb_username: config.smb_username ?? '', smb_password: config.smb_password ?? '', smb_domain: config.smb_domain ?? '', }) } }, [config]) function handleSave() { setSaveError(null) setScanResult(null) const payload = { target_dir: form.target_dir || null, results_dir: form.results_dir || null, results_dir_ref: form.results_dir_ref || null, // Convert minutes → seconds; empty/null clears the override avg_time_coe: form.avg_time_coe ? String(parseFloat(form.avg_time_coe) * 60) : null, avg_time_p2p: form.avg_time_p2p ? String(parseFloat(form.avg_time_p2p) * 60) : null, avg_time_p3p: form.avg_time_p3p ? String(parseFloat(form.avg_time_p3p) * 60) : null, smb_username: form.smb_username || null, smb_password: form.smb_password || null, smb_domain: form.smb_domain || null, } save(payload, { onSuccess: (data) => { if (data?.testCount !== null && data?.testCount !== undefined) { setScanResult({ testCount: data.testCount, completedCount: data.completedCount }) // Auto-close after showing result only if tests were found if (data.testCount > 0) setTimeout(onClose, 1500) } else { onClose() } }, onError: (err) => { setSaveError(err?.message ?? 'Failed to save settings') }, }) } async function handleSaveTimes() { setSaveError(null) setScanResult(null) setIsSavingTimes(true) try { await apiFetch('/config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ avg_time_coe: form.avg_time_coe ? String(parseFloat(form.avg_time_coe) * 60) : null, avg_time_p2p: form.avg_time_p2p ? String(parseFloat(form.avg_time_p2p) * 60) : null, avg_time_p3p: form.avg_time_p3p ? String(parseFloat(form.avg_time_p3p) * 60) : null, }), }) } catch (err) { setSaveError(err?.message ?? 'Failed to save') } finally { setIsSavingTimes(false) } } async function handleRescanResults() { setSaveError(null) setScanResult(null) setIsRescanning(true) try { const data = await apiFetch('/config/rescan-results', { method: 'POST' }) if (data?.testCount !== null && data?.testCount !== undefined) { setScanResult({ testCount: data.testCount, completedCount: data.completedCount }) } } catch (err) { setSaveError(err?.message ?? 'Rescan failed') } finally { setIsRescanning(false) } } const AVG_FIELDS = [ { key: 'avg_time_coe', label: 'COE avg time (min)' }, { key: 'avg_time_p2p', label: 'P2P avg time (min)' }, { key: 'avg_time_p3p', label: 'P3P avg time (min)' }, ] return ( <>
{/* Header */}

Settings

{/* Error banner */} {saveError && (
{saveError}
)} {/* Scan result banner */} {scanResult !== null && (
{scanResult.testCount === 0 ? 'No tests found — check that the target directory contains TC_WIFI_*.ini files in subdirectories.' : `Found ${scanResult.testCount} tests (${scanResult.completedCount} completed).`}
)} {isLoading ? (

Loading…

) : ( <> {/* Directories */}

Directories

{[ { key: 'target_dir', label: 'Target Tests Directory' }, { key: 'results_dir', label: 'Results Directory (DUT)' }, { key: 'results_dir_ref', label: 'Results Directory (Reference)' }, ].map(({ key, label }) => (
setForm(f => ({ ...f, [key]: e.target.value }))} placeholder="C:\path\to\folder" className="flex-1 bg-slate-800 border border-slate-700 text-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-blue-500" />
))}
{/* Avg Time Overrides */}

Avg Time Overrides

Manually set avg minutes per test type. Leave blank to use calculated average from completed tests.

{AVG_FIELDS.map(({ key, label }) => (
setForm(f => ({ ...f, [key]: e.target.value }))} placeholder="auto" className="w-full bg-slate-800 border border-slate-700 text-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-blue-500" />
))}
{/* SMB Credentials */}

SMB Credentials

Optional credentials for network shares used by target/results directories.

setForm(f => ({ ...f, smb_domain: e.target.value }))} placeholder="Domain" className="w-full bg-slate-800 border border-slate-700 text-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-blue-500" /> setForm(f => ({ ...f, smb_username: e.target.value }))} placeholder="Username" className="w-full bg-slate-800 border border-slate-700 text-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-blue-500" /> setForm(f => ({ ...f, smb_password: e.target.value }))} placeholder="Password" className="w-full bg-slate-800 border border-slate-700 text-slate-200 text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-blue-500" />
)}
{/* Footer */}
{browser && ( setForm(f => ({ ...f, [browser]: path }))} onClose={() => setBrowser(null)} /> )} ) }