import { useEffect, useState } from 'react'; import { useMutation } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { X, Stethoscope, CheckCircle2, XCircle, AlertTriangle, MinusCircle, Loader2, } from 'lucide-react'; import { api, type DiagnosticCheck, type DiagnosticStatus, type PrinterDiagnosticResult, } from '../api/client'; function StatusIcon({ status }: { status: DiagnosticStatus }) { if (status === 'pass') return ; if (status === 'fail') return ; if (status === 'warn') return ; return ; } /** * Presentational checklist — renders one row per diagnostic check plus an * overall banner. Shared by the modal and the bug-report panel. The title * and per-status detail text are localized via `diagnostic.check..*`. */ export function DiagnosticChecklist({ result }: { result: PrinterDiagnosticResult }) { const { t } = useTranslation(); const overallClass = result.overall === 'ok' ? 'bg-bambu-green/10 border-bambu-green/30 text-bambu-green' : result.overall === 'warnings' ? 'bg-amber-50 dark:bg-amber-500/10 border-amber-300 dark:border-amber-500/30 text-amber-700 dark:text-amber-300' : 'bg-red-50 dark:bg-red-500/10 border-red-300 dark:border-red-500/30 text-red-700 dark:text-red-300'; const renderCheck = (check: DiagnosticCheck) => { const params = check.id === 'port_rtsps' ? { protocol: 'RTSPS', port: 322, ...check.params } : check.params; // A check may carry a `reason` to select a more specific message variant // (e.g. external_storage skip on P1-series → skip_unsupported_model #2524); // fall back to the plain per-status text when no variant key exists. const reason = (check.params as { reason?: string } | undefined)?.reason; const detail = t( `diagnostic.check.${check.id}.${check.status}${reason ? `_${reason}` : ''}`, { ...params, defaultValue: reason ? t(`diagnostic.check.${check.id}.${check.status}`, { ...params, defaultValue: '' }) : '', }, ); return (
  • {t(`diagnostic.check.${check.id}.title`, params)}
    {detail &&
    {detail}
    }
  • ); }; return (
      {result.checks.map(renderCheck)}
    {t(`diagnostic.overall.${result.overall}`)}
    ); } type Connection = { ip_address: string; serial_number?: string; access_code?: string; }; // Keep in sync with backend `PUBLISH_WAIT_DEFAULT` in // backend/app/services/printer_diagnostic.py — that's the upper bound on how // long the existing-printer route waits for the printer's first status report // after a bridge reconnect. The countdown is purely cosmetic; if the two // drift the worst case is the hint text being off by a couple of seconds. const PUBLISH_WAIT_DEFAULT_SECONDS = 10; type ConnectionDiagnosticModalProps = { onClose: () => void; printerName?: string | null; } & ({ printerId: number } | { connection: Connection }); /** * Connection diagnostic modal. Opens straight into the test — used from the * printer card, the System page, and the Add-Printer flow on failure. */ export function ConnectionDiagnosticModal(props: ConnectionDiagnosticModalProps) { const { onClose, printerName } = props; const { t } = useTranslation(); const printerId = 'printerId' in props ? props.printerId : undefined; const connection = 'connection' in props ? props.connection : undefined; const diagnose = useMutation({ mutationFn: (): Promise => printerId !== undefined ? api.diagnosePrinter(printerId) : api.diagnoseConnection(connection as Connection), }); useEffect(() => { diagnose.mutate(); // Run once on mount — re-running is the explicit "Retry" button. // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose]); // Tick an elapsed-seconds counter while the diagnostic is running so the // existing-printer flow (which waits up to PUBLISH_WAIT_DEFAULT_SECONDS for // the printer's first status report) doesn't look hung. Resets on each // (re)run. No effect on the pre-add flow other than a ticking counter, // which is still useful feedback. const [elapsedSeconds, setElapsedSeconds] = useState(0); useEffect(() => { if (!diagnose.isPending) { setElapsedSeconds(0); return; } const startedAt = Date.now(); const interval = window.setInterval(() => { setElapsedSeconds(Math.floor((Date.now() - startedAt) / 1000)); }, 500); return () => window.clearInterval(interval); }, [diagnose.isPending]); const result = diagnose.data as PrinterDiagnosticResult | undefined; return (
    e.stopPropagation()} >

    {t('diagnostic.modalTitle', { name: printerName || '' })}

    {diagnose.isPending && (
    {elapsedSeconds > 0 ? t('diagnostic.runningElapsed', { elapsed: elapsedSeconds }) : t('diagnostic.running')}
    {printerId !== undefined && (

    {t('diagnostic.waitingForReportHint', { max: PUBLISH_WAIT_DEFAULT_SECONDS })}

    )}
    )} {diagnose.isError && (
    {t('diagnostic.runFailed', { error: (diagnose.error as Error).message })}
    )} {result && }
    ); }