import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query'; import { Loader2, Check, AlertTriangle, Eye, EyeOff, Info, ChevronDown, ChevronRight, ArrowRightLeft, Trash2, ShieldCheck, Copy, Stethoscope, } from 'lucide-react'; import { api, multiVirtualPrinterApi } from '../api/client'; import type { VirtualPrinterConfig } from '../api/client'; import { Card, CardContent } from './Card'; import { Button } from './Button'; import { ConfirmModal } from './ConfirmModal'; import { VirtualPrinterDiagnosticModal } from './VirtualPrinterDiagnosticModal'; import { useToast } from '../contexts/ToastContext'; import { copyTextToClipboard } from '../utils/clipboard'; type LocalMode = 'immediate' | 'review' | 'print_queue' | 'proxy'; const MODE_LABELS: Record = { immediate: 'archive', review: 'review', print_queue: 'queue', proxy: 'proxy', }; interface VirtualPrinterCardProps { printer: VirtualPrinterConfig; models: Record; } export function VirtualPrinterCard({ printer, models }: VirtualPrinterCardProps) { const { t } = useTranslation(); const queryClient = useQueryClient(); const { showToast } = useToast(); const [expanded, setExpanded] = useState(true); const [localEnabled, setLocalEnabled] = useState(printer.enabled); const [localName, setLocalName] = useState(printer.name); const [localAccessCode, setLocalAccessCode] = useState(''); const [localMode, setLocalMode] = useState( (printer.mode === 'queue' ? 'review' : printer.mode) as LocalMode ); const [localTargetPrinterId, setLocalTargetPrinterId] = useState(printer.target_printer_id); const [localBindIp, setLocalBindIp] = useState(printer.bind_ip || ''); const [localRemoteInterfaceIp, setLocalRemoteInterfaceIp] = useState(printer.remote_interface_ip || ''); const [localModel, setLocalModel] = useState(printer.model || ''); const [localAutoDispatch, setLocalAutoDispatch] = useState(printer.auto_dispatch ?? true); const [localQueueForceColorMatch, setLocalQueueForceColorMatch] = useState(printer.queue_force_color_match ?? false); const [localTailscaleDisabled, setLocalTailscaleDisabled] = useState(printer.tailscale_disabled ?? true); const [showAccessCode, setShowAccessCode] = useState(false); const [pendingAction, setPendingAction] = useState(null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDiagnostic, setShowDiagnostic] = useState(false); const [fqdnCopied, setFqdnCopied] = useState(false); // Host-level Tailscale identity (same for every VP) — shown inline on the card when // the user has marked this VP as "exposed over Tailscale". Cert handling does NOT // depend on this toggle; the slicer trusts the bambuddy CA the user imports once. const { data: tailscaleStatus } = useQuery({ queryKey: ['tailscale-status'], queryFn: multiVirtualPrinterApi.getTailscaleStatus, enabled: !localTailscaleDisabled, staleTime: 60_000, }); const tailscaleFqdn = tailscaleStatus?.available ? tailscaleStatus.fqdn : ''; const tailscaleIp = tailscaleStatus?.available ? tailscaleStatus.tailscale_ips?.[0] ?? '' : ''; const handleCopyFqdn = async (e: React.MouseEvent) => { e.stopPropagation(); const fqdn = tailscaleFqdn; if (!fqdn) return; const ok = await copyTextToClipboard(fqdn); if (ok) { setFqdnCopied(true); showToast(t('printers.copied')); setTimeout(() => setFqdnCopied(false), 2000); } else { showToast(t('virtualPrinter.toast.copyFailed'), 'error'); } }; // Sync local state when props change (e.g., after backend auto-disable) useEffect(() => { if (!pendingAction) { setLocalEnabled(printer.enabled); setLocalMode((printer.mode === 'queue' ? 'review' : printer.mode) as LocalMode); setLocalName(printer.name); setLocalTargetPrinterId(printer.target_printer_id); setLocalBindIp(printer.bind_ip || ''); setLocalRemoteInterfaceIp(printer.remote_interface_ip || ''); setLocalModel(printer.model || ''); setLocalAutoDispatch(printer.auto_dispatch ?? true); setLocalQueueForceColorMatch(printer.queue_force_color_match ?? false); setLocalTailscaleDisabled(printer.tailscale_disabled ?? true); } }, [printer, pendingAction]); // Fetch printers for dropdown const { data: printers } = useQuery({ queryKey: ['printers'], queryFn: api.getPrinters, }); // Fetch network interfaces const { data: networkInterfaces } = useQuery({ queryKey: ['network-interfaces'], queryFn: () => api.getNetworkInterfaces().then(res => res.interfaces), }); const updateMutation = useMutation({ mutationFn: (data: Parameters[1]) => multiVirtualPrinterApi.update(printer.id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['virtual-printers'] }); showToast(t('virtualPrinter.toast.updated')); setPendingAction(null); }, onError: (error: Error) => { showToast(error.message || t('virtualPrinter.toast.failedToUpdate'), 'error'); setLocalEnabled(printer.enabled); setLocalMode((printer.mode === 'queue' ? 'review' : printer.mode) as LocalMode); setLocalTargetPrinterId(printer.target_printer_id); setLocalBindIp(printer.bind_ip || ''); setLocalTailscaleDisabled(printer.tailscale_disabled ?? true); setPendingAction(null); }, }); const deleteMutation = useMutation({ mutationFn: () => multiVirtualPrinterApi.remove(printer.id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['virtual-printers'] }); showToast(t('virtualPrinter.toast.deleted')); setShowDeleteConfirm(false); }, onError: (error: Error) => { showToast(error.message || t('virtualPrinter.toast.failedToDelete'), 'error'); setShowDeleteConfirm(false); }, }); const handleToggleEnabled = (e: React.MouseEvent) => { e.stopPropagation(); const newEnabled = !localEnabled; if (newEnabled) { if (!localBindIp) { showToast(t('virtualPrinter.toast.bindIpRequired'), 'error'); return; } if (localMode === 'proxy') { if (!localTargetPrinterId) { showToast(t('virtualPrinter.toast.targetPrinterRequired'), 'error'); return; } } else { if (!localAccessCode && !printer.access_code_set) { showToast(t('virtualPrinter.toast.accessCodeRequired'), 'error'); return; } } } setLocalEnabled(newEnabled); setPendingAction('toggle'); updateMutation.mutate({ enabled: newEnabled }); }; const handleNameChange = () => { if (!localName.trim()) return; setPendingAction('name'); updateMutation.mutate({ name: localName.trim() }); }; const handleAccessCodeChange = () => { if (!localAccessCode) { showToast(t('virtualPrinter.toast.accessCodeEmpty'), 'error'); return; } if (localAccessCode.length !== 8) { showToast(t('virtualPrinter.toast.accessCodeLength'), 'error'); return; } setPendingAction('accessCode'); updateMutation.mutate({ access_code: localAccessCode }); setLocalAccessCode(''); }; const handleModeChange = (mode: LocalMode) => { setLocalMode(mode); setPendingAction('mode'); updateMutation.mutate({ mode }); }; const handleModelChange = (model: string) => { setLocalModel(model); setPendingAction('model'); updateMutation.mutate({ model }); }; const handleTargetPrinterChange = (printerId: number) => { setLocalTargetPrinterId(printerId); setPendingAction('targetPrinter'); updateMutation.mutate({ target_printer_id: printerId }); }; const handleRemoteInterfaceChange = (ip: string) => { setLocalRemoteInterfaceIp(ip); setPendingAction('remoteInterface'); updateMutation.mutate({ remote_interface_ip: ip }); }; const isRunning = printer.status?.running || false; const modeLabel = t(`virtualPrinter.mode.${MODE_LABELS[localMode] || 'archive'}`); const targetPrinterName = printers?.find(p => p.id === localTargetPrinterId)?.name; return ( <> {/* Collapsed header - always visible, clickable to expand */}
setExpanded(!expanded)} > {printer.name} {modeLabel} {printer.model_name && ( {printer.model_name} )} {targetPrinterName && ( {localMode === 'proxy' && } {targetPrinterName} )} {localBindIp && ( {localBindIp} )} {localRemoteInterfaceIp && ( {localRemoteInterfaceIp} )}
e.stopPropagation()}>
{/* Expanded content */} {expanded && (
{/* Name + delete */}
setLocalName(e.target.value)} onBlur={handleNameChange} onKeyDown={(e) => e.key === 'Enter' && handleNameChange()} className="flex-1 text-sm text-white bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-md px-3 py-1.5 focus:border-bambu-green focus:outline-none" />
{/* Tailscale identity (host-level) + serial — compact info row. Shown only when this VP is marked Tailscale-exposed AND the daemon is up. */}
{tailscaleFqdn && ( {tailscaleIp ? `${tailscaleIp} (${tailscaleFqdn})` : tailscaleFqdn} )} {printer.serial}
{/* Mode */}
{t('virtualPrinter.mode.title')}
{(['immediate', 'review', 'print_queue', 'proxy'] as const).map((mode) => ( ))}
{/* Auto-dispatch toggle - only for print_queue mode */} {localMode === 'print_queue' && (
{t('virtualPrinter.autoDispatch.title')}
{t('virtualPrinter.autoDispatch.description')}
)} {/* Force-color-match toggle - only for print_queue mode (#1188) */} {localMode === 'print_queue' && (
{t('virtualPrinter.queueForceColorMatch.title')}
{t('virtualPrinter.queueForceColorMatch.description')}
)} {/* Tailscale toggle */}
{t('virtualPrinter.tailscaleDisabled.title')}
{t('virtualPrinter.tailscaleDisabled.description')}
{/* Printer Model - for non-proxy modes */} {localMode !== 'proxy' && (
{t('virtualPrinter.model.title')}

{t('virtualPrinter.model.description')}

)} {/* Proxy mode: hint about using target printer's access code */} {localMode === 'proxy' && (

{t('virtualPrinter.proxy.accessCodeHint')}

)} {/* Access Code - only for non-proxy modes */} {localMode !== 'proxy' && (
{t('virtualPrinter.accessCode.title')}
{printer.access_code_set ? ( {t('virtualPrinter.accessCode.isSet')} ) : ( {t('virtualPrinter.accessCode.notSet')} )}
setLocalAccessCode(e.target.value)} placeholder={printer.access_code_set ? t('virtualPrinter.accessCode.placeholderChange') : t('virtualPrinter.accessCode.placeholder')} maxLength={8} className="w-full bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-md px-3 py-1.5 text-white text-sm placeholder-bambu-gray pr-10 font-mono" />
{localAccessCode && (

{t('virtualPrinter.accessCode.charCount', { count: localAccessCode.length })}

)}
)} {/* Target Printer */}
{t('virtualPrinter.targetPrinter.title')}
{/* Bind Interface */}
{t('virtualPrinter.bindIp.title')}

{t('virtualPrinter.bindIp.hint')}

{/* Remote Interface - always visible for configuration */}
{t('virtualPrinter.remoteInterface.title')}
{localRemoteInterfaceIp ? ( ) : ( )}
)} {showDeleteConfirm && ( deleteMutation.mutate()} onCancel={() => setShowDeleteConfirm(false)} /> )} {showDiagnostic && ( setShowDiagnostic(false)} /> )} ); }