diff --git a/backend/app/services/github_restore.py b/backend/app/services/github_restore.py index 179a3923c..97b0ae5c0 100644 --- a/backend/app/services/github_restore.py +++ b/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: diff --git a/backend/tests/unit/test_github_restore.py b/backend/tests/unit/test_github_restore.py index 80de3fa6c..52fd8e2fe 100644 --- a/backend/tests/unit/test_github_restore.py +++ b/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: diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index df92a495f..27ea22e55 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/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}}', }, }, diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index ebcc41dba..5aa4f4294 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/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}}', }, }, diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts index 4471aa16e..7bed56c7a 100644 --- a/frontend/src/i18n/locales/es.ts +++ b/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}}', }, }, diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index 823d4dd1b..2404bbbff 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/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}}', }, }, diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index b5c3351d6..8c63f8d90 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/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}}', }, }, diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 5460d253d..047a06965 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/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}}', }, }, diff --git a/frontend/src/i18n/locales/ko.ts b/frontend/src/i18n/locales/ko.ts index c18495031..dafa9c679 100644 --- a/frontend/src/i18n/locales/ko.ts +++ b/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: '기록', diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index 366c9d0e0..201f380c8 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/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}}', }, }, diff --git a/frontend/src/i18n/locales/ru.ts b/frontend/src/i18n/locales/ru.ts index 220136ac2..e6302cfe5 100644 --- a/frontend/src/i18n/locales/ru.ts +++ b/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: "История", diff --git a/frontend/src/i18n/locales/tr.ts b/frontend/src/i18n/locales/tr.ts index 17d944565..530f96dfa 100644 --- a/frontend/src/i18n/locales/tr.ts +++ b/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}}', }, }, diff --git a/frontend/src/i18n/locales/uk.ts b/frontend/src/i18n/locales/uk.ts index 4dc689506..4f51f0bad 100644 --- a/frontend/src/i18n/locales/uk.ts +++ b/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}}", }, }, diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 38a8595fa..8bc84d234 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/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}}', }, }, diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts index c506e7501..552698d94 100644 --- a/frontend/src/i18n/locales/zh-TW.ts +++ b/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}}', }, },