Parcourir la source

Read a print's destination from the report topic, not just the request one (issue #1820)

current_project_url was assigned in exactly one place, _handle_request_message,
and _on_message calls that only for the request topic. A print started from the
printer's own screen publishes nothing there, so the field stayed None for the
one case the storage verdict exists for: the file is already in the printer's
model library under /userdata/model/history/, which port 990 does not serve.

The verdict then fell through to the sdcard flag, and @ojimpo's H2S reports that
flag true -- its "card" is the internal eMMC -- so every such print ran the full
sweep before giving up. He measured one: 16 filename-and-directory attempts over
22 FTPS connections, 18 of them refused, 6.4 seconds, then a fallback archive
holding a name and nothing else.

The printer does announce where the file lives, as an unsolicited project_file
*response* on the report topic about two seconds before gcode_state reaches
PREPARE. _process_message now reads the url off it, gated on result SUCCESS and
a non-empty value so a refused dispatch cannot name a file that was never
written. Reading it there rather than only at the request topic also covers an
install neither of us had in view: some brokers refuse the request-topic
subscription, and on those no print of any kind had ever populated the field.

The new branch captures state and nothing else. The "External project_file
payload" diagnostic stays with the request-topic handler: our own dispatch is
echoed on both topics, the request-topic echo lands first and clears
_own_project_file_key, so reusing the diagnostic here would have logged every
Bambuddy-started print as somebody else's. A test pins that.

What the print names is now what gets tried -- the five directories a copy could
be in, rather than the ~110 connections that cannot succeed. The probe is still
worth running: an H2S keeps recently used jobs under /cache and archives them in
full while they last, which is why the reporter's two prints on the same day
behaved differently. Slicer-sent prints are unchanged.

The banner no longer describes a step that never happened. With no reason
recorded, a blank archive fell back to the original wording -- "Store sent files
on external storage" is off in your slicer -- which on that printer is on, and
which the internal-storage wording from #2780 already explains would not help on
an H2. The archive that most needed that explanation was the only one that could
not be given it.

So file:///userdata/ now earns its own reason, internal_history, separate from
the brtc://emmc dispatch case. A dispatch chose internal storage and can be
aimed elsewhere; a print of a file that was already there had no dispatch at
all, and telling that operator to pick External in Send names a dialog they
never opened. The banner and the connection diagnostic both read the verdict's
reason rather than a fixed one, so the two surfaces cannot give the same printer
different advice. Thirteen locales, and a wiki section the banner links to.

-----

Read the K-profile selection when the mutation runs, not when it is captured

Configure Slot sends cali_idx from selectedKProfile, and the mutation read it
through its own closure. React Query hands a mutation its options from an
effect, so a click landing between a commit and that effect flushing runs the
previous render's mutationFn -- one that captured the selection as it was before
the K-profile query resolved. The payload then carries cali_idx -1 and the
printer binds the default 0.020 instead of the calibrated K, while the dialog
shows the right profile selected throughout.

It surfaced as an intermittent failure of the per-nozzle K-profile test, about
one full-suite run in six. Reproducing it with staggered query resolution showed
the divergence directly: the select element held the correct profile immediately
before and after the click, and the payload still carried -1. That test's slot is
the most exposed case in the file -- a right-hotend slot carrying the left
hotend's index, where the "keep showing the active profile" safety net cannot
repair an empty recompute.

The selection now goes through a ref written during render, so the mutation
resolves it at execute time. An effect would have inherited the same flush
ordering this exists to escape. The K value and the profile's ids travel in the
same payload and had the same exposure, so they move with it.

Measured over a staggered-resolution grid: 2 failures in 15 runs before, 0 in 12
after. api.getSlicerPrinterModels was also missing from the test file's mock, so
that query ran with no query function and rejected in all 37 tests -- mocked now,
though on its own it changed nothing, which is how the ref was confirmed as the
fix rather than assumed.
maziggy il y a 1 semaine
Parent
commit
5dd7bd213f

Fichier diff supprimé car celui-ci est trop grand
+ 1 - 0
CHANGELOG.md


+ 12 - 2
backend/app/api/routes/archives.py

@@ -39,7 +39,11 @@ from backend.app.services.archive import ArchiveService
 from backend.app.services.bambu_ftp import ftps_handshake_blocked, list_files_result_async
 from backend.app.services.design_settings import overrides_from_config
 from backend.app.services.filament_requirements import annotate_rack_groups
-from backend.app.services.print_storage import REASON_INTERNAL_STORAGE, REASON_NO_EXTERNAL_STORAGE
+from backend.app.services.print_storage import (
+    REASON_INTERNAL_HISTORY,
+    REASON_INTERNAL_STORAGE,
+    REASON_NO_EXTERNAL_STORAGE,
+)
 from backend.app.services.printer_media import VIDEO_SUFFIXES, match_ipcam_chunks
 from backend.app.utils.archive_paths import archive_photos_dir, find_archive_photo
 from backend.app.utils.http import build_content_disposition, download_error_response, safe_download_filename
@@ -575,7 +579,13 @@ async def no_3mf_warning(
     # Most specific first. Archives predating this field carry no reason at
     # all, so an install with one H2C and three older printers still gets the
     # H2C explanation rather than the generic one.
-    for candidate in (REASON_INTERNAL_STORAGE, REASON_NO_EXTERNAL_STORAGE):
+    #
+    # REASON_INTERNAL_HISTORY comes last on purpose, even though it is the
+    # narrowest: it is the one cause with no remedy at all -- the file was
+    # already on the printer, in an area port 990 does not serve. The two ahead
+    # of it each end in something the operator can do, so when an install has
+    # both, the actionable explanation is the one worth the banner (#1820).
+    for candidate in (REASON_INTERNAL_STORAGE, REASON_NO_EXTERNAL_STORAGE, REASON_INTERNAL_HISTORY):
         if candidate in reasons:
             return {"has_fallback": True, "reason": candidate}
     return {"has_fallback": True, "reason": None}

+ 64 - 0
backend/app/services/bambu_mqtt.py

@@ -1888,6 +1888,65 @@ class BambuMQTTClient:
                     json.dumps(print_data),
                 )
 
+    def _capture_report_project_file(self, print_data: dict) -> None:
+        """Read a print's destination off a ``project_file`` *response* (#1820).
+
+        ``_handle_request_message`` only ever sees the request topic, so a print
+        started from the printer's own touchscreen -- which publishes nothing --
+        left ``current_project_url`` at None, and the storage verdict fell
+        through to the ``sdcard`` fallback for the one case it was written for.
+        On an H2S that flag is True (its "card" is the internal eMMC), so the
+        verdict came back reachable and the ~110-connection sweep ran in full.
+
+        The printer does announce it: an unsolicited ``project_file`` response
+        on the report topic, ~2 s before ``gcode_state`` reaches PREPARE,
+        carrying ``file:///userdata/model/history/<name>.gcode.3mf``.
+
+        This also covers an install nobody had in view: some brokers refuse the
+        request-topic subscription, and on those no print of any kind has ever
+        populated the field.
+
+        Both kinds of ``project_file`` on this topic are read -- the printer's
+        echo of a dispatch and a screen start -- because both name the
+        destination in ``url``, which is the only thing the verdict wants. What
+        this must NOT do is reuse ``_handle_request_message``'s "External
+        project_file payload" diagnostic: our own dispatch is echoed on *both*
+        topics, the request-topic echo arrives first and clears
+        ``_own_project_file_key``, so by the time this frame lands the key is
+        already None and every Bambuddy-started print would log itself as
+        someone else's.
+        """
+        # Same shape as _handle_request_message: the frame is whatever the
+        # printer put on the wire, and this is the first thing to touch it.
+        if not isinstance(print_data, dict) or print_data.get("command") != "project_file":
+            return
+        # A refused dispatch names a file that was never written. Acting on it
+        # would pin an archive on a destination nothing ever went to.
+        if print_data.get("result") != "SUCCESS":
+            return
+        url = print_data.get("url")
+        if not isinstance(url, str) or not url:
+            return
+        if self.state.current_project_url != url:
+            logger.info(
+                "[%s] Print destination from the report topic: %s",
+                self.serial_number,
+                url,
+            )
+        self.state.current_project_url = url
+        self.state.last_project_url = url
+        # On a screen start this frame is the only place the mapping appears --
+        # no slicer ever sent one. Fill a gap only: when the request topic
+        # already captured this print's mapping that copy is the slicer's own,
+        # and the echo can arrive without the field at all.
+        if self._captured_ams_mapping is None and isinstance(print_data.get("ams_mapping"), list):
+            self._captured_ams_mapping = print_data["ams_mapping"]
+            logger.info(
+                "[%s] Captured ams_mapping from print response: %s",
+                self.serial_number,
+                self._captured_ams_mapping,
+            )
+
     @staticmethod
     def _project_file_key(print_data: dict) -> str:
         """Identity of a project_file dispatch, for telling ours from a slicer's.
@@ -1999,6 +2058,11 @@ class BambuMQTTClient:
         if "print" in payload:
             print_data = payload["print"]
 
+            # Before anything reads the state: this is where a touchscreen-
+            # started print announces where its file lives, and the print-start
+            # handler asks ~2 s later (#1820).
+            self._capture_report_project_file(print_data)
+
             # Check if xcam is nested inside print data
             if "xcam" in print_data:
                 logger.debug("[%s] Found xcam inside print data: %s", self.serial_number, print_data["xcam"])

+ 23 - 1
backend/app/services/print_storage.py

@@ -66,6 +66,16 @@ _INTERNAL_FILE_PREFIXES = ("/userdata/",)
 REASON_INTERNAL_STORAGE = "internal_storage"
 REASON_NO_EXTERNAL_STORAGE = "no_external_storage"
 
+# Same verdict as REASON_INTERNAL_STORAGE, different cause -- and the cause is
+# the whole of the advice. `brtc://emmc/<name>` is a *dispatch* that chose
+# internal storage: a slicer sent the file and the printer filed it where port
+# 990 cannot serve it, which the operator can change by sending it elsewhere.
+# `file:///userdata/...` is a print of a file that was already on the printer --
+# a touchscreen re-print, a Handy start, a Studio send-to-storage printed later
+# -- so there was no dispatch to aim anywhere, and telling that operator to pick
+# "External" in Send describes a step they never took (#1820).
+REASON_INTERNAL_HISTORY = "internal_history"
+
 # Not a storage verdict — the file's location was never in question. The
 # printer's FTPS service was inside its post-failed-handshake cool-off when the
 # print started, so the sweep was skipped without a single connection. Stamped
@@ -230,6 +240,18 @@ def last_print_storage_verdict(state: object | None) -> StorageVerdict:
     return _verdict(getattr(state, "last_project_url", None), state)
 
 
+def _internal_reason(project_url: str | None) -> str:
+    """Which flavour of "internal" *project_url* names.
+
+    Only ever reached on a negative verdict, so the URL is one of the two
+    shapes :func:`url_is_external_storage` answers False for.
+    """
+    if not isinstance(project_url, str):
+        return REASON_INTERNAL_STORAGE
+    scheme = project_url.partition("://")[0].lower()
+    return REASON_INTERNAL_HISTORY if scheme == _LOCAL_FILE_SCHEME else REASON_INTERNAL_STORAGE
+
+
 def _verdict(project_url: str | None, state: object | None) -> StorageVerdict:
     if state is None:
         return _REACHABLE
@@ -245,7 +267,7 @@ def _verdict(project_url: str | None, state: object | None) -> StorageVerdict:
         # the printer already said.
         return StorageVerdict(
             reachable=False,
-            reason=REASON_INTERNAL_STORAGE,
+            reason=_internal_reason(project_url),
             probe_filename=probe_filename_from_url(project_url) if external_storage_present(state) else None,
         )
     if external is True:

+ 8 - 3
backend/app/services/printer_diagnostic.py

@@ -391,16 +391,21 @@ async def run_connection_diagnostic(
     elif not (last_verdict := last_print_storage_verdict(state)).reachable and not await _last_print_file_is_reachable(
         printer, last_verdict, ftps_ok=ftps_state == "ok"
     ):
-        # The toggle is on, a card is in, the printer said it put the last print
-        # on internal storage — and a probe confirmed the file really is out of
+        # The toggle is on, a card is in, the printer said the last print's file
+        # is on internal storage — and a probe confirmed it really is out of
         # reach. That is what H2-series and P2S firmware does, and no setting
         # here changes it (#2762 tracks reading that storage). A pass here would
         # be a lie; a fail would be unresolvable.
+        #
+        # The verdict's own reason, not a fixed one: a print started from the
+        # printer's screen reaches this branch too, and it never involved a
+        # slicer, so the advice attached to REASON_INTERNAL_STORAGE would name a
+        # dialog its operator never opened (#1820).
         checks.append(
             DiagnosticCheck(
                 id="external_storage",
                 status="warn",
-                params={"reason": REASON_INTERNAL_STORAGE},
+                params={"reason": last_verdict.reason or REASON_INTERNAL_STORAGE},
             )
         )
     elif not last_verdict.reachable:

+ 36 - 0
backend/tests/unit/services/test_printer_diagnostic.py

@@ -519,6 +519,42 @@ class TestExternalStorageCheck:
         paths = env.find_remote_file.await_args.args[2]
         assert paths[:2] == ["/Benchy.gcode.3mf", "/cache/Benchy.gcode.3mf"]
 
+    async def test_a_print_of_a_file_already_on_the_printer_gets_its_own_reason(self):
+        """Same verdict, different advice (#1820).
+
+        A print started from the printer's screen reaches this branch too, now
+        that the report topic is read. The internal-storage wording tells the
+        operator to use Send with External selected -- a dialog nobody opened,
+        for a print where nothing was sent at all.
+        """
+        state = _state(
+            store_to_sdcard=True,
+            sdcard=True,
+            sdcard_reported=True,
+            last_project_url="file:///userdata/model/history/JOB_A.gcode.3mf",
+        )
+        with _Env(state=state) as env:
+            result = await run_connection_diagnostic("192.168.1.50", printer=_printer(model="H2S"))
+        check = next(c for c in result.checks if c.id == "external_storage")
+        assert check.status == "warn"
+        assert check.params == {"reason": "internal_history"}
+        # Still asked first: an H2S keeps recently used jobs under /cache.
+        paths = env.find_remote_file.await_args.args[2]
+        assert paths[:2] == ["/JOB_A.gcode.3mf", "/cache/JOB_A.gcode.3mf"]
+
+    async def test_a_screen_started_print_whose_file_is_there_still_passes(self):
+        """The probe outranks the reason for this URL exactly as for the other
+        one -- that copy under /cache is what archives the print in full."""
+        state = _state(
+            store_to_sdcard=True,
+            sdcard=True,
+            sdcard_reported=True,
+            last_project_url="file:///userdata/model/history/JOB_A.gcode.3mf",
+        )
+        with _Env(state=state, file_found="/cache/JOB_A.gcode.3mf"):
+            result = await run_connection_diagnostic("192.168.1.50", printer=_printer(model="H2S"))
+        assert _statuses(result)["external_storage"] == "pass"
+
     async def test_the_internal_storage_warning_yields_to_the_file_being_there(self):
         """#2856. The URL says where the printer *put* the file, not whether
         port 990 can serve it: an H2D with a card in reports `brtc://emmc` and

+ 61 - 1
backend/tests/unit/test_print_storage_2780.py

@@ -14,6 +14,7 @@ So the tests below spend most of their weight on the second failure mode.
 import pytest
 
 from backend.app.services.print_storage import (
+    REASON_INTERNAL_HISTORY,
     REASON_INTERNAL_STORAGE,
     REASON_NO_EXTERNAL_STORAGE,
     external_storage_present,
@@ -140,7 +141,11 @@ class TestFileScheme:
         )
         verdict = print_file_reachable_over_ftp(state)
         assert verdict.reachable is False
-        assert verdict.reason == REASON_INTERNAL_STORAGE
+        # Its own reason, not the dispatch one: nothing was sent for this print,
+        # so the advice attached to REASON_INTERNAL_STORAGE -- pick External in
+        # the slicer's Send dialog -- describes a step that never happened
+        # (#1820).
+        assert verdict.reason == REASON_INTERNAL_HISTORY
 
     @pytest.mark.parametrize("url", [12345, [], {}, object()])
     def test_a_non_string_url_declines_too(self, url):
@@ -219,6 +224,7 @@ class TestReasonIsAlwaysPresentWhenUnreachable:
         [
             FakeState(current_project_url="brtc://emmc/x.3mf"),
             FakeState(sdcard=False, sdcard_reported=True),
+            FakeState(current_project_url="file:///userdata/model/history/x.3mf"),
         ],
     )
     def test_unreachable_carries_a_reason(self, state):
@@ -297,6 +303,60 @@ class TestTheGateUsesThePerPrintUrlOnly:
         assert "last_project_url" not in source.split('"""')[-1]
 
 
+class TestTheTwoInternalReasonsAreToldApart:
+    """Same verdict, different advice (#1820).
+
+    Both URLs mean "port 990 cannot serve this", and until the report topic was
+    read there was only ever one of them to see. A screen-started print names
+    the other, and giving it the dispatch reason puts a banner in front of the
+    operator telling them to pick External in a Send dialog they never opened.
+    """
+
+    def test_a_dispatch_that_chose_internal_storage(self):
+        verdict = print_file_reachable_over_ftp(
+            FakeState(current_project_url="brtc://emmc/Benchy.gcode.3mf", sdcard=True, sdcard_reported=True)
+        )
+
+        assert verdict.reason == REASON_INTERNAL_STORAGE
+
+    @pytest.mark.parametrize(
+        "url",
+        [
+            # Both forms measured on the H2S in #1820: the printer's own file
+            # library, reached from its screen and from Handy.
+            "file:///userdata/model/history/JOB_A.gcode.3mf",
+            "file:///userdata/model/history/Halterung Kuehlschrank V2.gcode.3mf",
+        ],
+    )
+    def test_a_print_of_a_file_that_was_already_there(self, url):
+        verdict = print_file_reachable_over_ftp(FakeState(current_project_url=url, sdcard=True, sdcard_reported=True))
+
+        assert verdict.reason == REASON_INTERNAL_HISTORY
+
+    def test_both_still_earn_a_probe(self):
+        """The reason split changes what the banner says, not what is tried.
+        An H2S keeps a copy of screen-started jobs under /cache for a while, and
+        that copy is what archived #1820's reporter's print 231."""
+        for url in ("brtc://emmc/Cube.gcode.3mf", "file:///userdata/model/history/Cube.gcode.3mf"):
+            verdict = print_file_reachable_over_ftp(
+                FakeState(current_project_url=url, sdcard=True, sdcard_reported=True)
+            )
+
+            assert verdict.probe_filename == "Cube.gcode.3mf"
+
+    def test_the_sticky_reading_splits_them_too(self):
+        """The diagnostic reads the same helper, so a divergence here would
+        surface as one wording in the banner and another in Settings."""
+        state = FakeState(
+            current_project_url=None,
+            last_project_url="file:///userdata/model/history/Cube.gcode.3mf",
+            sdcard=True,
+            sdcard_reported=True,
+        )
+
+        assert last_print_storage_verdict(state).reason == REASON_INTERNAL_HISTORY
+
+
 class TestTimelapseUsesTheNarrowerRule:
     """The printer writes its timelapse to the card itself.
 

+ 162 - 0
backend/tests/unit/test_report_topic_project_file_1820.py

@@ -0,0 +1,162 @@
+"""Where the print file went, read off the report topic (#1820).
+
+Until this landed, ``current_project_url`` was assigned in exactly one place --
+``_handle_request_message`` -- and ``_on_message`` calls that only for the
+request topic. A print started from the printer's own touchscreen publishes
+nothing there, so the field stayed None for the one case the storage verdict in
+``print_storage`` was written for. The verdict then fell through to the
+``sdcard`` fallback, and #1820's H2S reports ``sdcard: true`` (its "card" is the
+internal eMMC), so every such print swept ~110 doomed FTPS connections and
+archived blank with no stated reason.
+
+The printer does announce it, as an unsolicited ``project_file`` *response* on
+the report topic ~2 s before ``gcode_state`` reaches PREPARE. The frames below
+are from that reporter's sanitised capture, taken on an H2S + AMS 2 Pro.
+"""
+
+import pytest
+
+from backend.app.services.bambu_mqtt import BambuMQTTClient
+from backend.app.services.print_storage import (
+    REASON_INTERNAL_HISTORY,
+    print_file_reachable_over_ftp,
+)
+
+pytestmark = pytest.mark.unit
+
+
+@pytest.fixture
+def client():
+    return BambuMQTTClient(ip_address="10.0.0.7", serial_number="H2S1820", access_code="12345678", model="H2S")
+
+
+def screen_start(**overrides) -> dict:
+    """The frame an H2S publishes for a print started from its own screen.
+
+    Trimmed of the fields nothing here reads (``ams``, ``vt_tray``, the
+    calibration flags); everything kept is verbatim from the capture, including
+    the empty ``subtask_name`` and the printer's own ``sequence_id`` counter --
+    the two things that distinguish it from a slicer's dispatch.
+    """
+    print_data = {
+        "command": "project_file",
+        "result": "SUCCESS",
+        "reason": "SUCCESS",
+        "err_code": 0,
+        "sequence_id": "3338",
+        "subtask_name": "",
+        "task_type": 1,
+        "param": "Metadata/plate_1.gcode",
+        "plate": 1,
+        "ams_mapping": [0],
+        "mapping": [1],
+        "url": "file:///userdata/model/history/JOB_A.gcode.3mf",
+    }
+    print_data.update(overrides)
+    return {"print": print_data}
+
+
+class TestAScreenStartedPrintNamesItsFile:
+    def test_the_url_is_captured(self, client):
+        client._process_message(screen_start())
+
+        assert client.state.current_project_url == "file:///userdata/model/history/JOB_A.gcode.3mf"
+
+    def test_the_sticky_copy_is_captured_too(self, client):
+        """The connection diagnostic runs after the print, by which point the
+        per-print field has been cleared."""
+        client._process_message(screen_start())
+
+        assert client.state.last_project_url == "file:///userdata/model/history/JOB_A.gcode.3mf"
+
+    def test_the_verdict_now_skips_the_sweep(self, client):
+        """The whole point, in one assertion: with the URL in hand the H2S's
+        ``sdcard: true`` no longer decides the outcome."""
+        client.state.sdcard = True
+        client.state.sdcard_reported = True
+
+        client._process_message(screen_start())
+        verdict = print_file_reachable_over_ftp(client.state)
+
+        assert verdict.reachable is False
+        assert verdict.reason == REASON_INTERNAL_HISTORY
+        # Still probed, because this printer keeps screen-started jobs under
+        # /cache for a while and that copy archives in full when it is there.
+        assert verdict.probe_filename == "JOB_A.gcode.3mf"
+
+    def test_the_mapping_is_captured_when_no_slicer_sent_one(self, client):
+        """On a screen start this frame is the only place it appears."""
+        client._process_message(screen_start())
+
+        assert client._captured_ams_mapping == [0]
+
+    def test_a_mapping_from_the_request_topic_wins(self, client):
+        """The slicer's own mapping describes the same print and arrived first;
+        the echo can carry a different shape or none at all."""
+        client._captured_ams_mapping = [0, -1, -1, -1]
+
+        client._process_message(screen_start(ams_mapping=[3]))
+
+        assert client._captured_ams_mapping == [0, -1, -1, -1]
+
+
+class TestWhatMustNotBeCaptured:
+    def test_a_refused_dispatch_is_ignored(self, client):
+        """It names a file that was never written. Acting on it would pin the
+        next print's archive on a destination nothing went to."""
+        client._process_message(screen_start(result="FAIL", reason="STORAGE_FULL"))
+
+        assert client.state.current_project_url is None
+        assert client._captured_ams_mapping is None
+
+    @pytest.mark.parametrize("url", ["", None, 12345, [], {}])
+    def test_a_missing_or_non_string_url_is_ignored(self, client, url):
+        """The value is whatever the sender put on the wire."""
+        client._process_message(screen_start(url=url))
+
+        assert client.state.current_project_url is None
+
+    def test_another_command_on_the_same_topic_is_ignored(self, client):
+        """push_status carries a `url` field of its own on some firmwares."""
+        client._process_message({"print": {"command": "push_status", "url": "file:///userdata/model/history/x.3mf"}})
+
+        assert client.state.current_project_url is None
+
+    def test_our_own_dispatch_is_not_logged_as_someone_elses(self, client, caplog):
+        """Our publish is echoed on *both* topics. The request-topic echo lands
+        first and clears ``_own_project_file_key``, so a report-topic branch
+        that reused ``_handle_request_message``'s diagnostic would report every
+        Bambuddy-started print as an external one.
+        """
+        dispatch = {
+            "command": "project_file",
+            "sequence_id": "20002",
+            "file": "JOB_NORMAL.gcode.3mf",
+            "url": "brtc://emmc/JOB_NORMAL.gcode.3mf",
+            "subtask_name": "JOB_NORMAL",
+        }
+        client._own_project_file_key = client._project_file_key(dispatch)
+        client._handle_request_message({"print": dispatch})
+        assert client._own_project_file_key is None
+
+        caplog.clear()
+        client._process_message({"print": {**dispatch, "result": "SUCCESS", "is_from_mqtt": True}})
+
+        assert "External project_file payload" not in caplog.text
+        # ...and the capture still happened, which is what makes it worth having
+        # on this topic at all: some brokers refuse the request subscription.
+        assert client.state.current_project_url == "brtc://emmc/JOB_NORMAL.gcode.3mf"
+
+
+class TestTheSlicerPathIsUnchanged:
+    def test_an_external_dispatch_still_sweeps(self, client):
+        """The regression to fear: routing more URLs into the matcher must not
+        turn an ordinary Studio print into a blank archive."""
+        client.state.sdcard = True
+        client.state.sdcard_reported = True
+
+        client._process_message(
+            {"print": {"command": "project_file", "result": "SUCCESS", "url": "ftp://JOB_NORMAL.gcode.3mf"}}
+        )
+
+        assert print_file_reachable_over_ftp(client.state).reachable is True

+ 4 - 0
frontend/src/__tests__/components/ConfigureAmsSlotModal.test.tsx

@@ -17,6 +17,10 @@ vi.mock('../../api/client', () => ({
     getCloudSettingDetail: vi.fn(),
     saveSlotPreset: vi.fn(),
     getSettings: vi.fn().mockResolvedValue({}),
+    // Queried by the modal for the @BBL short-code matcher. Omitted, it is
+    // undefined here, so the query runs with no queryFn and rejects -- a
+    // stray render at an unpredictable moment in every one of these tests.
+    getSlicerPrinterModels: vi.fn().mockResolvedValue({}),
     updateSettings: vi.fn().mockResolvedValue({}),
     getLocalPresets: vi.fn(),
     getBuiltinFilaments: vi.fn(),

+ 21 - 1
frontend/src/__tests__/pages/ArchivesNo3MFBanner.test.tsx

@@ -8,6 +8,10 @@
  * storage that FTPS does not serve at all. #2780's reporter followed that
  * advice, and #1170's before them.
  *
+ * A third cause joined them in #1820: a print started from the printer's own
+ * screen, where no slicer was involved at all and both of the wordings above
+ * describe a step the operator never took.
+ *
  * So these assert the wording actually shown, not just that a banner rendered.
  */
 
@@ -62,6 +66,22 @@ describe('ArchivesPage no-3MF banner', () => {
     expect(screen.getByText('Why this happens')).toBeInTheDocument();
   });
 
+  it('does not blame a slicer that was never involved', async () => {
+    // #1820: a print started from the printer's own screen sends nothing, so
+    // both the generic wording ("turn the setting on") and the internal-storage
+    // wording ("use Send with External") describe a step that never happened.
+    mockWarning({ has_fallback: true, reason: 'internal_history' });
+
+    render(<ArchivesPage />);
+
+    expect(
+      await screen.findByText(/started from a file already on the printer/i),
+    ).toBeInTheDocument();
+    expect(screen.queryByText('See install step 4')).not.toBeInTheDocument();
+    expect(screen.queryByText(/Store sent files on external storage/i)).not.toBeInTheDocument();
+    expect(screen.getByText('Why this happens')).toBeInTheDocument();
+  });
+
   it('names the empty slot, and offers no link because there is nothing to read', async () => {
     mockWarning({ has_fallback: true, reason: 'no_external_storage' });
 
@@ -87,7 +107,7 @@ describe('ArchivesPage no-3MF banner', () => {
     // The variant suffix is built by string concatenation, so a typo in one
     // locale key surfaces as a raw "archives.no3mfBanner.titleX" on screen
     // instead of failing anything.
-    for (const reason of [null, 'internal_storage', 'no_external_storage']) {
+    for (const reason of [null, 'internal_storage', 'no_external_storage', 'internal_history']) {
       localStorage.clear();
       mockWarning({ has_fallback: true, reason });
 

+ 4 - 1
frontend/src/api/client.ts

@@ -4855,7 +4855,10 @@ export const api = {
   },
   rebuildSearchIndex: () => request<{ message: string }>('/archives/search/rebuild-index', { method: 'POST' }),
   getNo3MFWarning: () =>
-    request<{ has_fallback: boolean; reason: 'internal_storage' | 'no_external_storage' | null }>(
+    request<{
+      has_fallback: boolean;
+      reason: 'internal_storage' | 'no_external_storage' | 'internal_history' | null;
+    }>(
       '/archives/no-3mf-warning',
     ),
   updateArchive: (id: number, data: {

+ 16 - 4
frontend/src/components/ConfigureAmsSlotModal.tsx

@@ -333,6 +333,16 @@ export function ConfigureAmsSlotModal({
   const { t } = useTranslation();
   const [selectedPresetId, setSelectedPresetId] = useState<string>('');
   const [selectedKProfile, setSelectedKProfile] = useState<KProfile | null>(null);
+  // The same value, readable at mutation-execute time rather than at
+  // closure-capture time. useMutation hands its options to the observer from an
+  // *effect*, so a click that lands between a commit and that effect flushing
+  // runs the previous render's mutationFn — one that closed over the profile as
+  // it was before the K-profile query resolved. The picker showed the right
+  // profile and the printer was sent cali_idx -1, binding the default 0.020
+  // instead of the calibrated K. Written during render on purpose: an effect
+  // here would inherit the very flush ordering this exists to escape.
+  const selectedKProfileRef = useRef<KProfile | null>(null);
+  selectedKProfileRef.current = selectedKProfile;
   const [colorHex, setColorHex] = useState<string>(''); // Just the 6-char hex, no alpha
   const [colorInput, setColorInput] = useState<string>(''); // User's text input (name or hex)
   const [searchQuery, setSearchQuery] = useState('');
@@ -477,7 +487,7 @@ export function ConfigureAmsSlotModal({
       const parsed = parsePresetName(presetName);
 
       // Get cali_idx from selected K profile's slot_id (-1 = use default 0.020)
-      const caliIdx = selectedKProfile?.slot_id ?? -1;
+      const caliIdx = selectedKProfileRef.current?.slot_id ?? -1;
 
       // Use custom color if set, otherwise use current slot color or default
       const color = colorHex || slotInfo.trayColor?.slice(0, 6) || 'FFFFFF';
@@ -593,7 +603,9 @@ export function ConfigureAmsSlotModal({
       }
 
       // Parse K value from selected profile
-      const kValue = selectedKProfile?.k_value ? parseFloat(selectedKProfile.k_value) : 0;
+      const kValue = selectedKProfileRef.current?.k_value
+        ? parseFloat(selectedKProfileRef.current.k_value)
+        : 0;
 
       // Determine tray_type: prefer parsed material from preset name (handles "Support for"
       // patterns correctly) over stored filament_type which may have been parsed with old logic.
@@ -615,8 +627,8 @@ export function ConfigureAmsSlotModal({
         nozzle_diameter: nozzleDiameter,
         setting_id: settingId, // Full setting ID for slicer compatibility (empty for local)
         // Pass K profile's filament_id and setting_id for proper linking
-        kprofile_filament_id: selectedKProfile?.filament_id,
-        kprofile_setting_id: selectedKProfile?.setting_id || undefined,
+        kprofile_filament_id: selectedKProfileRef.current?.filament_id,
+        kprofile_setting_id: selectedKProfileRef.current?.setting_id || undefined,
         // Also pass the K value directly for extrusion_cali_set command
         k_value: kValue,
       });

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

@@ -898,6 +898,8 @@ export default {
       bodyInternalStorage: 'Bambu Studio hat die geslicte Datei im internen Speicher des Druckers statt auf der Karte abgelegt, daher gab es für Bambuddy nichts über FTP zu lesen. Bei der H2-Serie und dem P2S macht die Schaltfläche "Drucken" das immer — nur "Senden" bietet eine Auswahl, und auch die steht standardmäßig auf "Cache". Diese Drucke werden weiterhin mit Namen und Zeiten archiviert, nur ohne Vorschaubild und Slicer-Metadaten. Für vollständige Archive den Druck aus Bambuddy starten oder in OrcaSlicer slicen — oder in Bambu Studio "Senden" mit "Extern" verwenden und den Druck danach starten. Alle setzen eine Karte oder einen Stick im Drucker voraus.',
       titleNoExternalStorage: 'Einige kürzliche Drucke konnten nicht archiviert werden — kein Speicher im Drucker',
       bodyNoExternalStorage: 'Der Drucker meldet weder Karte noch Stick im Steckplatz, daher hatte die geslicte Datei keinen Ablageort und Bambuddy nichts zu lesen. Legen Sie einen ein, dann wird der nächste Druck vollständig archiviert.',
+      titleInternalHistory: 'Einige kürzliche Drucke wurden aus einer Datei gestartet, die bereits auf dem Drucker lag',
+      bodyInternalHistory: 'Diese Drucke liefen aus der eigenen Bibliothek des Druckers — ein erneuter Druck über sein Display, ein Start aus Handy oder eine früher gesendete und später gedruckte Datei. Bambuddy liest Druckdateien über FTP, und das bedient nur Karte oder Stick, während der Drucker diese Bibliothek in einem Bereich ablegt, den FTP nicht erreicht — es gab also keine 3MF zu lesen. Keine Slicer-Einstellung ändert das, denn für diese Drucke wurde nichts gesendet. Sie werden weiterhin mit Namen und Zeiten archiviert, und unter "Archiv bearbeiten" lässt sich das verbrauchte Filament von Hand eintragen. Für ein vollständiges Archiv starten Sie den Druck stattdessen aus Bambuddy oder aus Ihrem Slicer.',
       dismissLabel: 'Hinweis schließen',
     },
     searchPlaceholder: 'Archiv durchsuchen...',
@@ -6851,6 +6853,7 @@ export default {
         skip_unsupported_model: 'Dieses Modell hat einen SD-Slot, aber keine Möglichkeit, die Option zu aktivieren — die aktuelle P1-Firmware zeigt den Schalter in Bambu Studio nicht an und der Drucker hat kein Display. Hier gibt es nichts zu beheben; archivierten Drucken fehlen möglicherweise Vorschaubilder und Slicer-Metadaten, bis Bambu Lab dies per Firmware unterstützt.',
         fail_no_media: 'Die Option ist aktiviert, aber der Drucker meldet weder Karte noch Stick im Steckplatz, daher können gesendete Dateien nirgends abgelegt werden. Legen Sie einen ein und drucken Sie erneut — bis dahin fehlen jedem archivierten Druck Vorschaubild und Slicer-Metadaten.',
         warn_internal_storage: 'Die Option ist aktiviert und ein Speicher ist vorhanden, aber der letzte Druck landete dennoch im internen Speicher des Druckers, den Bambuddy nicht lesen kann. Bei der H2-Serie und dem P2S sendet die Schaltfläche "Drucken" in Bambu Studio unabhängig von dieser Option immer dorthin. Drucke werden mit Namen und Zeiten archiviert, aber ohne Vorschaubild und Slicer-Metadaten. Für vollständige Archive den Druck aus Bambuddy starten oder in OrcaSlicer slicen — oder in Bambu Studio "Senden" mit "Extern" verwenden und den Druck danach starten.',
+        warn_internal_history: 'Die Option ist aktiv und Speicher vorhanden, aber der letzte Druck lief aus einer Datei, die bereits auf dem Drucker lag — ein erneuter Druck über sein Display, ein Start aus Handy oder eine früher gesendete Datei. Diese Bibliothek liegt im internen Speicher, den Bambuddy nicht lesen kann, und keine Einstellung ändert das, denn für diesen Druck wurde nichts gesendet. Er wird weiterhin mit Namen und Zeiten archiviert, nur ohne Vorschaubild und Slicer-Metadaten. Für vollständige Archive Drucke aus Bambuddy starten oder aus dem Slicer senden.',
       },
       port_rtsps: {
         title: 'Kameraport ({{protocol}} {{port}})',

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

@@ -904,6 +904,8 @@ export default {
       bodyInternalStorage: 'Bambu Studio put the sliced file on the printer\'s internal storage instead of the card, so there was nothing for Bambuddy to read over FTP. On H2-series and P2S its Print button always does that — only Send offers a choice, and that defaults to Cache too. Those prints are still archived with their name and timing, just without a thumbnail or slicer metadata. For complete archives, start the print from Bambuddy, or slice in OrcaSlicer — or in Bambu Studio use Send with External selected and start the print afterwards. All of them need a card or stick in the printer.',
       titleNoExternalStorage: 'Some recent prints couldn\'t be archived — no storage in the printer',
       bodyNoExternalStorage: 'The printer reports no card or stick in its slot, so the sliced file had nowhere to land and Bambuddy had nothing to read. Insert one and the next print will archive in full.',
+      titleInternalHistory: 'Some recent prints were started from a file already on the printer',
+      bodyInternalHistory: 'Those prints ran from the printer\'s own library — a re-print from its screen, a start from Handy, or a file sent earlier and printed later. Bambuddy reads print files over FTP, which serves only the card or stick, while the printer keeps that library in an area FTP cannot reach, so there was no 3MF to read. No slicer setting changes this, because nothing was sent for these prints. They are still archived with their name and timing, and Edit Archive lets you fill in the filament used by hand. For a complete archive, start the print from Bambuddy or from your slicer instead.',
     },
     searchPlaceholder: 'Search archives...',
     filterByPrinter: 'Filter by printer',
@@ -6901,6 +6903,7 @@ export default {
         skip_unsupported_model: 'This model has an SD slot but no way to turn the option on — current P1-series firmware doesn\'t expose the toggle in Bambu Studio and the printer has no screen. Nothing to fix here; archived prints may lack thumbnails and slicer metadata until Bambu Lab adds firmware support.',
         fail_no_media: 'The option is on, but the printer reports no card or stick in its slot, so there is nowhere for sent files to go. Insert one and print again — until then every archived print will be missing its thumbnail and slicer metadata.',
         warn_internal_storage: 'The option is on and storage is present, but the last print still went to the printer\'s internal storage, which Bambuddy cannot read. On H2-series and P2S, Bambu Studio\'s Print button always sends there whatever this option is set to. Prints archive with their name and timing, but without a thumbnail or slicer metadata. For complete archives, start prints from Bambuddy or slice in OrcaSlicer — or in Bambu Studio use Send with External selected, then start the print.',
+        warn_internal_history: 'The option is on and storage is present, but the last print ran from a file that was already on the printer — a re-print from its screen, a start from Handy, or a file sent earlier. That library sits on internal storage Bambuddy cannot read, and no setting changes it, because nothing was sent for that print. It still archives with its name and timing, but without a thumbnail or slicer metadata. For complete archives, start prints from Bambuddy or send them from the slicer.',
       },
       port_rtsps: {
         title: 'Camera port ({{protocol}} {{port}})',

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

@@ -898,6 +898,8 @@ export default {
       bodyInternalStorage: 'Bambu Studio guardó el archivo laminado en el almacenamiento interno de la impresora en lugar de la tarjeta, así que Bambuddy no tenía nada que leer por FTP. En la serie H2 y la P2S su botón «Imprimir» siempre lo hace: solo «Enviar» ofrece elección, y también viene con «Caché» por defecto. Esas impresiones se siguen archivando con su nombre y sus tiempos, solo que sin miniatura ni metadatos del laminador. Para archivos completos, inicia la impresión desde Bambuddy o lamina en OrcaSlicer, o bien en Bambu Studio usa «Enviar» con «Externo» y luego inicia la impresión. Todas requieren una tarjeta o memoria en la impresora.',
       titleNoExternalStorage: 'Algunas impresiones recientes no se pudieron archivar — no hay almacenamiento en la impresora',
       bodyNoExternalStorage: 'La impresora no detecta ninguna tarjeta ni memoria en su ranura, así que el archivo laminado no tenía dónde aterrizar y Bambuddy nada que leer. Inserte una y la próxima impresión se archivará por completo.',
+      titleInternalHistory: 'Algunas impresiones recientes se iniciaron desde un archivo que ya estaba en la impresora',
+      bodyInternalHistory: 'Esas impresiones salieron de la propia biblioteca de la impresora: una reimpresión desde su pantalla, un inicio desde Handy o un archivo enviado antes e impreso después. Bambuddy lee los archivos de impresión por FTP, que solo sirve la tarjeta o la memoria, mientras que la impresora guarda esa biblioteca en una zona que FTP no alcanza, así que no había ningún 3MF que leer. Ninguna opción del laminador cambia esto, porque para estas impresiones no se envió nada. Se siguen archivando con su nombre y sus tiempos, y «Editar archivo» permite anotar a mano el filamento usado. Para un archivo completo, inicia la impresión desde Bambuddy o desde tu laminador.',
       dismissLabel: 'Descartar este aviso',
     },
     searchPlaceholder: 'Buscar archivos...',
@@ -6859,6 +6861,7 @@ export default {
         skip_unsupported_model: 'Este modelo tiene ranura SD pero no hay forma de activar la opción — el firmware actual de la serie P1 no muestra el interruptor en Bambu Studio y la impresora no tiene pantalla. Aquí no hay nada que arreglar; a las impresiones archivadas pueden faltarles miniaturas y metadatos del slicer hasta que Bambu Lab lo admita por firmware.',
         fail_no_media: 'La opción está activada, pero la impresora no detecta ninguna tarjeta ni memoria en su ranura, así que los archivos enviados no tienen dónde ir. Inserte una e imprima de nuevo — hasta entonces, cada impresión archivada carecerá de miniatura y de metadatos del laminador.',
         warn_internal_storage: 'La opción está activada y hay almacenamiento presente, pero la última impresión aun así fue al almacenamiento interno de la impresora, que Bambuddy no puede leer. En la serie H2 y la P2S, el botón «Imprimir» de Bambu Studio siempre envía ahí, sea cual sea este ajuste. Las impresiones se archivan con su nombre y sus tiempos, pero sin miniatura ni metadatos del laminador. Para archivos completos, inicia las impresiones desde Bambuddy o lamina en OrcaSlicer, o bien en Bambu Studio usa «Enviar» con «Externo» y luego inicia la impresión.',
+        warn_internal_history: 'La opción está activada y hay almacenamiento, pero la última impresión salió de un archivo que ya estaba en la impresora: una reimpresión desde su pantalla, un inicio desde Handy o un archivo enviado antes. Esa biblioteca reside en el almacenamiento interno, que Bambuddy no puede leer, y ninguna opción lo cambia, porque para esa impresión no se envió nada. Se sigue archivando con su nombre y sus tiempos, pero sin miniatura ni metadatos del laminador. Para archivos completos, inicia las impresiones desde Bambuddy o envíalas desde el laminador.',
       },
       port_rtsps: {
         title: 'Puerto de la cámara ({{protocol}} {{port}})',

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

@@ -898,6 +898,8 @@ export default {
       bodyInternalStorage: 'Bambu Studio a placé le fichier tranché dans le stockage interne de l\'imprimante au lieu de la carte, Bambuddy n\'avait donc rien à lire en FTP. Sur les séries H2 et P2S, son bouton « Imprimer » le fait toujours : seul « Envoyer » propose un choix, et il est lui aussi réglé sur « Cache » par défaut. Ces impressions restent archivées avec leur nom et leurs durées, simplement sans miniature ni métadonnées slicer. Pour des archives complètes, lancez l\'impression depuis Bambuddy ou tranchez dans OrcaSlicer, ou bien dans Bambu Studio utilisez « Envoyer » avec « Externe » puis lancez l\'impression. Toutes nécessitent une carte ou une clé dans l\'imprimante.',
       titleNoExternalStorage: 'Certaines impressions récentes n\'ont pas pu être archivées — aucun stockage dans l\'imprimante',
       bodyNoExternalStorage: 'L\'imprimante ne signale ni carte ni clé dans son emplacement, le fichier tranché n\'avait donc nulle part où atterrir et Bambuddy rien à lire. Insérez-en une et la prochaine impression sera archivée complètement.',
+      titleInternalHistory: 'Certaines impressions récentes ont été lancées depuis un fichier déjà présent sur l\'imprimante',
+      bodyInternalHistory: 'Ces impressions sont parties de la bibliothèque de l\'imprimante elle-même : une réimpression depuis son écran, un lancement depuis Handy, ou un fichier envoyé plus tôt et imprimé ensuite. Bambuddy lit les fichiers d\'impression en FTP, qui ne dessert que la carte ou la clé, tandis que l\'imprimante conserve cette bibliothèque dans une zone que le FTP n\'atteint pas : il n\'y avait donc aucun 3MF à lire. Aucun réglage du trancheur n\'y change quoi que ce soit, puisque rien n\'a été envoyé pour ces impressions. Elles restent archivées avec leur nom et leurs durées, et « Modifier l\'archive » permet de saisir à la main le filament utilisé. Pour une archive complète, lancez plutôt l\'impression depuis Bambuddy ou depuis votre trancheur.',
       dismissLabel: 'Ignorer ce message',
     },
     searchPlaceholder: 'Chercher dans les archives...',
@@ -6841,6 +6843,7 @@ export default {
         skip_unsupported_model: 'Ce modèle a un emplacement SD mais aucun moyen d\'activer l\'option — le firmware actuel de la série P1 n\'affiche pas le bouton dans Bambu Studio et l\'imprimante n\'a pas d\'écran. Il n\'y a rien à corriger ici ; les impressions archivées peuvent manquer de miniatures et de métadonnées du slicer jusqu\'à ce que Bambu Lab l\'ajoute par firmware.',
         fail_no_media: 'L\'option est activée, mais l\'imprimante ne signale ni carte ni clé dans son emplacement : les fichiers envoyés n\'ont nulle part où aller. Insérez-en une et relancez une impression — d\'ici là, chaque impression archivée sera dépourvue de miniature et de métadonnées slicer.',
         warn_internal_storage: 'L\'option est activée et un stockage est présent, mais la dernière impression est tout de même allée dans le stockage interne de l\'imprimante, que Bambuddy ne peut pas lire. Sur les séries H2 et P2S, le bouton « Imprimer » de Bambu Studio y envoie toujours, quel que soit ce réglage. Les impressions sont archivées avec leur nom et leurs durées, mais sans miniature ni métadonnées slicer. Pour des archives complètes, lancez les impressions depuis Bambuddy ou tranchez dans OrcaSlicer, ou bien dans Bambu Studio utilisez « Envoyer » avec « Externe » puis lancez l\'impression.',
+        warn_internal_history: 'L\'option est activée et un stockage est présent, mais la dernière impression est partie d\'un fichier déjà présent sur l\'imprimante : une réimpression depuis son écran, un lancement depuis Handy ou un fichier envoyé plus tôt. Cette bibliothèque réside dans le stockage interne, que Bambuddy ne peut pas lire, et aucun réglage n\'y change rien, puisque rien n\'a été envoyé pour cette impression. Elle reste archivée avec son nom et ses durées, mais sans miniature ni métadonnées slicer. Pour des archives complètes, lancez les impressions depuis Bambuddy ou envoyez-les depuis le trancheur.',
       },
       port_rtsps: {
         title: 'Port caméra ({{protocol}} {{port}})',

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

@@ -898,6 +898,8 @@ export default {
       bodyInternalStorage: 'Bambu Studio ha messo il file elaborato nella memoria interna della stampante anziché sulla scheda, quindi Bambuddy non aveva nulla da leggere via FTP. Sulla serie H2 e sulla P2S il pulsante «Stampa» lo fa sempre: solo «Invia» offre una scelta, e anche quella è impostata su «Cache». Quelle stampe restano archiviate con nome e tempi, solo senza miniatura né metadati dello slicer. Per archivi completi, avvia la stampa da Bambuddy oppure elabora in OrcaSlicer, oppure in Bambu Studio usa «Invia» con «Esterna» e avvia la stampa dopo. Tutte richiedono una scheda o una chiavetta nella stampante.',
       titleNoExternalStorage: 'Alcune stampe recenti non sono state archiviate — nessuna memoria nella stampante',
       bodyNoExternalStorage: 'La stampante non rileva né scheda né chiavetta nel suo slot, quindi il file elaborato non aveva dove finire e Bambuddy nulla da leggere. Inseriscine una e la prossima stampa verrà archiviata per intero.',
+      titleInternalHistory: 'Alcune stampe recenti sono partite da un file già presente sulla stampante',
+      bodyInternalHistory: 'Quelle stampe sono uscite dalla libreria della stampante stessa: una ristampa dal suo schermo, un avvio da Handy o un file inviato prima e stampato dopo. Bambuddy legge i file di stampa via FTP, che serve solo la scheda o la chiavetta, mentre la stampante tiene quella libreria in un\'area che l\'FTP non raggiunge, quindi non c\'era alcun 3MF da leggere. Nessuna impostazione dello slicer cambia questo, perché per queste stampe non è stato inviato nulla. Restano archiviate con nome e tempi, e «Modifica archivio» consente di inserire a mano il filamento usato. Per un archivio completo, avvia la stampa da Bambuddy o dal tuo slicer.',
       dismissLabel: 'Chiudi questo avviso',
     },
     searchPlaceholder: 'Cerca archivi...',
@@ -6840,6 +6842,7 @@ export default {
         skip_unsupported_model: 'Questo modello ha uno slot SD ma nessun modo per attivare l\'opzione — il firmware attuale della serie P1 non mostra l\'interruttore in Bambu Studio e la stampante non ha uno schermo. Non c\'è nulla da correggere qui; alle stampe archiviate potrebbero mancare miniature e metadati dello slicer finché Bambu Lab non aggiungerà il supporto via firmware.',
         fail_no_media: 'L\'opzione è attiva, ma la stampante non rileva né scheda né chiavetta nel suo slot, quindi i file inviati non hanno dove andare. Inseriscine una e stampa di nuovo — fino ad allora ogni stampa archiviata sarà priva di miniatura e metadati dello slicer.',
         warn_internal_storage: 'L\'opzione è attiva ed è presente una memoria, ma l\'ultima stampa è comunque finita nella memoria interna della stampante, che Bambuddy non può leggere. Sulla serie H2 e sulla P2S il pulsante «Stampa» di Bambu Studio invia sempre lì, a prescindere da questa opzione. Le stampe vengono archiviate con nome e tempi, ma senza miniatura né metadati dello slicer. Per archivi completi, avvia le stampe da Bambuddy oppure elabora in OrcaSlicer, oppure in Bambu Studio usa «Invia» con «Esterna» e avvia la stampa dopo.',
+        warn_internal_history: 'L\'opzione è attiva e la memoria è presente, ma l\'ultima stampa è partita da un file già presente sulla stampante: una ristampa dal suo schermo, un avvio da Handy o un file inviato prima. Quella libreria si trova nella memoria interna, che Bambuddy non può leggere, e nessuna impostazione lo cambia, perché per quella stampa non è stato inviato nulla. Resta archiviata con nome e tempi, ma senza miniatura né metadati dello slicer. Per archivi completi, avvia le stampe da Bambuddy o inviale dallo slicer.',
       },
       port_rtsps: {
         title: 'Porta fotocamera ({{protocol}} {{port}})',

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

@@ -897,6 +897,8 @@ export default {
       bodyInternalStorage: 'Bambu Studio がスライス済みファイルをカードではなくプリンターの内部ストレージに保存したため、Bambuddy が FTP で読み取れるものがありませんでした。H2 シリーズと P2S では「印刷」ボタンは常にそうなり、選択できるのは「送信」だけで、そちらも既定は「キャッシュ」です。これらの印刷は名前と時間付きでアーカイブされますが、サムネイルとスライサーのメタデータはありません。完全なアーカイブを残すには、Bambuddy から印刷を開始するか、OrcaSlicer でスライスしてください。Bambu Studio を使う場合は「送信」で「外部ストレージ」を選び、その後に印刷を開始します。いずれもプリンターにカードまたは USB メモリーが必要です。',
       titleNoExternalStorage: '最近の一部の印刷をアーカイブできませんでした — プリンターにストレージがありません',
       bodyNoExternalStorage: 'プリンターのスロットにカードもUSBメモリも検出されないため、スライス済みファイルの保存先がなく、Bambuddyが読み取るものもありませんでした。挿入すれば次の印刷は完全にアーカイブされます。',
+      titleInternalHistory: '最近の一部の印刷は、すでにプリンター内にあるファイルから開始されました',
+      bodyInternalHistory: 'これらの印刷はプリンター自身のライブラリから実行されました — 画面からの再印刷、Handy からの開始、または以前に送信して後から印刷したファイルです。Bambuddy は印刷ファイルを FTP で読み取りますが、FTP が扱えるのはカードまたは USB メモリだけで、プリンターはそのライブラリを FTP の届かない領域に保存するため、読み取れる 3MF がありませんでした。これらの印刷では何も送信されていないので、スライサーの設定を変えても解決しません。名前と時間付きでのアーカイブは続き、「アーカイブを編集」で使用フィラメントを手入力できます。完全なアーカイブにするには、Bambuddy またはスライサーから印刷を開始してください。',
       dismissLabel: 'この通知を閉じる',
     },
     searchPlaceholder: 'アーカイブを検索...',
@@ -6852,6 +6854,7 @@ export default {
         skip_unsupported_model: 'このモデルにはSDスロットがありますが、オプションを有効にする方法がありません — 現在のP1シリーズのファームウェアはBambu Studioにトグルを表示せず、プリンターに画面もありません。ここで修正すべきことはありません。Bambu Labがファームウェアで対応するまで、アーカイブされた印刷にはサムネイルやスライサーのメタデータが欠ける場合があります。',
         fail_no_media: 'オプションは有効ですが、プリンターのスロットにカードもUSBメモリも検出されないため、送信ファイルの保存先がありません。挿入して再度印刷してください。それまでアーカイブされる印刷にはサムネイルとスライサーメタデータがありません。',
         warn_internal_storage: 'オプションは有効でストレージも装着されていますが、直近の印刷は Bambuddy が読み取れないプリンターの内部ストレージに保存されました。H2 シリーズと P2S では、この設定に関係なく Bambu Studio の「印刷」ボタンは常にそちらへ送信します。印刷は名前と時間付きでアーカイブされますが、サムネイルとスライサーのメタデータはありません。完全なアーカイブを残すには、Bambuddy から印刷を開始するか、OrcaSlicer でスライスするか、Bambu Studio で「送信」から「外部ストレージ」を選んだあとに印刷を開始してください。',
+        warn_internal_history: 'オプションは有効でストレージもありますが、直近の印刷はすでにプリンター内にあったファイルから実行されました — 画面からの再印刷、Handy からの開始、または以前に送信したファイルです。そのライブラリは Bambuddy が読めない内部ストレージにあり、この印刷では何も送信されていないため、どの設定を変えても解決しません。名前と時間付きでアーカイブはされますが、サムネイルとスライサーのメタデータはありません。完全なアーカイブにするには、Bambuddy から印刷を開始するか、スライサーから送信してください。',
       },
       port_rtsps: {
         title: 'カメラポート ({{protocol}} {{port}})',

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

@@ -854,6 +854,8 @@ export default {
       bodyInternalStorage: '슬라이싱된 파일을 Bambu Studio가 카드가 아니라 프린터 내부 저장소에 저장해서 Bambuddy가 FTP로 읽을 것이 없었습니다. H2 시리즈와 P2S에서는 «인쇄» 버튼이 항상 그렇게 동작하며, 선택할 수 있는 것은 «보내기»뿐인데 그것도 기본값이 «캐시»입니다. 해당 출력물은 이름과 시간과 함께 계속 보관되지만 썸네일과 슬라이서 메타데이터는 없습니다. 완전한 기록을 남기려면 Bambuddy에서 출력을 시작하거나 OrcaSlicer로 슬라이싱하세요. Bambu Studio를 쓴다면 «보내기»에서 «외부 저장소»를 고른 뒤 출력을 시작하면 됩니다. 모두 프린터에 카드나 USB 메모리가 필요합니다.',
       titleNoExternalStorage: '최근 일부 출력물을 보관하지 못했습니다 — 프린터에 저장소가 없습니다',
       bodyNoExternalStorage: '프린터 슬롯에 카드도 USB도 감지되지 않아 슬라이싱된 파일이 저장될 곳이 없었고 Bambuddy가 읽을 것도 없었습니다. 하나 넣으면 다음 출력물은 온전히 보관됩니다.',
+      titleInternalHistory: '최근 일부 출력물은 이미 프린터에 있던 파일에서 시작되었습니다',
+      bodyInternalHistory: '해당 출력물은 프린터 자체 라이브러리에서 실행되었습니다 — 화면에서의 재출력, Handy에서의 시작, 또는 이전에 보내 두고 나중에 출력한 파일입니다. Bambuddy는 출력 파일을 FTP로 읽는데 FTP는 카드나 USB만 제공하고, 프린터는 그 라이브러리를 FTP가 닿지 않는 영역에 보관하므로 읽을 3MF가 없었습니다. 이 출력물들은 아무것도 전송되지 않았으므로 슬라이서 설정으로는 해결되지 않습니다. 이름과 시간과 함께 계속 보관되며, "아카이브 편집"에서 사용된 필라멘트를 직접 입력할 수 있습니다. 온전한 보관을 원하면 Bambuddy나 슬라이서에서 출력을 시작하세요.',
       dismissLabel: '이 알림 닫기'
     },
     searchPlaceholder: '아카이브 검색...',
@@ -6952,6 +6954,7 @@ export default {
         skip_unsupported_model: '이 모델에는 SD 슬롯이 있지만 옵션을 켤 방법이 없습니다 — 현재 P1 시리즈 펌웨어는 Bambu Studio에 토글을 표시하지 않으며 프린터에 화면도 없습니다. 여기서 고칠 것은 없습니다. Bambu Lab이 펌웨어로 지원할 때까지 보관된 출력물에는 썸네일과 슬라이서 메타데이터가 없을 수 있습니다.',
         fail_no_media: '옵션은 켜져 있지만 프린터 슬롯에 카드도 USB도 감지되지 않아 보낸 파일이 갈 곳이 없습니다. 하나 넣고 다시 출력하세요. 그전까지는 보관되는 모든 출력물에 썸네일과 슬라이서 메타데이터가 없습니다.',
         warn_internal_storage: '옵션이 켜져 있고 저장소도 있지만 마지막 출력물은 Bambuddy가 읽을 수 없는 프린터 내부 저장소에 저장되었습니다. H2 시리즈와 P2S에서는 이 설정과 관계없이 Bambu Studio의 «인쇄» 버튼이 항상 그쪽으로 보냅니다. 출력물은 이름과 시간과 함께 보관되지만 썸네일과 슬라이서 메타데이터는 없습니다. 완전한 기록을 남기려면 Bambuddy에서 출력을 시작하거나, OrcaSlicer로 슬라이싱하거나, Bambu Studio에서 «보내기»로 «외부 저장소»를 고른 뒤 출력을 시작하세요.',
+        warn_internal_history: '옵션은 켜져 있고 저장소도 있지만, 마지막 출력은 이미 프린터에 있던 파일에서 실행되었습니다 — 화면에서의 재출력, Handy에서의 시작, 또는 이전에 보낸 파일입니다. 그 라이브러리는 Bambuddy가 읽을 수 없는 내부 저장소에 있으며, 그 출력에서는 아무것도 전송되지 않았으므로 어떤 설정으로도 바뀌지 않습니다. 이름과 시간과 함께 보관되기는 하지만 썸네일과 슬라이서 메타데이터는 없습니다. 온전한 보관을 원하면 Bambuddy에서 출력을 시작하거나 슬라이서에서 전송하세요.',
       },
       port_rtsps: {
         title: '카메라 포트 ({{protocol}} {{port}})',

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

@@ -898,6 +898,8 @@ export default {
       bodyInternalStorage: 'O Bambu Studio colocou o arquivo fatiado no armazenamento interno da impressora em vez do cartão, então o Bambuddy não tinha nada para ler via FTP. Na série H2 e na P2S o botão «Imprimir» sempre faz isso: só «Enviar» oferece escolha, e ela também vem com «Cache» por padrão. Essas impressões continuam arquivadas com nome e tempos, apenas sem miniatura nem metadados do fatiador. Para arquivos completos, inicie a impressão pelo Bambuddy ou fatie no OrcaSlicer, ou então no Bambu Studio use «Enviar» com «Externo» e inicie a impressão depois. Todos precisam de um cartão ou pendrive na impressora.',
       titleNoExternalStorage: 'Algumas impressões recentes não puderam ser arquivadas — sem armazenamento na impressora',
       bodyNoExternalStorage: 'A impressora não detecta cartão nem pendrive no slot, então o arquivo fatiado não tinha onde ficar e o Bambuddy nada para ler. Insira um e a próxima impressão será arquivada por completo.',
+      titleInternalHistory: 'Algumas impressões recentes começaram a partir de um arquivo que já estava na impressora',
+      bodyInternalHistory: 'Essas impressões saíram da própria biblioteca da impressora: uma reimpressão pela tela dela, um início pelo Handy ou um arquivo enviado antes e impresso depois. O Bambuddy lê os arquivos de impressão por FTP, que só serve o cartão ou o pendrive, enquanto a impressora guarda essa biblioteca em uma área que o FTP não alcança, então não havia nenhum 3MF para ler. Nenhuma opção do fatiador muda isso, porque nada foi enviado para essas impressões. Elas continuam arquivadas com nome e tempos, e «Editar Arquivo» permite preencher à mão o filamento usado. Para um arquivo completo, inicie a impressão pelo Bambuddy ou pelo seu fatiador.',
       dismissLabel: 'Dispensar este aviso',
     },
     searchPlaceholder: 'Pesquisar arquivos...',
@@ -6840,6 +6842,7 @@ export default {
         skip_unsupported_model: 'Este modelo tem slot SD mas nenhuma forma de ativar a opção — o firmware atual da série P1 não mostra o botão no Bambu Studio e a impressora não tem tela. Não há nada a corrigir aqui; as impressões arquivadas podem ficar sem miniaturas e metadados do fatiador até que a Bambu Lab adicione suporte por firmware.',
         fail_no_media: 'A opção está ligada, mas a impressora não detecta cartão nem pendrive no slot, então os arquivos enviados não têm para onde ir. Insira um e imprima novamente — até lá, toda impressão arquivada ficará sem miniatura e sem metadados do fatiador.',
         warn_internal_storage: 'A opção está ligada e há armazenamento presente, mas a última impressão ainda assim foi para o armazenamento interno da impressora, que o Bambuddy não consegue ler. Na série H2 e na P2S o botão «Imprimir» do Bambu Studio sempre envia para lá, independentemente desta opção. As impressões são arquivadas com nome e tempos, mas sem miniatura nem metadados do fatiador. Para arquivos completos, inicie as impressões pelo Bambuddy ou fatie no OrcaSlicer, ou então no Bambu Studio use «Enviar» com «Externo» e inicie a impressão depois.',
+        warn_internal_history: 'A opção está ligada e há armazenamento, mas a última impressão saiu de um arquivo que já estava na impressora: uma reimpressão pela tela dela, um início pelo Handy ou um arquivo enviado antes. Essa biblioteca fica no armazenamento interno, que o Bambuddy não consegue ler, e nenhuma opção muda isso, porque nada foi enviado para essa impressão. Ela continua arquivada com nome e tempos, mas sem miniatura nem metadados do fatiador. Para arquivos completos, inicie as impressões pelo Bambuddy ou envie-as pelo fatiador.',
       },
       port_rtsps: {
         title: 'Porta da câmera ({{protocol}} {{port}})',

+ 3 - 0
frontend/src/i18n/locales/ru.ts

@@ -853,6 +853,8 @@ export default {
       bodyInternalStorage: 'Bambu Studio сохранил нарезанный файл во внутренней памяти принтера, а не на карте, поэтому Bambuddy было нечего читать по FTP. На серии H2 и P2S кнопка «Печать» всегда делает так: выбор есть только в «Отправить», и там по умолчанию тоже «Кэш». Эти печати по-прежнему архивируются с именем и временем, только без миниатюры и метаданных слайсера. Чтобы архивы были полными, запускайте печать из Bambuddy или нарезайте в OrcaSlicer, либо в Bambu Studio используйте «Отправить» с «Внешним накопителем», а печать запускайте после этого. Во всех случаях в принтере нужна карта или флешка.',
       titleNoExternalStorage: 'Некоторые недавние печати не удалось архивировать — в принтере нет накопителя',
       bodyNoExternalStorage: 'Принтер не видит ни карты, ни флешки в слоте, поэтому нарезанному файлу некуда было попасть, а Bambuddy нечего читать. Вставьте накопитель, и следующая печать будет заархивирована полностью.',
+      titleInternalHistory: 'Некоторые недавние печати запущены из файла, который уже был на принтере',
+      bodyInternalHistory: 'Эти печати шли из собственной библиотеки принтера — повторная печать с его экрана, запуск из Handy или файл, отправленный раньше и напечатанный позже. Bambuddy читает файлы печати по FTP, а он отдаёт только карту или флешку, тогда как принтер держит эту библиотеку в области, куда FTP не достаёт, — читать 3MF было негде. Настройки слайсера тут ничего не меняют: для этих печатей ничего не отправлялось. Они по-прежнему архивируются с именем и временем, а в «Редактировании архива» израсходованный филамент можно указать вручную. Чтобы архив был полным, запускайте печать из Bambuddy или из своего слайсера.',
       dismissLabel: "Закрыть это уведомление",
     },
     searchPlaceholder: "Поиск в архиве...",
@@ -6480,6 +6482,7 @@ export default {
         skip_unsupported_model: "В этой модели есть слот SD-карты, но включить соответствующую функцию невозможно: текущая прошивка серии P1 не предоставляет переключатель в Bambu Studio, а у принтера нет экрана. Исправлять здесь нечего. Пока Bambu Lab не добавит поддержку в прошивку, архивные задания могут сохраняться без миниатюр и метаданных слайсера.",
         fail_no_media: 'Параметр включён, но принтер не видит ни карты, ни флешки в слоте, поэтому отправленным файлам некуда деваться. Вставьте накопитель и напечатайте снова — до тех пор у каждой архивной печати не будет ни миниатюры, ни метаданных слайсера.',
         warn_internal_storage: 'Параметр включён и накопитель на месте, но последняя печать всё равно ушла во внутреннюю память принтера, которую Bambuddy не может прочитать. На серии H2 и P2S кнопка «Печать» в Bambu Studio всегда отправляет туда, независимо от этого параметра. Печати архивируются с именем и временем, но без миниатюры и метаданных слайсера. Чтобы архивы были полными, запускайте печать из Bambuddy, нарезайте в OrcaSlicer или в Bambu Studio используйте «Отправить» с «Внешним накопителем», а затем запускайте печать.',
+        warn_internal_history: 'Опция включена и накопитель на месте, но последняя печать шла из файла, который уже был на принтере, — повторная печать с его экрана, запуск из Handy или файл, отправленный раньше. Эта библиотека лежит во внутренней памяти, которую Bambuddy не читает, и никакие настройки этого не меняют: для той печати ничего не отправлялось. Она по-прежнему архивируется с именем и временем, но без миниатюры и метаданных слайсера. Чтобы архивы были полными, запускайте печать из Bambuddy или отправляйте её из слайсера.',
       },
       port_rtsps: {
         title: "Порт камеры ({{protocol}} {{port}})",

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

@@ -898,6 +898,8 @@ export default {
       bodyInternalStorage: 'Bambu Studio dilimlenmiş dosyayı karta değil yazıcının dahili depolamasına yazdı, bu yüzden Bambuddy\'nin FTP üzerinden okuyacağı bir şey yoktu. H2 serisi ve P2S\'de «Yazdır» düğmesi bunu her zaman yapar; seçim yalnızca «Gönder» ile mümkündür ve orada da varsayılan «Önbellek»tir. Bu baskılar adları ve süreleriyle yine arşivlenir, yalnızca küçük resim ve dilimleyici meta verileri olmadan. Eksiksiz arşivler için baskıyı Bambuddy üzerinden başlatın ya da OrcaSlicer ile dilimleyin; Bambu Studio kullanacaksanız «Gönder» ile «Harici» seçip baskıyı sonrasında başlatın. Hepsi yazıcıda kart ya da bellek gerektirir.',
       titleNoExternalStorage: 'Bazı son baskılar arşivlenemedi — yazıcıda depolama yok',
       bodyNoExternalStorage: 'Yazıcı yuvasında kart veya bellek bildirmiyor, bu yüzden dilimlenmiş dosyanın ineceği bir yer ve Bambuddy\'nin okuyacağı bir şey yoktu. Bir tane takın, sonraki baskı eksiksiz arşivlenecek.',
+      titleInternalHistory: 'Bazı son baskılar yazıcıda zaten bulunan bir dosyadan başlatıldı',
+      bodyInternalHistory: 'Bu baskılar yazıcının kendi kitaplığından çalıştı — ekranından yeniden baskı, Handy üzerinden başlatma ya da daha önce gönderilip sonra basılan bir dosya. Bambuddy baskı dosyalarını FTP üzerinden okur, FTP ise yalnızca kartı veya belleği sunar; yazıcı bu kitaplığı FTP\'nin ulaşamadığı bir alanda tutar, dolayısıyla okunacak bir 3MF yoktu. Hiçbir dilimleyici ayarı bunu değiştirmez, çünkü bu baskılar için hiçbir şey gönderilmedi. Adları ve süreleriyle arşivlenmeye devam ederler ve "Arşivi Düzenle" ile kullanılan filamenti elle girebilirsiniz. Eksiksiz bir arşiv için baskıyı Bambuddy\'den ya da dilimleyicinizden başlatın.',
       dismissLabel: 'Bu bildirimi kapat',
     },
     searchPlaceholder: 'Arşivlerde ara...',
@@ -6790,6 +6792,7 @@ export default {
         skip_unsupported_model: 'Bu modelde SD yuvası var ancak seçeneği açmanın bir yolu yok — mevcut P1 serisi bellenim, Bambu Studio\'da bu anahtarı göstermiyor ve yazıcının ekranı yok. Burada düzeltilecek bir şey yok; Bambu Lab bellenim desteği ekleyene kadar arşivlenen baskılarda küçük resimler ve dilimleyici meta verileri eksik olabilir.',
         fail_no_media: 'Seçenek açık, ancak yazıcı yuvasında kart veya bellek bildirmiyor, dolayısıyla gönderilen dosyaların gideceği bir yer yok. Bir tane takıp yeniden yazdırın — o zamana kadar arşivlenen her baskıda küçük resim ve dilimleyici meta verileri eksik olacak.',
         warn_internal_storage: 'Seçenek açık ve depolama takılı, ancak son baskı yine de Bambuddy\'nin okuyamadığı dahili depolamaya gitti. H2 serisi ve P2S\'de Bambu Studio\'nun «Yazdır» düğmesi bu ayardan bağımsız olarak her zaman oraya gönderir. Baskılar adları ve süreleriyle arşivlenir, ancak küçük resim ve dilimleyici meta verileri olmadan. Eksiksiz arşivler için baskıları Bambuddy üzerinden başlatın, OrcaSlicer ile dilimleyin ya da Bambu Studio\'da «Gönder» ile «Harici» seçip baskıyı sonrasında başlatın.',
+        warn_internal_history: 'Seçenek açık ve depolama takılı, ancak son baskı yazıcıda zaten bulunan bir dosyadan çalıştı — ekranından yeniden baskı, Handy üzerinden başlatma ya da daha önce gönderilmiş bir dosya. Bu kitaplık, Bambuddy\'nin okuyamadığı dahili depolamada durur ve o baskı için hiçbir şey gönderilmediğinden hiçbir ayar bunu değiştirmez. Adı ve süreleriyle yine arşivlenir, ama küçük resim ve dilimleyici meta verileri olmadan. Eksiksiz arşivler için baskıları Bambuddy\'den başlatın ya da dilimleyiciden gönderin.',
       },
       port_rtsps: {
         title: 'Kamera portu ({{protocol}} {{port}})',

+ 3 - 0
frontend/src/i18n/locales/uk.ts

@@ -902,6 +902,8 @@ export default {
       bodyInternalStorage: 'Bambu Studio зберіг нарізаний файл у внутрішній пам\'яті принтера, а не на картці, тож Bambuddy не мав чого читати через FTP. На серії H2 та P2S кнопка «Друк» завжди робить саме так: вибір є лише в «Надіслати», і там за замовчуванням теж «Кеш». Ці друки й далі архівуються з назвою та часом, лише без мініатюри та метаданих слайсера. Щоб архіви були повними, запускайте друк із Bambuddy або нарізайте в OrcaSlicer, або в Bambu Studio скористайтеся «Надіслати» із «Зовнішнім носієм», а друк запускайте потім. Усі варіанти потребують картки або флешки в принтері.',
       titleNoExternalStorage: 'Деякі нещодавні друки не вдалося заархівувати — у принтері немає носія',
       bodyNoExternalStorage: 'Принтер не бачить ані картки, ані флешки у слоті, тож нарізаному файлу не було куди потрапити, а Bambuddy — що читати. Вставте носій, і наступний друк заархівується повністю.',
+      titleInternalHistory: 'Деякі нещодавні друки запущено з файлу, який уже був на принтері',
+      bodyInternalHistory: 'Ці друки йшли з власної бібліотеки принтера — повторний друк з його екрана, запуск із Handy або файл, надісланий раніше й надрукований пізніше. Bambuddy читає файли друку через FTP, а той віддає лише картку чи флешку, тоді як принтер тримає цю бібліотеку в області, куди FTP не дістає, — читати 3MF не було де. Налаштування слайсера тут нічого не змінюють: для цих друків нічого не надсилалося. Вони й далі архівуються з назвою та часом, а в «Редагувати архів» витрачений філамент можна вписати вручну. Щоб архів був повним, запускайте друк із Bambuddy або зі свого слайсера.',
       dismissLabel: "Відхилити це повідомлення",
     },
     searchPlaceholder: "Пошук в архівах...",
@@ -6894,6 +6896,7 @@ export default {
         skip_unsupported_model: "Ця модель має слот для SD-картки, але не дає змоги ввімкнути цей параметр: поточна прошивка принтерів серії P1 не показує перемикач у Bambu Studio, а сам принтер не має екрана. Виправляти нічого не потрібно; доки Bambu Lab не додасть підтримку в прошивці, в архівованих друках можуть бути відсутні мініатюри та метадані слайсера.",
         fail_no_media: 'Параметр увімкнено, але принтер не бачить ані картки, ані флешки у слоті, тож надісланим файлам немає куди подітися. Вставте носій і надрукуйте ще раз — доти кожен заархівований друк буде без мініатюри та метаданих слайсера.',
         warn_internal_storage: 'Параметр увімкнено і носій на місці, але останній друк усе одно потрапив у внутрішню пам\'ять принтера, яку Bambuddy не може прочитати. На серії H2 та P2S кнопка «Друк» у Bambu Studio завжди надсилає туди, незалежно від цього параметра. Друки архівуються з назвою та часом, але без мініатюри та метаданих слайсера. Щоб архіви були повними, запускайте друк із Bambuddy, нарізайте в OrcaSlicer або в Bambu Studio скористайтеся «Надіслати» із «Зовнішнім носієм», а потім запускайте друк.',
+        warn_internal_history: 'Опція увімкнена й носій на місці, але останній друк ішов із файлу, який уже був на принтері, — повторний друк з його екрана, запуск із Handy або файл, надісланий раніше. Ця бібліотека лежить у внутрішній пам\'яті, яку Bambuddy не читає, і жодні налаштування цього не змінюють: для того друку нічого не надсилалося. Він і далі архівується з назвою та часом, але без мініатюри та метаданих слайсера. Щоб архіви були повними, запускайте друк із Bambuddy або надсилайте його зі слайсера.',
       },
       port_rtsps: {
         title: "Порт камери ({{protocol}} {{port}})",

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

@@ -898,6 +898,8 @@ export default {
       bodyInternalStorage: 'Bambu Studio 把切片文件保存到了打印机的内部存储而不是存储卡上,因此 Bambuddy 通过 FTP 读不到任何东西。在 H2 系列和 P2S 上,「打印」按钮总是这样做,只有「发送」才提供选择,而它的默认值也是「缓存」。这些打印仍会带着名称和时间归档,只是没有缩略图和切片元数据。要获得完整归档,请从 Bambuddy 启动打印,或改用 OrcaSlicer 切片;若要继续用 Bambu Studio,请用「发送」并选择「外部存储」,之后再启动打印。以上都需要打印机中插有存储卡或 U 盘。',
       titleNoExternalStorage: '最近有些打印无法归档 — 打印机中没有存储介质',
       bodyNoExternalStorage: '打印机的插槽中未检测到存储卡或U盘,切片文件无处存放,Bambuddy 也无从读取。插入一个,下次打印就会完整归档。',
+      titleInternalHistory: '最近有些打印是从打印机里已有的文件启动的',
+      bodyInternalHistory: '这些打印来自打印机自己的文件库 — 从它的屏幕重新打印、从 Handy 启动,或是先前发送、稍后才打印的文件。Bambuddy 通过 FTP 读取打印文件,而 FTP 只提供存储卡或 U 盘,打印机却把这个文件库放在 FTP 够不到的区域,所以没有 3MF 可读。切片软件的任何设置都改变不了这一点,因为这些打印根本没有发送过文件。它们仍会带着名称和时间归档,并且可以在「编辑归档」里手动填写已用耗材。要获得完整归档,请从 Bambuddy 或你的切片软件启动打印。',
       dismissLabel: '关闭此通知',
     },
     searchPlaceholder: '搜索归档...',
@@ -6839,6 +6841,7 @@ export default {
         skip_unsupported_model: '此型号有 SD 卡槽,但无法开启该选项 — 当前 P1 系列固件不会在 Bambu Studio 中显示此开关,且打印机没有屏幕。这里无需修复;在 Bambu Lab 通过固件添加支持之前,存档的打印可能缺少缩略图和切片元数据。',
         fail_no_media: '该选项已开启,但打印机的插槽中未检测到存储卡或U盘,发送的文件无处存放。插入一个再打印一次 — 在此之前,每一次归档的打印都会缺少缩略图和切片元数据。',
         warn_internal_storage: '该选项已开启且存储介质在位,但上一次打印仍进入了 Bambuddy 无法读取的打印机内部存储。在 H2 系列和 P2S 上,无论此选项如何设置,Bambu Studio 的「打印」按钮总是发送到那里。打印会带着名称和时间归档,但没有缩略图和切片元数据。要获得完整归档,请从 Bambuddy 启动打印、改用 OrcaSlicer 切片,或在 Bambu Studio 中用「发送」选择「外部存储」后再启动打印。',
+        warn_internal_history: '该选项已开启,存储介质也在,但最近一次打印来自打印机里已有的文件 — 从它的屏幕重新打印、从 Handy 启动,或是先前发送过的文件。这个文件库位于 Bambuddy 读不到的内部存储中,而且任何设置都改变不了,因为那次打印根本没有发送过文件。它仍会带着名称和时间归档,但没有缩略图和切片元数据。要获得完整归档,请从 Bambuddy 启动打印,或从切片软件发送。',
       },
       port_rtsps: {
         title: '摄像头端口({{protocol}} {{port}})',

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

@@ -898,6 +898,8 @@ export default {
       bodyInternalStorage: 'Bambu Studio 把切片檔案儲存到了印表機的內部儲存而不是記憶卡上,因此 Bambuddy 透過 FTP 讀不到任何東西。在 H2 系列與 P2S 上,「列印」按鈕總是這樣做,只有「傳送」才提供選擇,而它的預設值也是「快取」。這些列印仍會帶著名稱與時間歸檔,只是沒有縮圖與切片中繼資料。要取得完整歸檔,請從 Bambuddy 啟動列印,或改用 OrcaSlicer 切片;若要繼續用 Bambu Studio,請用「傳送」並選擇「外部儲存」,之後再啟動列印。以上都需要印表機中插有記憶卡或 USB 隨身碟。',
       titleNoExternalStorage: '最近有些列印無法歸檔 — 印表機中沒有儲存媒體',
       bodyNoExternalStorage: '印表機的插槽中未偵測到記憶卡或隨身碟,切片檔案無處存放,Bambuddy 也無從讀取。插入一個,下次列印就會完整歸檔。',
+      titleInternalHistory: '最近有些列印是從印表機裡既有的檔案啟動的',
+      bodyInternalHistory: '這些列印來自印表機自己的檔案庫 — 從它的螢幕重新列印、從 Handy 啟動,或是先前傳送、稍後才列印的檔案。Bambuddy 透過 FTP 讀取列印檔案,而 FTP 只提供記憶卡或隨身碟,印表機卻把這個檔案庫放在 FTP 搆不到的區域,因此沒有 3MF 可讀。切片軟體的任何設定都改變不了這一點,因為這些列印根本沒有傳送過檔案。它們仍會帶著名稱與時間歸檔,並且可以在「編輯歸檔」中手動填入已用耗材。要獲得完整歸檔,請從 Bambuddy 或你的切片軟體啟動列印。',
       dismissLabel: '關閉此通知',
     },
     searchPlaceholder: '搜尋歸檔...',
@@ -6839,6 +6841,7 @@ export default {
         skip_unsupported_model: '此型號有 SD 卡槽,但無法開啟該選項 — 目前 P1 系列韌體不會在 Bambu Studio 中顯示此開關,且印表機沒有螢幕。這裡無需修復;在 Bambu Lab 透過韌體加入支援之前,封存的列印可能缺少縮圖和切片中繼資料。',
         fail_no_media: '該選項已開啟,但印表機的插槽中未偵測到記憶卡或隨身碟,傳送的檔案無處存放。插入一個再列印一次 — 在此之前,每一次歸檔的列印都會缺少縮圖與切片中繼資料。',
         warn_internal_storage: '該選項已開啟且儲存媒體在位,但上一次列印仍進入了 Bambuddy 無法讀取的印表機內部儲存。在 H2 系列與 P2S 上,無論此選項如何設定,Bambu Studio 的「列印」按鈕總是傳送到那裡。列印會帶著名稱與時間歸檔,但沒有縮圖與切片中繼資料。要取得完整歸檔,請從 Bambuddy 啟動列印、改用 OrcaSlicer 切片,或在 Bambu Studio 中用「傳送」選擇「外部儲存」後再啟動列印。',
+        warn_internal_history: '該選項已開啟,儲存媒體也在,但最近一次列印來自印表機裡既有的檔案 — 從它的螢幕重新列印、從 Handy 啟動,或是先前傳送過的檔案。這個檔案庫位於 Bambuddy 讀不到的內部儲存中,而且任何設定都改變不了,因為那次列印根本沒有傳送過檔案。它仍會帶著名稱與時間歸檔,但沒有縮圖與切片中繼資料。要獲得完整歸檔,請從 Bambuddy 啟動列印,或從切片軟體傳送。',
       },
       port_rtsps: {
         title: '攝影機連接埠({{protocol}} {{port}})',

+ 13 - 6
frontend/src/pages/ArchivesPage.tsx

@@ -2867,23 +2867,29 @@ export function ArchivesPage() {
     setNo3MFWarningDismissed(true);
   };
   // Why the 3MF was missing decides what to tell the user, and the original
-  // single wording is wrong for two of the three cases: it sends H2-series and
+  // single wording is wrong for three of the four cases: it sends H2-series and
   // P2S owners to switch on a setting that is already on and would not have
-  // helped, and it blames the slicer when the real answer is an empty card
-  // slot (#2780). An unknown/absent reason keeps the original text.
+  // helped, it blames the slicer when the real answer is an empty card slot
+  // (#2780), and it blames a slicer that was never involved when the print was
+  // started from a file already on the printer (#1820). An unknown/absent
+  // reason keeps the original text.
   const no3MFVariant =
     no3MFWarning?.reason === 'internal_storage'
       ? 'InternalStorage'
       : no3MFWarning?.reason === 'no_external_storage'
         ? 'NoExternalStorage'
-        : '';
+        : no3MFWarning?.reason === 'internal_history'
+          ? 'InternalHistory'
+          : '';
   // Nothing to link for the empty-slot case — "put a card in" is the whole fix.
   const no3MFDocsHref =
     no3MFWarning?.reason === 'internal_storage'
       ? 'https://wiki.bambuddy.cool/reference/troubleshooting/#archive-card-has-only-a-name'
       : no3MFWarning?.reason === 'no_external_storage'
         ? null
-        : 'https://wiki.bambuddy.cool/getting-started/#step-4-enable-store-sent-files-on-external-storage';
+        : no3MFWarning?.reason === 'internal_history'
+          ? 'https://wiki.bambuddy.cool/reference/troubleshooting/#print-started-on-the-printer-has-no-thumbnail'
+          : 'https://wiki.bambuddy.cool/getting-started/#step-4-enable-store-sent-files-on-external-storage';
   const [isSelectionMode, setIsSelectionMode] = useState(false);
   const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
   const [showBatchTag, setShowBatchTag] = useState(false);
@@ -3713,7 +3719,8 @@ export function ArchivesPage() {
                     className="underline hover:text-amber-900 dark:hover:text-amber-100 inline-flex items-center gap-1"
                   >
                     {t(
-                      no3MFWarning?.reason === 'internal_storage'
+                      no3MFWarning?.reason === 'internal_storage' ||
+                        no3MFWarning?.reason === 'internal_history'
                         ? 'archives.no3mfBanner.docsLinkInternalStorage'
                         : 'archives.no3mfBanner.docsLink',
                     )}

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
static/assets/index-CA21Tb7f.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-CzJYDbxh.js"></script>
+    <script type="module" crossorigin src="/assets/index-CA21Tb7f.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DjndScv6.css">
   </head>
   <body>

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff