Browse Source

fix(queue): skip preheat entirely when no loaded filament wants a chamber (issue #3041)

    Preheat & Heat Soak delayed every PLA print by five to seven minutes and
    gave nothing back. The filament map correctly derived a chamber target of
    0, and the chamber phase correctly skipped -- but the stage then heated
    the bed, waited for it, and held the full soak anyway, because the soak
    had no idea it was holding for a chamber nobody asked for. The print's
    own G-code sets the bed the moment it starts, so the bed phase only moved
    the warm-up ahead of the FTP upload instead of overlapping with it.

    A 0 that comes out of the filament map now skips the stage before any
    command goes out. The one thing the skip still does is put the airduct
    flap back to cooling on the models that have one -- an H2D left in
    heating mode by the ABS job before it would otherwise cook the PLA that
    follows, and that costs one MQTT command and no waiting.

    Explicit instructions are untouched. A chamber target of 0 typed into a
    print's own override still heats the bed and runs the soak, which is what
    the queue documentation has always promised it does, as does forcing a
    print's Preheat override to On. Prints that want chamber heat are
    unaffected, including the P1S/P1P/A1 tier where the bed and the soak
    timer are the whole mechanism.

    The existing unit tests all ran with soak_seconds=0, which is why the
    production default was never exercised; the PLA test now runs at the real
    default and asserts nothing is dispatched and nothing is slept.

    Surfaced in the UI on the way through: the Settings hint claimed the
    derived 0 skipped "the chamber phase", and the per-print chamber override
    field said nothing about a typed 0 meaning bed-only -- a user reaching
    for 0 to turn preheat off got the delay instead.
maziggy 4 ngày trước cách đây
mục cha
commit
4440c95976

+ 65 - 6
backend/app/services/print_scheduler.py

@@ -5117,6 +5117,34 @@ class PrintScheduler:
                 return False
         return True
 
+    def _preheat_flap_to_cooling(self, item_id: int, printer: Printer) -> None:
+        """Put the airduct flap back to cooling for a print that wants no chamber heat.
+
+        The full preheat stage does this as part of its own dispatch: an H2D
+        left in heating mode by the ABS job before it would otherwise cook the
+        PLA that follows. The skip path never reaches that code, so it calls
+        this instead -- one idempotent MQTT command, no waiting, and nothing to
+        add to the rollback pin, because a flap set to cooling for a print that
+        needs no heat is where it should have been either way.
+
+        Best-effort like everything else in the stage: a refused command logs
+        and the dispatch carries on.
+        """
+        model = printer.model or ""
+        if not supports_airduct(model):
+            return
+        state = printer_manager.get_status(printer.id)
+        current = getattr(state, "airduct_mode", None) if state else None
+        if current == _AIRDUCT_MODE_COOLING:
+            return
+        client = printer_manager.get_client(printer.id)
+        if client is None:
+            return
+        try:
+            client.set_airduct_mode("cooling")
+        except Exception as exc:
+            logger.warning("Queue item %s: preheat-skip airduct cooling failed: %s", item_id, exc)
+
     async def _preheat_and_soak(
         self,
         db: AsyncSession,
@@ -5140,9 +5168,14 @@ class PrintScheduler:
           2. Chamber target — `item.preheat_chamber_target_override` if non-null;
              else max of `preheat_filament_targets[normalize(t.tray_type)]`
              across the trays `item.ams_mapping` names (every loaded slot when
-             it names none); else 0 (skips chamber phase, keeps bed phase +
-             soak timer).
-          3. Three hardware tiers branch the wait loop:
+             it names none).
+          3. A target of 0 off the filament map skips the whole stage: the
+             materials this print loads want no chamber, so there is nothing to
+             soak for and the bed phase would only delay the upload (#3041).
+             An explicit 0 typed into the per-item override, or a per-item
+             'on', still runs the bed phase and the soak — both are the user
+             asking for a warm bed in so many words.
+          4. Three hardware tiers branch the wait loop:
              - Chamber heater (H2C/H2D/H2DPro/H2S/X2D/X1E via supports_chamber_heater):
                send M141 to the resolved target, then wait for the chamber sensor
                to reach it (or the max-wait timeout to elapse).
@@ -5178,9 +5211,10 @@ class PrintScheduler:
         # Chamber target resolution:
         #   1. Explicit per-item override beats everything (user knows best).
         #   2. Otherwise derive from the filament types this print loads, via
-        #      the per-filament target map. PLA-only print derives 0 → chamber
-        #      phase auto-skips without the user touching anything, even when
-        #      an ASA spool is sitting in another slot of the same AMS (#2886).
+        #      the per-filament target map. A PLA-only print derives 0 and the
+        #      block below skips the stage without the user touching anything,
+        #      even when an ASA spool is sitting in another slot of the same
+        #      AMS (#2886).
         explicit_target = getattr(item, "preheat_chamber_target_override", None)
         if explicit_target is not None and explicit_target > 0:
             chamber_target = int(explicit_target)
@@ -5193,6 +5227,31 @@ class PrintScheduler:
             chamber_target = self._derive_chamber_target(printer, targets, item)
             chamber_source = "filament-map"
 
+        # Nothing to preheat *for*. A zero that came out of the filament map is
+        # the map saying this print's materials want no chamber conditioning --
+        # PLA, PETG, TPU and PVA all sit at 0 by default. Running the stage
+        # anyway heated the bed and then held it for the full soak, which
+        # delayed every PLA dispatch by minutes and bought nothing: the print's
+        # own G-code sets the bed the moment it starts, so preheating it here
+        # only moves that heating ahead of the upload instead of overlapping
+        # with it, and the soak has no chamber to condition (#3041).
+        #
+        # An explicit statement from the user still runs the stage. Forcing the
+        # per-item override to 'on', or typing a chamber target of exactly 0,
+        # both mean "preheat the bed for this print" -- the second is
+        # documented as doing precisely that. Only the automatic path, the
+        # global toggle plus the filament map, short-circuits here.
+        if chamber_target <= 0 and chamber_source == "filament-map" and override != "on":
+            logger.info(
+                "Queue item %s: preheat skipped -- the loaded filaments derive no chamber "
+                "target, so there is nothing to soak for (override=%s model=%s)",
+                item.id,
+                override,
+                printer.model or "",
+            )
+            self._preheat_flap_to_cooling(item.id, printer)
+            return True
+
         bed_target = int(archive.bed_temperature) if archive and archive.bed_temperature else 0
         if bed_target <= 0:
             # No bed temperature in the slicer metadata. When the print needs a

+ 109 - 9
backend/tests/unit/test_scheduler_preheat.py

@@ -200,11 +200,78 @@ async def test_filament_map_picks_max_across_loaded_slots(scheduler, item, archi
 
 
 @pytest.mark.asyncio
-async def test_pla_only_derives_zero_chamber_skips(scheduler, item, archive):
-    """PLA-only print: filament-map lookup returns 0 → chamber phase skips
-    automatically without the user touching anything."""
+async def test_pla_only_derives_zero_chamber_skips_the_whole_stage(scheduler, item, archive):
+    """PLA-only print: the filament map returns 0, and the stage skips entirely.
+
+    Not just the chamber phase (#3041). A derived 0 says the materials this
+    print loads want no chamber conditioning, so there is nothing to soak for
+    -- and the bed phase that used to run anyway put the bed warm-up plus the
+    full soak ahead of the FTP upload, delaying every PLA dispatch by minutes
+    for no gain. The print's own G-code sets the bed when it starts.
+
+    The soak is left at its production default here on purpose: the old
+    behaviour held for those 300s, and a test that zeroes the soak cannot see
+    the difference.
+    """
     db = AsyncMock()
     client = _make_client()
+    sleeper = AsyncMock()
+
+    with (
+        patch.object(scheduler, "_get_bool_setting", AsyncMock(return_value=True)),
+        patch.object(scheduler, "_get_int_setting", _ints()),
+        patch.object(scheduler, "_get_setting", AsyncMock(return_value=None)),
+        patch("backend.app.services.print_scheduler.printer_manager") as pm,
+        patch("backend.app.services.print_scheduler.asyncio.sleep", sleeper),
+    ):
+        pm.get_client.return_value = client
+        pm.get_status.return_value = _make_state(60.0, 0.0, trays=["PLA", "PLA"])
+        assert await scheduler._preheat_and_soak(db, item, _make_printer("H2D"), archive) is True
+
+    client.set_bed_temperature.assert_not_called()
+    client.set_chamber_temperature.assert_not_called()
+    sleeper.assert_not_awaited()
+
+
+@pytest.mark.asyncio
+async def test_a_zero_target_still_puts_the_flap_back_to_cooling(scheduler, item, archive):
+    """Skipping the stage must not skip the flap.
+
+    The airduct decision is the one thing a no-chamber print still needs: an
+    H2D left in heating mode by the ABS job before it would cook the PLA that
+    follows. It costs one MQTT command and no waiting, so it survives the
+    early return that everything else takes.
+    """
+    db = AsyncMock()
+    client = _make_client()
+
+    with (
+        patch.object(scheduler, "_get_bool_setting", AsyncMock(return_value=True)),
+        patch.object(scheduler, "_get_int_setting", _ints()),
+        patch.object(scheduler, "_get_setting", AsyncMock(return_value=None)),
+        patch("backend.app.services.print_scheduler.printer_manager") as pm,
+        patch("backend.app.services.print_scheduler.asyncio.sleep", AsyncMock()),
+    ):
+        pm.get_client.return_value = client
+        pm.get_status.return_value = _make_state(60.0, 0.0, trays=["PLA"], airduct_mode=1)
+        await scheduler._preheat_and_soak(db, item, _make_printer("H2D"), archive)
+
+    client.set_airduct_mode.assert_called_once_with("cooling")
+    client.set_bed_temperature.assert_not_called()
+
+
+@pytest.mark.asyncio
+async def test_an_explicit_item_zero_still_heats_the_bed_and_soaks(scheduler, item, archive):
+    """A 0 typed into the per-item chamber override is not the same as a 0
+    derived from the filament map.
+
+    The map's 0 is a default nobody chose; the field's 0 is the user saying
+    "warm the bed for this print, skip the chamber", which is what the queue
+    documentation promises it does. Only the automatic path short-circuits.
+    """
+    db = AsyncMock()
+    client = _make_client()
+    item.preheat_chamber_target_override = 0
 
     with (
         patch.object(scheduler, "_get_bool_setting", AsyncMock(return_value=True)),
@@ -214,7 +281,35 @@ async def test_pla_only_derives_zero_chamber_skips(scheduler, item, archive):
         patch("backend.app.services.print_scheduler.asyncio.sleep", AsyncMock()),
     ):
         pm.get_client.return_value = client
-        pm.get_status.return_value = _make_state(60.0, 0.0, trays=["PLA", "PLA"])
+        pm.get_status.return_value = _make_state(60.0, 0.0, trays=["PLA"])
+        await scheduler._preheat_and_soak(db, item, _make_printer("H2D"), archive)
+
+    client.set_bed_temperature.assert_called_once_with(60)
+    client.set_chamber_temperature.assert_not_called()
+
+
+@pytest.mark.asyncio
+async def test_forcing_the_item_override_on_still_heats_the_bed(scheduler, item, archive):
+    """`preheat_override='on'` for a PLA print is also an explicit act.
+
+    The user reached past the global toggle for this one print; the only thing
+    left to give them on a print with no chamber requirement is the warm bed,
+    so the stage runs rather than silently doing nothing.
+    """
+    db = AsyncMock()
+    client = _make_client()
+    item.preheat_override = "on"
+
+    with (
+        # Global off -- 'on' is carrying the whole decision.
+        patch.object(scheduler, "_get_bool_setting", AsyncMock(return_value=False)),
+        patch.object(scheduler, "_get_int_setting", _ints(preheat_soak_seconds=0)),
+        patch.object(scheduler, "_get_setting", AsyncMock(return_value=None)),
+        patch("backend.app.services.print_scheduler.printer_manager") as pm,
+        patch("backend.app.services.print_scheduler.asyncio.sleep", AsyncMock()),
+    ):
+        pm.get_client.return_value = client
+        pm.get_status.return_value = _make_state(60.0, 0.0, trays=["PLA"])
         await scheduler._preheat_and_soak(db, item, _make_printer("H2D"), archive)
 
     client.set_bed_temperature.assert_called_once_with(60)
@@ -224,24 +319,29 @@ async def test_pla_only_derives_zero_chamber_skips(scheduler, item, archive):
 @pytest.mark.asyncio
 async def test_unknown_filament_type_falls_to_default(scheduler, item, archive):
     """A loaded tray with a type not in the map uses the `default` entry —
-    keeps users with custom filament names safe (they get 0 by default,
-    can be tuned via the per-filament editor)."""
+    keeps users with custom filament names safe.
+
+    Asserted against a tuned map rather than the bundled one: `default` ships
+    at 0, and since #3041 a derived 0 skips the stage before any command goes
+    out, so the bundled map cannot tell "fell through to default" apart from
+    "found nothing at all". Raising `default` makes the fallback visible.
+    """
     db = AsyncMock()
     client = _make_client()
 
     with (
         patch.object(scheduler, "_get_bool_setting", AsyncMock(return_value=True)),
         patch.object(scheduler, "_get_int_setting", _ints(preheat_soak_seconds=0)),
-        patch.object(scheduler, "_get_setting", AsyncMock(return_value=None)),
+        patch.object(scheduler, "_get_setting", AsyncMock(return_value='{"PLA": 0, "default": 35}')),
         patch("backend.app.services.print_scheduler.printer_manager") as pm,
         patch("backend.app.services.print_scheduler.asyncio.sleep", AsyncMock()),
     ):
         pm.get_client.return_value = client
-        pm.get_status.return_value = _make_state(60.0, 0.0, trays=["MyCustomFilament"])
+        pm.get_status.return_value = _make_state(60.0, 36.0, trays=["MyCustomFilament"])
         await scheduler._preheat_and_soak(db, item, _make_printer("H2D"), archive)
 
     client.set_bed_temperature.assert_called_once_with(60)
-    client.set_chamber_temperature.assert_not_called()  # default = 0
+    client.set_chamber_temperature.assert_called_once_with(35)
 
 
 @pytest.mark.asyncio

+ 10 - 0
frontend/src/components/PrintModal/PrintOptions.tsx

@@ -198,6 +198,16 @@ export function PrintOptionsPanel({
                 />
               </div>
             )}
+            {/* A typed 0 and a derived 0 do different things (#3041): the
+                first is a request for a bed-only preheat and still runs the
+                soak, the second means no material here wants a chamber and
+                skips the stage. Nothing in the field said so, and a user
+                reaching for 0 to switch preheat off got the delay instead. */}
+            {options.preheat_override !== 'off' && (
+              <p className="text-[11px] text-bambu-gray mt-1">
+                {t('settings.preheatTargetOverrideHelp', '0 heats the bed and runs the soak without the chamber. Leave blank and a print with no chamber requirement skips preheat entirely.')}
+              </p>
+            )}
           </div>
         </div>
       )}

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

@@ -2332,7 +2332,7 @@ export default {
     preheatEnabled: 'Vorheizen & Soak aktivieren',
     preheatEnabledDesc: 'Wenn aus, starten Drucke aus der Warteschlange sofort. Jeder Warteschlangeneintrag kann das pro Druck überschreiben.',
     preheatFilamentTargetsLabel: 'Kammer-Ziel je Filament (°C)',
-    preheatFilamentTargetsHint: 'Bambuddy wählt das höchste Ziel über die geladenen AMS-Slots; reine PLA-Drucke ergeben 0 und überspringen die Kammerphase automatisch.',
+    preheatFilamentTargetsHint: 'Bambuddy wählt das höchste Ziel über die geladenen AMS-Slots. Ein Druck, dessen Slots alle 0 ergeben – PLA, PETG, TPU, PVA –, überspringt das Vorheizen vollständig und startet sofort.',
     preheatFilamentTargetsReset: 'Auf Standardwerte zurücksetzen',
     preheatFilamentTargetsDefaultRow: 'Sonstige / nicht zugeordnet',
     preheatMaxWait: 'Max. Wartezeit (Sekunden)',
@@ -2349,6 +2349,7 @@ export default {
     calibrationMode_on: 'An',
     calibrationMode_auto: 'Auto',
     preheatTargetOverride: 'Kammer-Ziel überschreiben (°C, leer = Filament-Standard)',
+    preheatTargetOverrideHelp: '0 heizt das Bett und führt das Halten ohne Kammer aus. Bleibt das Feld leer, überspringt ein Druck ohne Kammerbedarf das Vorheizen vollständig.',
     plateClear: 'Druckplatte-Bestätigung',
     requirePlateClear: 'Druckplatte-Bestätigung erforderlich',
     requirePlateClearDescription: 'Wenn aktiviert, wartet der Scheduler auf eine Druckplatten-Bestätigung pro Drucker, bevor geplante Drucke auf Druckern mit abgeschlossenen Aufträgen gestartet werden. Wenn dies deaktiviert ist, werden auch das Druckplatten-Status-Badge und die Schaltfläche "Druckplatte als freigegeben markieren" auf den Druckerkarten ausgeblendet.',

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

@@ -2352,7 +2352,7 @@ export default {
     preheatEnabled: 'Enable preheat & soak',
     preheatEnabledDesc: 'When off, queued prints dispatch immediately. Each queue item can override per print.',
     preheatFilamentTargetsLabel: 'Per-filament chamber target (°C)',
-    preheatFilamentTargetsHint: 'Bambuddy picks the highest target across the loaded AMS slots; PLA-only prints derive 0 and skip the chamber phase automatically.',
+    preheatFilamentTargetsHint: 'Bambuddy picks the highest target across the loaded AMS slots. A print whose slots all derive 0 — PLA, PETG, TPU, PVA — skips preheat entirely and starts immediately.',
     preheatFilamentTargetsReset: 'Reset to defaults',
     preheatFilamentTargetsDefaultRow: 'Other / unmapped',
     preheatMaxWait: 'Max wait (seconds)',
@@ -2369,6 +2369,7 @@ export default {
     calibrationMode_on: 'On',
     calibrationMode_auto: 'Auto',
     preheatTargetOverride: 'Chamber target override (°C, blank = filament default)',
+    preheatTargetOverrideHelp: '0 heats the bed and runs the soak without the chamber. Leave blank and a print with no chamber requirement skips preheat entirely.',
     plateClear: 'Plate-Clear Confirmation',
     requirePlateClear: 'Require plate-clear confirmation',
     requirePlateClearDescription: 'When enabled, the scheduler waits for per-printer plate-clear confirmation before starting queued prints on printers with finished jobs. Disabling this also hides the plate status badge and the "Mark plate as cleared" button on printer cards.',

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

@@ -2335,7 +2335,7 @@ export default {
     preheatEnabled: 'Activar precalentamiento y soak',
     preheatEnabledDesc: 'Si está desactivado, las impresiones en cola se inician de inmediato. Cada elemento puede anular por impresión.',
     preheatFilamentTargetsLabel: 'Objetivo de cámara por filamento (°C)',
-    preheatFilamentTargetsHint: 'Bambuddy elige el objetivo más alto entre los slots AMS cargados; las impresiones solo PLA derivan 0 y omiten la fase de cámara automáticamente.',
+    preheatFilamentTargetsHint: 'Bambuddy elige el objetivo más alto entre los slots AMS cargados. Una impresión cuyos slots derivan todos 0 (PLA, PETG, TPU, PVA) omite el precalentamiento por completo y empieza de inmediato.',
     preheatFilamentTargetsReset: 'Restablecer valores predeterminados',
     preheatFilamentTargetsDefaultRow: 'Otros / sin asignar',
     preheatMaxWait: 'Espera máx. (segundos)',
@@ -2352,6 +2352,7 @@ export default {
     calibrationMode_on: 'Activado',
     calibrationMode_auto: 'Automático',
     preheatTargetOverride: 'Sobrescribir objetivo de cámara (°C, vacío = por filamento)',
+    preheatTargetOverrideHelp: '0 calienta la cama y ejecuta el reposo sin cámara. Si se deja en blanco, una impresión sin necesidad de cámara omite el precalentamiento por completo.',
     plateClear: 'Confirmación de cama despejada',
     requirePlateClear: 'Requerir confirmación de cama despejada',
     requirePlateClearDescription: 'Cuando está activado, el planificador espera la confirmación de cama despejada por impresora antes de iniciar impresiones en cola en impresoras con trabajos finalizados. Desactivar esto también oculta la insignia de estado de la cama y el botón "Marcar cama como despejada" en las tarjetas de impresora.',

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

@@ -2288,7 +2288,7 @@ export default {
     preheatEnabled: 'Activer préchauffage & soak',
     preheatEnabledDesc: 'Si désactivé, les impressions en file démarrent immédiatement. Chaque élément peut surcharger par impression.',
     preheatFilamentTargetsLabel: 'Cible chambre par filament (°C)',
-    preheatFilamentTargetsHint: 'Bambuddy choisit la cible la plus haute parmi les slots AMS chargés ; les impressions PLA uniquement dérivent 0 et sautent la phase chambre automatiquement.',
+    preheatFilamentTargetsHint: 'Bambuddy choisit la cible la plus haute parmi les slots AMS chargés. Une impression dont tous les slots donnent 0 — PLA, PETG, TPU, PVA — saute entièrement le préchauffage et démarre immédiatement.',
     preheatFilamentTargetsReset: 'Restaurer les valeurs par défaut',
     preheatFilamentTargetsDefaultRow: 'Autre / non mappé',
     preheatMaxWait: 'Attente max (secondes)',
@@ -2305,6 +2305,7 @@ export default {
     calibrationMode_on: 'Activé',
     calibrationMode_auto: 'Auto',
     preheatTargetOverride: 'Surcharger la cible chambre (°C, vide = par filament)',
+    preheatTargetOverrideHelp: '0 chauffe le plateau et exécute le maintien sans la chambre. Laissé vide, une impression sans besoin de chambre saute entièrement le préchauffage.',
     plateClear: 'Confirmation de plateau libre',
     requirePlateClear: 'Exiger la confirmation de plateau libre',
     requirePlateClearDescription: 'Lorsque cette option est activée, le planificateur attend une confirmation de plateau libre par imprimante avant de lancer les impressions en file d\'attente sur les imprimantes ayant terminé. La désactiver masque également le badge d\'état du plateau et le bouton « Marquer le plateau comme dégagé » sur les cartes d\'imprimante.',

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

@@ -2288,7 +2288,7 @@ export default {
     preheatEnabled: 'Abilita preriscaldo & soak',
     preheatEnabledDesc: 'Se disattivato, le stampe in coda partono subito. Ogni elemento può sovrascrivere per singola stampa.',
     preheatFilamentTargetsLabel: 'Target camera per filamento (°C)',
-    preheatFilamentTargetsHint: 'Bambuddy sceglie il target più alto tra gli slot AMS caricati; le stampe solo PLA derivano 0 e saltano la fase camera automaticamente.',
+    preheatFilamentTargetsHint: 'Bambuddy sceglie il target più alto tra gli slot AMS caricati. Una stampa i cui slot derivano tutti 0 — PLA, PETG, TPU, PVA — salta del tutto il preriscaldamento e parte subito.',
     preheatFilamentTargetsReset: 'Ripristina valori predefiniti',
     preheatFilamentTargetsDefaultRow: 'Altro / non mappato',
     preheatMaxWait: 'Attesa max (secondi)',
@@ -2305,6 +2305,7 @@ export default {
     calibrationMode_on: 'Attivo',
     calibrationMode_auto: 'Auto',
     preheatTargetOverride: 'Sovrascrivi target camera (°C, vuoto = per filamento)',
+    preheatTargetOverrideHelp: '0 riscalda il piano ed esegue il mantenimento senza camera. Se lasciato vuoto, una stampa che non richiede la camera salta del tutto il preriscaldamento.',
     plateClear: 'Conferma piatto libero',
     requirePlateClear: 'Richiedi conferma piatto libero',
     requirePlateClearDescription: 'Quando questa opzione è abilitata, lo scheduler attende una conferma per stampante che il piatto sia libero prima di avviare le stampe in coda su stampanti con lavori completati. Disabilitandola vengono nascosti anche il badge di stato del piatto e il pulsante "Segna il piatto come liberato" sulle schede stampante.',

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

@@ -2331,7 +2331,7 @@ export default {
     preheatEnabled: 'プレヒート & ソークを有効化',
     preheatEnabledDesc: 'オフにすると、キュー内の印刷は即時開始されます。各キュー項目で印刷ごとに上書き可能です。',
     preheatFilamentTargetsLabel: 'フィラメント別チャンバー目標 (°C)',
-    preheatFilamentTargetsHint: 'Bambuddyはロード済みのAMSスロット中で最も高い目標値を選びます。PLAのみの印刷は0となり、チャンバー段階は自動でスキップされます。',
+    preheatFilamentTargetsHint: 'Bambuddyはロード済みのAMSスロット中で最も高い目標値を選びます。すべてのスロットが0となる印刷(PLA、PETG、TPU、PVA)は予熱を完全にスキップし、すぐに開始します。',
     preheatFilamentTargetsReset: 'デフォルトにリセット',
     preheatFilamentTargetsDefaultRow: 'その他 / 未登録',
     preheatMaxWait: '最大待機時間(秒)',
@@ -2348,6 +2348,7 @@ export default {
     calibrationMode_on: 'オン',
     calibrationMode_auto: '自動',
     preheatTargetOverride: 'チャンバー目標を上書き (°C、空欄でフィラメント既定値)',
+    preheatTargetOverrideHelp: '0 はチャンバーなしでベッドを加熱し、ソークを実行します。空欄のままにすると、チャンバーが不要な印刷は予熱を完全にスキップします。',
     plateClear: 'プレートクリア確認',
     requirePlateClear: 'プレートクリア確認を必須にする',
     requirePlateClearDescription: '有効にすると、スケジューラーは完了したプリンターでキューの印刷を開始する前に、プリンターごとのプレートクリア確認を待ちます。無効にすると、プリンターカード上のプレート状態バッジと「プレートをクリア済みにする」ボタンも非表示になります。',

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

@@ -2212,7 +2212,7 @@ export default {
     preheatEnabled: '예열 & 소크 활성화',
     preheatEnabledDesc: '꺼짐일 경우 대기열 인쇄가 즉시 시작됩니다. 각 항목은 인쇄별로 재정의할 수 있습니다.',
     preheatFilamentTargetsLabel: '필라멘트별 챔버 목표 (°C)',
-    preheatFilamentTargetsHint: 'Bambuddy는 로드된 AMS 슬롯 중 가장 높은 목표를 선택합니다. PLA만 사용하는 인쇄는 0으로 도출되어 챔버 단계를 자동으로 건너뜁니다.',
+    preheatFilamentTargetsHint: 'Bambuddy는 로드된 AMS 슬롯 중 가장 높은 목표를 선택합니다. 모든 슬롯이 0으로 도출되는 인쇄(PLA, PETG, TPU, PVA)는 예열을 완전히 건너뛰고 즉시 시작합니다.',
     preheatFilamentTargetsReset: '기본값으로 재설정',
     preheatFilamentTargetsDefaultRow: '기타 / 미매핑',
     preheatMaxWait: '최대 대기 (초)',
@@ -2229,6 +2229,7 @@ export default {
     calibrationMode_on: '켜기',
     calibrationMode_auto: '자동',
     preheatTargetOverride: '챔버 목표 재정의 (°C, 비우면 필라멘트 기본값)',
+    preheatTargetOverrideHelp: '0은 챔버 없이 베드를 가열하고 소크를 실행합니다. 비워 두면 챔버가 필요 없는 인쇄는 예열을 완전히 건너뜁니다.',
     plateClear: '플레이트 비움 확인',
     requirePlateClear: '플레이트 비움 확인 필요',
     requirePlateClearDescription: '활성화하면 스케줄러가 완료된 작업이 있는 프린터에서 대기 중인 인쇄를 시작하기 전에 프린터별 플레이트 비움 확인을 기다립니다.',

+ 2 - 1
frontend/src/i18n/locales/nl.ts

@@ -2352,7 +2352,7 @@ export default {
     preheatEnabled: 'Voorverwarmen en stabiliseren inschakelen',
     preheatEnabledDesc: 'Wanneer uitgeschakeld worden afdrukken uit de wachtrij direct verzonden. Elk wachtrij-item kan dit per afdruk overschrijven.',
     preheatFilamentTargetsLabel: 'Kamerdoel per filament (°C)',
-    preheatFilamentTargetsHint: 'Bambuddy kiest het hoogste doel van de geladen AMS-sleuven; afdrukken met alleen PLA krijgen 0 en slaan de kamerfase automatisch over.',
+    preheatFilamentTargetsHint: 'Bambuddy kiest het hoogste doel van de geladen AMS-sleuven. Een afdruk waarvan alle sleuven 0 opleveren — PLA, PETG, TPU, PVA — slaat het voorverwarmen volledig over en start meteen.',
     preheatFilamentTargetsReset: 'Standaardwaarden herstellen',
     preheatFilamentTargetsDefaultRow: 'Overig / niet gekoppeld',
     preheatMaxWait: 'Max. wachttijd (seconden)',
@@ -2369,6 +2369,7 @@ export default {
     calibrationMode_on: 'Aan',
     calibrationMode_auto: 'Auto',
     preheatTargetOverride: 'Kamerdoel overschrijven (°C, leeg = filamentstandaard)',
+    preheatTargetOverrideHelp: '0 verwarmt het bed en voert het doorwarmen uit zonder kamer. Laat het leeg en een afdruk zonder kamerbehoefte slaat het voorverwarmen volledig over.',
     plateClear: 'Bevestiging plaat vrij',
     requirePlateClear: 'Bevestiging vereisen dat plaat vrij is',
     requirePlateClearDescription: 'Wanneer ingeschakeld wacht de scheduler op een bevestiging per printer dat de plaat vrij is voordat afdrukken uit de wachtrij starten op printers met voltooide taken. Als je dit uitschakelt, worden ook de plaatstatusbadge en de knop "Plaat als leeg markeren" op printerkaarten verborgen.',

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

@@ -2288,7 +2288,7 @@ export default {
     preheatEnabled: 'Ativar pré-aquecimento & soak',
     preheatEnabledDesc: 'Quando desligado, as impressões na fila iniciam imediatamente. Cada item pode sobrescrever por impressão.',
     preheatFilamentTargetsLabel: 'Alvo da câmara por filamento (°C)',
-    preheatFilamentTargetsHint: 'Bambuddy escolhe o alvo mais alto entre os slots AMS carregados; impressões apenas com PLA derivam 0 e pulam a fase da câmara automaticamente.',
+    preheatFilamentTargetsHint: 'Bambuddy escolhe o alvo mais alto entre os slots AMS carregados. Uma impressão cujos slots derivam todos 0 — PLA, PETG, TPU, PVA — pula o preaquecimento por completo e começa imediatamente.',
     preheatFilamentTargetsReset: 'Redefinir para padrões',
     preheatFilamentTargetsDefaultRow: 'Outro / sem mapeamento',
     preheatMaxWait: 'Espera máx. (segundos)',
@@ -2305,6 +2305,7 @@ export default {
     calibrationMode_on: 'Ligado',
     calibrationMode_auto: 'Auto',
     preheatTargetOverride: 'Sobrescrever alvo da câmara (°C, vazio = por filamento)',
+    preheatTargetOverrideHelp: '0 aquece a mesa e executa a estabilização sem a câmara. Em branco, uma impressão que não precisa de câmara pula o preaquecimento por completo.',
     plateClear: 'Confirmação de placa livre',
     requirePlateClear: 'Exigir confirmação de placa livre',
     requirePlateClearDescription: 'Quando ativado, o agendador aguarda uma confirmação de placa livre por impressora antes de iniciar impressões na fila em impressoras com trabalhos concluídos. Desativar isso também oculta o indicador de status da placa e o botão "Marcar placa como liberada" nos cartões das impressoras.',

+ 2 - 1
frontend/src/i18n/locales/ru.ts

@@ -2212,7 +2212,7 @@ export default {
     preheatEnabled: "Включить преднагрев и выдержку",
     preheatEnabledDesc: "Если выключено, задания очереди отправляются сразу. Для каждого задания можно задать отдельное значение.",
     preheatFilamentTargetsLabel: "Целевая температура камеры по типу филамента (°C)",
-    preheatFilamentTargetsHint: "Bambuddy выбирает наибольшее значение среди задействованных слотов AMS; для печати только PLA автоматически используется 0 и этап прогрева камеры пропускается.",
+    preheatFilamentTargetsHint: "Bambuddy выбирает наибольшее значение среди задействованных слотов AMS. Печать, для всех слотов которой получается 0 — PLA, PETG, TPU, PVA, — полностью пропускает прогрев и начинается сразу.",
     preheatFilamentTargetsReset: "Восстановить значения по умолчанию",
     preheatFilamentTargetsDefaultRow: "Прочее / без сопоставления",
     preheatMaxWait: "Максимальное ожидание (с)",
@@ -2229,6 +2229,7 @@ export default {
     calibrationMode_on: "Включено",
     calibrationMode_auto: "Авто",
     preheatTargetOverride: "Переопределение температуры камеры (°C; пусто — значение для филамента)",
+    preheatTargetOverrideHelp: "0 нагревает стол и выполняет выдержку без камеры. Если поле пустое, печать, которой камера не нужна, полностью пропускает прогрев.",
     plateClear: "Подтверждение очистки пластины",
     requirePlateClear: "Требовать подтверждение очистки пластины",
     requirePlateClearDescription: "Если включено, планировщик не запускает следующее задание на принтере с завершённой печатью, пока не подтверждена очистка пластины. Отключение также скрывает индикатор состояния пластины и кнопку «Пластина очищена» на карточках принтеров.",

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

@@ -2336,7 +2336,7 @@ export default {
     preheatEnabled: 'Ön ısıtma & soak\'u etkinleştir',
     preheatEnabledDesc: 'Kapalıyken kuyruktaki baskılar hemen başlar. Her kuyruk öğesi baskı bazında geçersiz kılabilir.',
     preheatFilamentTargetsLabel: 'Filament başına oda hedefi (°C)',
-    preheatFilamentTargetsHint: 'Bambuddy yüklü AMS yuvaları arasındaki en yüksek hedefi seçer; yalnız PLA baskıları 0 türeterek oda aşamasını otomatik atlar.',
+    preheatFilamentTargetsHint: 'Bambuddy yüklü AMS yuvaları arasındaki en yüksek hedefi seçer. Tüm yuvaları 0 türeten baskılar — PLA, PETG, TPU, PVA — ön ısıtmayı tamamen atlar ve hemen başlar.',
     preheatFilamentTargetsReset: 'Varsayılanlara sıfırla',
     preheatFilamentTargetsDefaultRow: 'Diğer / eşlenmemiş',
     preheatMaxWait: 'Maks. bekleme (saniye)',
@@ -2353,6 +2353,7 @@ export default {
     calibrationMode_on: 'Açık',
     calibrationMode_auto: 'Otomatik',
     preheatTargetOverride: 'Oda hedefini geçersiz kıl (°C, boş = filament varsayılanı)',
+    preheatTargetOverrideHelp: '0, oda olmadan tablayı ısıtır ve bekleme süresini uygular. Boş bırakılırsa oda gerektirmeyen bir baskı ön ısıtmayı tamamen atlar.',
     plateClear: 'Plaka Temizleme Onayı',
     requirePlateClear: 'Plaka temizleme onayı gerektir',
     requirePlateClearDescription: 'Etkinleştirildiğinde, planlayıcı bitmiş işleri olan yazıcılarda kuyruktaki baskıları başlatmadan önce yazıcı başına plaka temizleme onayını bekler. Bunu devre dışı bırakmak ayrıca plaka durum rozetini ve yazıcı kartlarındaki "Plakayı temizlendi olarak işaretle" düğmesini gizler.',

+ 2 - 1
frontend/src/i18n/locales/uk.ts

@@ -2351,7 +2351,7 @@ export default {
     preheatEnabled: "Увімкнути попереднє нагрівання й термовитримку",
     preheatEnabledDesc: "Якщо вимкнено, завдання з черги надсилаються на друк негайно. Для кожного елемента черги це налаштування можна перевизначити окремо.",
     preheatFilamentTargetsLabel: "Цільова температура камери за типом філаменту (°C)",
-    preheatFilamentTargetsHint: "Bambuddy вибирає найвищу цільову температуру серед завантажених слотів AMS. Для друку лише з PLA визначається значення 0, тому етап нагрівання камери автоматично пропускається.",
+    preheatFilamentTargetsHint: "Bambuddy вибирає найвищу цільову температуру серед завантажених слотів AMS. Друк, для всіх слотів якого визначається 0 — PLA, PETG, TPU, PVA, — повністю пропускає прогрівання й починається одразу.",
     preheatFilamentTargetsReset: "Відновити налаштування за замовчуванням",
     preheatFilamentTargetsDefaultRow: "Інше / не зіставлене",
     preheatMaxWait: "Максимальний час очікування (секунди)",
@@ -2368,6 +2368,7 @@ export default {
     calibrationMode_on: "Увімкнено",
     calibrationMode_auto: "Авто",
     preheatTargetOverride: "Інша цільова температура камери (°C; порожньо — типове значення для філаменту)",
+    preheatTargetOverrideHelp: "0 нагріває стіл і виконує витримку без камери. Якщо поле порожнє, друк, якому камера не потрібна, повністю пропускає прогрівання.",
     plateClear: "Підтвердження очищення друкарської пластини",
     requirePlateClear: "Вимагати підтвердження очищення друкарської пластини",
     requirePlateClearDescription: "Якщо ввімкнено, планувальник перед запуском наступного завдання на принтері із завершеним друком очікує підтвердження, що друкарську пластину очищено. Вимкнення також приховує індикатор стану пластини та кнопку «Позначити пластину як очищену» на картках принтерів.",

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

@@ -2333,7 +2333,7 @@ export default {
     preheatEnabled: '启用预热与保温',
     preheatEnabledDesc: '关闭时,排队打印立即开始。每个队列项可按单次打印覆盖。',
     preheatFilamentTargetsLabel: '按耗材的腔体目标 (°C)',
-    preheatFilamentTargetsHint: 'Bambuddy 会在已装入的 AMS 槽位中选择最高目标;仅 PLA 的打印推导为 0 并自动跳过腔体阶段。',
+    preheatFilamentTargetsHint: 'Bambuddy 会在已装入的 AMS 槽位中选择最高目标;所有槽位都推导为 0 的打印(PLA、PETG、TPU、PVA)将完全跳过预热并立即开始。',
     preheatFilamentTargetsReset: '重置为默认值',
     preheatFilamentTargetsDefaultRow: '其他 / 未映射',
     preheatMaxWait: '最长等待(秒)',
@@ -2350,6 +2350,7 @@ export default {
     calibrationMode_on: '开启',
     calibrationMode_auto: '自动',
     preheatTargetOverride: '覆盖腔体目标 (°C,留空使用耗材默认)',
+    preheatTargetOverrideHelp: '0 表示只加热热床并执行保温,不加热腔体。留空时,不需要腔体加热的打印会完全跳过预热。',
     plateClear: '热床清空确认',
     requirePlateClear: '需要热床清空确认',
     requirePlateClearDescription: '启用后,调度器会在已完成打印的打印机上启动排队打印之前,等待每台打印机的热床清空确认。禁用后,也会隐藏打印机卡片上的打印板状态标记和“将打印板标记为已清理”按钮。',

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

@@ -2333,7 +2333,7 @@ export default {
     preheatEnabled: '啟用預熱與保溫',
     preheatEnabledDesc: '關閉時,佇列列印立即開始。每個佇列項目可依單次列印覆寫。',
     preheatFilamentTargetsLabel: '依耗材的腔體目標 (°C)',
-    preheatFilamentTargetsHint: 'Bambuddy 在已裝入的 AMS 槽位中挑選最高目標;只有 PLA 的列印推導為 0,並自動跳過腔體階段。',
+    preheatFilamentTargetsHint: 'Bambuddy 在已裝入的 AMS 槽位中挑選最高目標;所有槽位都推導為 0 的列印(PLA、PETG、TPU、PVA)會完全跳過預熱並立即開始。',
     preheatFilamentTargetsReset: '重設為預設值',
     preheatFilamentTargetsDefaultRow: '其他 / 未對應',
     preheatMaxWait: '最長等待(秒)',
@@ -2350,6 +2350,7 @@ export default {
     calibrationMode_on: '開啟',
     calibrationMode_auto: '自動',
     preheatTargetOverride: '覆寫腔體目標 (°C,留空使用耗材預設)',
+    preheatTargetOverrideHelp: '0 表示只加熱熱床並執行保溫,不加熱腔體。留空時,不需要腔體加熱的列印會完全跳過預熱。',
     plateClear: '熱床清空確認',
     requirePlateClear: '需要熱床清空確認',
     requirePlateClearDescription: '啟用後,排程器會在已完成列印的印表機上啟動佇列列印之前,等待每臺印表機的熱床清空確認。停用後,也會隱藏印表機卡片上的列印板狀態標記和「將列印板標記為已清理」按鈕。',

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
static/assets/index-BbLrhGQ7.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-DlHDB85v.js"></script>
+    <script type="module" crossorigin src="/assets/index-BbLrhGQ7.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-ChscM3lF.css">
   </head>
   <body>

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác