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'; interface SpoolmanSettingsData { spoolman_enabled: string; spoolman_url: string; spoolman_sync_mode: string; } async function getSpoolmanSettings(): Promise { const response = await fetch('/api/v1/settings/spoolman'); if (!response.ok) { throw new Error('Failed to load Spoolman settings'); } return response.json(); } async function updateSpoolmanSettings(data: Partial): Promise { const response = await fetch('/api/v1/settings/spoolman', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!response.ok) { throw new Error('Failed to save Spoolman settings'); } return response.json(); } export function SpoolmanSettings() { const queryClient = useQueryClient(); const [localEnabled, setLocalEnabled] = useState(false); const [localUrl, setLocalUrl] = useState(''); const [localSyncMode, setLocalSyncMode] = useState('auto'); 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: 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'); setIsInitialized(true); } }, [settings]); // Auto-save when settings change (after initial load) useEffect(() => { if (!isInitialized || !settings) return; const hasChanges = (settings.spoolman_enabled === 'true') !== localEnabled || (settings.spoolman_url || '') !== localUrl || (settings.spoolman_sync_mode || 'auto') !== localSyncMode; if (hasChanges) { const timeoutId = setTimeout(() => { saveMutation.mutate(); }, 500); return () => clearTimeout(timeoutId); } }, [localEnabled, localUrl, localSyncMode, isInitialized]); // Save mutation const saveMutation = useMutation({ mutationFn: () => updateSpoolmanSettings({ spoolman_enabled: localEnabled ? 'true' : 'false', spoolman_url: localUrl, spoolman_sync_mode: localSyncMode, }), 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'}

{/* 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}
  • ))}
)}
)}
)}
); }