import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Loader2, ScanEye, Check, X, AlertTriangle, Info } from 'lucide-react'; import { api } from '../api/client'; import { Card, CardContent, CardHeader } from './Card'; import { Button } from './Button'; import { Toggle } from './Toggle'; import { useToast } from '../contexts/ToastContext'; type TestResult = { ok: boolean; message: string } | null; export function FailureDetectionSettings() { const { t } = useTranslation(); const queryClient = useQueryClient(); const { showToast } = useToast(); const [enabled, setEnabled] = useState(false); const [mlUrl, setMlUrl] = useState(''); const [sensitivity, setSensitivity] = useState<'low' | 'medium' | 'high'>('medium'); const [action, setAction] = useState<'notify' | 'pause' | 'pause_and_off'>('notify'); const [pollInterval, setPollInterval] = useState(10); const [enabledPrinters, setEnabledPrinters] = useState(null); // null = all const [testResult, setTestResult] = useState(null); const [initialized, setInitialized] = useState(false); const { data: settings } = useQuery({ queryKey: ['settings'], queryFn: api.getSettings, }); const { data: status, refetch: refetchStatus } = useQuery({ queryKey: ['obico-status'], queryFn: api.getObicoStatus, refetchInterval: 10000, }); const { data: printers } = useQuery({ queryKey: ['printers'], queryFn: api.getPrinters, }); useEffect(() => { if (!settings) return; setEnabled(settings.obico_enabled ?? false); setMlUrl(settings.obico_ml_url ?? ''); setSensitivity(settings.obico_sensitivity ?? 'medium'); setAction(settings.obico_action ?? 'notify'); setPollInterval(settings.obico_poll_interval ?? 10); try { const list = settings.obico_enabled_printers ? (JSON.parse(settings.obico_enabled_printers) as number[]) : null; setEnabledPrinters(Array.isArray(list) ? list : null); } catch { setEnabledPrinters(null); } setInitialized(true); }, [settings]); const saveMutation = useMutation({ mutationFn: () => api.updateSettings({ obico_enabled: enabled, obico_ml_url: mlUrl, obico_sensitivity: sensitivity, obico_action: action, obico_poll_interval: pollInterval, obico_enabled_printers: enabledPrinters === null ? '' : JSON.stringify(enabledPrinters), }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['settings'] }); queryClient.invalidateQueries({ queryKey: ['obico-status'] }); showToast(t('settings.toast.settingsSaved')); }, }); // Auto-save on change (debounced) useEffect(() => { if (!initialized || !settings) return; const changed = settings.obico_enabled !== enabled || settings.obico_ml_url !== mlUrl || settings.obico_sensitivity !== sensitivity || settings.obico_action !== action || settings.obico_poll_interval !== pollInterval || settings.obico_enabled_printers !== (enabledPrinters === null ? '' : JSON.stringify(enabledPrinters)); if (!changed) return; const id = setTimeout(() => saveMutation.mutate(), 500); return () => clearTimeout(id); // eslint-disable-next-line react-hooks/exhaustive-deps }, [enabled, mlUrl, sensitivity, action, pollInterval, enabledPrinters, initialized]); const handleTest = async () => { setTestResult(null); try { const res = await api.testObicoConnection(mlUrl); if (res.ok) { setTestResult({ ok: true, message: t('failureDetection.testSuccess') }); } else { setTestResult({ ok: false, message: res.error || `HTTP ${res.status_code ?? '?'} — ${res.body ?? t('failureDetection.testFailed')}`, }); } } catch (e: unknown) { setTestResult({ ok: false, message: e instanceof Error ? e.message : String(e) }); } }; const togglePrinter = (printerId: number, checked: boolean) => { if (enabledPrinters === null) { // switch from "all" to an explicit list const allIds = printers?.map((p) => p.id) ?? []; const next = checked ? allIds : allIds.filter((id) => id !== printerId); setEnabledPrinters(next); return; } if (checked) { setEnabledPrinters([...enabledPrinters, printerId]); } else { setEnabledPrinters(enabledPrinters.filter((id) => id !== printerId)); } }; return (

{t('failureDetection.title')}

{t('failureDetection.description')}

setMlUrl(e.target.value)} placeholder="http://192.168.1.10:3333" className="flex-1 bg-gray-800 border border-gray-700 rounded px-3 py-2 text-white text-sm" disabled={!enabled} />

{t('failureDetection.mlUrlHint')}

{testResult && (
{testResult.ok ? : } {testResult.message}
)}

{t('failureDetection.sensitivityHint')}

setPollInterval(Math.max(5, Math.min(120, Number(e.target.value) || 10)))} min={5} max={120} disabled={!enabled} className="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-white text-sm" />

{t('failureDetection.pollIntervalHint')}

{status && !status.external_url_configured && enabled && (
{t('failureDetection.externalUrlMissing')}
{t('failureDetection.externalUrlHint')}
)}

{t('failureDetection.perPrinterTitle')}

{t('failureDetection.perPrinterHint')}

{enabledPrinters !== null && printers && (
{printers.map((p) => ( ))}
)}

{t('failureDetection.statusTitle')}

{!status ? (
{t('common.loading')}
) : (
{t('failureDetection.serviceRunning')} {status.is_running ? t('common.yes') : t('common.no')}
{t('failureDetection.thresholds')} {status.thresholds.low.toFixed(2)} / {status.thresholds.high.toFixed(2)}
{status.last_error && (
{status.last_error}
)}
{t('failureDetection.activePrinters')}
{Object.keys(status.per_printer).length === 0 ? (
{t('failureDetection.noActivePrints')}
) : (
{Object.entries(status.per_printer).map(([pid, info]) => { const printer = printers?.find((p) => String(p.id) === pid); const colorClass = info.class === 'failure' ? 'text-red-400' : info.class === 'warning' ? 'text-amber-400' : 'text-green-400'; return (
{printer?.name ?? `Printer ${pid}`} {info.class} ({info.score.toFixed(3)}, {info.frame_count}f)
); })}
)}
)}

{t('failureDetection.historyTitle')}

{!status || status.history.length === 0 ? (
{t('failureDetection.noHistory')}
) : (
{status.history.map((ev, idx) => { const printer = printers?.find((p) => p.id === ev.printer_id); const colorClass = ev.class === 'failure' ? 'text-red-400' : ev.class === 'warning' ? 'text-amber-400' : 'text-bambu-gray'; return (
{new Date(ev.timestamp).toLocaleTimeString()} {printer?.name ?? `#${ev.printer_id}`} {ev.class} {ev.score.toFixed(3)}
); })}
)}
); }