import { useState, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Loader2, Check, X, RefreshCw, Link2, Link2Off, Database, ChevronDown, Info, AlertTriangle } from 'lucide-react'; import { api } from '../api/client'; import type { SpoolmanSyncResult, Printer } from '../api/client'; import { Card, CardContent, CardHeader } from './Card'; import { Button } from './Button'; export function SpoolmanSettings() { const queryClient = useQueryClient(); const [localEnabled, setLocalEnabled] = useState(false); const [localUrl, setLocalUrl] = useState(''); const [localSyncMode, setLocalSyncMode] = useState('auto'); const [localDisableWeightSync, setLocalDisableWeightSync] = useState(false); const [localReportPartialUsage, setLocalReportPartialUsage] = useState(true); const [showSaved, setShowSaved] = useState(false); const [selectedPrinterId, setSelectedPrinterId] = useState('all'); const [isInitialized, setIsInitialized] = useState(false); const [showAllSkipped, setShowAllSkipped] = useState(false); // Fetch Spoolman settings const { data: settings, isLoading: settingsLoading } = useQuery({ queryKey: ['spoolman-settings'], queryFn: api.getSpoolmanSettings, }); // Fetch Spoolman status const { data: status, isLoading: statusLoading, refetch: refetchStatus } = useQuery({ queryKey: ['spoolman-status'], queryFn: api.getSpoolmanStatus, refetchInterval: 30000, // Refresh every 30 seconds }); // Fetch printers for the dropdown const { data: printers } = useQuery({ queryKey: ['printers'], queryFn: api.getPrinters, }); // Initialize local state from settings useEffect(() => { if (settings) { setLocalEnabled(settings.spoolman_enabled === 'true'); setLocalUrl(settings.spoolman_url || ''); setLocalSyncMode(settings.spoolman_sync_mode || 'auto'); setLocalDisableWeightSync(settings.spoolman_disable_weight_sync === 'true'); setLocalReportPartialUsage(settings.spoolman_report_partial_usage !== 'false'); setIsInitialized(true); } }, [settings]); // Auto-save when settings change (after initial load) // Intentionally omit saveMutation and settings from deps to avoid infinite loops useEffect(() => { if (!isInitialized || !settings) return; const hasChanges = (settings.spoolman_enabled === 'true') !== localEnabled || (settings.spoolman_url || '') !== localUrl || (settings.spoolman_sync_mode || 'auto') !== localSyncMode || (settings.spoolman_disable_weight_sync === 'true') !== localDisableWeightSync || (settings.spoolman_report_partial_usage !== 'false') !== localReportPartialUsage; if (hasChanges) { const timeoutId = setTimeout(() => { saveMutation.mutate(); }, 500); return () => clearTimeout(timeoutId); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [localEnabled, localUrl, localSyncMode, localDisableWeightSync, localReportPartialUsage, isInitialized]); // Save mutation const saveMutation = useMutation({ mutationFn: () => api.updateSpoolmanSettings({ spoolman_enabled: localEnabled ? 'true' : 'false', spoolman_url: localUrl, spoolman_sync_mode: localSyncMode, spoolman_disable_weight_sync: localDisableWeightSync ? 'true' : 'false', spoolman_report_partial_usage: localReportPartialUsage ? 'true' : 'false', }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['spoolman-settings'] }); queryClient.invalidateQueries({ queryKey: ['spoolman-status'] }); setShowSaved(true); setTimeout(() => setShowSaved(false), 2000); }, }); // Connect mutation const connectMutation = useMutation({ mutationFn: api.connectSpoolman, onSuccess: () => { refetchStatus(); }, }); // Disconnect mutation const disconnectMutation = useMutation({ mutationFn: api.disconnectSpoolman, onSuccess: () => { refetchStatus(); }, }); // Sync all mutation const syncAllMutation = useMutation({ mutationFn: api.syncAllPrintersAms, onSuccess: (data: SpoolmanSyncResult) => { if (data.success) { // Show success message } }, }); // Sync single printer mutation const syncPrinterMutation = useMutation({ mutationFn: (printerId: number) => api.syncPrinterAms(printerId), onSuccess: (data: SpoolmanSyncResult) => { if (data.success) { // Show success message } }, }); // Helper to handle sync based on selection const handleSync = () => { if (selectedPrinterId === 'all') { syncAllMutation.mutate(); } else { syncPrinterMutation.mutate(selectedPrinterId); } }; // Combine mutation states const isSyncing = syncAllMutation.isPending || syncPrinterMutation.isPending; const syncResult = selectedPrinterId === 'all' ? syncAllMutation.data : syncPrinterMutation.data; const syncSuccess = selectedPrinterId === 'all' ? syncAllMutation.isSuccess : syncPrinterMutation.isSuccess; if (settingsLoading) { return (

Spoolman Integration

); } return (

Spoolman Integration

{saveMutation.isPending && ( )} {showSaved && ( )}

Connect to Spoolman for filament inventory tracking. AMS data will sync automatically.

{/* Info banner about sync requirements */}

How Sync Works

  • Only official Bambu Lab spools with RFID are synced
  • New spools are auto-created in Spoolman on first sync
  • Non-Bambu Lab spools (third-party, refilled) are skipped

Linking Existing Spools

To link existing Spoolman spools to your AMS, hover over an AMS slot and click "Link to Spoolman".

{/* Enable toggle */}

Enable Spoolman

Sync filament data with Spoolman server

{/* URL input */}
setLocalUrl(e.target.value)} disabled={!localEnabled} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray/50 focus:border-bambu-green focus:outline-none disabled:opacity-50" />

URL of your Spoolman server (e.g., http://localhost:7912)

{/* Sync mode */}

{localSyncMode === 'auto' ? 'AMS data syncs automatically when changes are detected' : 'Only sync when manually triggered'}

{/* Disable Weight Sync toggle - only show when sync mode is auto */} {localSyncMode === 'auto' && (

Disable AMS Estimated Weight Sync

Don't update remaining capacity from AMS estimates. Use this if you prefer Spoolman's usage tracking over AMS percentage-based estimates. New spools will still use the AMS estimate as their initial weight.

)} {/* Report Partial Usage toggle - only show when weight sync is disabled */} {localDisableWeightSync && (

Report Partial Usage for Failed Prints

When a print fails or is cancelled, report the estimated filament used up to that point based on layer progress.

)} {/* Connection status */} {localEnabled && (
Status: {statusLoading ? ( ) : status?.connected ? ( Connected ) : ( Disconnected )}
{status?.connected ? ( ) : ( )}
{/* Error display */} {connectMutation.isError && (
{(connectMutation.error as Error).message}
)} {/* Manual sync section */} {status?.connected && (

Sync AMS Data

Manually sync printer AMS data to Spoolman

{/* Printer selector */}
{/* Sync button */}
)} {/* Sync result */} {syncSuccess && syncResult && (
{/* Main result */}
{syncResult.success ? `Synced ${syncResult.synced_count} spool${syncResult.synced_count !== 1 ? 's' : ''} successfully` : `Synced ${syncResult.synced_count} spool${syncResult.synced_count !== 1 ? 's' : ''} with ${syncResult.errors.length} error${syncResult.errors.length !== 1 ? 's' : ''}`}
{/* Skipped spools */} {syncResult.skipped_count > 0 && (
{syncResult.skipped_count} spool{syncResult.skipped_count !== 1 ? 's' : ''} skipped
{syncResult.skipped_count > 5 && ( )}
    {(showAllSkipped ? syncResult.skipped : syncResult.skipped.slice(0, 5)).map((s, i) => (
  • {s.color && ( )} {s.location} - {s.reason}
  • ))} {!showAllSkipped && syncResult.skipped_count > 5 && (
  • ...and {syncResult.skipped_count - 5} more
  • )}
)} {/* Errors */} {syncResult.errors.length > 0 && (
Errors:
    {syncResult.errors.map((err, i) => (
  • {err}
  • ))}
)}
)}
)}
); }