Browse Source

K-profile read: no answer is an error, not an empty table

- get_kprofiles returned [] when the printer answered none of its
  attempts, the same as a printer without profiles for the nozzle.
  It now raises KProfileReadError (also when not connected)
- GET /printers/{id}/kprofiles/ answers 504 for it. The K-Profiles
  page says the profiles could not be read and offers Refresh instead
  of "Create first profile"; the Configure AMS Slot dialog says so
  above the default K and no longer asks again itself
- Git restore: an unread nozzle is skipped and its profiles count
  failed, instead of all being sent as new next to the originals
- The edit read-back catches the error and leaves it to the acks; an
  empty table is now proof the edited profile is missing
- Restore note "does not answer still counts as restored" now says
  it is about the write's confirmation, in all locales
maziggy 1 day ago
parent
commit
080d866fbb
62 changed files with 328 additions and 47 deletions
  1. 1 0
      CHANGELOG.md
  2. 15 7
      backend/app/api/routes/kprofiles.py
  3. 17 4
      backend/app/services/bambu_mqtt.py
  4. 21 6
      backend/app/services/github_restore.py
  5. 37 3
      backend/tests/integration/test_kprofile_save_confirmation_g7.py
  6. 22 2
      backend/tests/unit/services/test_bambu_mqtt.py
  7. 23 5
      backend/tests/unit/test_github_restore.py
  8. 49 0
      frontend/src/__tests__/components/ConfigureAmsSlotModal.test.tsx
  9. 35 0
      frontend/src/__tests__/components/KProfilesViewWiring.test.tsx
  10. 12 1
      frontend/src/components/ConfigureAmsSlotModal.tsx
  11. 18 1
      frontend/src/components/KProfilesView.tsx
  12. 5 1
      frontend/src/i18n/locales/de.ts
  13. 5 1
      frontend/src/i18n/locales/en.ts
  14. 5 1
      frontend/src/i18n/locales/es.ts
  15. 5 1
      frontend/src/i18n/locales/fr.ts
  16. 5 1
      frontend/src/i18n/locales/it.ts
  17. 5 1
      frontend/src/i18n/locales/ja.ts
  18. 5 1
      frontend/src/i18n/locales/ko.ts
  19. 5 1
      frontend/src/i18n/locales/nl.ts
  20. 5 1
      frontend/src/i18n/locales/pt-BR.ts
  21. 5 1
      frontend/src/i18n/locales/ru.ts
  22. 5 1
      frontend/src/i18n/locales/sv.ts
  23. 5 1
      frontend/src/i18n/locales/tr.ts
  24. 5 1
      frontend/src/i18n/locales/uk.ts
  25. 5 1
      frontend/src/i18n/locales/zh-CN.ts
  26. 5 1
      frontend/src/i18n/locales/zh-TW.ts
  27. 0 0
      static/assets/ArchivesPage-BJ2vVvGo.js
  28. 0 0
      static/assets/CamWallPage-DgRPwaXC.js
  29. 0 0
      static/assets/CameraPage-BVGNkoZj.js
  30. 0 0
      static/assets/ConfigureAmsSlotModal-BsMI63Ni.js
  31. 0 0
      static/assets/ConfigureAmsSlotModal-CbooDTZJ.js
  32. 0 0
      static/assets/FileManagerPage-DkNuKge0.js
  33. 0 0
      static/assets/InventoryPage-Bx7z-agm.js
  34. 0 0
      static/assets/Layout-BfiKv1bK.js
  35. 0 0
      static/assets/ModelSourcesPage-CekpMszo.js
  36. 0 0
      static/assets/PhotoGalleryModal-XnIZOi3y.js
  37. 0 0
      static/assets/PrintersPage-3k66ABR-.js
  38. 0 0
      static/assets/ProfilesPage-8GDEGaLA.js
  39. 0 0
      static/assets/ProfilesPage-CFPee9BW.js
  40. 0 0
      static/assets/SettingsPage-B-uSedft.js
  41. 0 0
      static/assets/SliceModal-BW3hVqSt.js
  42. 0 0
      static/assets/SpoolBuddyAmsPage-DOLpK5lm.js
  43. 0 0
      static/assets/SpoolBuddyInventoryPage-DVi08XQ1.js
  44. 1 1
      static/assets/SpoolBuddySettingsPage-Ct0FC3gN.js
  45. 0 0
      static/assets/de-E7ez1oXe.js
  46. 0 0
      static/assets/es-DCeY0opY.js
  47. 0 0
      static/assets/fr-DGcwEKjT.js
  48. 0 0
      static/assets/index-DNGzxHyh.js
  49. 0 0
      static/assets/it-HRBtJJfM.js
  50. 0 0
      static/assets/ja-BSPFM8dm.js
  51. 0 0
      static/assets/ko-6FBaLiqg.js
  52. 0 0
      static/assets/locationSensorDefaults-FtgqFuCO.js
  53. 0 0
      static/assets/nl-PEZg_jnk.js
  54. 0 0
      static/assets/pt-BR-Bq3BZ7oq.js
  55. 0 0
      static/assets/ru-DH108Xll.js
  56. 0 0
      static/assets/spoolCost-kv-UPdK-.js
  57. 0 0
      static/assets/sv-BWB2ntTK.js
  58. 0 0
      static/assets/tr-S6MWMoKv.js
  59. 0 0
      static/assets/uk-BjPP3399.js
  60. 0 0
      static/assets/zh-CN-KPG8R5hX.js
  61. 0 0
      static/assets/zh-TW-jw-jlGKe.js
  62. 2 2
      static/index.html

+ 1 - 0
CHANGELOG.md

@@ -199,6 +199,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
 
 ### Fixed
+- **A printer that does not answer the K-profile read is no longer a printer without K-profiles (#3404, reported by @Spegeli)** — When the printer did not answer any of the three requests, the read returned an empty table, the same as a printer with no profiles for that nozzle. The K-Profiles page then said "No K-Profiles" and offered to create the first one, and the Configure AMS Slot dialog offered only the default K. Worse, a Git restore matched the backed-up profiles against that empty table and sent every one of them as a new profile, next to the originals still on the printer. The read is now an error: the API answers HTTP 504 "The printer did not answer the K-profile request", the K-Profiles page says the profiles could not be read and offers Refresh, and the slot dialog says so above the default. A restore skips that nozzle, counts its profiles failed and says the printer did not answer. The read-back after an edit no longer has to treat an empty table as a possible timeout.
 - **The Slice dialog picks the right material for H2C filaments with OrcaSlicer preferred (#3403, reported by @pleasantone)** — Bambu Cloud lists its filament profiles without their material, which Bambuddy takes from the bundled profile of the same name. The OrcaSlicer bundle has no H2C profiles, so every "@BBL H2C" filament stayed without one, and a PETG-CF slot could be preselected as Generic PETG, also with "Only loaded spools" on. Such a profile now takes the material of the same filament's bundled profiles for other printers ("Generic PETG-CF @BBL X1C" for "Generic PETG-CF @BBL H2C 0.4 nozzle"), when they all agree. An OrcaSlicer Cloud filament saved without a material of its own takes it from the profile it was saved from.
 - **SpoolBuddy (PN5180): NTAG tags with 7-byte UIDs are read through every cascade level (#3258, contributed by @Thomansky in #3346)** - They are no longer reported as 88 + three bytes. Spools linked with the short form are still found, in the built-in inventory and in Spoolman, and their link is upgraded on the next scan.
 - **The stock forecast names a SKU's colour once (#3400, reported by @ojimpo)** — In Spoolman mode, a filament with no colour name takes its subtype as one, so a filament named "Black" with material "PLA Basic" read "Bambu Lab PLA Basic Black Black" in the forecast list, the chart legend, the alerts, the add-to-list dialog and the shopping list. A colour that is the same word as the subtype is now left out of the name. The SKU itself, its settings and the shopping list entries are unchanged.

+ 15 - 7
backend/app/api/routes/kprofiles.py

@@ -21,7 +21,7 @@ from backend.app.schemas.kprofile import (
     KProfileNoteResponse,
     KProfilesResponse,
 )
-from backend.app.services.bambu_mqtt import CALI_ACK_UNCONFIRMED
+from backend.app.services.bambu_mqtt import CALI_ACK_UNCONFIRMED, KProfileReadError
 from backend.app.services.printer_manager import printer_manager
 
 logger = logging.getLogger(__name__)
@@ -53,8 +53,12 @@ async def get_kprofiles(
     if not client or not client.state.connected:
         raise HTTPException(400, "Printer not connected")
 
-    # Request K-profiles from printer
-    profiles = await client.get_kprofiles(nozzle_diameter=nozzle_diameter)
+    # Request K-profiles from printer. No answer is an error, not an empty
+    # table: that one means the printer has none for this nozzle (#3404).
+    try:
+        profiles = await client.get_kprofiles(nozzle_diameter=nozzle_diameter)
+    except KProfileReadError as e:
+        raise HTTPException(504, "The printer did not answer the K-profile request") from e
 
     # Convert from MQTT dataclass to Pydantic schema
     return KProfilesResponse(
@@ -196,12 +200,16 @@ async def set_kprofile(
 
     if is_edit and not is_dual_nozzle:
         # The edit deleted the profile and added it again: read the table back
-        # to see the edited one landed. An empty table is also what a read that
-        # timed out returns, so only a table without it is proof. Same pause as
+        # to see the edited one landed. A read the printer did not answer
+        # leaves it to the acks: the write has gone out by then. Same pause as
         # before the add.
         await asyncio.sleep(0.5)
-        table = await client.get_kprofiles(nozzle_diameter=profile.nozzle_diameter)
-        if table:
+        try:
+            table = await client.get_kprofiles(nozzle_diameter=profile.nozzle_diameter)
+        except KProfileReadError as e:
+            logger.warning("[API] Edit: could not read the K-profile table back: %s", e)
+            table = None
+        if table is not None:
             if not any(_is_saved_profile(stored, profile) for stored in table):
                 raise HTTPException(
                     502,

+ 17 - 4
backend/app/services/bambu_mqtt.py

@@ -891,6 +891,13 @@ class NozzleInfo:
 # in time. Not a refusal: firmware that predates the ack never answers.
 CALI_ACK_UNCONFIRMED = "no acknowledgement from printer"
 
+
+class KProfileReadError(Exception):
+    """The printer's K-profile table could not be read: not connected, or no
+    answer to any attempt. Not an empty table, which is a printer without
+    profiles for that nozzle (#3404)."""
+
+
 # Time the printer has to list a skipped object in s_obj before the skip is
 # logged as not confirmed.
 SKIP_CONFIRM_TIMEOUT_S = 60.0
@@ -7215,18 +7222,24 @@ class BambuMQTTClient:
             max_retries: Maximum number of retry attempts
 
         Returns:
-            List of KProfile objects
+            List of KProfile objects; empty when the printer has none for
+            the nozzle.
+
+        Raises:
+            KProfileReadError: not connected, or no answer to any attempt.
+                Returning an empty list made that a printer without profiles
+                (#3404).
         """
         if not self._client or not self.state.connected:
             logger.warning("[%s] Cannot get K-profiles: not connected", self.serial_number)
-            return []
+            raise KProfileReadError("Printer not connected")
 
         # Capture current event loop for thread-safe callback
         try:
             self._loop = asyncio.get_running_loop()
         except RuntimeError:
             logger.warning("[%s] No running event loop", self.serial_number)
-            return []
+            raise KProfileReadError("No running event loop") from None
 
         for attempt in range(max_retries):
             # Register this attempt under its own sequence_id so a concurrent
@@ -7272,7 +7285,7 @@ class BambuMQTTClient:
                 self._pending_kprofile_requests.pop(seq_id, None)
 
         logger.error("[%s] Failed to get K-profiles after %s attempts", self.serial_number, max_retries)
-        return []
+        raise KProfileReadError("The printer did not answer the K-profile request")
 
     def _publish_cali_write(self, command: dict, seq_id: str) -> bool:
         """Publish a K-profile write and arm its ack slot.

+ 21 - 6
backend/app/services/github_restore.py

@@ -2142,7 +2142,7 @@ class GitHubRestoreService:
         # narrowed to what is actually left uncertain.
         tally.note(
             "kprofilesAckUnreliable",
-            "A printer that does not answer still counts as restored — verify the profiles on the printer",
+            "A printer that does not confirm the write still counts as restored — verify the profiles on the printer",
         )
 
         for serial, entries in sorted(by_serial.items()):
@@ -2185,6 +2185,19 @@ class GitHubRestoreService:
                 # is a silent no-op — the printer drops it and we would still
                 # report the profile restored. So resolve the live index first.
                 current = await self._current_kprofile_index(client, nozzle, serial)
+                if current is None:
+                    # Unread is not empty: matching against nothing would send
+                    # every profile as a new one beside the originals still on
+                    # the printer (#3404). Nothing is sent.
+                    tally.failed += len(profiles)
+                    tally.note(
+                        "kprofilesReadFailed",
+                        f"{printer.name} ({serial}) did not answer the K-profile read for {nozzle} - not restored",
+                        nozzle=nozzle,
+                        printer=printer.name,
+                        serial=serial,
+                    )
+                    continue
 
                 profile_dicts = []
                 unmatched = 0
@@ -2329,17 +2342,19 @@ class GitHubRestoreService:
             return True, ""
 
     @staticmethod
-    async def _current_kprofile_index(client, nozzle: str, serial: str) -> list:
-        """Read the printer's live profiles for one nozzle.
+    async def _current_kprofile_index(client, nozzle: str, serial: str) -> list | None:
+        """Read the printer's live profiles for one nozzle, or None when the
+        read failed.
 
-        Best-effort: a read failure degrades to "nothing matched", which makes
-        every profile an add rather than aborting the restore.
+        A failed read used to be "nothing matched", which made every profile
+        an add next to the ones still on the printer (#3404). The caller skips
+        the nozzle instead; the rest of the restore goes on.
         """
         try:
             return list(await client.get_kprofiles(nozzle_diameter=nozzle) or [])
         except Exception as e:
             logger.warning("Could not read live K-profiles for %s nozzle %s: %s", serial, nozzle, e)
-            return []
+            return None
 
     @staticmethod
     def _match_kprofile(entry: dict, current: list, claimed: set[int]):

+ 37 - 3
backend/tests/integration/test_kprofile_save_confirmation_g7.py

@@ -14,7 +14,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
 import pytest
 from httpx import AsyncClient
 
-from backend.app.services.bambu_mqtt import CALI_ACK_UNCONFIRMED, KProfile
+from backend.app.services.bambu_mqtt import CALI_ACK_UNCONFIRMED, KProfile, KProfileReadError
 
 pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
 
@@ -128,14 +128,23 @@ class TestASingleNozzleEditIsReadBack:
         assert response.status_code == 502
 
     async def test_an_unreadable_table_leaves_it_to_the_ack(self, async_client, printer_factory):
-        """An empty answer is also what a read that timed out returns."""
-        client = _client((True, CALI_ACK_UNCONFIRMED), table=[])
+        """The write has gone out: a read the printer did not answer is no error."""
+        client = _client((True, CALI_ACK_UNCONFIRMED))
+        client.get_kprofiles = AsyncMock(side_effect=KProfileReadError("no answer"))
 
         response = await _post(async_client, printer_factory, client, body=self.EDIT)
 
         assert response.status_code == 200
         assert response.json()["confirmed"] is False
 
+    async def test_an_empty_table_is_a_lost_profile(self, async_client, printer_factory):
+        """#3404: an answered read without profiles is proof now, not a timeout."""
+        client = _client((True, ""), table=[])
+
+        response = await _post(async_client, printer_factory, client, body=self.EDIT)
+
+        assert response.status_code == 502
+
     async def test_an_unanswered_delete_leaves_it_unconfirmed(self, async_client, printer_factory):
         """The edited profile landed, but the old one may still be there too."""
         client = _client((True, ""), table=[_stored(slot_id=5, k_value="0.025000")])
@@ -145,3 +154,28 @@ class TestASingleNozzleEditIsReadBack:
 
         assert response.status_code == 200
         assert response.json()["confirmed"] is False
+
+
+class TestTheTableRead:
+    """#3404: a printer that did not answer is not a printer without profiles."""
+
+    async def _get(self, async_client, printer_factory, client):
+        printer = await printer_factory(model="X1C")
+        with patch("backend.app.api.routes.kprofiles.printer_manager") as pm:
+            pm.get_client.return_value = client
+            return await async_client.get(f"/api/v1/printers/{printer.id}/kprofiles/?nozzle_diameter=0.4")
+
+    async def test_no_answer_is_504(self, async_client, printer_factory):
+        client = _client((True, ""))
+        client.get_kprofiles = AsyncMock(side_effect=KProfileReadError("no answer"))
+
+        response = await self._get(async_client, printer_factory, client)
+
+        assert response.status_code == 504
+        assert response.json()["detail"] == "The printer did not answer the K-profile request"
+
+    async def test_no_profiles_is_an_empty_list(self, async_client, printer_factory):
+        response = await self._get(async_client, printer_factory, _client((True, ""), table=[]))
+
+        assert response.status_code == 200
+        assert response.json() == {"profiles": [], "nozzle_diameter": "0.4"}

+ 22 - 2
backend/tests/unit/services/test_bambu_mqtt.py

@@ -8245,10 +8245,30 @@ class TestKProfileRequestCorrelation:
     async def test_pending_entry_is_released_on_timeout(self, mqtt_client):
         # A timed-out attempt must not leave its entry behind, or a later
         # broadcast would be matched to a request nobody is waiting on.
-        profiles = await mqtt_client.get_kprofiles(nozzle_diameter="0.8", timeout=0.01, max_retries=1)
-        assert profiles == []
+        from backend.app.services.bambu_mqtt import KProfileReadError
+
+        with pytest.raises(KProfileReadError):
+            await mqtt_client.get_kprofiles(nozzle_diameter="0.8", timeout=0.01, max_retries=1)
         assert mqtt_client._pending_kprofile_requests == {}
 
+    @pytest.mark.asyncio
+    async def test_no_answer_is_an_error_not_an_empty_table(self, mqtt_client):
+        """#3404: an empty list is a printer without profiles for the nozzle."""
+        from backend.app.services.bambu_mqtt import KProfileReadError
+
+        with pytest.raises(KProfileReadError, match="did not answer"):
+            await mqtt_client.get_kprofiles(nozzle_diameter="0.4", timeout=0.01, max_retries=2)
+        assert mqtt_client._client.publish.call_count == 2
+
+    @pytest.mark.asyncio
+    async def test_not_connected_is_an_error(self, mqtt_client):
+        from backend.app.services.bambu_mqtt import KProfileReadError
+
+        mqtt_client.state.connected = False
+        with pytest.raises(KProfileReadError, match="not connected"):
+            await mqtt_client.get_kprofiles(nozzle_diameter="0.4", timeout=0.01)
+        mqtt_client._client.publish.assert_not_called()
+
 
 class TestConnectRefusalReporting:
     """#2698: a refused CONNACK must leave a trace.

+ 23 - 5
backend/tests/unit/test_github_restore.py

@@ -19,6 +19,7 @@ from backend.app.models.spool import Spool
 from backend.app.models.spool_usage_history import SpoolUsageHistory
 from backend.app.models.user import User
 from backend.app.schemas.github_backup import GitHubRestoreRequest, RestoreCategory
+from backend.app.services.bambu_mqtt import KProfileReadError
 from backend.app.services.github_restore import (
     _COMPANION_CREDENTIAL_ENV,
     _COMPANION_CREDENTIALS,
@@ -1796,7 +1797,7 @@ class TestRestoreKprofiles:
         # A refusal is now read and counted failed, so the caveat is narrowed to
         # what is genuinely left uncertain: a printer that never answers.
         assert any("verify the profiles on the printer" in note for note in _messages(tally))
-        assert any("does not answer still counts as restored" in note for note in _messages(tally))
+        assert any("does not confirm the write still counts as restored" in note for note in _messages(tally))
         assert not any("without acknowledgement" in note for note in _messages(tally))
         assert any("always overwrite" in note for note in _messages(tally))
 
@@ -1873,11 +1874,29 @@ class TestRestoreKprofiles:
         assert profiles[0]["cali_idx"] == -1
 
     @pytest.mark.asyncio
-    async def test_unreadable_live_index_degrades_to_adding(self, db_session, printer_factory):
-        # A failed read must not abort the restore.
+    async def test_unreadable_live_index_skips_the_nozzle(self, db_session, printer_factory):
+        # #3404: matching against an unread table sent every profile as a new
+        # one beside the originals. A failed read must not abort the restore
+        # either: the nozzle's profiles count failed and nothing is sent.
         await printer_factory(serial_number="00M09A123456789")
         client = self._client()
-        client.get_kprofiles = AsyncMock(side_effect=RuntimeError("mqtt timeout"))
+        client.get_kprofiles = AsyncMock(side_effect=KProfileReadError("no answer"))
+        tally = _CategoryTally()
+
+        with patch("backend.app.services.github_restore.printer_manager") as manager:
+            manager.get_client = MagicMock(return_value=client)
+            await _service()._restore_kprofiles(db_session, self._payload(), tally)
+
+        client.set_kprofiles_batch.assert_not_called()
+        assert (tally.restored, tally.failed) == (0, 1)
+        assert any(n["code"] == "kprofilesReadFailed" and n["params"]["nozzle"] == "0.4" for n in tally.notes)
+
+    @pytest.mark.asyncio
+    async def test_an_empty_live_table_still_adds(self, db_session, printer_factory):
+        """An answered read without profiles is a printer that has none: add them."""
+        await printer_factory(serial_number="00M09A123456789")
+        client = self._client()
+        client.get_kprofiles = AsyncMock(return_value=[])
         tally = _CategoryTally()
 
         with patch("backend.app.services.github_restore.printer_manager") as manager:
@@ -1886,7 +1905,6 @@ class TestRestoreKprofiles:
 
         profiles, _ = client.set_kprofiles_batch.call_args.args
         assert profiles[0]["cali_idx"] == -1
-        assert tally.restored == 1
 
     @pytest.mark.asyncio
     async def test_sole_profile_for_a_filament_matches_without_setting_id_or_name(self, db_session, printer_factory):

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

@@ -4,6 +4,7 @@
 
 import { describe, it, expect, vi, beforeEach } from 'vitest';
 import { screen, fireEvent, waitFor } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { render } from '../utils';
 import { ConfigureAmsSlotModal } from '../../components/ConfigureAmsSlotModal';
 import { api } from '../../api/client';
@@ -680,6 +681,54 @@ describe('ConfigureAmsSlotModal', () => {
     expect(screen.getByRole('button', { name: /Reset Slot/i })).toBeInTheDocument();
   });
 
+  it('says the K-profiles could not be read when the printer did not answer (#3404)', async () => {
+    (api.getKProfiles as ReturnType<typeof vi.fn>).mockRejectedValue(
+      new Error('The printer did not answer the K-profile request'),
+    );
+    const slotInfo = { ...defaultProps.slotInfo, savedPresetId: 'GFSL05_09' };
+    // Retrying the way the app's client does: the dialog must not ask again,
+    // the backend has already asked the printer three times.
+    const retrying = new QueryClient({ defaultOptions: { queries: { retry: 1, retryDelay: 0 } } });
+    render(
+      <QueryClientProvider client={retrying}>
+        <ConfigureAmsSlotModal {...defaultProps} slotInfo={slotInfo} />
+      </QueryClientProvider>,
+    );
+
+    await waitFor(() => {
+      expect(screen.getAllByText(/The printer did not answer, so its K-profiles could not be read/).length).toBeGreaterThan(0);
+    });
+    expect(screen.queryByText(/No matching K profiles found/)).not.toBeInTheDocument();
+    expect(api.getKProfiles).toHaveBeenCalledTimes(1);
+  });
+
+  it('opened again after a failed read, it loads instead of saying the printer did not answer (#3404)', async () => {
+    // A read without data goes back to pending when it runs again, so the
+    // dialog shows its loading state, not the failed read's hint.
+    let answer: (v: unknown) => void = () => {};
+    (api.getKProfiles as ReturnType<typeof vi.fn>)
+      .mockRejectedValueOnce(new Error('The printer did not answer the K-profile request'))
+      .mockReturnValueOnce(new Promise((resolve) => { answer = resolve; }));
+    const slotInfo = { ...defaultProps.slotInfo, savedPresetId: 'GFSL05_09' };
+    const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+    const modal = (open: boolean) => (
+      <QueryClientProvider client={client}>
+        <ConfigureAmsSlotModal {...defaultProps} isOpen={open} slotInfo={slotInfo} />
+      </QueryClientProvider>
+    );
+    const unread = /The printer did not answer, so its K-profiles could not be read/;
+    const { rerender } = render(modal(true));
+    await waitFor(() => expect(screen.getAllByText(unread).length).toBeGreaterThan(0));
+
+    rerender(modal(false));
+    rerender(modal(true));
+    await waitFor(() => expect(api.getKProfiles).toHaveBeenCalledTimes(2));
+    expect(screen.queryByText(unread)).not.toBeInTheDocument();
+
+    answer(mockKProfiles);
+    await waitFor(() => expect(screen.getByRole('option', { name: /PLA Basic \(K=0.020\)/ })).toBeInTheDocument());
+  });
+
   it('surfaces a K-profile whose name does not match the preset when filament_id agrees (#1688)', async () => {
     // Spool was edited with slicer_filament = "GFSL05_09" (the setting_id form
     // for Bambu PLA Basic). The printer has a *custom* K-profile saved on the

+ 35 - 0
frontend/src/__tests__/components/KProfilesViewWiring.test.tsx

@@ -383,3 +383,38 @@ describe('KProfilesView wiring: permissions', () => {
     expect(modalTrash()).toBeDisabled();
   });
 });
+
+describe('KProfilesView: a printer that did not answer (#3404)', () => {
+  it('a 504 says the read failed, not that there are no profiles', async () => {
+    let reads = 0;
+    server.use(
+      http.get('*/api/v1/printers/:id/kprofiles/', () => {
+        reads += 1;
+        return reads === 1
+          ? HttpResponse.json({ detail: 'The printer did not answer the K-profile request' }, { status: 504 })
+          : HttpResponse.json({ profiles: PROFILES, nozzle_diameter: '0.4' });
+      }),
+    );
+    const user = userEvent.setup();
+    render(<KProfilesView />);
+
+    await waitFor(() => expect(screen.getByText("Couldn't read K-Profiles")).toBeInTheDocument());
+    expect(screen.getByText(/did not answer the K-profile request\. Its profiles may still be there/)).toBeInTheDocument();
+    expect(screen.queryByText('No K-Profiles')).not.toBeInTheDocument();
+    expect(screen.queryByRole('button', { name: 'Create First Profile' })).not.toBeInTheDocument();
+
+    const card = screen.getByText("Couldn't read K-Profiles").parentElement!;
+    await user.click(within(card).getByRole('button', { name: 'Refresh' }));
+    await waitFor(() => expect(screen.getByText('S Bambu PLA Basic')).toBeInTheDocument());
+  });
+
+  it('an answered empty table still offers the first profile', async () => {
+    server.use(
+      http.get('*/api/v1/printers/:id/kprofiles/', () => HttpResponse.json({ profiles: [], nozzle_diameter: '0.4' })),
+    );
+    render(<KProfilesView />);
+
+    await waitFor(() => expect(screen.getByText('No K-Profiles')).toBeInTheDocument());
+    expect(screen.queryByText("Couldn't read K-Profiles")).not.toBeInTheDocument();
+  });
+});

+ 12 - 1
frontend/src/components/ConfigureAmsSlotModal.tsx

@@ -342,10 +342,13 @@ export function ConfigureAmsSlotModal({
   });
 
   // Fetch K profiles
-  const { data: kprofilesData, isLoading: kprofilesLoading } = useQuery({
+  // A printer that doesn't answer is an error (#3404), after the backend has
+  // already asked it three times: asking again only keeps the dialog loading.
+  const { data: kprofilesData, isLoading: kprofilesLoading, isError: kprofilesUnread } = useQuery({
     queryKey: ['kprofiles', printerId, nozzleDiameter],
     queryFn: () => api.getKProfiles(printerId, nozzleDiameter),
     enabled: isOpen && !!printerId,
+    retry: false,
   });
 
   // Fetch color catalog
@@ -1445,6 +1448,10 @@ export function ConfigureAmsSlotModal({
                       </select>
                       <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
                     </div>
+                  ) : kprofilesUnread ? (
+                    <p className="text-sm text-amber-700 dark:text-amber-400 py-2">
+                      {t('configureAmsSlot.kProfilesUnread')}
+                    </p>
                   ) : selectedPresetId ? (
                     <p className="text-sm text-bambu-gray italic py-2">
                       {t('configureAmsSlot.noMatchingKProfiles')}
@@ -1690,6 +1697,10 @@ export function ConfigureAmsSlotModal({
                     </select>
                     <ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
                   </div>
+                ) : kprofilesUnread ? (
+                  <p className="text-sm text-amber-700 dark:text-amber-400 py-2">
+                    {t('configureAmsSlot.kProfilesUnread')}
+                  </p>
                 ) : selectedPresetId ? (
                   <p className="text-sm text-bambu-gray italic py-2">
                     {t('configureAmsSlot.noMatchingKProfiles')}

+ 18 - 1
frontend/src/components/KProfilesView.tsx

@@ -19,7 +19,7 @@ import {
   Square,
   StickyNote,
 } from 'lucide-react';
-import { api } from '../api/client';
+import { api, ApiError } from '../api/client';
 import type { KProfile, KProfileCreate, KProfileDelete, Permission } from '../api/client';
 import {
   buildFilamentPresetOptions,
@@ -931,6 +931,9 @@ export function KProfilesView() {
 
   // Check if error is due to printer not being connected
   const isOfflineError = kprofilesError?.message?.includes('not connected');
+  // Any other failed read, mostly a printer that didn't answer (504): not a
+  // printer without profiles, so no "Create first profile" (#3404).
+  const isNoAnswerError = kprofilesError instanceof ApiError && kprofilesError.status === 504;
 
   // Auto-select first connected printer
   useEffect(() => {
@@ -1515,6 +1518,20 @@ export function KProfilesView() {
             </Button>
           </CardContent>
         </Card>
+      ) : kprofilesError ? (
+        <Card>
+          <CardContent className="py-12 text-center">
+            <AlertCircle className="w-12 h-12 text-amber-700 dark:text-amber-400 mx-auto mb-4" />
+            <h3 className="text-lg font-semibold text-white mb-2">{t('kProfiles.readFailed')}</h3>
+            <p className="text-bambu-gray mb-4">
+              {isNoAnswerError ? t('kProfiles.noAnswerDesc') : kprofilesError.message}
+            </p>
+            <Button variant="secondary" onClick={() => refetchProfiles()}>
+              <RefreshCw className="w-4 h-4" />
+              {t('common.refresh')}
+            </Button>
+          </CardContent>
+        </Card>
       ) : filteredProfiles.length > 0 ? (
         isDualNozzle ? (
           // Dual-nozzle: show Left/Right columns

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

@@ -5823,12 +5823,13 @@ export default {
         settingsAddressKept: '{{keys}} unverändert gelassen - sie bestimmen, wohin Bambuddy Daten, E-Mails oder Backups sendet; ändere sie in den Einstellungen',
         settingsMqttRelayFailed: 'MQTT-Einstellungen wiederhergestellt, aber das Relay konnte nicht neu verbunden werden - Bambuddy neu starten',
         kprofilesAlwaysOverwrite: 'K-Profile überschreiben immer den passenden Slot auf dem Drucker',
-        kprofilesAckUnreliable: 'Ein Drucker, der nicht antwortet, zählt weiterhin als wiederhergestellt - überprüfen Sie die Profile am Drucker',
+        kprofilesAckUnreliable: 'Ein Drucker, der das Schreiben nicht bestätigt, zählt weiterhin als wiederhergestellt - überprüfen Sie die Profile am Drucker',
         kprofilesPrinterMissing: 'Kein Drucker mit der Seriennummer {{serial}} - übersprungen',
         kprofilesPrinterOffline: '{{printer}} ({{serial}}) ist nicht verbunden - übersprungen',
         kprofilesUnknownNozzle: 'Unerwarteter Düsendurchmesser {{nozzle}} für {{serial}} - unverändert gesendet',
         kprofilesUnmatched: '{{count}} Profile für {{nozzle}} hatten kein Gegenstück auf {{printer}} - als neue Profile hinzugefügt',
         kprofilesSendFailed: '{{nozzle}}-Profile konnten nicht an {{printer}} ({{serial}}) gesendet werden',
+        kprofilesReadFailed: '{{printer}} ({{serial}}) hat auf das Lesen der K-Profile für {{nozzle}} nicht geantwortet - nicht wiederhergestellt',
         kprofilesRefused: '{{printer}} ({{serial}}) hat die {{nozzle}}-Profile abgelehnt: {{reason}}',
         kprofilesStepFailed: 'Der K-Profil-Schritt konnte nicht abgeschlossen werden - {{reason}}. Was zuvor wiederhergestellt wurde, ist trotzdem gespeichert.',
       },
@@ -6052,6 +6053,8 @@ export default {
     loadingProfiles: 'Lade K-Profile...',
     printerOffline: 'Drucker offline',
     printerOfflineDesc: 'Der ausgewählte Drucker ist nicht verbunden. Schalten Sie ihn ein, um K-Profile anzuzeigen.',
+    readFailed: 'K-Profile konnten nicht gelesen werden',
+    noAnswerDesc: 'Der Drucker hat auf die K-Profil-Anfrage nicht geantwortet. Seine Profile sind möglicherweise noch da: versuchen Sie es erneut.',
     noMatchingProfiles: 'Keine passenden Profile',
     noMatchingProfilesDesc: 'Keine Profile entsprechen Ihren Suchkriterien',
     noKProfiles: 'Keine K-Profile',
@@ -7169,6 +7172,7 @@ export default {
     filteringFor: 'Filtern nach: {{material}}',
     noKProfile: 'Kein K-Profil (Standard 0.020 verwenden)',
     noMatchingKProfiles: 'Keine passenden K-Profile gefunden. Standard K=0.020 wird verwendet.',
+    kProfilesUnread: 'Der Drucker hat nicht geantwortet, seine K-Profile konnten nicht gelesen werden. Es wird der Standardwert K=0.020 verwendet.',
     otherKProfiles: 'Weitere K-Profile auf diesem Drucker',
     selectFilamentFirst: 'Zuerst ein Filamentprofil auswählen',
     kFromCalibration: 'K={{value}} aus Druckerkalibrierung',

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

@@ -5875,12 +5875,13 @@ export default {
         settingsAddressKept: '{{keys}} left as they are - they decide where Bambuddy sends data, emails or backups; change them in Settings',
         settingsMqttRelayFailed: 'MQTT settings restored, but the relay could not be reconnected - restart Bambuddy',
         kprofilesAlwaysOverwrite: 'K-profiles always overwrite the matching slot on the printer',
-        kprofilesAckUnreliable: 'A printer that does not answer still counts as restored - verify the profiles on the printer',
+        kprofilesAckUnreliable: 'A printer that does not confirm the write still counts as restored - verify the profiles on the printer',
         kprofilesPrinterMissing: 'No printer with serial {{serial}} - skipped',
         kprofilesPrinterOffline: '{{printer}} ({{serial}}) is not connected - skipped',
         kprofilesUnknownNozzle: 'Unexpected nozzle diameter {{nozzle}} for {{serial}} - sent as-is',
         kprofilesUnmatched: '{{count}} profile(s) for {{nozzle}} had no counterpart on {{printer}} - added as new profiles',
         kprofilesSendFailed: 'Failed to send {{nozzle}} profiles to {{printer}} ({{serial}})',
+        kprofilesReadFailed: '{{printer}} ({{serial}}) did not answer the K-profile read for {{nozzle}} - not restored',
         kprofilesRefused: '{{printer}} ({{serial}}) refused the {{nozzle}} profiles: {{reason}}',
         kprofilesStepFailed: 'The K-profile step could not be completed - {{reason}}. Anything restored before it is still saved.',
       },
@@ -6105,6 +6106,8 @@ export default {
     loadingProfiles: 'Loading K-Profiles...',
     printerOffline: 'Printer Offline',
     printerOfflineDesc: 'The selected printer is not connected. Power it on to view K-profiles.',
+    readFailed: 'Couldn\'t read K-Profiles',
+    noAnswerDesc: 'The printer did not answer the K-profile request. Its profiles may still be there: try again.',
     noMatchingProfiles: 'No Matching Profiles',
     noMatchingProfilesDesc: 'No profiles match your search criteria',
     noKProfiles: 'No K-Profiles',
@@ -7222,6 +7225,7 @@ export default {
     filteringFor: 'Filtering for: {{material}}',
     noKProfile: 'No K profile (use default 0.020)',
     noMatchingKProfiles: 'No matching K profiles found. Default K=0.020 will be used.',
+    kProfilesUnread: 'The printer did not answer, so its K-profiles could not be read. Default K=0.020 will be used.',
     otherKProfiles: 'Other K profiles on this printer',
     selectFilamentFirst: 'Select a filament profile first',
     kFromCalibration: 'K={{value}} from printer calibration',

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

@@ -5830,12 +5830,13 @@ export default {
         settingsAddressKept: '{{keys}} se han dejado como están - deciden adónde envía Bambuddy datos, correos o copias de seguridad; cámbialos en Ajustes',
         settingsMqttRelayFailed: 'Ajustes MQTT restaurados, pero no se pudo reconectar el relé - reinicia Bambuddy',
         kprofilesAlwaysOverwrite: 'Los perfiles K siempre sobrescriben la ranura correspondiente en la impresora',
-        kprofilesAckUnreliable: 'Una impresora que no responde sigue contando como restaurada - verifica los perfiles en la impresora',
+        kprofilesAckUnreliable: 'Una impresora que no confirma la escritura sigue contando como restaurada - verifica los perfiles en la impresora',
         kprofilesPrinterMissing: 'No hay ninguna impresora con el número de serie {{serial}} - omitido',
         kprofilesPrinterOffline: '{{printer}} ({{serial}}) no está conectada - omitido',
         kprofilesUnknownNozzle: 'Diámetro de boquilla inesperado {{nozzle}} para {{serial}} - enviado tal cual',
         kprofilesUnmatched: '{{count}} perfiles para {{nozzle}} no tenían equivalente en {{printer}} - añadidos como perfiles nuevos',
         kprofilesSendFailed: 'No se pudieron enviar los perfiles de {{nozzle}} a {{printer}} ({{serial}})',
+        kprofilesReadFailed: '{{printer}} ({{serial}}) no respondió a la lectura de perfiles K para {{nozzle}} - no restaurado',
         kprofilesRefused: '{{printer}} ({{serial}}) rechazó los perfiles de {{nozzle}}: {{reason}}',
         kprofilesStepFailed: 'No se pudo completar el paso de los perfiles K - {{reason}}. Lo que se restauró antes sigue guardado.',
       },
@@ -6060,6 +6061,8 @@ export default {
     loadingProfiles: 'Cargando perfiles K...',
     printerOffline: 'Impresora desconectada',
     printerOfflineDesc: 'La impresora seleccionada no está conectada. Enciéndala para ver los perfiles K.',
+    readFailed: 'No se pudieron leer los perfiles K',
+    noAnswerDesc: 'La impresora no respondió a la solicitud de perfiles K. Es posible que sus perfiles sigan ahí: inténtelo de nuevo.',
     noMatchingProfiles: 'No hay perfiles coincidentes',
     noMatchingProfilesDesc: 'Ningún perfil coincide con sus criterios de búsqueda',
     noKProfiles: 'No hay perfiles K',
@@ -7177,6 +7180,7 @@ export default {
     filteringFor: 'Filtrando por: {{material}}',
     noKProfile: 'Sin perfil K (usar el predeterminado 0,020)',
     noMatchingKProfiles: 'No se encontraron perfiles K coincidentes. Se usará el K=0,020 predeterminado.',
+    kProfilesUnread: 'La impresora no respondió, así que no se pudieron leer sus perfiles K. Se usará el valor predeterminado K=0.020.',
     otherKProfiles: 'Otros perfiles K en esta impresora',
     selectFilamentFirst: 'Seleccione primero un perfil de filamento',
     kFromCalibration: 'K={{value}} de la calibración de la impresora',

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

@@ -5810,12 +5810,13 @@ export default {
         settingsAddressKept: '{{keys}} laissés tels quels - ils déterminent où Bambuddy envoie des données, des e-mails ou des sauvegardes ; modifiez-les dans les Paramètres',
         settingsMqttRelayFailed: 'Réglages MQTT restaurés, mais le relais n\'a pas pu être reconnecté - redémarrez Bambuddy',
         kprofilesAlwaysOverwrite: 'Les profils K écrasent toujours l\'emplacement correspondant sur l\'imprimante',
-        kprofilesAckUnreliable: 'Une imprimante qui ne répond pas compte quand même comme restaurée - vérifiez les profils sur l\'imprimante',
+        kprofilesAckUnreliable: 'Une imprimante qui ne confirme pas l\'écriture compte quand même comme restaurée - vérifiez les profils sur l\'imprimante',
         kprofilesPrinterMissing: 'Aucune imprimante avec le numéro de série {{serial}} - ignoré',
         kprofilesPrinterOffline: '{{printer}} ({{serial}}) n\'est pas connectée - ignoré',
         kprofilesUnknownNozzle: 'Diamètre de buse inattendu {{nozzle}} pour {{serial}} - envoyé tel quel',
         kprofilesUnmatched: '{{count}} profils pour {{nozzle}} n\'avaient pas d\'équivalent sur {{printer}} - ajoutés comme nouveaux profils',
         kprofilesSendFailed: 'Impossible d\'envoyer les profils {{nozzle}} à {{printer}} ({{serial}})',
+        kprofilesReadFailed: '{{printer}} ({{serial}}) n\'a pas répondu à la lecture des profils K pour {{nozzle}} - non restauré',
         kprofilesRefused: '{{printer}} ({{serial}}) a refusé les profils {{nozzle}} : {{reason}}',
         kprofilesStepFailed: 'L\'étape des profils K n\'a pas pu être terminée - {{reason}}. Ce qui a été restauré auparavant reste enregistré.',
       },
@@ -6040,6 +6041,8 @@ export default {
     loadingProfiles: 'Chargement des K-Profiles...',
     printerOffline: 'Imprimante hors ligne',
     printerOfflineDesc: 'L\'imprimante doit être allumée.',
+    readFailed: 'Impossible de lire les profils K',
+    noAnswerDesc: 'L\'imprimante n\'a pas répondu à la demande de profils K. Ses profils sont peut-être toujours là : réessayez.',
     noMatchingProfiles: 'Aucun profil correspondant',
     noMatchingProfilesDesc: 'Ajustez votre recherche',
     noKProfiles: 'Aucun K-Profile',
@@ -7157,6 +7160,7 @@ export default {
     filteringFor: 'Filtrage pour : {{material}}',
     noKProfile: 'Pas de profil K (utiliser défaut 0.020)',
     noMatchingKProfiles: 'Aucun profil K trouvé. K=0.020 par défaut sera utilisé.',
+    kProfilesUnread: 'L\'imprimante n\'a pas répondu, ses profils K n\'ont pas pu être lus. La valeur par défaut K=0.020 sera utilisée.',
     otherKProfiles: 'Autres profils K sur cette imprimante',
     selectFilamentFirst: 'Sélectionnez d\'abord un profil filament',
     kFromCalibration: 'K={{value}} de la calibration imprimante',

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

@@ -5809,12 +5809,13 @@ export default {
         settingsAddressKept: '{{keys}} lasciati invariati - decidono dove Bambuddy invia dati, email o backup; modificali nelle Impostazioni',
         settingsMqttRelayFailed: 'Impostazioni MQTT ripristinate, ma il relay non è stato riconnesso - riavvia Bambuddy',
         kprofilesAlwaysOverwrite: 'I profili K sovrascrivono sempre lo slot corrispondente sulla stampante',
-        kprofilesAckUnreliable: 'Una stampante che non risponde conta comunque come ripristinata - verifica i profili sulla stampante',
+        kprofilesAckUnreliable: 'Una stampante che non conferma la scrittura conta comunque come ripristinata - verifica i profili sulla stampante',
         kprofilesPrinterMissing: 'Nessuna stampante con numero di serie {{serial}} - saltato',
         kprofilesPrinterOffline: '{{printer}} ({{serial}}) non è connessa - saltato',
         kprofilesUnknownNozzle: 'Diametro ugello inatteso {{nozzle}} per {{serial}} - inviato così com\'è',
         kprofilesUnmatched: '{{count}} profili per {{nozzle}} non avevano corrispondenza su {{printer}} - aggiunti come nuovi profili',
         kprofilesSendFailed: 'Impossibile inviare i profili {{nozzle}} a {{printer}} ({{serial}})',
+        kprofilesReadFailed: '{{printer}} ({{serial}}) non ha risposto alla lettura dei profili K per {{nozzle}} - non ripristinati',
         kprofilesRefused: '{{printer}} ({{serial}}) ha rifiutato i profili {{nozzle}}: {{reason}}',
         kprofilesStepFailed: 'Non è stato possibile completare il passaggio dei profili K - {{reason}}. Quanto ripristinato prima resta salvato.',
       },
@@ -6039,6 +6040,8 @@ export default {
     loadingProfiles: 'Caricamento K-Profiles...',
     printerOffline: 'Stampante offline',
     printerOfflineDesc: 'La stampante selezionata non e connessa. Accendila per vedere i K-profiles.',
+    readFailed: 'Impossibile leggere i profili K',
+    noAnswerDesc: 'La stampante non ha risposto alla richiesta dei profili K. I suoi profili potrebbero essere ancora presenti: riprova.',
     noMatchingProfiles: 'Nessun profilo corrispondente',
     noMatchingProfilesDesc: 'Nessun profilo corrisponde ai criteri di ricerca',
     noKProfiles: 'Nessun K-Profile',
@@ -7156,6 +7159,7 @@ export default {
     filteringFor: 'Filtrando per: {{material}}',
     noKProfile: 'Nessun profilo K (usa predefinito 0.020)',
     noMatchingKProfiles: 'Nessun profilo K corrispondente. Verrà usato K=0.020 predefinito.',
+    kProfilesUnread: 'La stampante non ha risposto, quindi i suoi profili K non sono stati letti. Verrà usato il valore predefinito K=0.020.',
     otherKProfiles: 'Altri profili K su questa stampante',
     selectFilamentFirst: 'Seleziona prima un profilo filamento',
     kFromCalibration: 'K={{value}} dalla calibrazione stampante',

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

@@ -5823,12 +5823,13 @@ export default {
         settingsAddressKept: '{{keys}} はそのままにしました - Bambuddy がデータ、メール、バックアップを送る先を決める設定です。設定画面で変更してください',
         settingsMqttRelayFailed: 'MQTT 設定を復元しましたが、リレーを再接続できませんでした - Bambuddy を再起動してください',
         kprofilesAlwaysOverwrite: 'K プロファイルは常にプリンター側の該当スロットを上書きします',
-        kprofilesAckUnreliable: '応答しないプリンターも復元済みとして数えます - プロファイルはプリンター側で確認してください',
+        kprofilesAckUnreliable: '書き込みを確認しないプリンターも復元済みとして数えます - プロファイルはプリンター側で確認してください',
         kprofilesPrinterMissing: 'シリアル {{serial}} のプリンターがありません - スキップしました',
         kprofilesPrinterOffline: '{{printer}} ({{serial}}) は接続されていません - スキップしました',
         kprofilesUnknownNozzle: '{{serial}} に想定外のノズル径 {{nozzle}} - そのまま送信しました',
         kprofilesUnmatched: '{{nozzle}} 用のプロファイル {{count}} 件は {{printer}} に該当がありませんでした - 新規プロファイルとして追加しました',
         kprofilesSendFailed: '{{nozzle}} のプロファイルを {{printer}} ({{serial}}) に送信できませんでした',
+        kprofilesReadFailed: '{{printer}}({{serial}})が{{nozzle}}のKプロファイルの読み込みに応答しませんでした - 復元されていません',
         kprofilesRefused: '{{printer}} ({{serial}}) が {{nozzle}} のプロファイルを拒否しました: {{reason}}',
         kprofilesStepFailed: 'K プロファイルの処理を完了できませんでした - {{reason}}。それまでに復元された内容は保存されています。',
       },
@@ -6053,6 +6054,8 @@ export default {
     loadingProfiles: 'Kプロファイルを読み込み中...',
     printerOffline: 'プリンターオフライン',
     printerOfflineDesc: '選択したプリンターは接続されていません。電源を入れてKプロファイルを表示してください。',
+    readFailed: 'Kプロファイルを読み込めませんでした',
+    noAnswerDesc: 'プリンターがKプロファイルの要求に応答しませんでした。プロファイルはまだ残っている可能性があります。もう一度お試しください。',
     noMatchingProfiles: '一致するプロファイルなし',
     noMatchingProfilesDesc: '検索条件に一致するプロファイルがありません',
     noKProfiles: 'Kプロファイルなし',
@@ -7170,6 +7173,7 @@ export default {
     filteringFor: 'フィルター中: {{material}}',
     noKProfile: 'Kプロファイルなし(デフォルト0.020を使用)',
     noMatchingKProfiles: '一致するKプロファイルが見つかりません。デフォルトK=0.020が使用されます。',
+    kProfilesUnread: 'プリンターが応答しなかったため、Kプロファイルを読み込めませんでした。デフォルトのK=0.020が使用されます。',
     otherKProfiles: 'このプリンターの他のKプロファイル',
     selectFilamentFirst: 'まずフィラメントプロファイルを選択してください',
     kFromCalibration: 'K={{value}}(プリンターキャリブレーションから)',

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

@@ -5586,12 +5586,13 @@ export default {
         settingsAddressKept: '{{keys}}을(를) 그대로 두었습니다 - Bambuddy가 데이터, 이메일 또는 백업을 보내는 곳을 정합니다. 설정에서 변경하세요',
         settingsMqttRelayFailed: 'MQTT 설정을 복원했지만 릴레이를 다시 연결하지 못했습니다 - Bambuddy를 재시작하세요',
         kprofilesAlwaysOverwrite: 'K 프로파일은 항상 프린터의 해당 슬롯을 덮어씁니다',
-        kprofilesAckUnreliable: '응답하지 않는 프린터도 복원됨으로 집계됩니다 - 프린터에서 프로파일을 확인하세요',
+        kprofilesAckUnreliable: '쓰기를 확인하지 않는 프린터도 복원됨으로 집계됩니다 - 프린터에서 프로파일을 확인하세요',
         kprofilesPrinterMissing: '시리얼 {{serial}}인 프린터가 없습니다 - 건너뛰었습니다',
         kprofilesPrinterOffline: '{{printer}}({{serial}})이(가) 연결되어 있지 않습니다 - 건너뛰었습니다',
         kprofilesUnknownNozzle: '{{serial}}의 예상치 못한 노즐 직경 {{nozzle}} - 그대로 전송했습니다',
         kprofilesUnmatched: '{{nozzle}}용 프로파일 {{count}}개가 {{printer}}에 대응 항목이 없습니다 - 새 프로파일로 추가했습니다',
         kprofilesSendFailed: '{{nozzle}} 프로파일을 {{printer}}({{serial}})에 보내지 못했습니다',
+        kprofilesReadFailed: '{{printer}} ({{serial}})이(가) {{nozzle}}의 K 프로필 읽기에 응답하지 않았습니다 - 복원되지 않음',
         kprofilesRefused: '{{printer}}({{serial}})이(가) {{nozzle}} 프로파일을 거부했습니다: {{reason}}',
         kprofilesStepFailed: 'K 프로파일 단계를 완료하지 못했습니다 - {{reason}}. 그 전에 복원된 항목은 그대로 저장되어 있습니다.',
       },
@@ -5799,6 +5800,8 @@ export default {
     loadingProfiles: 'K-프로필 불러오는 중...',
     printerOffline: '프린터 오프라인',
     printerOfflineDesc: '선택한 프린터가 연결되지 않았습니다. 켜서 K-프로필을 확인하세요.',
+    readFailed: 'K 프로필을 읽을 수 없습니다',
+    noAnswerDesc: '프린터가 K 프로필 요청에 응답하지 않았습니다. 프로필이 아직 남아 있을 수 있습니다. 다시 시도하세요.',
     noMatchingProfiles: '일치하는 프로필 없음',
     noMatchingProfilesDesc: '검색 기준과 일치하는 프로필이 없습니다',
     noKProfiles: 'K-프로필 없음',
@@ -6859,6 +6862,7 @@ export default {
     filteringFor: '필터링 중: {{material}}',
     noKProfile: 'K 프로필 없음 (기본값 0.020 사용)',
     noMatchingKProfiles: '일치하는 K 프로필을 찾을 수 없습니다. 기본값 K=0.020이 사용됩니다.',
+    kProfilesUnread: '프린터가 응답하지 않아 K 프로필을 읽을 수 없습니다. 기본값 K=0.020이 사용됩니다.',
     otherKProfiles: '이 프린터의 다른 K 프로필',
     selectFilamentFirst: '먼저 필라멘트 프로필을 선택하세요',
     kFromCalibration: 'K={{value}} (프린터 보정에서)',

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

@@ -5873,12 +5873,13 @@ export default {
         settingsAddressKept: '{{keys}} ongewijzigd gelaten - ze bepalen waar Bambuddy gegevens, e-mails of back-ups naartoe stuurt; wijzig ze in Instellingen',
         settingsMqttRelayFailed: 'MQTT-instellingen hersteld, maar de relay kon niet opnieuw verbinden - start Bambuddy opnieuw',
         kprofilesAlwaysOverwrite: 'K-profielen overschrijven altijd de overeenkomende sleuf op de printer',
-        kprofilesAckUnreliable: 'Een printer die niet antwoordt telt nog steeds als hersteld - controleer de profielen op de printer',
+        kprofilesAckUnreliable: 'Een printer die het schrijven niet bevestigt telt nog steeds als hersteld - controleer de profielen op de printer',
         kprofilesPrinterMissing: 'Geen printer met serienummer {{serial}} - overgeslagen',
         kprofilesPrinterOffline: '{{printer}} ({{serial}}) is niet verbonden - overgeslagen',
         kprofilesUnknownNozzle: 'Onverwachte nozzlediameter {{nozzle}} voor {{serial}} - ongewijzigd verzonden',
         kprofilesUnmatched: '{{count}} profiel(en) voor {{nozzle}} hadden geen tegenhanger op {{printer}} - als nieuwe profielen toegevoegd',
         kprofilesSendFailed: 'Verzenden van {{nozzle}}-profielen naar {{printer}} ({{serial}}) mislukt',
+        kprofilesReadFailed: '{{printer}} ({{serial}}) heeft niet geantwoord op het lezen van de K-profielen voor {{nozzle}} - niet hersteld',
         kprofilesRefused: '{{printer}} ({{serial}}) weigerde de {{nozzle}}-profielen: {{reason}}',
         kprofilesStepFailed: 'De K-profielstap kon niet worden voltooid - {{reason}}. Alles wat daarvoor is hersteld blijft opgeslagen.',
       },
@@ -6103,6 +6104,8 @@ export default {
     loadingProfiles: 'K-profielen laden...',
     printerOffline: 'Printer offline',
     printerOfflineDesc: 'De geselecteerde printer is niet verbonden. Schakel hem in om de K-profielen te bekijken.',
+    readFailed: 'K-profielen konden niet worden gelezen',
+    noAnswerDesc: 'De printer heeft niet geantwoord op het K-profielverzoek. De profielen staan er mogelijk nog: probeer het opnieuw.',
     noMatchingProfiles: 'Geen overeenkomende profielen',
     noMatchingProfilesDesc: 'Geen profielen komen overeen met je zoekcriteria',
     noKProfiles: 'Geen K-profielen',
@@ -7220,6 +7223,7 @@ export default {
     filteringFor: 'Filteren op: {{material}}',
     noKProfile: 'Geen K-profiel (gebruik standaard 0.020)',
     noMatchingKProfiles: 'Geen overeenkomende K-profielen gevonden. Standaard K=0.020 wordt gebruikt.',
+    kProfilesUnread: 'De printer heeft niet geantwoord, dus de K-profielen konden niet worden gelezen. De standaardwaarde K=0.020 wordt gebruikt.',
     otherKProfiles: 'Andere K-profielen op deze printer',
     selectFilamentFirst: 'Selecteer eerst een filamentprofiel',
     kFromCalibration: 'K={{value}} uit printerkalibratie',

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

@@ -5809,12 +5809,13 @@ export default {
         settingsAddressKept: '{{keys}} ficaram como estão - definem para onde o Bambuddy envia dados, e-mails ou backups; altere em Configurações',
         settingsMqttRelayFailed: 'Configurações MQTT restauradas, mas o relay não pôde ser reconectado - reinicie o Bambuddy',
         kprofilesAlwaysOverwrite: 'Os perfis K sempre sobrescrevem o slot correspondente na impressora',
-        kprofilesAckUnreliable: 'Uma impressora que não responde ainda conta como restaurada - verifique os perfis na impressora',
+        kprofilesAckUnreliable: 'Uma impressora que não confirma a gravação ainda conta como restaurada - verifique os perfis na impressora',
         kprofilesPrinterMissing: 'Nenhuma impressora com o número de série {{serial}} - ignorado',
         kprofilesPrinterOffline: '{{printer}} ({{serial}}) não está conectada - ignorado',
         kprofilesUnknownNozzle: 'Diâmetro de bico inesperado {{nozzle}} para {{serial}} - enviado como está',
         kprofilesUnmatched: '{{count}} perfis para {{nozzle}} não tinham correspondente em {{printer}} - adicionados como novos perfis',
         kprofilesSendFailed: 'Não foi possível enviar os perfis de {{nozzle}} para {{printer}} ({{serial}})',
+        kprofilesReadFailed: '{{printer}} ({{serial}}) não respondeu à leitura dos perfis K para {{nozzle}} - não restaurado',
         kprofilesRefused: '{{printer}} ({{serial}}) recusou os perfis de {{nozzle}}: {{reason}}',
         kprofilesStepFailed: 'Não foi possível concluir a etapa dos perfis K - {{reason}}. O que foi restaurado antes continua salvo.',
       },
@@ -6039,6 +6040,8 @@ export default {
     loadingProfiles: 'Carregando K-Profiles...',
     printerOffline: 'Impressora Offline',
     printerOfflineDesc: 'A impressora selecionada não está conectada. Ligue-a para visualizar os K-profiles.',
+    readFailed: 'Não foi possível ler os perfis K',
+    noAnswerDesc: 'A impressora não respondeu à solicitação de perfis K. Os perfis podem ainda estar lá: tente novamente.',
     noMatchingProfiles: 'Nenhum Perfil Correspondente',
     noMatchingProfilesDesc: 'Nenhum perfil corresponde aos seus critérios de pesquisa',
     noKProfiles: 'Nenhum K-Profile',
@@ -7156,6 +7159,7 @@ export default {
     filteringFor: 'Filtrando por: {{material}}',
     noKProfile: 'Nenhum perfil K (usar padrão 0.020)',
     noMatchingKProfiles: 'Nenhum perfil K correspondente encontrado. O K padrão=0.020 será usado.',
+    kProfilesUnread: 'A impressora não respondeu, então os perfis K não puderam ser lidos. O padrão K=0.020 será usado.',
     otherKProfiles: 'Outros perfis K nesta impressora',
     selectFilamentFirst: 'Selecione um perfil de filamento primeiro',
     kFromCalibration: 'K={{value}} da calibração da impressora',

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

@@ -5578,12 +5578,13 @@ export default {
         settingsAddressKept: '{{keys}} оставлены без изменений - они определяют, куда Bambuddy отправляет данные, письма или резервные копии; измените их в настройках',
         settingsMqttRelayFailed: 'Настройки MQTT восстановлены, но переподключить реле не удалось - перезапустите Bambuddy',
         kprofilesAlwaysOverwrite: 'K-профили всегда перезаписывают соответствующий слот на принтере',
-        kprofilesAckUnreliable: 'Принтер, который не отвечает, всё равно считается восстановленным - проверьте профили на принтере',
+        kprofilesAckUnreliable: 'Принтер, который не подтверждает запись, всё равно считается восстановленным - проверьте профили на принтере',
         kprofilesPrinterMissing: 'Нет принтера с серийным номером {{serial}} - пропущено',
         kprofilesPrinterOffline: '{{printer}} ({{serial}}) не подключён - пропущено',
         kprofilesUnknownNozzle: 'Неожиданный диаметр сопла {{nozzle}} для {{serial}} - отправлено как есть',
         kprofilesUnmatched: 'Профилей для {{nozzle}} без соответствия на {{printer}}: {{count}} - добавлены как новые профили',
         kprofilesSendFailed: 'Не удалось отправить профили {{nozzle}} на {{printer}} ({{serial}})',
+        kprofilesReadFailed: '{{printer}} ({{serial}}) не ответил на чтение K-профилей для {{nozzle}} - не восстановлено',
         kprofilesRefused: '{{printer}} ({{serial}}) отклонил профили {{nozzle}}: {{reason}}',
         kprofilesStepFailed: 'Не удалось завершить этап K-профилей - {{reason}}. Всё, что было восстановлено до него, сохранено.',
       },
@@ -5787,6 +5788,8 @@ export default {
     loadingProfiles: "Загрузка K-профилей...",
     printerOffline: "Принтер не в сети",
     printerOfflineDesc: "Выбранный принтер не подключён. Включите его, чтобы просмотреть K-профили.",
+    readFailed: 'Не удалось прочитать K-профили',
+    noAnswerDesc: 'Принтер не ответил на запрос K-профилей. Профили, возможно, всё ещё на месте: попробуйте ещё раз.',
     noMatchingProfiles: "Подходящие профили не найдены",
     noMatchingProfilesDesc: "Ни один профиль не соответствует условиям поиска",
     noKProfiles: "K-профили отсутствуют",
@@ -6846,6 +6849,7 @@ export default {
     filteringFor: "Фильтр по материалу: {{material}}",
     noKProfile: "Без K-профиля (стандартное значение 0,020)",
     noMatchingKProfiles: "Подходящие K-профили не найдены. Будет использовано стандартное значение K=0,020.",
+    kProfilesUnread: 'Принтер не ответил, поэтому K-профили не удалось прочитать. Будет использовано значение по умолчанию K=0.020.',
     otherKProfiles: "Другие K-профили на этом принтере",
     selectFilamentFirst: "Сначала выберите профиль филамента",
     kFromCalibration: "K={{value}} из калибровки принтера",

+ 5 - 1
frontend/src/i18n/locales/sv.ts

@@ -5871,12 +5871,13 @@ errors: {
         settingsAddressKept: '{{keys}} lämnades oförändrade - de avgör vart Bambuddy skickar data, e-post eller säkerhetskopior; ändra dem i Inställningar',
         settingsMqttRelayFailed: 'MQTT-inställningar återställda, men reläet kunde inte återanslutas — starta om Bambuddy',
         kprofilesAlwaysOverwrite: 'K-profiler skriver alltid över det matchande facket på skrivaren',
-        kprofilesAckUnreliable: 'En skrivare som inte svarar räknas ändå som återställd — verifiera profilerna på skrivaren',
+        kprofilesAckUnreliable: 'En skrivare som inte bekräftar skrivningen räknas ändå som återställd — verifiera profilerna på skrivaren',
         kprofilesPrinterMissing: 'Ingen skrivare med serienummer {{serial}} — hoppade över',
         kprofilesPrinterOffline: '{{printer}} ({{serial}}) är inte ansluten — hoppade över',
         kprofilesUnknownNozzle: 'Oväntad munstyckesdiameter {{nozzle}} för {{serial}} — skickad som den är',
         kprofilesUnmatched: '{{count}} profil(er) för {{nozzle}} hade ingen motsvarighet på {{printer}} — lades till som nya profiler',
         kprofilesSendFailed: 'Kunde inte skicka {{nozzle}}-profiler till {{printer}} ({{serial}})',
+        kprofilesReadFailed: '{{printer}} ({{serial}}) svarade inte på läsningen av K-profiler för {{nozzle}} — inte återställt',
         kprofilesRefused: '{{printer}} ({{serial}}) vägrade {{nozzle}}-profilerna: {{reason}}',
         kprofilesStepFailed: 'K-profil-steget kunde inte slutföras — {{reason}}. Allt som återställdes före det är fortfarande sparat.',
       },
@@ -6101,6 +6102,8 @@ errors: {
     loadingProfiles: 'Laddar K-profiler...',
     printerOffline: 'Skrivare offline',
     printerOfflineDesc: 'Den valda skrivaren är inte ansluten. Sätt på den för att visa K-profiler.',
+    readFailed: 'Kunde inte läsa K-profiler',
+    noAnswerDesc: 'Skrivaren svarade inte på begäran om K-profiler. Profilerna kan fortfarande finnas kvar: försök igen.',
     noMatchingProfiles: 'Inga matchande profiler',
     noMatchingProfilesDesc: 'Inga profiler matchar dina sökvillkor',
     noKProfiles: 'Inga K-profiler',
@@ -7218,6 +7221,7 @@ errors: {
     filteringFor: 'Filtrerar för: {{material}}',
     noKProfile: 'Ingen K-profil (använd standard 0,020)',
     noMatchingKProfiles: 'Inga matchande K-profiler hittade. Standard K=0,020 kommer att användas.',
+    kProfilesUnread: 'Skrivaren svarade inte, så dess K-profiler kunde inte läsas. Standardvärdet K=0.020 används.',
     otherKProfiles: 'Andra K-profiler på denna skrivare',
     selectFilamentFirst: 'Välj en filamentprofil först',
     kFromCalibration: 'K={{value}} från skrivarkalibrering',

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

@@ -5799,12 +5799,13 @@ export default {
         settingsAddressKept: "{{keys}} olduğu gibi bırakıldı - Bambuddy'nin verileri, e-postaları veya yedekleri nereye gönderdiğini belirler; Ayarlar'dan değiştirin",
         settingsMqttRelayFailed: 'MQTT ayarları geri yüklendi ancak röle yeniden bağlanamadı - Bambuddy\'yi yeniden başlatın',
         kprofilesAlwaysOverwrite: 'K profilleri yazıcıdaki eşleşen yuvanın her zaman üzerine yazar',
-        kprofilesAckUnreliable: 'Yanıt vermeyen bir yazıcı yine de geri yüklendi sayılır - profilleri yazıcıda doğrulayın',
+        kprofilesAckUnreliable: 'Yazmayı onaylamayan bir yazıcı yine de geri yüklendi sayılır - profilleri yazıcıda doğrulayın',
         kprofilesPrinterMissing: '{{serial}} seri numaralı yazıcı yok - atlandı',
         kprofilesPrinterOffline: '{{printer}} ({{serial}}) bağlı değil - atlandı',
         kprofilesUnknownNozzle: '{{serial}} için beklenmeyen nozul çapı {{nozzle}} - olduğu gibi gönderildi',
         kprofilesUnmatched: '{{nozzle}} için {{count}} profilin {{printer}} üzerinde karşılığı yoktu - yeni profil olarak eklendi',
         kprofilesSendFailed: '{{nozzle}} profilleri {{printer}} ({{serial}}) yazıcısına gönderilemedi',
+        kprofilesReadFailed: '{{printer}} ({{serial}}) {{nozzle}} için K profili okumasına yanıt vermedi - geri yüklenmedi',
         kprofilesRefused: '{{printer}} ({{serial}}) {{nozzle}} profillerini reddetti: {{reason}}',
         kprofilesStepFailed: 'K profili adımı tamamlanamadı - {{reason}}. Bundan önce geri yüklenenler yine de kaydedildi.',
       },
@@ -6024,6 +6025,8 @@ export default {
     loadingProfiles: 'K-Profilleri yükleniyor...',
     printerOffline: 'Yazıcı Çevrimdışı',
     printerOfflineDesc: 'Seçilen yazıcı bağlı değil. K-profillerini görüntülemek için açın.',
+    readFailed: 'K profilleri okunamadı',
+    noAnswerDesc: 'Yazıcı K profili isteğine yanıt vermedi. Profilleri hâlâ orada olabilir: tekrar deneyin.',
     noMatchingProfiles: 'Eşleşen Profil Yok',
     noMatchingProfilesDesc: 'Arama kriterlerinizle eşleşen profil yok',
     noKProfiles: 'K-Profili Yok',
@@ -7113,6 +7116,7 @@ export default {
     filteringFor: 'Şu için filtreleniyor: {{material}}',
     noKProfile: 'K profili yok (varsayılan 0.020 kullan)',
     noMatchingKProfiles: 'Eşleşen K profili bulunamadı. Varsayılan K=0.020 kullanılacak.',
+    kProfilesUnread: 'Yazıcı yanıt vermediği için K profilleri okunamadı. Varsayılan K=0.020 kullanılacak.',
     otherKProfiles: 'Bu yazıcıdaki diğer K profilleri',
     selectFilamentFirst: 'Önce bir filament profili seçin',
     kFromCalibration: 'Yazıcı kalibrasyonundan K={{value}}',

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

@@ -5863,12 +5863,13 @@ export default {
         settingsAddressKept: '{{keys}} залишено без змін - вони визначають, куди Bambuddy надсилає дані, листи чи резервні копії; змініть їх у налаштуваннях',
         settingsMqttRelayFailed: "Налаштування MQTT відновлено, але реле не вдалося перепідключити - перезапустіть Bambuddy",
         kprofilesAlwaysOverwrite: "K-профілі завжди перезаписують відповідний слот на принтері",
-        kprofilesAckUnreliable: "Принтер, який не відповідає, усе одно вважається відновленим - перевірте профілі на принтері",
+        kprofilesAckUnreliable: 'Принтер, який не підтверджує запис, усе одно вважається відновленим - перевірте профілі на принтері',
         kprofilesPrinterMissing: "Немає принтера із серійним номером {{serial}} - пропущено",
         kprofilesPrinterOffline: "{{printer}} ({{serial}}) не підключено - пропущено",
         kprofilesUnknownNozzle: "Неочікуваний діаметр сопла {{nozzle}} для {{serial}} - надіслано як є",
         kprofilesUnmatched: "Профілів для {{nozzle}} без відповідника на {{printer}}: {{count}} - додано як нові профілі",
         kprofilesSendFailed: "Не вдалося надіслати профілі {{nozzle}} на {{printer}} ({{serial}})",
+        kprofilesReadFailed: '{{printer}} ({{serial}}) не відповів на читання K-профілів для {{nozzle}} - не відновлено',
         kprofilesRefused: "{{printer}} ({{serial}}) відхилив профілі {{nozzle}}: {{reason}}",
         kprofilesStepFailed: "Не вдалося завершити етап K-профілів - {{reason}}. Усе, що було відновлено до нього, збережено.",
       },
@@ -6093,6 +6094,8 @@ export default {
     loadingProfiles: "Завантаження K-профілів...",
     printerOffline: "Принтер не в мережі",
     printerOfflineDesc: "Вибраний принтер не підключено. Увімкніть його, щоб переглянути K-профілі.",
+    readFailed: 'Не вдалося прочитати K-профілі',
+    noAnswerDesc: 'Принтер не відповів на запит K-профілів. Профілі, можливо, все ще на місці: спробуйте ще раз.',
     noMatchingProfiles: "Немає відповідних профілів",
     noMatchingProfilesDesc: "Немає профілів, які відповідають вашим критеріям пошуку",
     noKProfiles: "Без K-профілів",
@@ -7210,6 +7213,7 @@ export default {
     filteringFor: "Фільтрування за: {{material}}",
     noKProfile: "Немає профілю K (використовуйте значення за замовчуванням 0,020)",
     noMatchingKProfiles: "Не знайдено відповідних K профілів. Використовуватиметься K=0,020 за замовчуванням.",
+    kProfilesUnread: 'Принтер не відповів, тому K-профілі не вдалося прочитати. Буде використано типове значення K=0.020.',
     otherKProfiles: "Інші K-профілі на цьому принтері",
     selectFilamentFirst: "Спочатку виберіть профіль філаменту",
     kFromCalibration: "K={{value}} від калібрування принтера",

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

@@ -5809,12 +5809,13 @@ export default {
         settingsAddressKept: '{{keys}} 保持不变 - 它们决定 Bambuddy 将数据、邮件或备份发送到哪里;请在设置中修改',
         settingsMqttRelayFailed: 'MQTT 设置已恢复,但中继无法重新连接 - 请重启 Bambuddy',
         kprofilesAlwaysOverwrite: 'K 值配置总是覆盖打印机上对应的槽位',
-        kprofilesAckUnreliable: '打印机不回应时仍计为已恢复 - 请在打印机上核对配置',
+        kprofilesAckUnreliable: '打印机未确认写入时仍计为已恢复 - 请在打印机上核对配置',
         kprofilesPrinterMissing: '没有序列号为 {{serial}} 的打印机 - 已跳过',
         kprofilesPrinterOffline: '{{printer}}({{serial}})未连接 - 已跳过',
         kprofilesUnknownNozzle: '{{serial}} 的喷嘴直径 {{nozzle}} 不在预期范围内 - 已原样发送',
         kprofilesUnmatched: '{{nozzle}} 的 {{count}} 个配置在 {{printer}} 上没有对应项 - 已作为新配置添加',
         kprofilesSendFailed: '无法将 {{nozzle}} 的配置发送到 {{printer}}({{serial}})',
+        kprofilesReadFailed: '{{printer}}({{serial}})未响应 {{nozzle}} 的 K 配置文件读取 - 未恢复',
         kprofilesRefused: '{{printer}}({{serial}})拒绝了 {{nozzle}} 的配置:{{reason}}',
         kprofilesStepFailed: 'K 值配置步骤未能完成 - {{reason}}。在此之前恢复的内容仍已保存。',
       },
@@ -6039,6 +6040,8 @@ export default {
     loadingProfiles: '加载 K 值配置中...',
     printerOffline: '打印机离线',
     printerOfflineDesc: '所选打印机未连接。开启电源以查看 K 值配置。',
+    readFailed: '无法读取 K 配置文件',
+    noAnswerDesc: '打印机未响应 K 配置文件请求。其配置文件可能仍然存在:请重试。',
     noMatchingProfiles: '无匹配的配置',
     noMatchingProfilesDesc: '没有配置匹配您的搜索条件',
     noKProfiles: '无 K 值配置',
@@ -7154,6 +7157,7 @@ export default {
     filteringFor: '筛选:{{material}}',
     noKProfile: '无 K 值配置(使用默认值 0.020)',
     noMatchingKProfiles: '未找到匹配的 K 值配置。将使用默认 K=0.020。',
+    kProfilesUnread: '打印机未响应,无法读取其 K 配置文件。将使用默认值 K=0.020。',
     otherKProfiles: '此打印机上的其他 K 值配置',
     selectFilamentFirst: '请先选择耗材配置',
     kFromCalibration: 'K={{value}}(来自打印机校准)',

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

@@ -5809,12 +5809,13 @@ export default {
         settingsAddressKept: '{{keys}} 維持不變 - 它們決定 Bambuddy 將資料、電子郵件或備份傳送到哪裡;請在設定中修改',
         settingsMqttRelayFailed: 'MQTT 設定已還原,但中繼無法重新連線 - 請重新啟動 Bambuddy',
         kprofilesAlwaysOverwrite: 'K 值設定檔一律覆寫印表機上對應的插槽',
-        kprofilesAckUnreliable: '印表機未回應時仍計為已還原 - 請在印表機上核對設定檔',
+        kprofilesAckUnreliable: '印表機未確認寫入時仍計為已還原 - 請在印表機上核對設定檔',
         kprofilesPrinterMissing: '沒有序號為 {{serial}} 的印表機 - 已略過',
         kprofilesPrinterOffline: '{{printer}}({{serial}})未連線 - 已略過',
         kprofilesUnknownNozzle: '{{serial}} 的噴嘴直徑 {{nozzle}} 不在預期範圍內 - 已原樣傳送',
         kprofilesUnmatched: '{{nozzle}} 的 {{count}} 個設定檔在 {{printer}} 上沒有對應項 - 已新增為新設定檔',
         kprofilesSendFailed: '無法將 {{nozzle}} 的設定檔傳送到 {{printer}}({{serial}})',
+        kprofilesReadFailed: '{{printer}}({{serial}})未回應 {{nozzle}} 的 K 設定檔讀取 - 未還原',
         kprofilesRefused: '{{printer}}({{serial}})拒絕了 {{nozzle}} 的設定檔:{{reason}}',
         kprofilesStepFailed: 'K 值設定檔步驟未能完成 - {{reason}}。在此之前還原的內容仍已儲存。',
       },
@@ -6039,6 +6040,8 @@ export default {
     loadingProfiles: '載入 K 值設定中...',
     printerOffline: '印表機離線',
     printerOfflineDesc: '所選印表機未連線。開啟電源以檢視 K 值設定。',
+    readFailed: '無法讀取 K 設定檔',
+    noAnswerDesc: '印表機未回應 K 設定檔請求。其設定檔可能仍然存在:請重試。',
     noMatchingProfiles: '無匹配的設定',
     noMatchingProfilesDesc: '沒有設定匹配您的搜尋條件',
     noKProfiles: '無 K 值設定',
@@ -7154,6 +7157,7 @@ export default {
     filteringFor: '篩選:{{material}}',
     noKProfile: '無 K 值設定(使用預設值 0.020)',
     noMatchingKProfiles: '未找到匹配的 K 值設定。將使用預設 K=0.020。',
+    kProfilesUnread: '印表機未回應,無法讀取其 K 設定檔。將使用預設值 K=0.020。',
     otherKProfiles: '此印表機上的其他 K 值設定',
     selectFilamentFirst: '請先選擇耗材設定',
     kFromCalibration: 'K={{value}}(來自印表機校準)',

File diff suppressed because it is too large
+ 0 - 0
static/assets/ArchivesPage-BJ2vVvGo.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/CamWallPage-DgRPwaXC.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/CameraPage-BVGNkoZj.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/ConfigureAmsSlotModal-BsMI63Ni.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/ConfigureAmsSlotModal-CbooDTZJ.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/FileManagerPage-DkNuKge0.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/InventoryPage-Bx7z-agm.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/Layout-BfiKv1bK.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/ModelSourcesPage-CekpMszo.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/PhotoGalleryModal-XnIZOi3y.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/PrintersPage-3k66ABR-.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/ProfilesPage-8GDEGaLA.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/ProfilesPage-CFPee9BW.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/SettingsPage-B-uSedft.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/SliceModal-BW3hVqSt.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/SpoolBuddyAmsPage-DOLpK5lm.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/SpoolBuddyInventoryPage-DVi08XQ1.js


File diff suppressed because it is too large
+ 1 - 1
static/assets/SpoolBuddySettingsPage-Ct0FC3gN.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/de-E7ez1oXe.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/es-DCeY0opY.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/fr-DGcwEKjT.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-DNGzxHyh.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/it-HRBtJJfM.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/ja-BSPFM8dm.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/ko-6FBaLiqg.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/locationSensorDefaults-FtgqFuCO.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/nl-PEZg_jnk.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/pt-BR-Bq3BZ7oq.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/ru-DH108Xll.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/spoolCost-kv-UPdK-.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/sv-BWB2ntTK.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/tr-S6MWMoKv.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/uk-BjPP3399.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/zh-CN-KPG8R5hX.js


File diff suppressed because it is too large
+ 0 - 0
static/assets/zh-TW-jw-jlGKe.js


+ 2 - 2
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-DvOZfyJ0.js"></script>
+    <script type="module" crossorigin src="/assets/index-DNGzxHyh.js"></script>
     <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-aKtaBQYM.js">
     <link rel="modulepreload" crossorigin href="/assets/client-Wixdakxn.js">
     <link rel="modulepreload" crossorigin href="/assets/react-dom-9hW1swN1.js">
@@ -122,7 +122,7 @@
     <link rel="modulepreload" crossorigin href="/assets/useColorCatalogVersion-me0iWO3m.js">
     <link rel="modulepreload" crossorigin href="/assets/circle-minus-BWJkPGS-.js">
     <link rel="modulepreload" crossorigin href="/assets/ConnectionDiagnostic-DSIiTSOB.js">
-    <link rel="modulepreload" crossorigin href="/assets/Layout-DEAv6Qce.js">
+    <link rel="modulepreload" crossorigin href="/assets/Layout-BfiKv1bK.js">
     <link rel="stylesheet" crossorigin href="/assets/index-C8pvVtiW.css">
   </head>
   <body>

Some files were not shown because too many files changed in this diff