Просмотр исходного кода

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.
maziggy 1 месяц назад
Родитель
Сommit
47a2a77cd3

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
CHANGELOG.md


+ 10 - 1
backend/app/services/bambu_mqtt.py

@@ -3885,8 +3885,17 @@ class BambuMQTTClient:
             # H2S falls through this gate now (#1386): it is single-nozzle and was
             # hitting the dual-nozzle bypass, which caused 07FF_8012 when printing
             # without an AMS attached.
+            #
+            # Only an *explicit* external/virtual spool (254/255) may downgrade to
+            # use_ams=False. An unresolved slot (-1) must NOT: it means the mapping
+            # was never resolved — e.g. a frontend status-load race that persisted
+            # [-1] (#2589) — and treating that as "external" silently started the
+            # print against an empty external feed, pausing with a runout. A genuine
+            # external selection is >=254; unresolved is -1. Keep them distinct so an
+            # unresolved mapping fails loudly (or is recomputed upstream) instead of
+            # silently going external.
             if ams_mapping and use_ams and not is_dual_nozzle:
-                if all(t is None or int(t) < 0 or int(t) >= 254 for t in ams_mapping):
+                if all(t is None or int(t) >= 254 for t in ams_mapping):
                     use_ams = False
                     logger.info(
                         "[%s] All filament slots use external spool — setting use_ams=False",

+ 79 - 19
backend/app/services/print_scheduler.py

@@ -154,6 +154,25 @@ def _canonical_filament_type(ftype: str) -> str:
     return _FILAMENT_EQUIV_MAP.get(upper, upper)
 
 
+def _mapping_is_all_unresolved(mapping: list | None) -> bool:
+    """True if ``mapping`` is a non-empty list whose every entry is the
+    unresolved sentinel (-1 / None) — i.e. no required slot ever matched a tray.
+
+    Such a mapping is a bug artifact: a frontend status-load race can serialize
+    ``[-1]`` before the printer's AMS trays are known (#2589). It must be
+    recomputed from live status at dispatch rather than trusted, otherwise it
+    reaches the print command and is silently downgraded to external-spool mode.
+
+    A partially-resolved mapping (``[-1, -1, 5]`` where slot 3 matched, or a
+    padding ``-1`` for a slot this plate does not print) is NOT unresolved. An
+    explicit external selection (``>= 254``) is NOT unresolved either — those
+    keep their meaning.
+    """
+    if not isinstance(mapping, list) or not mapping:
+        return False
+    return all(t is None or (isinstance(t, int) and t < 0) for t in mapping)
+
+
 def _installed_nozzle_diameters(status) -> list[float]:
     """Parse the installed nozzle diameters from a PrinterState (#1899).
 
@@ -503,15 +522,11 @@ class PrintScheduler:
                             )
                             continue
 
-                    # Compute AMS mapping if not already set
-                    if not item.ams_mapping:
-                        computed_mapping = await self._compute_ams_mapping_for_printer(db, item.printer_id, item)
-                        if computed_mapping:
-                            item.ams_mapping = json.dumps(computed_mapping)
-                            logger.info(
-                                f"Queue item {item.id}: Computed AMS mapping for printer {item.printer_id}: {computed_mapping}"
-                            )
-                            await db.commit()
+                    # Resolve the AMS mapping when it's missing OR unresolved
+                    # (all -1). A stored all-[-1] mapping is a bug artifact — a
+                    # frontend status-load race can persist [-1] (#2589) — and
+                    # must be recomputed from live trays rather than trusted.
+                    await self._ensure_ams_mapping(db, item.printer_id, item)
 
                     # Filament-deficit pre-dispatch check (#1496). If the
                     # assigned spool can't satisfy any required slot grams,
@@ -677,16 +692,11 @@ class PrintScheduler:
                             db=db,
                         )
 
-                        # Compute AMS mapping for the assigned printer if not already set
-                        # This is critical for model-based jobs where mapping wasn't computed upfront
-                        if not item.ams_mapping:
-                            computed_mapping = await self._compute_ams_mapping_for_printer(db, printer_id, item)
-                            if computed_mapping:
-                                item.ams_mapping = json.dumps(computed_mapping)
-                                logger.info(
-                                    f"Queue item {item.id}: Computed AMS mapping for printer {printer_id}: {computed_mapping}"
-                                )
-                                await db.commit()
+                        # Resolve the AMS mapping for the assigned printer when it's
+                        # missing OR unresolved (all -1). Critical for model-based
+                        # jobs where mapping wasn't computed upfront, and it also
+                        # self-heals a bogus stored [-1] (#2589).
+                        await self._ensure_ams_mapping(db, printer_id, item)
 
                         # Filament-deficit pre-dispatch check (#1496).
                         if await self._block_on_filament_deficit(db, item):
@@ -1122,6 +1132,56 @@ class PrintScheduler:
                 matches += 1
         return matches
 
+    async def _ensure_ams_mapping(self, db: AsyncSession, printer_id: int, item: PrintQueueItem) -> None:
+        """Ensure the queue item carries a usable AMS mapping before dispatch.
+
+        Recomputes from live printer status when the stored mapping is missing OR
+        unresolved (all -1). A stored all-[-1] mapping is a bug artifact — a
+        frontend status-load race can serialize [-1] before the printer's AMS
+        trays are known (#2589) — and must not be trusted: downstream it would be
+        silently downgraded to external-spool mode and print against an empty
+        feed. A resolved mapping (including manual overrides, or a partially
+        padded one) is left untouched.
+
+        When recompute cannot resolve it either (no compatible tray loaded), the
+        bogus [-1] is cleared to None so it is not later mistaken for an explicit
+        external selection; the print command then keeps use_ams=True and the
+        firmware surfaces a clear AMS-mapping error instead of silently printing
+        to the empty external feed.
+        """
+        stored_mapping: list | None = None
+        if item.ams_mapping:
+            try:
+                stored_mapping = json.loads(item.ams_mapping)
+            except (json.JSONDecodeError, TypeError):
+                stored_mapping = None
+
+        # Already resolved (present and not all-unresolved) — keep as-is so a
+        # user's manual mapping is never overwritten.
+        if item.ams_mapping and not _mapping_is_all_unresolved(stored_mapping):
+            return
+
+        computed_mapping = await self._compute_ams_mapping_for_printer(db, printer_id, item)
+        if computed_mapping and not _mapping_is_all_unresolved(computed_mapping):
+            item.ams_mapping = json.dumps(computed_mapping)
+            logger.info(
+                "Queue item %s: Computed AMS mapping for printer %s: %s",
+                item.id,
+                printer_id,
+                computed_mapping,
+            )
+            await db.commit()
+        elif _mapping_is_all_unresolved(stored_mapping):
+            logger.warning(
+                "Queue item %s: stored ams_mapping %s is unresolved and could not be recomputed "
+                "from live status on printer %s; clearing it so dispatch does not treat it as external",
+                item.id,
+                stored_mapping,
+                printer_id,
+            )
+            item.ams_mapping = None
+            await db.commit()
+
     async def _compute_ams_mapping_for_printer(
         self, db: AsyncSession, printer_id: int, item: PrintQueueItem
     ) -> list[int] | None:

+ 9 - 3
backend/tests/unit/services/test_bambu_mqtt.py

@@ -3931,12 +3931,18 @@ class TestStartPrintAmsMapping:
         cmd = self._get_published_command(mqtt_client)
         assert cmd["use_ams"] is False
 
-    def test_all_unmapped_sets_use_ams_false(self, mqtt_client):
-        """All unmapped slots on non-H2D printer sets use_ams=False."""
+    def test_all_unresolved_keeps_use_ams_true(self, mqtt_client):
+        """All-unresolved (-1) is NOT external — must keep use_ams=True (#2589).
+
+        A stored [-1] comes from an unresolved mapping (e.g. a frontend
+        status-load race), not an explicit external-spool selection. Treating it
+        as external silently started the print against the empty external feed.
+        Only >=254 may downgrade to use_ams=False.
+        """
         mqtt_client.start_print("test.3mf", ams_mapping=[-1, -1], use_ams=True)
 
         cmd = self._get_published_command(mqtt_client)
-        assert cmd["use_ams"] is False
+        assert cmd["use_ams"] is True
 
     def test_mixed_ams_and_external_keeps_use_ams_true(self, mqtt_client):
         """AMS tray + external spool keeps use_ams=True."""

+ 179 - 0
backend/tests/unit/test_ams_mapping_unresolved_2589.py

@@ -0,0 +1,179 @@
+"""Regression tests for the unresolved-mapping -> silent-external bug (#2589).
+
+A P1S queue item persisted with ``ams_mapping=[-1]`` (an unresolved mapping from
+a frontend status-load race) was silently dispatched with ``use_ams=False`` — the
+print then started against the empty external feed and paused with a filament
+runout. Two behaviours combined:
+
+1. ``start_print`` treated an all-``-1`` mapping as "all external spool" and
+   forced ``use_ams=False``. Only an explicit external selection (``>=254``) may
+   do that; unresolved ``-1`` must not.
+2. The scheduler trusted a stored ``[-1]`` (non-empty, so "already resolved") and
+   skipped the recompute that would have matched the live AMS trays.
+
+These tests lock in the fix at both layers.
+"""
+
+import json
+from unittest.mock import AsyncMock, MagicMock
+
+import pytest
+
+from backend.app.services.bambu_mqtt import BambuMQTTClient
+from backend.app.services.print_scheduler import PrintScheduler, _mapping_is_all_unresolved
+
+
+class TestMappingIsAllUnresolved:
+    """Unit tests for the ``_mapping_is_all_unresolved`` predicate."""
+
+    def test_all_minus_one_is_unresolved(self):
+        assert _mapping_is_all_unresolved([-1]) is True
+        assert _mapping_is_all_unresolved([-1, -1]) is True
+
+    def test_none_entries_are_unresolved(self):
+        assert _mapping_is_all_unresolved([None]) is True
+        assert _mapping_is_all_unresolved([-1, None]) is True
+
+    def test_partially_resolved_is_not_unresolved(self):
+        # A plate that only prints slot 3 pads earlier slots with -1.
+        assert _mapping_is_all_unresolved([-1, -1, 5]) is False
+        assert _mapping_is_all_unresolved([5, -1]) is False
+
+    def test_explicit_external_is_not_unresolved(self):
+        # 254/255 are explicit external/virtual spool selections, not unresolved.
+        assert _mapping_is_all_unresolved([254]) is False
+        assert _mapping_is_all_unresolved([255, 254]) is False
+
+    def test_resolved_ams_is_not_unresolved(self):
+        assert _mapping_is_all_unresolved([0]) is False
+        assert _mapping_is_all_unresolved([4, 8]) is False
+
+    def test_empty_or_none_is_not_unresolved(self):
+        # Absent/empty is "needs computing", handled by the missing-mapping path,
+        # not this predicate — which is specifically about a *bogus* stored value.
+        assert _mapping_is_all_unresolved([]) is False
+        assert _mapping_is_all_unresolved(None) is False
+
+
+class TestStartPrintExternalDowngrade:
+    """``start_print`` must only force use_ams=False for *explicit* external."""
+
+    @pytest.fixture
+    def mqtt_client(self):
+        client = BambuMQTTClient(
+            ip_address="192.168.1.100",
+            serial_number="01P00A452600691",
+            access_code="12345678",
+        )
+        # Single-nozzle P1S so the dual-nozzle bypass does not apply.
+        client.model = "P1S"
+        client._client = MagicMock()
+        client.state.connected = True
+        return client
+
+    def _sent_command(self, mqtt_client) -> dict:
+        """Parse the JSON payload the client published."""
+        assert mqtt_client._client.publish.called, "start_print did not publish"
+        payload = mqtt_client._client.publish.call_args.args[1]
+        return json.loads(payload)["print"]
+
+    def test_unresolved_mapping_keeps_use_ams_true(self, mqtt_client):
+        """[-1] is unresolved, NOT external — must not silently go external."""
+        assert mqtt_client.start_print("Turm.3mf", ams_mapping=[-1], use_ams=True) is True
+        cmd = self._sent_command(mqtt_client)
+        assert cmd["use_ams"] is True
+
+    def test_explicit_external_forces_use_ams_false(self, mqtt_client):
+        """An explicit external selection (254) still downgrades to use_ams=False."""
+        assert mqtt_client.start_print("Turm.3mf", ams_mapping=[254], use_ams=True) is True
+        cmd = self._sent_command(mqtt_client)
+        assert cmd["use_ams"] is False
+
+    def test_resolved_ams_keeps_use_ams_true(self, mqtt_client):
+        """A real AMS tray keeps use_ams=True."""
+        assert mqtt_client.start_print("Turm.3mf", ams_mapping=[5], use_ams=True) is True
+        cmd = self._sent_command(mqtt_client)
+        assert cmd["use_ams"] is True
+
+    def test_padded_partial_mapping_keeps_use_ams_true(self, mqtt_client):
+        """A padded mapping ([-1, -1, tray]) is not all-external."""
+        assert mqtt_client.start_print("Turm.3mf", ams_mapping=[-1, -1, 5], use_ams=True) is True
+        cmd = self._sent_command(mqtt_client)
+        assert cmd["use_ams"] is True
+
+
+class TestEnsureAmsMapping:
+    """The scheduler must recompute a stored unresolved [-1], not trust it."""
+
+    @pytest.fixture
+    def scheduler(self):
+        return PrintScheduler()
+
+    def _item(self, ams_mapping):
+        item = MagicMock()
+        item.id = 92
+        item.printer_id = 82
+        item.ams_mapping = ams_mapping
+        return item
+
+    @pytest.mark.asyncio
+    async def test_stored_unresolved_is_recomputed(self, scheduler):
+        """A stored [-1] triggers recompute; the live-resolved mapping wins."""
+        db = AsyncMock()
+        item = self._item(json.dumps([-1]))
+        scheduler._compute_ams_mapping_for_printer = AsyncMock(return_value=[5])
+
+        await scheduler._ensure_ams_mapping(db, 82, item)
+
+        scheduler._compute_ams_mapping_for_printer.assert_awaited_once()
+        assert json.loads(item.ams_mapping) == [5]
+        db.commit.assert_awaited()
+
+    @pytest.mark.asyncio
+    async def test_missing_mapping_is_computed(self, scheduler):
+        """No stored mapping still computes one (existing behaviour preserved)."""
+        db = AsyncMock()
+        item = self._item(None)
+        scheduler._compute_ams_mapping_for_printer = AsyncMock(return_value=[4, 8])
+
+        await scheduler._ensure_ams_mapping(db, 82, item)
+
+        assert json.loads(item.ams_mapping) == [4, 8]
+
+    @pytest.mark.asyncio
+    async def test_resolved_mapping_is_left_untouched(self, scheduler):
+        """A resolved stored mapping (e.g. a manual override) is never recomputed."""
+        db = AsyncMock()
+        item = self._item(json.dumps([5, 9]))
+        scheduler._compute_ams_mapping_for_printer = AsyncMock(return_value=[0, 0])
+
+        await scheduler._ensure_ams_mapping(db, 82, item)
+
+        scheduler._compute_ams_mapping_for_printer.assert_not_awaited()
+        assert json.loads(item.ams_mapping) == [5, 9]
+
+    @pytest.mark.asyncio
+    async def test_unresolvable_stored_mapping_is_cleared(self, scheduler):
+        """If recompute also can't resolve it, the bogus [-1] is cleared to None
+        so dispatch never mistakes it for an explicit external selection."""
+        db = AsyncMock()
+        item = self._item(json.dumps([-1]))
+        # Live status has no compatible tray -> matcher returns another all-[-1].
+        scheduler._compute_ams_mapping_for_printer = AsyncMock(return_value=[-1])
+
+        await scheduler._ensure_ams_mapping(db, 82, item)
+
+        assert item.ams_mapping is None
+        db.commit.assert_awaited()
+
+    @pytest.mark.asyncio
+    async def test_recompute_none_leaves_missing_untouched(self, scheduler):
+        """Missing mapping + recompute returns None (no status) -> stays None,
+        no spurious clear-warning path, no crash."""
+        db = AsyncMock()
+        item = self._item(None)
+        scheduler._compute_ams_mapping_for_printer = AsyncMock(return_value=None)
+
+        await scheduler._ensure_ams_mapping(db, 82, item)
+
+        assert item.ams_mapping is None

+ 44 - 0
frontend/src/__tests__/hooks/useFilamentMapping.test.ts

@@ -6,11 +6,13 @@
  */
 
 import { describe, it, expect } from 'vitest';
+import { renderHook } from '@testing-library/react';
 import {
   buildAmsMapping,
   buildFilamentComparison,
   buildLoadedFilaments,
   computeAmsMapping,
+  useFilamentMapping,
 } from '../../hooks/useFilamentMapping';
 import { effectivePreferLowest } from '../../utils/amsHelpers';
 import type { PrinterStatus } from '../../api/client';
@@ -1243,3 +1245,45 @@ describe('per-plate mapping vs. the whole-file union (#2551 follow-up)', () => {
     expect(buildAmsMapping(buildFilamentComparison(PLATE_1, loaded, {}))).toEqual([0]);
   });
 });
+
+describe('useFilamentMapping — no [-1] mapping during a status-load race (#2589)', () => {
+  // The file requires one PETG slot; two compatible PETG spools are loaded once
+  // the printer status arrives.
+  const filamentReqs = {
+    filaments: [{ slot_id: 1, type: 'PETG', color: '#161616', used_grams: 141.86 }],
+  };
+  const statusWithPetg = createPrinterStatus([
+    {
+      id: 0,
+      tray: [
+        { id: 0, tray_type: 'PETG', tray_color: 'BCBCBC' },
+        { id: 1, tray_type: 'PETG', tray_color: 'FFFFFF' },
+      ],
+    },
+  ]);
+
+  it('returns undefined (not [-1]) while printerStatus is still loading', () => {
+    // printerStatus undefined = query not resolved yet. Serializing [-1] here is
+    // exactly what dispatched the P1S print to the empty external feed.
+    const { result } = renderHook(() => useFilamentMapping(filamentReqs, undefined, {}));
+    expect(result.current.amsMapping).toBeUndefined();
+  });
+
+  it('resolves to an AMS tray once status has loaded (type-only match, strict color off)', () => {
+    // Black requested, only gray/white PETG loaded: a type-only match is valid.
+    const { result } = renderHook(() => useFilamentMapping(filamentReqs, statusWithPetg, {}));
+    expect(result.current.amsMapping).toEqual([0]);
+    expect(result.current.hasTypeMismatch).toBe(false);
+  });
+
+  it('still emits [-1] for a genuine mismatch when trays are present', () => {
+    // Status loaded but nothing compatible (only PLA) -> the mapping legitimately
+    // carries -1 and the mismatch is surfaced; this must NOT be suppressed.
+    const statusWithPla = createPrinterStatus([
+      { id: 0, tray: [{ id: 0, tray_type: 'PLA', tray_color: 'FF0000' }] },
+    ]);
+    const { result } = renderHook(() => useFilamentMapping(filamentReqs, statusWithPla, {}));
+    expect(result.current.amsMapping).toEqual([-1]);
+    expect(result.current.hasTypeMismatch).toBe(true);
+  });
+});

+ 19 - 1
frontend/src/components/PrintModal/index.tsx

@@ -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}>

+ 11 - 1
frontend/src/hooks/useFilamentMapping.ts

@@ -439,7 +439,17 @@ export function useFilamentMapping(
     [filamentReqs, loadedFilaments, manualMappings, preferLowest, ftsActive, inventoryByTrayId],
   );
 
-  const amsMapping = useMemo(() => buildAmsMapping(filamentComparison), [filamentComparison]);
+  // Don't emit a mapping until the printer's trays are known. With no loaded
+  // filaments (e.g. printerStatus still loading), buildFilamentComparison marks
+  // every required slot unmatched and buildAmsMapping would serialize an
+  // all-[-1] array — which the backend used to treat as an explicit
+  // external-spool selection, silently printing to an empty feed (#2589).
+  // Return undefined instead so the scheduler resolves the mapping from live
+  // status at dispatch. Mirrors the guard in computeAmsMapping.
+  const amsMapping = useMemo(
+    () => (loadedFilaments.length === 0 ? undefined : buildAmsMapping(filamentComparison)),
+    [filamentComparison, loadedFilaments.length],
+  );
 
   const hasTypeMismatch = filamentComparison.some((f) => f.status === 'mismatch');
   const hasColorMismatch = filamentComparison.some((f) => f.status === 'type_only');

+ 1 - 0
frontend/src/i18n/locales/de.ts

@@ -4596,6 +4596,7 @@ export default {
     overrideWith: 'Ersetzen mit',
     resetToOriginal: 'Auf Original zurücksetzen',
     insufficientFilamentTitle: 'Nicht genug Filament',
+    waitingForAmsStatus: 'Warte auf AMS-Status von {{printer}}…',
     insufficientFilamentMessage: 'Einige zugewiesene Spulen haben weniger Filament als dieser Druck benötigt:',
     insufficientFilamentLine: '{{printer}} - {{slot}}: benötigt {{required}}g, verbleibend {{remaining}}g',
     printAnyway: 'Trotzdem drucken',

+ 1 - 0
frontend/src/i18n/locales/en.ts

@@ -4639,6 +4639,7 @@ export default {
     overrideWith: 'Override with',
     resetToOriginal: 'Reset to original',
     insufficientFilamentTitle: 'Not enough filament',
+    waitingForAmsStatus: 'Waiting for AMS status from {{printer}}…',
     insufficientFilamentMessage: 'Some assigned spools have less filament remaining than this print needs:',
     insufficientFilamentLine: '{{printer}} - {{slot}}: needs {{required}}g, remaining {{remaining}}g',
     printAnyway: 'Print anyway',

+ 1 - 0
frontend/src/i18n/locales/es.ts

@@ -4604,6 +4604,7 @@ export default {
     overrideWith: 'Anular con',
     resetToOriginal: 'Restablecer al original',
     insufficientFilamentTitle: 'No hay suficiente filamento',
+    waitingForAmsStatus: 'Esperando el estado del AMS de {{printer}}…',
     insufficientFilamentMessage: 'Algunas bobinas asignadas tienen menos filamento restante del que necesita esta impresión:',
     insufficientFilamentLine: '{{printer}} - {{slot}}: necesita {{required}} g, restante {{remaining}} g',
     printAnyway: 'Imprimir de todos modos',

+ 1 - 0
frontend/src/i18n/locales/fr.ts

@@ -4585,6 +4585,7 @@ export default {
     overrideWith: 'Remplacer par',
     resetToOriginal: 'Revenir à l\'original',
     insufficientFilamentTitle: 'Filament insuffisant',
+    waitingForAmsStatus: 'En attente de l\'état de l\'AMS de {{printer}}…',
     insufficientFilamentMessage: 'Certaines bobines assignées ont moins de filament restant que nécessaire pour cette impression :',
     insufficientFilamentLine: '{{printer}} - {{slot}} : nécessite {{required}}g, restant {{remaining}}g',
     printAnyway: 'Imprimer quand même',

+ 1 - 0
frontend/src/i18n/locales/it.ts

@@ -4584,6 +4584,7 @@ export default {
     overrideWith: 'Sostituisci con',
     resetToOriginal: 'Ripristina originale',
     insufficientFilamentTitle: 'Filamento insufficiente',
+    waitingForAmsStatus: 'In attesa dello stato AMS di {{printer}}…',
     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',

+ 1 - 0
frontend/src/i18n/locales/ja.ts

@@ -4596,6 +4596,7 @@ export default {
     overrideWith: '変更先',
     resetToOriginal: 'オリジナルに戻す',
     insufficientFilamentTitle: 'フィラメントが不足しています',
+    waitingForAmsStatus: '{{printer}} のAMSステータスを待機しています…',
     insufficientFilamentMessage: '割り当てられたスプールの一部は、この印刷に必要な量より残量が少ないです:',
     insufficientFilamentLine: '{{printer}} - {{slot}}: 必要 {{required}}g、残り {{remaining}}g',
     printAnyway: 'それでも印刷',

+ 1 - 0
frontend/src/i18n/locales/ko.ts

@@ -4368,6 +4368,7 @@ export default {
     overrideWith: '재정의',
     resetToOriginal: '원래대로 초기화',
     insufficientFilamentTitle: '필라멘트 부족',
+    waitingForAmsStatus: '{{printer}}의 AMS 상태를 기다리는 중…',
     insufficientFilamentMessage: '일부 할당된 스풀에 이 인쇄에 필요한 것보다 적은 필라멘트가 남아 있습니다:',
     insufficientFilamentLine: '{{printer}} - {{slot}}: {{required}}g 필요, {{remaining}}g 남음',
     printAnyway: '그래도 인쇄',

+ 1 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -4584,6 +4584,7 @@ export default {
     overrideWith: 'Substituir por',
     resetToOriginal: 'Restaurar original',
     insufficientFilamentTitle: 'Filamento insuficiente',
+    waitingForAmsStatus: 'Aguardando o status do AMS de {{printer}}…',
     insufficientFilamentMessage: 'Alguns dos carretéis atribuídos têm menos filamento restante do que o necessário para esta impressão:',
     insufficientFilamentLine: '{{printer}} - {{slot}}: necessário {{required}}g, restante {{remaining}}g',
     printAnyway: 'Imprimir mesmo assim',

+ 1 - 0
frontend/src/i18n/locales/tr.ts

@@ -4574,6 +4574,7 @@ export default {
     overrideWith: 'Şununla geçersiz kıl',
     resetToOriginal: 'Orijinale sıfırla',
     insufficientFilamentTitle: 'Yeterli filament yok',
+    waitingForAmsStatus: '{{printer}} için AMS durumu bekleniyor…',
     insufficientFilamentMessage: 'Bazı atanmış makaralarda bu baskının ihtiyaç duyduğundan daha az filament kaldı:',
     insufficientFilamentLine: '{{printer}} - {{slot}}: {{required}}g gerekli, {{remaining}}g kaldı',
     printAnyway: 'Yine de yazdır',

+ 1 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -4584,6 +4584,7 @@ export default {
     overrideWith: '覆盖为',
     resetToOriginal: '恢复为原始',
     insufficientFilamentTitle: '耗材不足',
+    waitingForAmsStatus: '正在等待 {{printer}} 的 AMS 状态…',
     insufficientFilamentMessage: '部分已分配线轴的剩余耗材少于本次打印所需:',
     insufficientFilamentLine: '{{printer}} - {{slot}}:需要 {{required}}g,剩余 {{remaining}}g',
     printAnyway: '仍然打印',

+ 1 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -4584,6 +4584,7 @@ export default {
     overrideWith: '覆蓋為',
     resetToOriginal: '恢復為原始',
     insufficientFilamentTitle: '耗材不足',
+    waitingForAmsStatus: '正在等待 {{printer}} 的 AMS 狀態…',
     insufficientFilamentMessage: '部分已分配料盤的剩餘耗材少於本次列印所需:',
     insufficientFilamentLine: '{{printer}} - {{slot}}:需要 {{required}}g,剩餘 {{remaining}}g',
     printAnyway: '仍然列印',

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-z4krdH8i.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-MvmZEMye.js"></script>
+    <script type="module" crossorigin src="/assets/index-z4krdH8i.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-BikDm6kr.css">
   </head>
   <body>

Некоторые файлы не были показаны из-за большого количества измененных файлов