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: 'バックアップの復元',