mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
feat(ams-backup): add status badge + toggle, fix prefer-lowest (#1766)
Two tightly-coupled deliverables in one drop -- a new AMS Filament Backup status/control surface, and the #1766 fix that depends on it. Added -- AMS Filament Backup status + control - Parse bit 18 of top-level print.cfg into PrinterState.ams_filament_backup on every push_status. Verified against OrcaSlicer source (DeviceManager.cpp:4961) and a live H2D ON/OFF capture. Tri-state (None = A1 family / pre-cfg push) preserves today's behaviour. - Hold-timer guard (3 s) prevents stale frames from flickering the badge back to the printer's old cfg after a user-initiated toggle. - POST /printers/{id}/ams-backup toggle, set_ams_filament_backup() client method calling _set_print_option("auto_switch_filament", enabled). - GET /printers/{id}/inventory-remain endpoint exposes the same map the dispatcher uses (internal and Spoolman modes both work uniformly). - Small icon badge in the printer card's "Filaments" section header (placement reads as printer-wide because the cfg bit is printer-wide, not per-AMS). Click to toggle, success toast. - 5 i18n keys x 11 locales for the badge UI. Fixed -- #1766: prefer_lowest didn't pick lowest, ignored backup state - Backend gate in _compute_ams_mapping_for_printer: coerce prefer_lowest to False when status.ams_filament_backup is False; log the skip. - New effectivePreferLowest(setting, backup) helper applied at every frontend sort entry point: single-printer PrintModal, multi-printer hook per-printer, PrinterSelector InlineMappingEditor, FilamentMapping standalone editor (the last had NO preferLowest awareness at all before this change). - New preferLowestSortKey(f, inventoryByTrayId) mirrors backend's two-tier key exactly, including the banding tie-break (regular AMS < AMS-HT < external) so the client-side pre-compute matches the dispatch-time pick. An earlier draft used a flat `amsId * 4 + trayId` priority which gave external slots (ams_id = -1) a NEGATIVE priority -- caught in code review before commit. - Settings -> Filament -> "Prefer lowest remaining filament" gets an explanatory note about the printer-side AMS Backup dependency, with i18n key in all 11 locales.
This commit is contained in:
@@ -4,7 +4,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Circle, Check, AlertTriangle, RefreshCw, ChevronDown, ChevronUp, Palette } from 'lucide-react';
|
||||
import { api } from '../../api/client';
|
||||
import { useFilamentMapping } from '../../hooks/useFilamentMapping';
|
||||
import { getGlobalTrayId } from '../../utils/amsHelpers';
|
||||
import { getGlobalTrayId, effectivePreferLowest } from '../../utils/amsHelpers';
|
||||
import { getColorName } from '../../utils/colors';
|
||||
import { useFilamentLabels } from './useFilamentLabels';
|
||||
import type { FilamentMappingProps } from './types';
|
||||
@@ -42,8 +42,35 @@ export function FilamentMapping({
|
||||
enabled: !!printerId,
|
||||
});
|
||||
|
||||
// Settings + inventory map drive the same prefer-lowest + AMS-backup gate
|
||||
// the dispatcher uses (#1766). Without this, the per-slot dropdown's
|
||||
// auto-suggestion could disagree with what actually gets dispatched.
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ['settings'],
|
||||
queryFn: api.getSettings,
|
||||
});
|
||||
const { data: inventoryRemain } = useQuery({
|
||||
queryKey: ['printer-inventory-remain', printerId],
|
||||
queryFn: () => api.getInventoryRemain(printerId),
|
||||
enabled: !!printerId,
|
||||
staleTime: 30 * 1000,
|
||||
});
|
||||
const inventoryByTrayId = useMemo(() => {
|
||||
if (!inventoryRemain?.inventory_remain_g) return undefined;
|
||||
const map = new Map<number, number>();
|
||||
Object.entries(inventoryRemain.inventory_remain_g).forEach(([key, grams]) => {
|
||||
const gtid = Number(key);
|
||||
if (!Number.isNaN(gtid)) map.set(gtid, grams);
|
||||
});
|
||||
return map;
|
||||
}, [inventoryRemain]);
|
||||
const gatedPreferLowest = effectivePreferLowest(
|
||||
settings?.prefer_lowest_filament,
|
||||
printerStatus?.ams_filament_backup,
|
||||
);
|
||||
|
||||
const { loadedFilaments, filamentComparison, hasTypeMismatch, hasColorMismatch } =
|
||||
useFilamentMapping(filamentReqs, printerStatus, manualMappings);
|
||||
useFilamentMapping(filamentReqs, printerStatus, manualMappings, gatedPreferLowest, inventoryByTrayId);
|
||||
|
||||
// Per-slot sub-brand + material-disambiguated colour labels (#1718). Same
|
||||
// shared hook the model-mode FilamentOverride uses so both panels render
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
colorsAreSimilar,
|
||||
autoMatchFilament,
|
||||
filterFilamentsByNozzle,
|
||||
effectivePreferLowest,
|
||||
} from '../../utils/amsHelpers';
|
||||
import type { PrinterSelectorProps, AssignmentMode } from './types';
|
||||
import type { PrinterMappingResult, PerPrinterConfig } from '../../hooks/useMultiPrinterFilamentMapping';
|
||||
@@ -108,7 +109,13 @@ function InlineMappingEditor({
|
||||
} else {
|
||||
const usedTrayIds = new Set<number>(Object.values(printerResult.config.manualMappings));
|
||||
const cachedSettings = queryClient.getQueryData<{ prefer_lowest_filament?: boolean }>(['settings']);
|
||||
loaded = autoMatchFilament(req, printerResult.loadedFilaments, usedTrayIds, cachedSettings?.prefer_lowest_filament) as LoadedFilament | undefined;
|
||||
loaded = autoMatchFilament(
|
||||
req,
|
||||
printerResult.loadedFilaments,
|
||||
usedTrayIds,
|
||||
effectivePreferLowest(cachedSettings?.prefer_lowest_filament, printerResult.status?.ams_filament_backup),
|
||||
printerResult.inventoryByTrayId,
|
||||
) as LoadedFilament | undefined;
|
||||
}
|
||||
|
||||
// Determine status
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { AlertCircle, AlertTriangle, Calendar, Code, Layers, Loader2, Pencil, Printer, X } from 'lucide-react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -15,7 +15,7 @@ import { getColorName } from '../../utils/colors';
|
||||
import { getCurrencySymbol } from '../../utils/currency';
|
||||
import { getBedTypeInfo } from '../../utils/bedType';
|
||||
import { toDateTimeLocalValue, parseUTCDate } from '../../utils/date';
|
||||
import { getGlobalTrayId, isPlaceholderDate } from '../../utils/amsHelpers';
|
||||
import { getGlobalTrayId, isPlaceholderDate, effectivePreferLowest } from '../../utils/amsHelpers';
|
||||
import { FilamentMapping } from './FilamentMapping';
|
||||
import { FilamentOverride } from './FilamentOverride';
|
||||
import { PlateSelector } from './PlateSelector';
|
||||
@@ -270,6 +270,34 @@ export function PrintModal({
|
||||
enabled: ((mode === 'reprint' || mode === 'add-to-queue') && assignmentMode === 'printer') || (isLibraryFile && mode === 'reprint'),
|
||||
});
|
||||
|
||||
// Fetch per-printer Map<globalTrayId, gramsRemaining> via the dedicated
|
||||
// backend endpoint (#1766). Server-side mirrors `_build_inventory_remain_overrides`
|
||||
// so internal and Spoolman modes both work uniformly, VT/external slots are
|
||||
// excluded, and negative grams are clamped — single source of truth between
|
||||
// the client-side preview and dispatch-time picks.
|
||||
const inventoryRemainQueries = useQueries({
|
||||
queries: selectedPrinters.map((printerId) => ({
|
||||
queryKey: ['printer-inventory-remain', printerId],
|
||||
queryFn: () => api.getInventoryRemain(printerId),
|
||||
staleTime: 30 * 1000,
|
||||
enabled: selectedPrinters.length > 0,
|
||||
})),
|
||||
});
|
||||
const inventoryByTrayIdPerPrinter = useMemo(() => {
|
||||
const result = new Map<number, Map<number, number>>();
|
||||
selectedPrinters.forEach((printerId, idx) => {
|
||||
const data = inventoryRemainQueries[idx]?.data?.inventory_remain_g;
|
||||
if (!data) return;
|
||||
const printerMap = new Map<number, number>();
|
||||
Object.entries(data).forEach(([key, grams]) => {
|
||||
const gtid = Number(key);
|
||||
if (!Number.isNaN(gtid)) printerMap.set(gtid, grams);
|
||||
});
|
||||
result.set(printerId, printerMap);
|
||||
});
|
||||
return result;
|
||||
}, [selectedPrinters, inventoryRemainQueries]);
|
||||
|
||||
// Fetch archive details to get sliced_for_model
|
||||
const { data: archiveDetails } = useQuery({
|
||||
queryKey: ['archive', archiveId],
|
||||
@@ -346,8 +374,22 @@ export function PrintModal({
|
||||
enabled: !!effectivePrinterId,
|
||||
});
|
||||
|
||||
// Single-printer flow: gate prefer_lowest on this printer's backup state.
|
||||
// Multi-printer flow gates per-printer inside the hook (different printers
|
||||
// may have different backup states), so we pass the raw setting down.
|
||||
const singlePrinterPreferLowest = effectivePreferLowest(
|
||||
settings?.prefer_lowest_filament,
|
||||
printerStatus?.ams_filament_backup,
|
||||
);
|
||||
|
||||
// Get AMS mapping from hook (only when single printer selected)
|
||||
const { amsMapping } = useFilamentMapping(effectiveFilamentReqs, printerStatus, manualMappings, settings?.prefer_lowest_filament);
|
||||
const { amsMapping } = useFilamentMapping(
|
||||
effectiveFilamentReqs,
|
||||
printerStatus,
|
||||
manualMappings,
|
||||
singlePrinterPreferLowest,
|
||||
effectivePrinterId ? inventoryByTrayIdPerPrinter.get(effectivePrinterId) : undefined,
|
||||
);
|
||||
|
||||
// Multi-printer filament mapping (for per-printer configuration)
|
||||
const multiPrinterMapping = useMultiPrinterFilamentMapping(
|
||||
@@ -358,6 +400,7 @@ export function PrintModal({
|
||||
perPrinterConfigs,
|
||||
setPerPrinterConfigs,
|
||||
settings?.prefer_lowest_filament,
|
||||
inventoryByTrayIdPerPrinter,
|
||||
);
|
||||
|
||||
// Auto-select first plate when plates load (single or multi-plate)
|
||||
|
||||
Reference in New Issue
Block a user