import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Loader2, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink, Globe, Droplets, Thermometer, FileText, Edit2, Send, CheckCircle, XCircle, History, Trash2, Upload, Zap, TrendingUp, Calendar, DollarSign, Power, PowerOff, Key, Copy, Database, Info, X, Shield, Printer, Cylinder } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { api } from '../api/client'; import { formatDateOnly } from '../utils/date'; import type { AppSettings, AppSettingsUpdate, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus } from '../api/client'; import { Card, CardContent, CardHeader } from '../components/Card'; import { Button } from '../components/Button'; import { SmartPlugCard } from '../components/SmartPlugCard'; import { AddSmartPlugModal } from '../components/AddSmartPlugModal'; import { NotificationProviderCard } from '../components/NotificationProviderCard'; import { AddNotificationModal } from '../components/AddNotificationModal'; import { NotificationTemplateEditor } from '../components/NotificationTemplateEditor'; import { NotificationLogViewer } from '../components/NotificationLogViewer'; import { ConfirmModal } from '../components/ConfirmModal'; import { BackupModal } from '../components/BackupModal'; import { RestoreModal } from '../components/RestoreModal'; import { SpoolmanSettings } from '../components/SpoolmanSettings'; import { ExternalLinksSettings } from '../components/ExternalLinksSettings'; import { VirtualPrinterSettings } from '../components/VirtualPrinterSettings'; import { APIBrowser } from '../components/APIBrowser'; import { virtualPrinterApi } from '../api/client'; import { defaultNavItems, getDefaultView, setDefaultView } from '../components/Layout'; import { availableLanguages } from '../i18n'; import { useToast } from '../contexts/ToastContext'; import { useTheme, type ThemeStyle, type DarkBackground, type LightBackground, type ThemeAccent } from '../contexts/ThemeContext'; import { useState, useEffect, useRef, useCallback } from 'react'; import { Palette } from 'lucide-react'; export function SettingsPage() { const queryClient = useQueryClient(); const { t, i18n } = useTranslation(); const { showToast, showPersistentToast, dismissToast } = useToast(); const { mode, darkStyle, darkBackground, darkAccent, lightStyle, lightBackground, lightAccent, setDarkStyle, setDarkBackground, setDarkAccent, setLightStyle, setLightBackground, setLightAccent, } = useTheme(); const [localSettings, setLocalSettings] = useState(null); const [showPlugModal, setShowPlugModal] = useState(false); const [editingPlug, setEditingPlug] = useState(null); const [showNotificationModal, setShowNotificationModal] = useState(false); const [editingProvider, setEditingProvider] = useState(null); const [editingTemplate, setEditingTemplate] = useState(null); const [showLogViewer, setShowLogViewer] = useState(false); const [defaultView, setDefaultViewState] = useState(getDefaultView()); const [activeTab, setActiveTab] = useState<'general' | 'plugs' | 'notifications' | 'filament' | 'apikeys' | 'virtual-printer'>('general'); const [showCreateAPIKey, setShowCreateAPIKey] = useState(false); const [newAPIKeyName, setNewAPIKeyName] = useState(''); const [newAPIKeyPermissions, setNewAPIKeyPermissions] = useState({ can_queue: true, can_control_printer: false, can_read_status: true, }); const [createdAPIKey, setCreatedAPIKey] = useState(null); const [showDeleteAPIKeyConfirm, setShowDeleteAPIKeyConfirm] = useState(null); const [testApiKey, setTestApiKey] = useState(''); // Confirm modal states const [showClearLogsConfirm, setShowClearLogsConfirm] = useState(false); const [showClearStorageConfirm, setShowClearStorageConfirm] = useState(false); const [showBulkPlugConfirm, setShowBulkPlugConfirm] = useState<'on' | 'off' | null>(null); const [showBackupModal, setShowBackupModal] = useState(false); const [showRestoreModal, setShowRestoreModal] = useState(false); const [showTelemetryInfo, setShowTelemetryInfo] = useState(false); const [showReleaseNotes, setShowReleaseNotes] = useState(false); const handleDefaultViewChange = (path: string) => { setDefaultViewState(path); setDefaultView(path); }; const handleResetSidebarOrder = () => { localStorage.removeItem('sidebarOrder'); window.location.reload(); }; const { data: settings, isLoading } = useQuery({ queryKey: ['settings'], queryFn: api.getSettings, }); const { data: smartPlugs, isLoading: plugsLoading } = useQuery({ queryKey: ['smart-plugs'], queryFn: api.getSmartPlugs, }); // Fetch energy data for all smart plugs when on the plugs tab const { data: plugEnergySummary, isLoading: energyLoading } = useQuery({ queryKey: ['smart-plugs-energy', smartPlugs?.map(p => p.id)], queryFn: async () => { if (!smartPlugs || smartPlugs.length === 0) return null; const statuses = await Promise.all( smartPlugs.filter(p => p.enabled).map(async (plug) => { try { const status = await api.getSmartPlugStatus(plug.id); return { plug, status }; } catch { return { plug, status: null as SmartPlugStatus | null }; } }) ); // Aggregate energy data let totalPower = 0; let totalToday = 0; let totalYesterday = 0; let totalLifetime = 0; let reachableCount = 0; for (const { status } of statuses) { if (status?.reachable && status.energy) { reachableCount++; if (status.energy.power != null) totalPower += status.energy.power; if (status.energy.today != null) totalToday += status.energy.today; if (status.energy.yesterday != null) totalYesterday += status.energy.yesterday; if (status.energy.total != null) totalLifetime += status.energy.total; } } return { totalPower, totalToday, totalYesterday, totalLifetime, reachableCount, totalPlugs: smartPlugs.filter(p => p.enabled).length, }; }, enabled: activeTab === 'plugs' && !!smartPlugs && smartPlugs.length > 0, refetchInterval: activeTab === 'plugs' ? 10000 : false, // Refresh every 10s when on plugs tab }); const { data: notificationProviders, isLoading: providersLoading } = useQuery({ queryKey: ['notification-providers'], queryFn: api.getNotificationProviders, }); const { data: apiKeys, isLoading: apiKeysLoading } = useQuery({ queryKey: ['api-keys'], queryFn: api.getAPIKeys, }); const createAPIKeyMutation = useMutation({ mutationFn: (data: { name: string; can_queue: boolean; can_control_printer: boolean; can_read_status: boolean }) => api.createAPIKey(data), onSuccess: (data) => { setCreatedAPIKey(data.key || null); setShowCreateAPIKey(false); setNewAPIKeyName(''); queryClient.invalidateQueries({ queryKey: ['api-keys'] }); showToast('API key created'); }, onError: (error: Error) => { showToast(`Failed to create API key: ${error.message}`, 'error'); }, }); const deleteAPIKeyMutation = useMutation({ mutationFn: (id: number) => api.deleteAPIKey(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['api-keys'] }); showToast('API key deleted'); }, onError: (error: Error) => { showToast(`Failed to delete API key: ${error.message}`, 'error'); }, }); const { data: printers } = useQuery({ queryKey: ['printers'], queryFn: api.getPrinters, }); const { data: notificationTemplates, isLoading: templatesLoading } = useQuery({ queryKey: ['notification-templates'], queryFn: api.getNotificationTemplates, }); // Virtual printer status for tab indicator const { data: virtualPrinterSettings } = useQuery({ queryKey: ['virtual-printer-settings'], queryFn: virtualPrinterApi.getSettings, refetchInterval: 10000, }); const virtualPrinterRunning = virtualPrinterSettings?.status?.running ?? false; const { data: ffmpegStatus } = useQuery({ queryKey: ['ffmpeg-status'], queryFn: api.checkFfmpeg, }); const { data: versionInfo } = useQuery({ queryKey: ['version'], queryFn: api.getVersion, }); const { data: updateCheck, refetch: refetchUpdateCheck, isRefetching: isCheckingUpdate } = useQuery({ queryKey: ['updateCheck'], queryFn: api.checkForUpdates, staleTime: 5 * 60 * 1000, }); const { data: updateStatus, refetch: refetchUpdateStatus } = useQuery({ queryKey: ['updateStatus'], queryFn: api.getUpdateStatus, refetchInterval: (query) => { const status = query.state.data as UpdateStatus | undefined; // Poll while update is in progress if (status?.status === 'downloading' || status?.status === 'installing') { return 1000; } return false; }, }); const applyUpdateMutation = useMutation({ mutationFn: api.applyUpdate, onSuccess: (data) => { if (data.is_docker) { showToast(data.message, 'error'); } else { refetchUpdateStatus(); } }, }); // Test all notification providers const [testAllResult, setTestAllResult] = useState<{ tested: number; success: number; failed: number; results: Array<{ provider_id: number; provider_name: string; provider_type: string; success: boolean; message: string; }>; } | null>(null); const testAllMutation = useMutation({ mutationFn: api.testAllNotificationProviders, onSuccess: (data) => { setTestAllResult(data); queryClient.invalidateQueries({ queryKey: ['notification-providers'] }); if (data.failed === 0) { showToast(`All ${data.tested} providers tested successfully!`, 'success'); } else { showToast(`${data.success}/${data.tested} providers succeeded`, data.failed > 0 ? 'error' : 'success'); } }, onError: (error: Error) => { showToast(`Failed to test providers: ${error.message}`, 'error'); }, }); // Bulk action for smart plugs const bulkPlugActionMutation = useMutation({ mutationFn: async (action: 'on' | 'off') => { if (!smartPlugs) return { success: 0, failed: 0 }; const enabledPlugs = smartPlugs.filter(p => p.enabled); const results = await Promise.all( enabledPlugs.map(async (plug) => { try { await api.controlSmartPlug(plug.id, action); return { success: true }; } catch { return { success: false }; } }) ); return { success: results.filter(r => r.success).length, failed: results.filter(r => !r.success).length, }; }, onSuccess: (data, action) => { queryClient.invalidateQueries({ queryKey: ['smart-plugs'] }); queryClient.invalidateQueries({ queryKey: ['smart-plugs-energy'] }); if (data.failed === 0) { showToast(`All ${data.success} plugs turned ${action}`, 'success'); } else { showToast(`${data.success} plugs turned ${action}, ${data.failed} failed`, 'error'); } }, onError: (error: Error) => { showToast(`Failed: ${error.message}`, 'error'); }, }); // Ref for debounce timeout const saveTimeoutRef = useRef | null>(null); const isInitialLoadRef = useRef(true); // Sync local state when settings load useEffect(() => { if (settings && !localSettings) { setLocalSettings(settings); // Mark initial load complete after a short delay setTimeout(() => { isInitialLoadRef.current = false; }, 100); } }, [settings, localSettings]); const updateMutation = useMutation({ mutationFn: api.updateSettings, onSuccess: (data) => { queryClient.setQueryData(['settings'], data); // Invalidate archive stats to reflect energy tracking mode change queryClient.invalidateQueries({ queryKey: ['archiveStats'] }); showToast('Settings saved', 'success'); }, onError: (error: Error) => { showToast(`Failed to save: ${error.message}`, 'error'); }, }); // Debounced auto-save when localSettings change useEffect(() => { // Skip if initial load or no settings if (isInitialLoadRef.current || !localSettings || !settings) { return; } // Check if there are actual changes const hasChanges = settings.auto_archive !== localSettings.auto_archive || settings.save_thumbnails !== localSettings.save_thumbnails || settings.capture_finish_photo !== localSettings.capture_finish_photo || settings.default_filament_cost !== localSettings.default_filament_cost || settings.currency !== localSettings.currency || settings.energy_cost_per_kwh !== localSettings.energy_cost_per_kwh || settings.energy_tracking_mode !== localSettings.energy_tracking_mode || settings.check_updates !== localSettings.check_updates || settings.notification_language !== localSettings.notification_language || settings.telemetry_enabled !== localSettings.telemetry_enabled || settings.ams_humidity_good !== localSettings.ams_humidity_good || settings.ams_humidity_fair !== localSettings.ams_humidity_fair || settings.ams_temp_good !== localSettings.ams_temp_good || settings.ams_temp_fair !== localSettings.ams_temp_fair || settings.ams_history_retention_days !== localSettings.ams_history_retention_days || settings.date_format !== localSettings.date_format || settings.time_format !== localSettings.time_format || settings.default_printer_id !== localSettings.default_printer_id || settings.ftp_retry_enabled !== localSettings.ftp_retry_enabled || settings.ftp_retry_count !== localSettings.ftp_retry_count || settings.ftp_retry_delay !== localSettings.ftp_retry_delay || settings.ftp_timeout !== localSettings.ftp_timeout; if (!hasChanges) { return; } // Clear existing timeout if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); } // Set new debounced save (500ms delay) saveTimeoutRef.current = setTimeout(() => { // Only send the fields we manage on this page (exclude virtual_printer_* which are managed separately) const settingsToSave: AppSettingsUpdate = { auto_archive: localSettings.auto_archive, save_thumbnails: localSettings.save_thumbnails, capture_finish_photo: localSettings.capture_finish_photo, default_filament_cost: localSettings.default_filament_cost, currency: localSettings.currency, energy_cost_per_kwh: localSettings.energy_cost_per_kwh, energy_tracking_mode: localSettings.energy_tracking_mode, check_updates: localSettings.check_updates, notification_language: localSettings.notification_language, telemetry_enabled: localSettings.telemetry_enabled, ams_humidity_good: localSettings.ams_humidity_good, ams_humidity_fair: localSettings.ams_humidity_fair, ams_temp_good: localSettings.ams_temp_good, ams_temp_fair: localSettings.ams_temp_fair, ams_history_retention_days: localSettings.ams_history_retention_days, date_format: localSettings.date_format, time_format: localSettings.time_format, default_printer_id: localSettings.default_printer_id, ftp_retry_enabled: localSettings.ftp_retry_enabled, ftp_retry_count: localSettings.ftp_retry_count, ftp_retry_delay: localSettings.ftp_retry_delay, ftp_timeout: localSettings.ftp_timeout, }; updateMutation.mutate(settingsToSave); }, 500); // Cleanup on unmount or when localSettings changes again return () => { if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); } }; }, [localSettings, settings, updateMutation]); const updateSetting = useCallback((key: K, value: AppSettings[K]) => { setLocalSettings(prev => prev ? { ...prev, [key]: value } : null); }, []); if (isLoading || !localSettings) { return (
); } return (

Settings

Configure Bambuddy

{/* Tab Navigation */}
{/* General Tab */} {activeTab === 'general' && (
{/* Left Column - General Settings */}

{t('settings.general')}

{t('settings.languageDescription')}

{t('settings.defaultViewDescription')}

Pre-select this printer for uploads, reprints, and other operations.

Sidebar order

Drag items in the sidebar to reorder. Reset to default order here.

Appearance

{/* Dark Mode Settings */}

Dark Mode {mode === 'dark' && (active)}

{/* Light Mode Settings */}

Light Mode {mode === 'light' && (active)}

Toggle between dark and light mode using the sun/moon icon in the sidebar.

Archive Settings

Auto-archive prints

Automatically save 3MF files when prints complete

Save thumbnails

Extract and save preview images from 3MF files

Capture finish photo

Take a photo from printer camera when print completes

{localSettings.capture_finish_photo && ffmpegStatus && !ffmpegStatus.installed && (

ffmpeg not installed

Camera capture requires ffmpeg. Install it via{' '} brew install ffmpeg (macOS) or{' '} apt install ffmpeg (Linux).

)}
{/* Second Column - Cost, AMS & Spoolman */}

Cost Tracking

updateSetting('default_filament_cost', parseFloat(e.target.value) || 0) } className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" />
updateSetting('energy_cost_per_kwh', parseFloat(e.target.value) || 0) } className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" />

{localSettings.energy_tracking_mode === 'print' ? 'Dashboard shows sum of energy used during prints' : 'Dashboard shows lifetime energy from smart plugs'}

AMS Display Thresholds

Configure color thresholds for AMS humidity and temperature indicators.

{/* Humidity Thresholds */}
Humidity
updateSetting('ams_humidity_good', parseInt(e.target.value) || 40)} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" /> %
updateSetting('ams_humidity_fair', parseInt(e.target.value) || 60)} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" /> %

Above fair threshold shows as red (bad)

{/* Temperature Thresholds */}
Temperature
updateSetting('ams_temp_good', parseFloat(e.target.value) || 28)} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" /> °C
updateSetting('ams_temp_fair', parseFloat(e.target.value) || 35)} className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" /> °C

Above fair threshold shows as red (hot)

{/* History Retention */}
History Retention
updateSetting('ams_history_retention_days', parseInt(e.target.value) || 30)} className="w-24 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" /> days

Older humidity and temperature data will be automatically deleted

{/* FTP Retry Settings */}

FTP Retry

Retry FTP operations when printer WiFi is unreliable. Applies to 3MF downloads, print uploads, timelapse downloads, and firmware updates.

Enable retry

Automatically retry failed FTP operations

{localSettings.ftp_retry_enabled && (
updateSetting('ftp_retry_count', Math.min(10, Math.max(1, parseInt(e.target.value) || 3)))} className="w-24 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" /> times

Number of retry attempts before giving up (1-10)

updateSetting('ftp_retry_delay', Math.min(30, Math.max(1, parseInt(e.target.value) || 2)))} className="w-24 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" /> seconds

Wait time between retries (1-30)

)}
updateSetting('ftp_timeout', Math.min(120, Math.max(10, parseInt(e.target.value) || 30)))} className="w-24 px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" /> seconds

Socket timeout for slow connections. Increase for A1/A1 Mini printers with weak WiFi (10-120)

{/* Third Column - Updates */}

Updates

Check for updates

Automatically check for new versions on startup

{t('settings.telemetry')}

{t('settings.telemetryDescription')}

Current version

v{versionInfo?.version || '...'}

{updateCheck?.update_available ? (

Update available: v{updateCheck.latest_version}

{updateCheck.release_name && updateCheck.release_name !== updateCheck.latest_version && (

{updateCheck.release_name}

)}
{updateCheck.release_notes && ( )} {updateCheck.release_url && ( )}
{updateStatus?.status === 'downloading' || updateStatus?.status === 'installing' ? (
{updateStatus.message}
) : updateStatus?.status === 'complete' ? (
{updateStatus.message}
) : updateStatus?.status === 'error' ? (
{updateStatus.error || updateStatus.message}
) : updateCheck?.is_docker ? (

Update via Docker Compose:

docker compose pull && docker compose up -d
) : ( )}
) : updateCheck?.error ? (
Failed to check for updates: {updateCheck.error}
) : updateCheck && !updateCheck.update_available ? (

You're running the latest version

) : null}
{/* Data Management */}

Data Management

{/* Backup/Restore */}

Backup Data

Export settings, providers, printers, and more

Restore Backup

Import settings from a backup file with duplicate handling options

Clear Notification Logs

Delete notification logs older than 30 days

Reset UI Preferences

Reset sidebar order, theme, view modes, and layout preferences. Printers, archives, and settings are not affected.

)} {/* Smart Plugs Tab */} {activeTab === 'plugs' && (

Smart Plugs

Connect Tasmota-based smart plugs to automate power control and track energy usage for your printers.

{smartPlugs && smartPlugs.filter(p => p.enabled).length > 1 && ( <> )}
{/* Energy Summary Card */} {smartPlugs && smartPlugs.length > 0 && (

Energy Summary {energyLoading && ( )}

{plugEnergySummary ? (
{/* Current Power */}
Current Power
{plugEnergySummary.totalPower.toFixed(1)} W
{plugEnergySummary.reachableCount}/{plugEnergySummary.totalPlugs} plugs online
{/* Today */}
Today
{plugEnergySummary.totalToday.toFixed(2)} kWh
{localSettings && localSettings.energy_cost_per_kwh > 0 && (
~{(plugEnergySummary.totalToday * localSettings.energy_cost_per_kwh).toFixed(2)} {localSettings.currency}
)}
{/* Yesterday */}
Yesterday
{plugEnergySummary.totalYesterday.toFixed(2)} kWh
{localSettings && localSettings.energy_cost_per_kwh > 0 && (
~{(plugEnergySummary.totalYesterday * localSettings.energy_cost_per_kwh).toFixed(2)} {localSettings.currency}
)}
{/* Total Lifetime */}
Total
{plugEnergySummary.totalLifetime.toFixed(1)} kWh
{localSettings && localSettings.energy_cost_per_kwh > 0 && (
~{(plugEnergySummary.totalLifetime * localSettings.energy_cost_per_kwh).toFixed(2)} {localSettings.currency}
)}
) : !energyLoading ? (

Enable plugs to see energy summary

) : null}
)} {plugsLoading ? (
) : smartPlugs && smartPlugs.length > 0 ? (
{smartPlugs.map((plug) => ( { setEditingPlug(p); setShowPlugModal(true); }} /> ))}
) : (

No smart plugs configured

Add a Tasmota-based smart plug to track energy usage and automate power control.

)}
)} {/* Notifications Tab */} {activeTab === 'notifications' && (
{/* Left Column: Providers */}

Providers

{notificationProviders && notificationProviders.length > 0 && ( )}
{/* Notification Language Setting */}

{t('settings.notificationLanguage')}

{t('settings.notificationLanguageDescription')}

{/* Test All Results */} {testAllResult && (
Test Results
{testAllResult.success} passed {testAllResult.failed > 0 && ( {testAllResult.failed} failed )}
{testAllResult.results.filter(r => !r.success).length > 0 && (
{testAllResult.results.filter(r => !r.success).map((result) => (
{result.provider_name}: {result.message}
))}
)}
)} {providersLoading ? (
) : notificationProviders && notificationProviders.length > 0 ? (
{notificationProviders.map((provider) => ( { setEditingProvider(p); setShowNotificationModal(true); }} /> ))}
) : (

No providers configured

Add a provider to receive alerts.

)}
{/* Right Column: Templates */}

Message Templates

Customize notification messages for each event.

{templatesLoading ? (
) : notificationTemplates && notificationTemplates.length > 0 ? (
{notificationTemplates.map((template) => ( setEditingTemplate(template)} >

{template.name}

{template.title_template}

))}
) : (

No templates available. Restart the backend to seed default templates.

)}
)} {/* API Keys Tab */} {activeTab === 'apikeys' && (
{/* Left Column - API Keys Management */}

API Keys

Create API keys for external integrations and webhooks.

{/* Created Key Display */} {createdAPIKey && (

API Key Created Successfully

Copy this key now - it won't be shown again!

{createdAPIKey}
)} {/* Create Key Form */} {showCreateAPIKey && (

Create New API Key

setNewAPIKeyName(e.target.value)} placeholder="e.g., Home Assistant, OctoPrint" className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none" />
)} {/* Existing Keys List */} {apiKeysLoading ? (
) : apiKeys && apiKeys.length > 0 ? (
{apiKeys.map((key) => (

{key.name}

{key.key_prefix}•••••••• {key.last_used && ` · Last used: ${formatDateOnly(key.last_used)}`}

{key.can_read_status && ( Read )} {key.can_queue && ( Queue )} {key.can_control_printer && ( Control )}
))}
) : (

No API keys

Create an API key to integrate with external services.

)} {/* Webhook Documentation */}

Webhook Endpoints

Use your API key in the X-API-Key header.

GET{' '} /api/v1/webhook/status - Get all printer status
GET{' '} /api/v1/webhook/status/:id - Get specific printer status
POST{' '} /api/v1/webhook/queue - Add to print queue
POST{' '} /api/v1/webhook/printer/:id/pause - Pause print
POST{' '} /api/v1/webhook/printer/:id/resume - Resume print
POST{' '} /api/v1/webhook/printer/:id/stop - Stop print
{/* Right Column - API Browser */}

API Browser

Explore and test all available API endpoints.

{/* API Key Input for Testing */} setTestApiKey(e.target.value)} placeholder="Paste your API key here to test authenticated endpoints..." className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white font-mono text-sm focus:border-bambu-green focus:outline-none" />

This key will be sent as X-API-Key header with requests.

)} {/* Virtual Printer Tab */} {activeTab === 'virtual-printer' && ( )} {/* Filament Tab */} {activeTab === 'filament' && (
)} {/* Delete API Key Confirmation */} {showDeleteAPIKeyConfirm !== null && ( { deleteAPIKeyMutation.mutate(showDeleteAPIKeyConfirm); setShowDeleteAPIKeyConfirm(null); }} onCancel={() => setShowDeleteAPIKeyConfirm(null)} /> )} {/* Smart Plug Modal */} {showPlugModal && ( { setShowPlugModal(false); setEditingPlug(null); }} /> )} {/* Notification Modal */} {showNotificationModal && ( { setShowNotificationModal(false); setEditingProvider(null); }} /> )} {/* Template Editor Modal */} {editingTemplate && ( setEditingTemplate(null)} /> )} {/* Notification Log Viewer */} {showLogViewer && ( setShowLogViewer(false)} /> )} {/* Confirm Modal: Clear Notification Logs */} {showClearLogsConfirm && ( { setShowClearLogsConfirm(false); try { const result = await api.clearNotificationLogs(30); showToast(result.message, 'success'); } catch { showToast('Failed to clear logs', 'error'); } }} onCancel={() => setShowClearLogsConfirm(false)} /> )} {/* Confirm Modal: Clear Local Storage */} {showClearStorageConfirm && ( { setShowClearStorageConfirm(false); localStorage.clear(); showToast('UI preferences reset. Refreshing...', 'success'); setTimeout(() => window.location.reload(), 1000); }} onCancel={() => setShowClearStorageConfirm(false)} /> )} {/* Confirm Modal: Bulk Plug Action */} {showBulkPlugConfirm && ( p.enabled).length || 0} enabled smart plugs. ${showBulkPlugConfirm === 'off' ? 'Any running printers may be affected!' : ''}`} confirmText={`Turn All ${showBulkPlugConfirm === 'on' ? 'On' : 'Off'}`} variant={showBulkPlugConfirm === 'off' ? 'danger' : 'warning'} onConfirm={() => { const action = showBulkPlugConfirm; setShowBulkPlugConfirm(null); bulkPlugActionMutation.mutate(action); }} onCancel={() => setShowBulkPlugConfirm(null)} /> )} {/* Backup Modal */} {showBackupModal && ( setShowBackupModal(false)} onExport={async (categories) => { setShowBackupModal(false); const toastId = 'backup-progress'; const includesArchives = categories.archives; // Show persistent loading toast for archive backups (can be large) if (includesArchives) { showPersistentToast(toastId, t('backup.preparing', { defaultValue: 'Preparing backup...' }), 'loading'); } try { const { blob, filename } = await api.exportBackup(categories); // Dismiss loading toast before download starts if (includesArchives) { dismissToast(toastId); } const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); showToast(t('backup.downloaded', { defaultValue: 'Backup downloaded' }), 'success'); } catch { // Dismiss loading toast on error if (includesArchives) { dismissToast(toastId); } showToast(t('backup.failed', { defaultValue: 'Failed to create backup' }), 'error'); } }} /> )} {/* Restore Modal */} {showRestoreModal && ( setShowRestoreModal(false)} onRestore={async (file, overwrite) => { return await api.importBackup(file, overwrite); }} onSuccess={() => { // Reset local settings to force re-sync from restored data setLocalSettings(null); isInitialLoadRef.current = true; queryClient.invalidateQueries(); }} /> )} {/* Telemetry Info Modal */} {showTelemetryInfo && (
setShowTelemetryInfo(false)} > e.stopPropagation()}>

{t('settings.telemetryInfoTitle')}

{t('settings.telemetryInfoIntro')}

{t('settings.telemetryInfoCollected')}

  • {t('settings.telemetryInfoItem1')}
  • {t('settings.telemetryInfoItem2')}
  • {t('settings.telemetryInfoItem3')}
  • {t('settings.telemetryInfoItem4')}

{t('settings.telemetryInfoNotCollected')}

  • {t('settings.telemetryInfoNotItem1')}
  • {t('settings.telemetryInfoNotItem2')}
  • {t('settings.telemetryInfoNotItem3')}
  • {t('settings.telemetryInfoNotItem4')}

{t('settings.telemetryInfoFooter')}

)} {/* Release Notes Modal */} {showReleaseNotes && updateCheck?.release_notes && (
setShowReleaseNotes(false)} > e.stopPropagation()}>

Release Notes - v{updateCheck.latest_version}

{updateCheck.release_name && updateCheck.release_name !== updateCheck.latest_version && (

{updateCheck.release_name}

)}
                {updateCheck.release_notes}
              
{updateCheck.release_url && ( )}
)}
); }