fix(filament): don't dispatch an unresolved AMS mapping to the external spool (#2589)

A P1S queue row with use_ams=true but ams_mapping=[-1] was silently
printed with no AMS, starting against the empty external feed and pausing
with a runout. Two faults combined:

- start_print treated -1 (unresolved) the same as >=254 (explicit
  external) when deciding to force use_ams=False. Only genuine external
  now downgrades; -1 never does.
- The scheduler trusted a stored [-1] as "already resolved" and passed it
  through. It now recomputes from live AMS trays whenever the stored
  mapping is entirely unresolved, and clears it if nothing matches rather
  than sending a doomed command.

Frontend: the Print dialog no longer serializes an all-[-1] mapping while
the printer status is still loading (the hook returns no mapping), and
submit waits for AMS status with a "Waiting for AMS status" notice.

Tests: new backend + frontend regression coverage; corrected one existing
test that pinned the old [-1] -> use_ams=False behavior.
This commit is contained in:
maziggy
2026-07-18 08:11:44 +02:00
parent 1555fad539
commit 47a2a77cd3
21 changed files with 377 additions and 39 deletions
+19 -1
View File
@@ -372,7 +372,7 @@ export function PrintModal({
});
// Only fetch printer status when single printer selected (for filament mapping)
const { data: printerStatus } = useQuery({
const { data: printerStatus, isLoading: printerStatusLoading } = useQuery({
queryKey: ['printer-status', effectivePrinterId],
queryFn: () => api.getPrinterStatus(effectivePrinterId!),
enabled: !!effectivePrinterId,
@@ -1100,6 +1100,12 @@ export function PrintModal({
// the rest — the banner above says which state we are in.
if (perPlateReqsPending || perPlateReqsFailed) return false;
// A single-printer AMS job must wait for the printer's live status before it
// can resolve the filament mapping. Submitting mid-load matched against zero
// known trays and serialized an all-[-1] mapping, which dispatched the print
// to the empty external feed (#2589).
if (assignmentMode === 'printer' && selectedPrinters.length === 1 && printerStatusLoading) return false;
return true;
}, [
selectedPrinters.length,
@@ -1111,6 +1117,7 @@ export function PrintModal({
isPending,
perPlateReqsPending,
perPlateReqsFailed,
printerStatusLoading,
]);
// Quantity only applies for single-printer or model-based assignment (not multi-printer)
@@ -1482,6 +1489,17 @@ export function PrintModal({
</div>
)}
{/* Waiting for the printer's AMS status: submitting now would map
against zero known trays and dispatch to the empty external feed (#2589). */}
{assignmentMode === 'printer' && selectedPrinters.length === 1 && printerStatusLoading && (
<div className="mb-4 p-3 bg-blue-100 dark:bg-blue-500/20 border border-blue-500/50 rounded-lg text-sm text-blue-700 dark:text-blue-400 flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
{t('printModal.waitingForAmsStatus', {
printer: printers?.find((p) => p.id === effectivePrinterId)?.name ?? '',
})}
</div>
)}
{/* Actions */}
<div className="flex gap-3 pt-2">
<Button type="button" variant="secondary" onClick={onClose} className="flex-1" disabled={isSubmitting}>