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

fix(backup): read the printer's verdict before counting a K-profile restored (#2656)

`18938a10` on `dev` changed `set_kprofiles_batch` from returning a `bool` to
returning the sequence_id it published the command under, and moved the
verdict to a separate `await client.await_cali_ack(seq)` returning
`(ok, detail)`. Every caller in `api/routes/kprofiles.py` was updated with it.
`_restore_kprofiles` was not — it still did `sent = client.set_kprofiles_batch(...)`
and branched on `if sent:`.

A sequence_id string is truthy, so that compiled, passed, and silently made
the restore the one path left in the codebase that reports a refused
K-profile write as saved — exactly the defect `18938a10` closed everywhere
else.

Keep the sequence_id, await the ack per batch, and route an explicit refusal
into `tally.failed` with a new `kprofilesRefused` note carrying the printer's
own `reason`. Reusing `kprofilesSendFailed` would have been wrong: the
command was sent, and the printer answered.

Silence still counts restored. That is `await_cali_ack`'s own contract and
the maintainer's rule — no answer is not evidence of refusal, and firmware
predating the ack never answers. An exception reading the ack degrades the
same way rather than inventing a failure out of a write that most likely
landed.

`kprofilesAckUnreliable` is reworded to match: a refusal is now believed, so
the caveat narrows to what is genuinely left uncertain. The ack is only worth
reading at all because `18938a10` also changed the payload's `tray_id` from
`-1` to `0` — single-nozzle firmware answered `result: "fail"` to `-1` on
writes that demonstrably applied. This restore builds no `tray_id` of its
own, so it inherits that fix for free.

Tests: 4 regression (the ack is awaited for the returned sequence_id; a
refused batch counts failed and surfaces the printer's reason; one refused
nozzle does not condemn the other; the reworded caveat) + 3 controls (a
silent printer still counts restored; an unreadable ack does not fail the
batch; `None` keeps the existing send-failed path and awaits nothing). All
four confirmed failing against the pre-fix service.
jmoore-skild 1 месяц назад
Родитель
Сommit
4ee9c0eecb

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

@@ -1473,9 +1473,12 @@ class GitHubRestoreService:
         # the profile occupying a slot, so writing is always an overwrite on the
         # printer side.
         tally.note("kprofilesAlwaysOverwrite", "K-profiles always overwrite the matching slot on the printer")
+        # A refusal is now believed and counted failed (#2718 made the ack worth
+        # reading), but silence still counts restored, so the caveat stands —
+        # narrowed to what is actually left uncertain.
         tally.note(
             "kprofilesAckUnreliable",
-            "The printer's acknowledgement is not reliable — verify the profiles on the printer",
+            "A printer that does not answer still counts as restored — verify the profiles on the printer",
         )
 
         for serial, entries in sorted(by_serial.items()):
@@ -1565,14 +1568,12 @@ class GitHubRestoreService:
                     )
 
                 try:
-                    sent = client.set_kprofiles_batch(profile_dicts, nozzle)
+                    seq = client.set_kprofiles_batch(profile_dicts, nozzle)
                 except Exception as e:
                     logger.warning("K-profile restore failed for %s nozzle %s: %s", serial, nozzle, e)
-                    sent = False
+                    seq = None
 
-                if sent:
-                    tally.restored += len(profile_dicts)
-                else:
+                if not seq:
                     tally.failed += len(profile_dicts)
                     tally.note(
                         "kprofilesSendFailed",
@@ -1581,6 +1582,44 @@ class GitHubRestoreService:
                         printer=printer.name,
                         serial=serial,
                     )
+                    continue
+
+                # What came back is the sequence_id the command was published
+                # under, not a verdict (#2718) — a truthy string only means the
+                # command left the building. The printer answers separately, and
+                # every other caller of this API now reads that answer; without
+                # this the restore would be the one path left that reports a
+                # refused write as saved.
+                ok, detail = await self._kprofile_ack(client, seq, serial, nozzle)
+                if ok:
+                    tally.restored += len(profile_dicts)
+                else:
+                    tally.failed += len(profile_dicts)
+                    tally.note(
+                        "kprofilesRefused",
+                        f"{printer.name} ({serial}) refused the {nozzle} profiles: {detail}",
+                        nozzle=nozzle,
+                        printer=printer.name,
+                        serial=serial,
+                        reason=detail,
+                    )
+
+    @staticmethod
+    async def _kprofile_ack(client, seq: str, serial: str, nozzle: str) -> tuple[bool, str]:
+        """Read the printer's verdict on one batch write.
+
+        ``await_cali_ack`` already treats silence as success — no answer is not
+        evidence of refusal, and firmware that predates the ack never answers at
+        all. An exception reading it is the same situation one layer up, so it
+        degrades the same way rather than turning a write that most likely
+        landed into a reported failure.
+        """
+        try:
+            ok, detail = await client.await_cali_ack(seq)
+            return bool(ok), str(detail or "")
+        except Exception as e:
+            logger.warning("Could not read the K-profile ack for %s nozzle %s: %s", serial, nozzle, e)
+            return True, ""
 
     @staticmethod
     async def _current_kprofile_index(client, nozzle: str, serial: str) -> list:

+ 104 - 12
backend/tests/unit/test_github_restore.py

@@ -1236,10 +1236,17 @@ class TestRestoreKprofiles:
         """One profile as the printer currently reports it."""
         return SimpleNamespace(slot_id=slot_id, filament_id=filament_id, name=name, setting_id=setting_id)
 
-    def _client(self, live=None, sent=True):
+    def _client(self, live=None, sent="7", ack=(True, "")):
+        """A connected printer client.
+
+        ``set_kprofiles_batch`` returns the sequence_id it published under, not
+        a success flag (#2718), and the verdict arrives separately from
+        ``await_cali_ack`` as ``(ok, detail)``.
+        """
         client = MagicMock()
         client.state.connected = True
         client.set_kprofiles_batch = MagicMock(return_value=sent)
+        client.await_cali_ack = AsyncMock(return_value=ack)
         client.get_kprofiles = AsyncMock(return_value=list(live or []))
         return client
 
@@ -1266,9 +1273,7 @@ class TestRestoreKprofiles:
     @pytest.mark.asyncio
     async def test_sends_batch_to_connected_printer(self, db_session, printer_factory):
         printer = await printer_factory(serial_number="00M09A123456789")
-        client = MagicMock()
-        client.state.connected = True
-        client.set_kprofiles_batch = MagicMock(return_value=True)
+        client = self._client()
         tally = _CategoryTally()
 
         with patch("backend.app.services.github_restore.printer_manager") as manager:
@@ -1293,9 +1298,10 @@ class TestRestoreKprofiles:
             manager.get_client = MagicMock(return_value=client)
             await _service()._restore_kprofiles(db_session, self._payload(), tally)
 
-        # The printer does answer extrusion_cali_set, but it reports "fail" on
-        # writes that land, so the note must not promise either way.
+        # 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 not any("without acknowledgement" in note for note in _messages(tally))
         assert any("always overwrite" in note for note in _messages(tally))
 
@@ -1560,10 +1566,10 @@ class TestRestoreKprofiles:
 
     @pytest.mark.asyncio
     async def test_publish_failure_counts_as_failed(self, db_session, printer_factory):
+        # None is what set_kprofiles_batch returns when it could not publish —
+        # a disconnected client. There is no ack to wait for in that case.
         await printer_factory(serial_number="00M09A123456789")
-        client = MagicMock()
-        client.state.connected = True
-        client.set_kprofiles_batch = MagicMock(return_value=False)
+        client = self._client(sent=None)
         tally = _CategoryTally()
 
         with patch("backend.app.services.github_restore.printer_manager") as manager:
@@ -1572,6 +1578,9 @@ class TestRestoreKprofiles:
 
         assert tally.failed == 1
         assert tally.restored == 0
+        assert "kprofilesSendFailed" in _codes(tally)
+        assert "kprofilesRefused" not in _codes(tally), "nothing was sent, so the printer refused nothing"
+        client.await_cali_ack.assert_not_awaited()
 
     @pytest.mark.asyncio
     async def test_publish_exception_is_contained(self, db_session, printer_factory):
@@ -1587,13 +1596,96 @@ class TestRestoreKprofiles:
 
         assert tally.failed == 1
 
+    # --- the printer's verdict decides the tally, not the publish ------------
+    #
+    # #2718 changed set_kprofiles_batch from returning a bool to returning the
+    # sequence_id it published under. A sequence_id string is truthy, so a
+    # restore that branches on the return value alone reports every refused
+    # write as saved — the defect that fix closed in every other caller.
+
+    @pytest.mark.asyncio
+    async def test_awaits_the_ack_for_the_sequence_id_it_was_given(self, db_session, printer_factory):
+        await printer_factory(serial_number="00M09A123456789")
+        client = self._client(sent="4211")
+        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.await_cali_ack.assert_awaited_once_with("4211")
+        assert tally.restored == 1
+
+    @pytest.mark.asyncio
+    async def test_a_refused_batch_counts_failed_not_restored(self, db_session, printer_factory):
+        await printer_factory(serial_number="00M09A123456789", name="Shelf Printer")
+        client = self._client(ack=(False, "invalid tray_id"))
+        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)
+
+        assert tally.restored == 0
+        assert tally.failed == 1
+        assert "kprofilesRefused" in _codes(tally)
+        assert "kprofilesSendFailed" not in _codes(tally), "it was sent — the printer answered no"
+        note = next(n for n in tally.notes if n["code"] == "kprofilesRefused")
+        assert note["params"]["reason"] == "invalid tray_id", "the printer's own reason has to survive"
+        assert "Shelf Printer" in note["message"] and "invalid tray_id" in note["message"]
+
+    @pytest.mark.asyncio
+    async def test_a_silent_printer_still_counts_restored(self, db_session, printer_factory):
+        # maziggy's rule, and await_cali_ack's own contract: no answer is not
+        # evidence of refusal. Firmware that predates the ack never answers.
+        await printer_factory(serial_number="00M09A123456789")
+        client = self._client(ack=(True, "no acknowledgement from printer"))
+        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)
+
+        assert tally.restored == 1
+        assert tally.failed == 0
+        assert "kprofilesRefused" not in _codes(tally)
+
+    @pytest.mark.asyncio
+    async def test_an_unreadable_ack_does_not_fail_the_batch(self, db_session, printer_factory):
+        # Same situation one layer up: the write most likely landed, so this
+        # degrades the way a timeout does rather than inventing a failure.
+        await printer_factory(serial_number="00M09A123456789")
+        client = self._client()
+        client.await_cali_ack = AsyncMock(side_effect=RuntimeError("mqtt down"))
+        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)
+
+        assert tally.restored == 1
+        assert tally.failed == 0
+
+    @pytest.mark.asyncio
+    async def test_one_refused_nozzle_does_not_condemn_the_other(self, db_session, printer_factory):
+        await printer_factory(serial_number="00M09A123456789")
+        payload = {**self._payload(nozzle="0.4"), **self._payload(nozzle="0.8")}
+        client = self._client()
+        client.await_cali_ack = AsyncMock(side_effect=[(False, "busy"), (True, "")])
+        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, payload, tally)
+
+        assert tally.restored == 1
+        assert tally.failed == 1
+
     @pytest.mark.asyncio
     async def test_each_nozzle_is_sent_separately(self, db_session, printer_factory):
         await printer_factory(serial_number="00M09A123456789")
         payload = {**self._payload(nozzle="0.4"), **self._payload(nozzle="0.8")}
-        client = MagicMock()
-        client.state.connected = True
-        client.set_kprofiles_batch = MagicMock(return_value=True)
+        client = self._client()
         tally = _CategoryTally()
 
         with patch("backend.app.services.github_restore.printer_manager") as manager:

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

@@ -4922,12 +4922,13 @@ export default {
         settingsCompanionSkipped: '{{keys}} bleiben ausgeschaltet - die jeweils benötigten Zugangsdaten lassen sich nicht aus einem Backup wiederherstellen und sind auf dieser Instanz nicht hinterlegt, ein Einschalten würde die Integration also ohne Authentifizierung lassen',
         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: 'Die Bestätigung des Druckers ist nicht zuverlässig - überprüfen Sie die Profile am Drucker',
+        kprofilesAckUnreliable: 'Ein Drucker, der nicht antwortet, 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',
+        kprofilesRefused: '{{printer}} ({{serial}}) hat die {{nozzle}}-Profile abgelehnt: {{reason}}',
       },
     },
 

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

@@ -4970,12 +4970,13 @@ export default {
         settingsCompanionSkipped: '{{keys}} left switched off - the credential each one needs cannot be restored from a backup and this instance has none stored, so switching them on would leave the integration unauthenticated',
         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: 'The printer\'s acknowledgement is not reliable - verify the profiles on the printer',
+        kprofilesAckUnreliable: 'A printer that does not answer 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}})',
+        kprofilesRefused: '{{printer}} ({{serial}}) refused the {{nozzle}} profiles: {{reason}}',
       },
     },
 

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

@@ -4930,12 +4930,13 @@ export default {
         settingsCompanionSkipped: '{{keys}} se han dejado desactivados - la credencial que cada uno necesita no puede restaurarse desde una copia y esta instancia no tiene ninguna guardada, así que activarlos dejaría la integración sin autenticación',
         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: 'La confirmación de la impresora no es fiable - verifica los perfiles en la impresora',
+        kprofilesAckUnreliable: 'Una impresora que no responde 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}})',
+        kprofilesRefused: '{{printer}} ({{serial}}) rechazó los perfiles de {{nozzle}}: {{reason}}',
       },
     },
 

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

@@ -4911,12 +4911,13 @@ export default {
         settingsCompanionSkipped: '{{keys}} laissés désactivés - l\'identifiant dont chacun a besoin ne peut pas être restauré depuis une sauvegarde et cette instance n\'en a aucun enregistré ; les activer laisserait donc l\'intégration sans authentification',
         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: 'L\'accusé de réception de l\'imprimante n\'est pas fiable - vérifiez les profils 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',
         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}})',
+        kprofilesRefused: '{{printer}} ({{serial}}) a refusé les profils {{nozzle}} : {{reason}}',
       },
     },
 

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

@@ -4910,12 +4910,13 @@ export default {
         settingsCompanionSkipped: '{{keys}} lasciati disattivati - la credenziale richiesta da ciascuno non può essere ripristinata da un backup e questa istanza non ne ha nessuna salvata, quindi attivarli lascerebbe l\'integrazione senza autenticazione',
         settingsMqttRelayFailed: 'Impostazioni MQTT ripristinate, ma il relay non è stato riconnesso - riavvia Bambuddy',
         kprofilesAlwaysOverwrite: 'I profili K sovrascrivono sempre lo slot corrispondente sulla stampante',
-        kprofilesAckUnreliable: 'La conferma della stampante non è affidabile - verifica i profili sulla stampante',
+        kprofilesAckUnreliable: 'Una stampante che non risponde 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}})',
+        kprofilesRefused: '{{printer}} ({{serial}}) ha rifiutato i profili {{nozzle}}: {{reason}}',
       },
     },
 

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

@@ -4922,12 +4922,13 @@ export default {
         settingsCompanionSkipped: '{{keys}} はオフのままにしました - 各項目に必要な認証情報はバックアップから復元できず、このインスタンスにも保存されていないため、オンにすると連携が未認証のままになります',
         settingsMqttRelayFailed: 'MQTT 設定を復元しましたが、リレーを再接続できませんでした - Bambuddy を再起動してください',
         kprofilesAlwaysOverwrite: 'K プロファイルは常にプリンター側の該当スロットを上書きします',
-        kprofilesAckUnreliable: 'プリンターの応答は信頼できません - プロファイルはプリンター側で確認してください',
+        kprofilesAckUnreliable: '応答しないプリンターも復元済みとして数えます - プロファイルはプリンター側で確認してください',
         kprofilesPrinterMissing: 'シリアル {{serial}} のプリンターがありません - スキップしました',
         kprofilesPrinterOffline: '{{printer}} ({{serial}}) は接続されていません - スキップしました',
         kprofilesUnknownNozzle: '{{serial}} に想定外のノズル径 {{nozzle}} - そのまま送信しました',
         kprofilesUnmatched: '{{nozzle}} 用のプロファイル {{count}} 件は {{printer}} に該当がありませんでした - 新規プロファイルとして追加しました',
         kprofilesSendFailed: '{{nozzle}} のプロファイルを {{printer}} ({{serial}}) に送信できませんでした',
+        kprofilesRefused: '{{printer}} ({{serial}}) が {{nozzle}} のプロファイルを拒否しました: {{reason}}',
       },
     },
 

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

@@ -4687,12 +4687,13 @@ export default {
         settingsCompanionSkipped: '{{keys}}을(를) 꺼진 상태로 두었습니다 - 각 항목에 필요한 자격 증명은 백업에서 복원할 수 없고 이 인스턴스에도 저장되어 있지 않아, 켜면 연동이 인증 없이 열립니다',
         settingsMqttRelayFailed: 'MQTT 설정을 복원했지만 릴레이를 다시 연결하지 못했습니다 - Bambuddy를 재시작하세요',
         kprofilesAlwaysOverwrite: 'K 프로파일은 항상 프린터의 해당 슬롯을 덮어씁니다',
-        kprofilesAckUnreliable: '프린터의 응답은 신뢰할 수 없습니다 - 프린터에서 프로파일을 확인하세요',
+        kprofilesAckUnreliable: '응답하지 않는 프린터도 복원됨으로 집계됩니다 - 프린터에서 프로파일을 확인하세요',
         kprofilesPrinterMissing: '시리얼 {{serial}}인 프린터가 없습니다 - 건너뛰었습니다',
         kprofilesPrinterOffline: '{{printer}}({{serial}})이(가) 연결되어 있지 않습니다 - 건너뛰었습니다',
         kprofilesUnknownNozzle: '{{serial}}의 예상치 못한 노즐 직경 {{nozzle}} - 그대로 전송했습니다',
         kprofilesUnmatched: '{{nozzle}}용 프로파일 {{count}}개가 {{printer}}에 대응 항목이 없습니다 - 새 프로파일로 추가했습니다',
         kprofilesSendFailed: '{{nozzle}} 프로파일을 {{printer}}({{serial}})에 보내지 못했습니다',
+        kprofilesRefused: '{{printer}}({{serial}})이(가) {{nozzle}} 프로파일을 거부했습니다: {{reason}}',
       },
     },
     history: '기록',

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

@@ -4910,12 +4910,13 @@ export default {
         settingsCompanionSkipped: '{{keys}} ficaram desligados - a credencial que cada um precisa não pode ser restaurada de um backup e esta instância não tem nenhuma armazenada, então ligá-los deixaria a integração sem autenticação',
         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: 'A confirmação da impressora não é confiável - verifique os perfis na impressora',
+        kprofilesAckUnreliable: 'Uma impressora que não responde 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}})',
+        kprofilesRefused: '{{printer}} ({{serial}}) recusou os perfis de {{nozzle}}: {{reason}}',
       },
     },
 

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

@@ -4679,12 +4679,13 @@ export default {
         settingsCompanionSkipped: '{{keys}} оставлены выключенными - нужные им учётные данные нельзя восстановить из резервной копии, и в этом экземпляре они не сохранены, поэтому включение оставило бы интеграцию без аутентификации',
         settingsMqttRelayFailed: 'Настройки MQTT восстановлены, но переподключить реле не удалось - перезапустите Bambuddy',
         kprofilesAlwaysOverwrite: 'K-профили всегда перезаписывают соответствующий слот на принтере',
-        kprofilesAckUnreliable: 'Подтверждение принтера ненадёжно - проверьте профили на принтере',
+        kprofilesAckUnreliable: 'Принтер, который не отвечает, всё равно считается восстановленным - проверьте профили на принтере',
         kprofilesPrinterMissing: 'Нет принтера с серийным номером {{serial}} - пропущено',
         kprofilesPrinterOffline: '{{printer}} ({{serial}}) не подключён - пропущено',
         kprofilesUnknownNozzle: 'Неожиданный диаметр сопла {{nozzle}} для {{serial}} - отправлено как есть',
         kprofilesUnmatched: 'Профилей для {{nozzle}} без соответствия на {{printer}}: {{count}} - добавлены как новые профили',
         kprofilesSendFailed: 'Не удалось отправить профили {{nozzle}} на {{printer}} ({{serial}})',
+        kprofilesRefused: '{{printer}} ({{serial}}) отклонил профили {{nozzle}}: {{reason}}',
       },
     },
     history: "История",

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

@@ -4900,12 +4900,13 @@ export default {
         settingsCompanionSkipped: '{{keys}} kapalı bırakıldı - her birinin ihtiyaç duyduğu kimlik bilgisi bir yedekten geri yüklenemez ve bu örnekte kayıtlı değil, dolayısıyla açmak entegrasyonu kimlik doğrulamasız bırakırdı',
         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: 'Yazıcının onayı güvenilir değil - profilleri yazıcıda doğrulayın',
+        kprofilesAckUnreliable: 'Yanıt vermeyen 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',
+        kprofilesRefused: '{{printer}} ({{serial}}) {{nozzle}} profillerini reddetti: {{reason}}',
       },
     },
 

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

@@ -4965,12 +4965,13 @@ export default {
         settingsCompanionSkipped: "{{keys}} залишено вимкненими - потрібні їм облікові дані не можна відновити з резервної копії, і в цьому екземплярі вони не збережені, тож увімкнення залишило б інтеграцію без автентифікації",
         settingsMqttRelayFailed: "Налаштування MQTT відновлено, але реле не вдалося перепідключити - перезапустіть Bambuddy",
         kprofilesAlwaysOverwrite: "K-профілі завжди перезаписують відповідний слот на принтері",
-        kprofilesAckUnreliable: "Підтвердження принтера ненадійне - перевірте профілі на принтері",
+        kprofilesAckUnreliable: "Принтер, який не відповідає, усе одно вважається відновленим - перевірте профілі на принтері",
         kprofilesPrinterMissing: "Немає принтера із серійним номером {{serial}} - пропущено",
         kprofilesPrinterOffline: "{{printer}} ({{serial}}) не підключено - пропущено",
         kprofilesUnknownNozzle: "Неочікуваний діаметр сопла {{nozzle}} для {{serial}} - надіслано як є",
         kprofilesUnmatched: "Профілів для {{nozzle}} без відповідника на {{printer}}: {{count}} - додано як нові профілі",
         kprofilesSendFailed: "Не вдалося надіслати профілі {{nozzle}} на {{printer}} ({{serial}})",
+        kprofilesRefused: "{{printer}} ({{serial}}) відхилив профілі {{nozzle}}: {{reason}}",
       },
     },
 

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

@@ -4910,12 +4910,13 @@ export default {
         settingsCompanionSkipped: '{{keys}} 保持关闭 - 它们各自所需的凭据无法从备份恢复,本实例也没有存储,开启会让集成处于未认证状态',
         settingsMqttRelayFailed: 'MQTT 设置已恢复,但中继无法重新连接 - 请重启 Bambuddy',
         kprofilesAlwaysOverwrite: 'K 值配置总是覆盖打印机上对应的槽位',
-        kprofilesAckUnreliable: '打印机的确认信息不可靠 - 请在打印机上核对配置',
+        kprofilesAckUnreliable: '打印机不回应时仍计为已恢复 - 请在打印机上核对配置',
         kprofilesPrinterMissing: '没有序列号为 {{serial}} 的打印机 - 已跳过',
         kprofilesPrinterOffline: '{{printer}}({{serial}})未连接 - 已跳过',
         kprofilesUnknownNozzle: '{{serial}} 的喷嘴直径 {{nozzle}} 不在预期范围内 - 已原样发送',
         kprofilesUnmatched: '{{nozzle}} 的 {{count}} 个配置在 {{printer}} 上没有对应项 - 已作为新配置添加',
         kprofilesSendFailed: '无法将 {{nozzle}} 的配置发送到 {{printer}}({{serial}})',
+        kprofilesRefused: '{{printer}}({{serial}})拒绝了 {{nozzle}} 的配置:{{reason}}',
       },
     },
 

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

@@ -4910,12 +4910,13 @@ export default {
         settingsCompanionSkipped: '{{keys}} 維持關閉 - 它們各自所需的憑證無法從備份還原,本執行個體也沒有儲存,開啟會讓整合處於未驗證狀態',
         settingsMqttRelayFailed: 'MQTT 設定已還原,但中繼無法重新連線 - 請重新啟動 Bambuddy',
         kprofilesAlwaysOverwrite: 'K 值設定檔一律覆寫印表機上對應的插槽',
-        kprofilesAckUnreliable: '印表機的確認訊息不可靠 - 請在印表機上核對設定檔',
+        kprofilesAckUnreliable: '印表機未回應時仍計為已還原 - 請在印表機上核對設定檔',
         kprofilesPrinterMissing: '沒有序號為 {{serial}} 的印表機 - 已略過',
         kprofilesPrinterOffline: '{{printer}}({{serial}})未連線 - 已略過',
         kprofilesUnknownNozzle: '{{serial}} 的噴嘴直徑 {{nozzle}} 不在預期範圍內 - 已原樣傳送',
         kprofilesUnmatched: '{{nozzle}} 的 {{count}} 個設定檔在 {{printer}} 上沒有對應項 - 已新增為新設定檔',
         kprofilesSendFailed: '無法將 {{nozzle}} 的設定檔傳送到 {{printer}}({{serial}})',
+        kprofilesRefused: '{{printer}}({{serial}})拒絕了 {{nozzle}} 的設定檔:{{reason}}',
       },
     },