Explorar o código

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 hai 13 horas
pai
achega
069ee8fc87

+ 1 - 0
CHANGELOG.md

@@ -29,6 +29,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **Every FTP session Bambuddy opens now records how it closed (#3009, reported by @grengojbo)** — the report traced a print completion that opened two FTP connections to the printer, deleted one file and then, as far as the log showed, did nothing else until the printer was powered off 21 minutes later, and concluded the connections were being left open. They were not: the post-print SD-card cleanup opens one connection per candidate filename and closes each in a `finally`, which a run against a real FTPS server confirms at the server end for both the delete and the 550 not-here case. The trouble is that nothing in the log could have said so. Neither the clean close nor the hard socket drop logged anything at any level, so a session closed properly and a socket genuinely abandoned produced the same output — none — and the only way to tell them apart was to read the source. Both now log one DEBUG line naming the printer, whether QUIT was acknowledged or the socket had to be dropped without it, why, and how long the session was held. Every connect in a debug log is now paired with a close, so the next person suspecting a leaked FTP connection can settle it from a support bundle rather than by inference. Nothing about the connection handling itself changed, and at default log level nothing new is printed. This does not explain the SD-card read/write error in that report or in #645; it only removes one theory from the list by making it checkable.
 
 ### Fixed
+- **Preheat & Heat Soak delayed PLA prints by minutes with nothing to preheat for (#3041)** — with the feature on, a queued PLA print sat for five to seven minutes before the file was even uploaded, and turning the feature off started it immediately. The per-filament map does what it says and gives PLA a chamber target of 0, and the chamber phase duly skipped — but the stage then heated the bed to the print's own temperature, waited for it, and held the full soak anyway, because the soak had no idea it was holding for a chamber that was never asked for. That is pure delay: the print's G-code sets the bed the moment it starts, so preheating it here only moved the warm-up ahead of the upload instead of overlapping with it, and there was no chamber to condition. A chamber target of 0 derived from the filament map now skips the whole stage, so PLA, PETG, TPU and PVA prints dispatch straight away with the feature left on. The one thing the skip still does is put the airduct flap back to cooling, which is a single command and no wait — an H2D left in heating mode by the ABS job before it would otherwise cook the PLA that follows. 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 genuinely want chamber heat are unaffected, including on the P1S/P1P/A1 tier where the bed and the soak timer are the whole mechanism.
 - **Server-side slicing rejected MakerWorld 3MFs over filament-index fields, and the plate preview was never sanitised at all (#3030, reported by @kielsucks)** — Bambu Studio writes `0` into `wall_filament`, `sparse_infill_filament` and `solid_infill_filament` to mean "use whichever filament the object is set to". Bambu Studio and OrcaSlicer 2.4 both accept that; OrcaSlicer 2.3 and earlier counted these from 1 and reject it outright with `wall_filament: 0 not in range [1.000000,...]`, so a sidecar pinned to one of those images refused the file before it ever looked at the profiles Bambuddy supplied. This is the same shape as the `-1` inherit markers fixed in #1201, with a different marker value, so the sanitiser's allowlist is now a key-to-marker map rather than one global constant — a `-1` on a filament index is a real value and is left alone, as is a `0` on a raft field. The key is removed rather than rewritten, which is what makes it safe on every build: the slicer then falls back to its own default, which is the same `0` on the versions where `0` was legal and `1` on the older ones, which is what "the active filament" means there. Fixed alongside, and the more consequential half: **the plate preview never ran the sanitiser**, so a file that sliced fine could still fail the automatic preview and fall back to a guessed filament list — and it matters more there than in a real slice, because the preview deliberately runs on the file's own embedded settings and so has no `--load-settings` pass that could supply a replacement for a rejected field. Both paths now share one sanitiser.
 - **The Slice action offered Bambu Studio files its URI handler cannot load (#3029)** — "Open in Slicer" was offered for `.stl`, `.step` and `.stp` as well as `.3mf`, on the reasonable assumption that a slicer which opens an STL from its own File menu will open one from a link. Bambu Studio does not. Every `bambustudio://` and `bambustudioopen://` URL goes through one import path that refuses any filename which is not `.3mf`, and it refuses it *before* fetching anything — the user gets "Download failed, unknown file format", which names the format and so reads as a broken model rather than an unsupported handoff. OrcaSlicer has no such limit: only MakerWorld links go down its equivalent path, and a link to your own Bambuddy goes to its general downloader, which does not look at the extension at all. So the action is now offered per slicer. With **Open in Slicer** set to Bambu Studio the File Manager offers Slice on 3MF only; set it to OrcaSlicer and STL, STEP and STP come back. In the 3D preview, where both slicers are reachable from one split button, a file the configured slicer cannot take is offered to the other one instead, with the button naming it rather than quietly sending the file somewhere the setting did not ask for. None of this touches the in-app slicer: with **Use Slicer API** on, Slice goes to the sidecar and still takes STL and 3MF whichever desktop slicer is configured.
 - **"Slice" handed the slicer a download link that only worked once (#3029)** — the Slice action in the File Manager, and Open in Slicer on the Archives page, mint a short-lived token and put it in the URL, because a protocol handler cannot carry an `Authorization` header. That token was spent by the first request to reach the endpoint, which made the handoff depend on the slicer fetching the URL exactly once — and nothing guarantees that. Bambu Studio's own downloader retries three times after a failed attempt, transfers get resumed, on-access scanners fetch. Whichever request arrived first won and every later one got a 403, so the slicer opened on an error about the file instead of on the file, and the message it showed described the 403 body it had been handed rather than anything wrong with the model — which made this very hard to read from the user's side. The three protocol-handler downloads — a library file, an archive's sliced 3MF, an archive's source 3MF — now accept their token for the rest of its five-minute life. Nothing else about it changed: it is still bound to the single file it was minted for, still expires in five minutes, and is still refused everywhere else. The two downloads that are not handoffs stay single-use, because what they hand over is itself consumed — a prepared printer-file bundle is deleted the moment it has been streamed. Fixed on the way through: **Open source 3MF in slicer never worked at all with authentication enabled**. The gateway that lets these token-in-URL downloads past the login check matches the path fragment `/dl/`, and the source 3MF's route says `source-dl`, which does not contain it — so the slicer's header-less request was turned away as unauthenticated before the route's own token check ever ran. It failed on every install with auth on, and only there.

+ 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: '啟用後,排程器會在已完成列印的印表機上啟動佇列列印之前,等待每臺印表機的熱床清空確認。停用後,也會隱藏印表機卡片上的列印板狀態標記和「將列印板標記為已清理」按鈕。',

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 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>

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio