From 12e758e68ec1880325567b030621b7f9178c3a2b Mon Sep 17 00:00:00 2001 From: Matteo Parenti Date: Fri, 13 Feb 2026 22:37:06 +0100 Subject: [PATCH] Add insufficient filament warning --- backend/app/api/routes/settings.py | 5 + backend/app/schemas/settings.py | 5 + frontend/src/api/client.ts | 8 +- frontend/src/components/ConfirmModal.tsx | 2 +- frontend/src/components/PrintModal/index.tsx | 131 ++++++++++++++++++- frontend/src/components/SpoolmanSettings.tsx | 28 +++- frontend/src/i18n/locales/de.ts | 6 + frontend/src/i18n/locales/en.ts | 6 + frontend/src/i18n/locales/fr.ts | 6 + frontend/src/i18n/locales/it.ts | 15 +++ frontend/src/i18n/locales/ja.ts | 6 + 11 files changed, 208 insertions(+), 10 deletions(-) diff --git a/backend/app/api/routes/settings.py b/backend/app/api/routes/settings.py index 2df8615d8..24092c270 100644 --- a/backend/app/api/routes/settings.py +++ b/backend/app/api/routes/settings.py @@ -87,6 +87,7 @@ async def get_settings( "spoolman_enabled", "spoolman_disable_weight_sync", "spoolman_report_partial_usage", + "disable_filament_warnings", "check_updates", "check_printer_firmware", "virtual_printer_enabled", @@ -237,6 +238,7 @@ async def get_spoolman_settings( spoolman_sync_mode = await get_setting(db, "spoolman_sync_mode") or "auto" spoolman_disable_weight_sync = await get_setting(db, "spoolman_disable_weight_sync") or "false" spoolman_report_partial_usage = await get_setting(db, "spoolman_report_partial_usage") or "true" + disable_filament_warnings = await get_setting(db, "disable_filament_warnings") or "false" return { "spoolman_enabled": spoolman_enabled, @@ -244,6 +246,7 @@ async def get_spoolman_settings( "spoolman_sync_mode": spoolman_sync_mode, "spoolman_disable_weight_sync": spoolman_disable_weight_sync, "spoolman_report_partial_usage": spoolman_report_partial_usage, + "disable_filament_warnings": disable_filament_warnings, } @@ -264,6 +267,8 @@ async def update_spoolman_settings( await set_setting(db, "spoolman_disable_weight_sync", settings["spoolman_disable_weight_sync"]) if "spoolman_report_partial_usage" in settings: await set_setting(db, "spoolman_report_partial_usage", settings["spoolman_report_partial_usage"]) + if "disable_filament_warnings" in settings: + await set_setting(db, "disable_filament_warnings", settings["disable_filament_warnings"]) await db.commit() db.expire_all() diff --git a/backend/app/schemas/settings.py b/backend/app/schemas/settings.py index 3c4df280a..9e83682c5 100644 --- a/backend/app/schemas/settings.py +++ b/backend/app/schemas/settings.py @@ -31,6 +31,10 @@ class AppSettings(BaseModel): default=True, description="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.", ) + disable_filament_warnings: bool = Field( + default=False, + description="Disable insufficient filament warnings when printing or queueing prints", + ) # Updates check_updates: bool = Field(default=True, description="Automatically check for updates on startup") @@ -158,6 +162,7 @@ class AppSettingsUpdate(BaseModel): spoolman_sync_mode: str | None = None spoolman_disable_weight_sync: bool | None = None spoolman_report_partial_usage: bool | None = None + disable_filament_warnings: bool | None = None check_updates: bool | None = None check_printer_firmware: bool | None = None notification_language: str | None = None diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 4e7bbbc13..517d2e4a3 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -748,6 +748,8 @@ export interface AppSettings { // Date/time format settings date_format: 'system' | 'us' | 'eu' | 'iso'; time_format: 'system' | '12h' | '24h'; + // Filament tracking + disable_filament_warnings: boolean; // Disable insufficient filament warnings when printing/queueing // Default printer default_printer_id: number | null; // Dark mode theme settings @@ -3360,9 +3362,9 @@ export const api = { body: JSON.stringify({ tray_uuid: trayUuid }), }), getSpoolmanSettings: () => - request<{ spoolman_enabled: string; spoolman_url: string; spoolman_sync_mode: string; spoolman_disable_weight_sync: string; spoolman_report_partial_usage: string; }>('/settings/spoolman'), - updateSpoolmanSettings: (data: { spoolman_enabled?: string; spoolman_url?: string; spoolman_sync_mode?: string; spoolman_disable_weight_sync?: string; spoolman_report_partial_usage?: string; }) => - request<{ spoolman_enabled: string; spoolman_url: string; spoolman_sync_mode: string; spoolman_disable_weight_sync: string; spoolman_report_partial_usage: string; }>('/settings/spoolman', { + request<{ spoolman_enabled: string; spoolman_url: string; spoolman_sync_mode: string; spoolman_disable_weight_sync: string; spoolman_report_partial_usage: string; disable_filament_warnings: string; }>('/settings/spoolman'), + updateSpoolmanSettings: (data: { spoolman_enabled?: string; spoolman_url?: string; spoolman_sync_mode?: string; spoolman_disable_weight_sync?: string; spoolman_report_partial_usage?: string; disable_filament_warnings?: string; }) => + request<{ spoolman_enabled: string; spoolman_url: string; spoolman_sync_mode: string; spoolman_disable_weight_sync: string; spoolman_report_partial_usage: string; disable_filament_warnings: string; }>('/settings/spoolman', { method: 'PUT', body: JSON.stringify(data), }), diff --git a/frontend/src/components/ConfirmModal.tsx b/frontend/src/components/ConfirmModal.tsx index 895b23619..ef4c20254 100644 --- a/frontend/src/components/ConfirmModal.tsx +++ b/frontend/src/components/ConfirmModal.tsx @@ -79,7 +79,7 @@ export function ConfirmModal({

{title}

-

{message}

+

{message}

diff --git a/frontend/src/components/PrintModal/index.tsx b/frontend/src/components/PrintModal/index.tsx index fc0698752..6efef374c 100644 --- a/frontend/src/components/PrintModal/index.tsx +++ b/frontend/src/components/PrintModal/index.tsx @@ -3,13 +3,14 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { X, Printer, Loader2, Calendar, Pencil, AlertCircle, AlertTriangle } from 'lucide-react'; import { api } from '../../api/client'; -import type { PrintQueueItemCreate, PrintQueueItemUpdate } from '../../api/client'; +import type { PrintQueueItemCreate, PrintQueueItemUpdate, SpoolAssignment } from '../../api/client'; import { Card, CardContent } from '../Card'; import { Button } from '../Button'; +import { ConfirmModal } from '../ConfirmModal'; import { useToast } from '../../contexts/ToastContext'; -import { useFilamentMapping } from '../../hooks/useFilamentMapping'; +import { buildLoadedFilaments, useFilamentMapping } from '../../hooks/useFilamentMapping'; import { useMultiPrinterFilamentMapping, type PerPrinterConfig } from '../../hooks/useMultiPrinterFilamentMapping'; -import { isPlaceholderDate } from '../../utils/amsHelpers'; +import { getGlobalTrayId, isPlaceholderDate } from '../../utils/amsHelpers'; import { toDateTimeLocalValue } from '../../utils/date'; import { PrinterSelector } from './PrinterSelector'; import { PlateSelector } from './PlateSelector'; @@ -50,6 +51,13 @@ export function PrintModal({ // Determine if we're printing a library file const isLibraryFile = !!libraryFileId && !archiveId; + type FilamentWarningItem = { + printerName: string; + slotLabel: string; + requiredGrams: number; + remainingGrams: number; + }; + // Multiple printer selection (used for all modes now) const [selectedPrinters, setSelectedPrinters] = useState(() => { // Initialize with the queue item's printer if editing @@ -155,6 +163,8 @@ export function PrintModal({ const [isSubmitting, setIsSubmitting] = useState(false); const [submitProgress, setSubmitProgress] = useState({ current: 0, total: 0 }); + const [filamentWarningItems, setFilamentWarningItems] = useState(null); + // Track which printers have had the "Expand custom mapping by default" setting applied // This ensures the setting only affects initial state, not preventing unchecking const [initialExpandApplied, setInitialExpandApplied] = useState>(new Set()); @@ -175,6 +185,13 @@ export function PrintModal({ queryFn: api.getPrinters, }); + const { data: spoolAssignments } = useQuery({ + queryKey: ['spool-assignments'], + queryFn: () => api.getAssignments(), + staleTime: 30 * 1000, + enabled: isLibraryFile && mode === 'reprint', + }); + // Fetch archive details to get sliced_for_model const { data: archiveDetails } = useQuery({ queryKey: ['archive', archiveId], @@ -329,6 +346,35 @@ export function PrintModal({ const isMultiPlate = platesData?.is_multi_plate ?? false; const plates = platesData?.plates ?? []; + const spoolAssignmentsByPrinter = useMemo(() => { + const map = new Map>(); + if (!spoolAssignments) return map; + spoolAssignments.forEach((assignment) => { + const globalTrayId = getGlobalTrayId( + assignment.ams_id, + assignment.tray_id, + assignment.ams_id < 0 + ); + const printerMap = map.get(assignment.printer_id) ?? new Map(); + printerMap.set(globalTrayId, assignment); + map.set(assignment.printer_id, printerMap); + }); + return map; + }, [spoolAssignments]); + + const filamentWarningMessage = useMemo(() => { + if (!filamentWarningItems || filamentWarningItems.length === 0) return ''; + const lines = filamentWarningItems.map((item) => + t('printModal.insufficientFilamentLine', { + printer: item.printerName, + slot: item.slotLabel, + required: Math.round(item.requiredGrams), + remaining: Math.round(item.remainingGrams), + }) + ); + return [t('printModal.insufficientFilamentMessage'), ...lines].join('\n'); + }, [filamentWarningItems, t]); + // Add to queue mutation (single printer) const addToQueueMutation = useMutation({ mutationFn: (data: PrintQueueItemCreate) => api.addToQueue(data), @@ -348,9 +394,71 @@ export function PrintModal({ }, }); - const handleSubmit = async (e?: React.FormEvent) => { + const handleSubmit = async (e?: React.FormEvent, options?: { skipFilamentCheck?: boolean }) => { e?.preventDefault(); + if ( + !options?.skipFilamentCheck && + !settings?.disable_filament_warnings && + (mode === 'reprint' || mode === 'add-to-queue') && + assignmentMode === 'printer' + ) { + const warningItems: FilamentWarningItem[] = []; + const filamentReqs = effectiveFilamentReqs?.filaments ?? []; + + if (filamentReqs.length > 0 && spoolAssignmentsByPrinter.size > 0) { + const getRemainingWeight = (labelWeight: number, weightUsed: number) => { + if (!Number.isFinite(labelWeight) || labelWeight <= 0) return null; + if (!Number.isFinite(weightUsed) || weightUsed < 0) return null; + return Math.max(0, labelWeight - weightUsed); + }; + + for (const printerId of selectedPrinters) { + const printerMapping = selectedPrinters.length > 1 + ? multiPrinterMapping.getFinalMapping(printerId) + : amsMapping; + if (!printerMapping) continue; + + const printerStatusForWarning = selectedPrinters.length > 1 + ? multiPrinterMapping.printerResults.find((result) => result.printerId === printerId)?.status + : printerStatus; + + const loadedFilaments = buildLoadedFilaments(printerStatusForWarning); + const slotLabelByTray = new Map(loadedFilaments.map((f) => [f.globalTrayId, f.label])); + const assignments = spoolAssignmentsByPrinter.get(printerId); + const printerName = printers?.find((p) => p.id === printerId)?.name ?? `Printer ${printerId}`; + + if (!assignments) continue; + + filamentReqs.forEach((req) => { + if (!req.slot_id || req.slot_id <= 0) return; + const globalTrayId = printerMapping[req.slot_id - 1]; + if (!Number.isFinite(globalTrayId) || globalTrayId < 0) return; + + const assignment = assignments.get(globalTrayId); + const spool = assignment?.spool; + if (!spool) return; + + const remainingGrams = getRemainingWeight(spool.label_weight, spool.weight_used); + if (remainingGrams === null) return; + if (remainingGrams >= req.used_grams) return; + + warningItems.push({ + printerName, + slotLabel: slotLabelByTray.get(globalTrayId) ?? `Slot ${req.slot_id}`, + requiredGrams: req.used_grams, + remainingGrams, + }); + }); + } + } + + if (warningItems.length > 0) { + setFilamentWarningItems(warningItems); + return; + } + } + // Validate printer/model selection if (assignmentMode === 'printer' && selectedPrinters.length === 0) { showToast('Please select at least one printer', 'error'); @@ -738,6 +846,21 @@ export function PrintModal({ + + {filamentWarningItems && filamentWarningItems.length > 0 && ( + { + setFilamentWarningItems(null); + void handleSubmit(undefined, { skipFilamentCheck: true }); + }} + onCancel={() => setFilamentWarningItems(null)} + /> + )}
); } diff --git a/frontend/src/components/SpoolmanSettings.tsx b/frontend/src/components/SpoolmanSettings.tsx index 6a77f9074..6afbc1d3d 100644 --- a/frontend/src/components/SpoolmanSettings.tsx +++ b/frontend/src/components/SpoolmanSettings.tsx @@ -17,6 +17,7 @@ export function SpoolmanSettings() { const [localSyncMode, setLocalSyncMode] = useState('auto'); const [localDisableWeightSync, setLocalDisableWeightSync] = useState(false); const [localReportPartialUsage, setLocalReportPartialUsage] = useState(true); + const [localDisableFilamentWarnings, setLocalDisableFilamentWarnings] = useState(false); const [selectedPrinterId, setSelectedPrinterId] = useState('all'); const [isInitialized, setIsInitialized] = useState(false); const [showAllSkipped, setShowAllSkipped] = useState(false); @@ -48,6 +49,7 @@ export function SpoolmanSettings() { setLocalSyncMode(settings.spoolman_sync_mode || 'auto'); setLocalDisableWeightSync(settings.spoolman_disable_weight_sync === 'true'); setLocalReportPartialUsage(settings.spoolman_report_partial_usage !== 'false'); + setLocalDisableFilamentWarnings(settings.disable_filament_warnings === 'true'); setIsInitialized(true); } }, [settings]); @@ -62,7 +64,8 @@ export function SpoolmanSettings() { (settings.spoolman_url || '') !== localUrl || (settings.spoolman_sync_mode || 'auto') !== localSyncMode || (settings.spoolman_disable_weight_sync === 'true') !== localDisableWeightSync || - (settings.spoolman_report_partial_usage !== 'false') !== localReportPartialUsage; + (settings.spoolman_report_partial_usage !== 'false') !== localReportPartialUsage || + (settings.disable_filament_warnings === 'true') !== localDisableFilamentWarnings; if (hasChanges) { const timeoutId = setTimeout(() => { @@ -71,7 +74,7 @@ export function SpoolmanSettings() { return () => clearTimeout(timeoutId); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [localEnabled, localUrl, localSyncMode, localDisableWeightSync, localReportPartialUsage, isInitialized]); + }, [localEnabled, localUrl, localSyncMode, localDisableWeightSync, localReportPartialUsage, localDisableFilamentWarnings, isInitialized]); // Save mutation const saveMutation = useMutation({ @@ -82,10 +85,12 @@ export function SpoolmanSettings() { spoolman_sync_mode: localSyncMode, spoolman_disable_weight_sync: localDisableWeightSync ? 'true' : 'false', spoolman_report_partial_usage: localReportPartialUsage ? 'true' : 'false', + disable_filament_warnings: localDisableFilamentWarnings ? 'true' : 'false', }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['spoolman-settings'] }); queryClient.invalidateQueries({ queryKey: ['spoolman-status'] }); + queryClient.invalidateQueries({ queryKey: ['settings'] }); showToast(t('settings.toast.settingsSaved')); }, }); @@ -233,6 +238,25 @@ export function SpoolmanSettings() { + {/* Disable Filament Warnings toggle - applies to both modes */} +
+
+

{t('settings.disableFilamentWarnings')}

+

+ {t('settings.disableFilamentWarningsDesc')} +

+
+ +
+ {/* Built-in Inventory details */} {!localEnabled && (
diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 2049b7fdd..6e3eb73e2 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -1162,6 +1162,8 @@ export default { // Filament Tracking Mode filamentTracking: 'Filament-Verfolgung', filamentTrackingDesc: 'Wählen Sie, wie Sie Ihre Filamentspulen verfolgen möchten. Sie können das integrierte Inventar oder einen externen Spoolman-Server verwenden.', + disableFilamentWarnings: 'Filament-Warnungen deaktivieren', + disableFilamentWarningsDesc: 'Keine Warnungen über unzureichendes Filament beim Drucken oder Einreihen anzeigen', trackingModeBuiltIn: 'Integriertes Inventar', trackingModeBuiltInDesc: 'RFID-Erkennung und Verbrauchserfassung inklusive', trackingModeSpoolmanDesc: 'Externer Filament-Management-Server', @@ -2649,6 +2651,10 @@ export default { rightNozzle: 'R', leftNozzleTooltip: 'Linke Düse', rightNozzleTooltip: 'Rechte Düse', + insufficientFilamentTitle: 'Not enough filament', + insufficientFilamentMessage: 'Some assigned spools have less filament remaining than this print needs:', + insufficientFilamentLine: '{{printer}} - {{slot}}: needs {{required}}g, remaining {{remaining}}g', + printAnyway: 'Print anyway', }, // Backup diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 8139c3b69..1964383df 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1162,6 +1162,8 @@ export default { // Filament Tracking Mode filamentTracking: 'Filament Tracking', filamentTrackingDesc: 'Choose how to track your filament spools. You can use the built-in inventory or connect an external Spoolman server.', + disableFilamentWarnings: 'Disable filament warnings', + disableFilamentWarningsDesc: 'Don\'t show warnings about insufficient filament when printing or queueing', trackingModeBuiltIn: 'Built-in Inventory', trackingModeBuiltInDesc: 'RFID auto-matching and usage tracking included', trackingModeSpoolmanDesc: 'External filament management server', @@ -2653,6 +2655,10 @@ export default { rightNozzle: 'R', leftNozzleTooltip: 'Left nozzle', rightNozzleTooltip: 'Right nozzle', + insufficientFilamentTitle: 'Not enough filament', + insufficientFilamentMessage: 'Some assigned spools have less filament remaining than this print needs:', + insufficientFilamentLine: '{{printer}} - {{slot}}: needs {{required}}g, remaining {{remaining}}g', + printAnyway: 'Print anyway', }, // Backup diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index 64bcefcef..d88408e6d 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -1158,6 +1158,8 @@ export default { // Filament Tracking Mode filamentTracking: 'Suivi de Filament', filamentTrackingDesc: 'Choisissez comment suivre vos bobines. Utilisez l\'inventaire intégré ou connectez un serveur Spoolman.', + disableFilamentWarnings: 'Désactiver les avertissements de filament', + disableFilamentWarningsDesc: 'Ne pas afficher les avertissements de filament insuffisant lors de l\'impression ou de la mise en file d\'attente', trackingModeBuiltIn: 'Inventaire Intégré', trackingModeBuiltInDesc: 'Correspondance RFID et suivi de consommation inclus', trackingModeSpoolmanDesc: 'Serveur de gestion externe', @@ -2649,6 +2651,10 @@ export default { rightNozzle: 'D', leftNozzleTooltip: 'Buse gauche', rightNozzleTooltip: 'Buse droite', + insufficientFilamentTitle: 'Not enough filament', + insufficientFilamentMessage: 'Some assigned spools have less filament remaining than this print needs:', + insufficientFilamentLine: '{{printer}} - {{slot}}: needs {{required}}g, remaining {{remaining}}g', + printAnyway: 'Print anyway', }, // Backup diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index 77e150cb0..1c308dfb1 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -1092,6 +1092,17 @@ export default { turnOn: 'Accendi', turnOff: 'Spegni', }, + // Filament Tracking Mode + filamentTracking: 'Tracciamento filamento', + filamentTrackingDesc: 'Scegli come tracciare le tue bobine di filamento. Puoi utilizzare l\'inventario integrato o collegare un server Spoolman esterno.', + disableFilamentWarnings: 'Disabilita avvisi filamento', + disableFilamentWarningsDesc: 'Non mostrare avvisi per filamento insufficiente durante la stampa o l\'accodamento', + trackingModeBuiltIn: 'Inventario integrato', + trackingModeBuiltInDesc: 'Corrispondenza RFID automatica e tracciamento utilizzo inclusi', + trackingModeSpoolmanDesc: 'Server di gestione filamenti esterno', + builtInFeatureRfid: 'Rileva automaticamente le bobine RFID Bambu Lab nell\'AMS', + builtInFeatureUsage: 'Traccia il consumo di filamento per stampa', + builtInFeatureCatalog: 'Gestisci bobine, colori e profili K-factor', // Spoolman spoolmanEnabled: 'Abilita integrazione Spoolman', spoolmanUrl: 'URL Spoolman', @@ -2379,6 +2390,10 @@ export default { rightNozzle: 'R', leftNozzleTooltip: 'Ugello sinistro', rightNozzleTooltip: 'Ugello destro', + insufficientFilamentTitle: 'Filamento insufficiente', + insufficientFilamentMessage: 'Alcune bobine assegnate hanno meno filamento rimanente di quanto necessario per questa stampa:', + insufficientFilamentLine: '{{printer}} - {{slot}}: necessita di {{required}}g, rimanenti {{remaining}}g', + printAnyway: 'Stampa comunque', }, // Backup diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index c3dca2061..4a8eaa50c 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -1402,6 +1402,8 @@ export default { // フィラメント追跡モード filamentTracking: 'フィラメント追跡', filamentTrackingDesc: 'フィラメントスプールの追跡方法を選択してください。内蔵インベントリまたは外部Spoolmanサーバーを使用できます。', + disableFilamentWarnings: 'フィラメント警告を無効化', + disableFilamentWarningsDesc: '印刷またはキュー追加時にフィラメント不足の警告を表示しない', trackingModeBuiltIn: '内蔵インベントリ', trackingModeBuiltInDesc: 'RFID自動検出と使用量追跡を含む', trackingModeSpoolmanDesc: '外部フィラメント管理サーバー', @@ -2569,6 +2571,10 @@ export default { rightNozzle: 'R', leftNozzleTooltip: '左ノズル', rightNozzleTooltip: '右ノズル', + insufficientFilamentTitle: 'Not enough filament', + insufficientFilamentMessage: 'Some assigned spools have less filament remaining than this print needs:', + insufficientFilamentLine: '{{printer}} - {{slot}}: needs {{required}}g, remaining {{remaining}}g', + printAnyway: 'Print anyway', }, backup: { restoreBackup: 'バックアップの復元',