diff --git a/frontend/src/__tests__/components/GitHubRestoreModal.test.tsx b/frontend/src/__tests__/components/GitHubRestoreModal.test.tsx index 94c97eeb8..86d57782d 100644 --- a/frontend/src/__tests__/components/GitHubRestoreModal.test.tsx +++ b/frontend/src/__tests__/components/GitHubRestoreModal.test.tsx @@ -63,6 +63,17 @@ const mockPreview = { ], }; +// The default fixture has no K-profiles in the commit, which is the one category +// whose row cannot be selected there. +const mockPreviewWithKprofiles = { + ...mockPreview, + categories: mockPreview.categories.map((c) => + c.category === 'kprofiles' + ? { category: 'kprofiles', available: true, item_count: 3, detail: null, detail_code: null, detail_params: {} } + : c + ), +}; + type JsonBody = Record; function mockEndpoints(overrides: { preview?: JsonBody; commits?: JsonBody } = {}) { @@ -330,6 +341,69 @@ describe('GitHubRestoreModal', () => { }); }); + // Overwrite-off says existing entries stay as they are. K-profiles are the one + // category that cannot honour that — writing a slot always replaces the + // calibration on the printer — and the backend's note saying so only arrives + // in the result panel, after the MQTT send. So the disclosure has to be on the + // screen where the promise is made, before the user commits to it. + describe('the K-profile exception to overwrite-off', () => { + beforeEach(() => { + mockEndpoints({ preview: mockPreviewWithKprofiles as unknown as JsonBody }); + }); + + it('appears beside the category as soon as it is selected', async () => { + render(); + + const checkboxes = await waitFor(() => screen.getAllByRole('checkbox') as HTMLInputElement[]); + await userEvent.click(checkboxes[3]); + + await waitFor(() => { + expect(screen.getByText(/K-profiles are the exception/)).toBeInTheDocument(); + }); + + // And it goes once overwrite is on, where nothing is promising otherwise. + await userEvent.click(screen.getByRole('switch')); + expect(screen.queryByText(/K-profiles are the exception/)).not.toBeInTheDocument(); + }); + + it('is part of the confirmation the user actually clicks through', async () => { + render(); + + const checkboxes = await waitFor(() => screen.getAllByRole('checkbox') as HTMLInputElement[]); + await userEvent.click(checkboxes[3]); + await userEvent.click(screen.getByRole('button', { name: /Restore$/ })); + + await waitFor(() => screen.getByText('Restore from backup?')); + expect( + screen.getByText(/existing entries stay as they are\. K-profiles are the exception/) + ).toBeInTheDocument(); + }); + + it('stays out of the confirmation for the categories that do keep the promise', async () => { + render(); + + const checkboxes = await waitFor(() => screen.getAllByRole('checkbox') as HTMLInputElement[]); + await userEvent.click(checkboxes[1]); + await userEvent.click(screen.getByRole('button', { name: /Restore$/ })); + + await waitFor(() => screen.getByText('Restore from backup?')); + expect(screen.getByText(/existing entries stay as they are\.$/)).toBeInTheDocument(); + expect(screen.queryByText(/K-profiles are the exception/)).not.toBeInTheDocument(); + }); + + it('is redundant with overwrite on, so it is not shown there', async () => { + render(); + + const checkboxes = await waitFor(() => screen.getAllByRole('checkbox') as HTMLInputElement[]); + await userEvent.click(checkboxes[3]); + await userEvent.click(screen.getByRole('switch')); + await userEvent.click(screen.getByRole('button', { name: /Restore$/ })); + + await waitFor(() => screen.getByText(/This cannot be undone/)); + expect(screen.queryByText(/K-profiles are the exception/)).not.toBeInTheDocument(); + }); + }); + it('surfaces a preview failure instead of an empty category list', async () => { mockEndpoints({ preview: { diff --git a/frontend/src/components/GitHubRestoreModal.tsx b/frontend/src/components/GitHubRestoreModal.tsx index 3ec798a6c..deee95624 100644 --- a/frontend/src/components/GitHubRestoreModal.tsx +++ b/frontend/src/components/GitHubRestoreModal.tsx @@ -131,6 +131,16 @@ export function GitHubRestoreModal({ onClose }: GitHubRestoreModalProps) { ); const selectedCount = selectedCategories.length; + // Overwrite-off tells the user that existing entries stay as they are, and for + // three of the four categories it keeps that promise. K-profiles cannot: + // _restore_kprofiles takes no overwrite flag, because writing a slot is always + // an overwrite on the printer — resolving the live cali_idx and publishing + // extrusion_cali_set replaces whatever calibration that slot holds. The + // backend does say so, but as a note in the result panel, i.e. after the MQTT + // send has already happened and cannot be taken back. So the one screen that + // explains overwrite-off has to carry the exception too, before the click. + const warnKprofilesOverwrite = !overwriteExisting && selectedCategories.includes('kprofiles'); + const restoreMutation = useMutation({ mutationFn: () => api.restoreFromGitHub({ @@ -440,6 +450,11 @@ export function GitHubRestoreModal({ onClose }: GitHubRestoreModalProps) { ) : null} {info?.detail &&
{info.detail}
} + {category.id === 'kprofiles' && isChecked && warnKprofilesOverwrite && ( +
+ {t('backup.restoreFromGit.kprofilesOverwriteCaveat')} +
+ )} ); @@ -525,7 +540,9 @@ export function GitHubRestoreModal({ onClose }: GitHubRestoreModalProps) { message={ overwriteExisting ? t('backup.restoreFromGit.confirmMessageOverwrite') - : t('backup.restoreFromGit.confirmMessage') + : warnKprofilesOverwrite + ? `${t('backup.restoreFromGit.confirmMessage')} ${t('backup.restoreFromGit.kprofilesOverwriteCaveat')}` + : t('backup.restoreFromGit.confirmMessage') } confirmText={t('backup.restore')} isLoading={isRestoring} diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 5a549730b..a37d42ec3 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -4891,6 +4891,7 @@ export default { confirmTitle: 'Aus Backup wiederherstellen?', confirmMessage: 'Die ausgewählten Kategorien werden aus diesem Commit wiederhergestellt. Fehlende Einträge werden ergänzt, vorhandene bleiben unverändert.', confirmMessageOverwrite: 'Die ausgewählten Kategorien werden aus diesem Commit wiederhergestellt und lokal vorhandene Einträge überschrieben. Dies kann nicht rückgängig gemacht werden.', + kprofilesOverwriteCaveat: 'K-Profile sind die Ausnahme: Das Schreiben eines Slots ersetzt immer die Kalibrierung auf dem Drucker.', tally: '{{restored}} wiederhergestellt, {{skipped}} übersprungen, {{failed}} fehlgeschlagen', reloadHint: 'Bambuddy neu laden, damit die wiederhergestellten Daten überall erscheinen.', failed: 'Wiederherstellung fehlgeschlagen.', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 0f4950a13..80d78270d 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -4934,6 +4934,7 @@ export default { confirmTitle: 'Restore from backup?', confirmMessage: 'The selected categories will be restored from this commit. Missing entries are added; existing entries stay as they are.', confirmMessageOverwrite: 'The selected categories will be restored from this commit, overwriting entries that already exist locally. This cannot be undone.', + kprofilesOverwriteCaveat: 'K-profiles are the exception: writing a slot always replaces the calibration on the printer.', tally: '{{restored}} restored, {{skipped}} skipped, {{failed}} failed', reloadHint: 'Reload Bambuddy so the restored data appears everywhere.', failed: 'Restore failed.', diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts index 6a03eb316..eaaf50667 100644 --- a/frontend/src/i18n/locales/es.ts +++ b/frontend/src/i18n/locales/es.ts @@ -4899,6 +4899,7 @@ export default { confirmTitle: '¿Restaurar desde la copia?', confirmMessage: 'Las categorías seleccionadas se restaurarán desde este commit. Se añaden las entradas que falten y las existentes se mantienen igual.', confirmMessageOverwrite: 'Las categorías seleccionadas se restaurarán desde este commit y se sobrescribirán las entradas que ya existan localmente. Esto no se puede deshacer.', + kprofilesOverwriteCaveat: 'Los perfiles K son la excepción: escribir una ranura siempre reemplaza la calibración en la impresora.', tally: '{{restored}} restaurados, {{skipped}} omitidos, {{failed}} fallidos', reloadHint: 'Recarga Bambuddy para que los datos restaurados aparezcan en todas partes.', failed: 'La restauración ha fallado.', diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index 82d791c21..7a5bfcab7 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -4880,6 +4880,7 @@ export default { confirmTitle: 'Restaurer depuis la sauvegarde ?', confirmMessage: 'Les catégories sélectionnées seront restaurées depuis ce commit. Les entrées manquantes sont ajoutées, les existantes restent inchangées.', confirmMessageOverwrite: 'Les catégories sélectionnées seront restaurées depuis ce commit et les entrées déjà présentes localement seront écrasées. Cette action est irréversible.', + kprofilesOverwriteCaveat: "Les profils K sont l'exception : écrire un emplacement remplace toujours la calibration sur l'imprimante.", tally: '{{restored}} restaurés, {{skipped}} ignorés, {{failed}} en échec', reloadHint: 'Rechargez Bambuddy pour que les données restaurées apparaissent partout.', failed: 'Échec de la restauration.', diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index 7cfc9b578..13f43cc5c 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -4879,6 +4879,7 @@ export default { confirmTitle: 'Ripristinare dal backup?', confirmMessage: 'Le categorie selezionate verranno ripristinate da questo commit. Le voci mancanti vengono aggiunte, quelle esistenti restano invariate.', confirmMessageOverwrite: 'Le categorie selezionate verranno ripristinate da questo commit sovrascrivendo le voci già presenti in locale. Operazione non annullabile.', + kprofilesOverwriteCaveat: "I profili K sono l'eccezione: scrivere uno slot sostituisce sempre la calibrazione sulla stampante.", tally: '{{restored}} ripristinati, {{skipped}} saltati, {{failed}} non riusciti', reloadHint: 'Ricarica Bambuddy per vedere i dati ripristinati in tutte le sezioni.', failed: 'Ripristino non riuscito.', diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 3d1a7d381..ae5c9fe97 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -4891,6 +4891,7 @@ export default { confirmTitle: 'バックアップから復元しますか?', confirmMessage: '選択したカテゴリをこのコミットから復元します。不足しているエントリが追加され、既存のエントリはそのまま残ります。', confirmMessageOverwrite: '選択したカテゴリをこのコミットから復元し、ローカルに既存のエントリを上書きします。この操作は取り消せません。', + kprofilesOverwriteCaveat: 'Kプロファイルは例外です。スロットへの書き込みは、プリンター上のキャリブレーションを常に置き換えます。', tally: '復元 {{restored}} 件、スキップ {{skipped}} 件、失敗 {{failed}} 件', reloadHint: '復元したデータを全体に反映するには Bambuddy を再読み込みしてください。', failed: '復元に失敗しました。', diff --git a/frontend/src/i18n/locales/ko.ts b/frontend/src/i18n/locales/ko.ts index bd5e01d42..c0165c0f9 100644 --- a/frontend/src/i18n/locales/ko.ts +++ b/frontend/src/i18n/locales/ko.ts @@ -4656,6 +4656,7 @@ export default { confirmTitle: '백업에서 복원하시겠습니까?', confirmMessage: '선택한 항목을 이 커밋에서 복원합니다. 없는 항목은 추가되고 기존 항목은 그대로 유지됩니다.', confirmMessageOverwrite: '선택한 항목을 이 커밋에서 복원하고 로컬에 이미 있는 항목을 덮어씁니다. 이 작업은 취소할 수 없습니다.', + kprofilesOverwriteCaveat: 'K 프로파일은 예외입니다. 슬롯에 쓰면 프린터의 캘리브레이션이 항상 교체됩니다.', tally: '복원 {{restored}}개, 건너뜀 {{skipped}}개, 실패 {{failed}}개', reloadHint: '복원된 데이터가 모든 화면에 반영되도록 Bambuddy를 새로 고치세요.', failed: '복원에 실패했습니다.', diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index dd0e9b650..db0802fb9 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/frontend/src/i18n/locales/pt-BR.ts @@ -4879,6 +4879,7 @@ export default { confirmTitle: 'Restaurar do backup?', confirmMessage: 'As categorias selecionadas serão restauradas deste commit. As entradas ausentes são adicionadas e as existentes permanecem como estão.', confirmMessageOverwrite: 'As categorias selecionadas serão restauradas deste commit, sobrescrevendo as entradas que já existem localmente. Não é possível desfazer.', + kprofilesOverwriteCaveat: 'Os perfis K são a exceção: gravar um slot sempre substitui a calibração na impressora.', tally: '{{restored}} restaurados, {{skipped}} ignorados, {{failed}} com falha', reloadHint: 'Recarregue o Bambuddy para que os dados restaurados apareçam em todos os lugares.', failed: 'Falha na restauração.', diff --git a/frontend/src/i18n/locales/ru.ts b/frontend/src/i18n/locales/ru.ts index 88e02e8da..dfa20c448 100644 --- a/frontend/src/i18n/locales/ru.ts +++ b/frontend/src/i18n/locales/ru.ts @@ -4648,6 +4648,7 @@ export default { confirmTitle: 'Восстановить из резервной копии?', confirmMessage: 'Выбранные категории будут восстановлены из этого коммита. Отсутствующие записи будут добавлены, существующие останутся без изменений.', confirmMessageOverwrite: 'Выбранные категории будут восстановлены из этого коммита с перезаписью уже существующих локальных записей. Отменить это действие нельзя.', + kprofilesOverwriteCaveat: 'K-профили — исключение: запись в слот всегда заменяет калибровку на принтере.', tally: 'восстановлено: {{restored}}, пропущено: {{skipped}}, с ошибкой: {{failed}}', reloadHint: 'Перезагрузите Bambuddy, чтобы восстановленные данные отобразились везде.', failed: 'Не удалось выполнить восстановление.', diff --git a/frontend/src/i18n/locales/tr.ts b/frontend/src/i18n/locales/tr.ts index 003334f1c..b0e19a876 100644 --- a/frontend/src/i18n/locales/tr.ts +++ b/frontend/src/i18n/locales/tr.ts @@ -4869,6 +4869,7 @@ export default { confirmTitle: 'Yedekten geri yüklensin mi?', confirmMessage: 'Seçilen kategoriler bu commit\'ten geri yüklenecek. Eksik kayıtlar eklenir, mevcut kayıtlar olduğu gibi kalır.', confirmMessageOverwrite: 'Seçilen kategoriler bu commit\'ten geri yüklenecek ve yerelde bulunan kayıtların üzerine yazılacak. Bu işlem geri alınamaz.', + kprofilesOverwriteCaveat: 'K profilleri istisnadır: bir yuvaya yazmak yazıcıdaki kalibrasyonu her zaman değiştirir.', tally: '{{restored}} geri yüklendi, {{skipped}} atlandı, {{failed}} başarısız', reloadHint: 'Geri yüklenen verilerin her yerde görünmesi için Bambuddy\'yi yeniden yükleyin.', failed: 'Geri yükleme başarısız oldu.', diff --git a/frontend/src/i18n/locales/uk.ts b/frontend/src/i18n/locales/uk.ts index 06eff5116..e1eb64b66 100644 --- a/frontend/src/i18n/locales/uk.ts +++ b/frontend/src/i18n/locales/uk.ts @@ -4934,6 +4934,7 @@ export default { confirmTitle: "Відновити з резервної копії?", confirmMessage: "Вибрані категорії буде відновлено з цього коміту. Відсутні записи буде додано, наявні залишаться без змін.", confirmMessageOverwrite: "Вибрані категорії буде відновлено з цього коміту з перезаписом записів, які вже існують локально. Цю дію не можна скасувати.", + kprofilesOverwriteCaveat: 'K-профілі — виняток: запис у слот завжди замінює калібрування на принтері.', tally: "відновлено: {{restored}}, пропущено: {{skipped}}, з помилкою: {{failed}}", reloadHint: "Перезавантажте Bambuddy, щоб відновлені дані відобразилися всюди.", failed: "Не вдалося виконати відновлення.", diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 1a792108e..2bb77316a 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -4879,6 +4879,7 @@ export default { confirmTitle: '要从备份恢复吗?', confirmMessage: '将从此提交恢复所选类别。缺失的条目会被添加,已有条目保持不变。', confirmMessageOverwrite: '将从此提交恢复所选类别,并覆盖本地已存在的条目。此操作无法撤销。', + kprofilesOverwriteCaveat: 'K 值配置是例外:写入插槽总会替换打印机上的校准数据。', tally: '已恢复 {{restored}} 项,跳过 {{skipped}} 项,失败 {{failed}} 项', reloadHint: '请重新加载 Bambuddy,以便恢复的数据在各处生效。', failed: '恢复失败。', diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts index e7dd3c2ef..01f9ab04e 100644 --- a/frontend/src/i18n/locales/zh-TW.ts +++ b/frontend/src/i18n/locales/zh-TW.ts @@ -4879,6 +4879,7 @@ export default { confirmTitle: '要從備份還原嗎?', confirmMessage: '將從此提交還原所選類別。缺少的項目會被新增,既有項目保持不變。', confirmMessageOverwrite: '將從此提交還原所選類別,並覆寫本機已存在的項目。此操作無法復原。', + kprofilesOverwriteCaveat: 'K 值設定檔是例外:寫入插槽一定會取代印表機上的校準資料。', tally: '已還原 {{restored}} 筆、略過 {{skipped}} 筆、失敗 {{failed}} 筆', reloadHint: '請重新載入 Bambuddy,讓還原的資料在各處生效。', failed: '還原失敗。',