mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
fix(backup): tell a restore apart from a backup in Backup History (#2656)
A restore writes a `github_backup_logs` row too — same table, same status values, and it already carried `trigger: 'restore'`, which the API already returned. The history table rendered date / status / commit only, so the row read as a successful backup dated now while "Last backup" said something else: `last_backup_at` is only stamped by an actual backup, and the two disagreeing is alarming with nothing on screen to explain it. Adds the Type column the trigger was always there to fill. Unknown values fall back to the raw string rather than rendering blank, matching the `backup.pathCheck.*` lookup a few hundred lines up — a trigger kind added later shows up as itself instead of vanishing. Backend unchanged: it has recorded this correctly since the restore path was written. Three tests, all three failing without the column. 13 locales in parity at 5776 leaves — pt-BR takes "Backup manual" rather than the parenthesised form because "Backup (manual)" is identical to en, which the parity check counts as untranslated. Bundle rebuilt: `index-DhOfNgMz.js` → `index-CadgB7UN.js`. It also picks up the `archivesOwnerUnknown` leaves from the previous commit, which changed i18n without rebuilding.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Backup History must distinguish a restore from a backup (#2656).
|
||||
*
|
||||
* A restore writes a `github_backup_logs` row too — same table, same statuses,
|
||||
* `trigger: 'restore'`. The table rendered date / status / commit only, so the
|
||||
* row read as a successful backup dated now, while "Last backup" said something
|
||||
* else entirely. The column below is the only thing telling the two apart.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { screen, within } from '@testing-library/react';
|
||||
import { render } from '../utils';
|
||||
import { GitHubBackupSettings } from '../../components/GitHubBackupSettings';
|
||||
import { api } from '../../api/client';
|
||||
|
||||
vi.mock('../../api/client', () => ({
|
||||
api: {
|
||||
getGitHubBackupConfig: vi.fn().mockResolvedValue({
|
||||
id: 1,
|
||||
repository_url: 'https://github.com/someone/backup',
|
||||
enabled: true,
|
||||
}),
|
||||
getGitHubBackupStatus: vi.fn().mockResolvedValue({ is_running: false, configured: true, enabled: true }),
|
||||
getGitHubBackupLogs: vi.fn(),
|
||||
getCloudStatus: vi.fn().mockResolvedValue({ is_authenticated: false }),
|
||||
getPrinters: vi.fn().mockResolvedValue([]),
|
||||
getPrinterStatus: vi.fn().mockResolvedValue({ connected: false }),
|
||||
getSettings: vi.fn().mockResolvedValue({}),
|
||||
updateSettings: vi.fn().mockResolvedValue({}),
|
||||
getLocalBackups: vi.fn().mockResolvedValue([]),
|
||||
getLocalBackupStatus: vi.fn().mockResolvedValue({
|
||||
enabled: false,
|
||||
is_running: false,
|
||||
last_backup_at: null,
|
||||
last_status: null,
|
||||
last_message: null,
|
||||
next_run: null,
|
||||
}),
|
||||
checkLocalBackupPath: vi.fn().mockResolvedValue({ writable: true, path: '/data', code: 'ok' }),
|
||||
},
|
||||
}));
|
||||
|
||||
const log = (id: number, trigger: string) => ({
|
||||
id,
|
||||
config_id: 1,
|
||||
started_at: '2026-08-02T09:00:00',
|
||||
completed_at: '2026-08-02T09:00:05',
|
||||
status: 'success',
|
||||
trigger,
|
||||
commit_sha: null,
|
||||
files_changed: 0,
|
||||
error_message: null,
|
||||
});
|
||||
|
||||
const historyRows = async () => {
|
||||
const table = (await screen.findByText('History')).closest('div[id="card-backup-history"]');
|
||||
return within(table as HTMLElement).getAllByRole('row').slice(1); // drop the header
|
||||
};
|
||||
|
||||
describe('GitHubBackupSettings — backup history', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('labels a restore as a restore, not a successful backup', async () => {
|
||||
vi.mocked(api.getGitHubBackupLogs).mockResolvedValue([log(1, 'restore')]);
|
||||
|
||||
render(<GitHubBackupSettings />);
|
||||
|
||||
const [row] = await historyRows();
|
||||
expect(within(row).getByText('Restore')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('tells the three trigger kinds apart in one history', async () => {
|
||||
vi.mocked(api.getGitHubBackupLogs).mockResolvedValue([
|
||||
log(1, 'restore'),
|
||||
log(2, 'scheduled'),
|
||||
log(3, 'manual'),
|
||||
]);
|
||||
|
||||
render(<GitHubBackupSettings />);
|
||||
|
||||
const rows = await historyRows();
|
||||
expect(within(rows[0]).getByText('Restore')).toBeInTheDocument();
|
||||
expect(within(rows[1]).getByText('Backup (scheduled)')).toBeInTheDocument();
|
||||
expect(within(rows[2]).getByText('Backup (manual)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to the raw trigger rather than blanking an unknown one', async () => {
|
||||
vi.mocked(api.getGitHubBackupLogs).mockResolvedValue([log(1, 'something-new')]);
|
||||
|
||||
render(<GitHubBackupSettings />);
|
||||
|
||||
const [row] = await historyRows();
|
||||
expect(within(row).getByText('something-new')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1032,6 +1032,7 @@ export function GitHubBackupSettings() {
|
||||
<thead>
|
||||
<tr className="text-bambu-gray border-b border-bambu-dark-tertiary">
|
||||
<th className="text-left py-2 px-2">{t('backup.date')}</th>
|
||||
<th className="text-left py-2 px-2">{t('backup.trigger')}</th>
|
||||
<th className="text-left py-2 px-2">{t('backup.status')}</th>
|
||||
<th className="text-left py-2 px-2">{t('backup.commit')}</th>
|
||||
</tr>
|
||||
@@ -1040,6 +1041,12 @@ export function GitHubBackupSettings() {
|
||||
{logs.slice(0, 10).map((log) => (
|
||||
<tr key={log.id} className="border-b border-bambu-dark-tertiary/50 hover:bg-bambu-dark-secondary">
|
||||
<td className="py-2 px-2 text-white">{formatDateTime(log.started_at)}</td>
|
||||
{/* A restore writes a log row too, and without this it
|
||||
was indistinguishable from a backup: a successful
|
||||
run dated now, while Last backup said otherwise. */}
|
||||
<td className="py-2 px-2 text-bambu-gray">
|
||||
{t(`backup.triggers.${log.trigger}`, { defaultValue: log.trigger })}
|
||||
</td>
|
||||
<td className="py-2 px-2"><StatusBadge status={log.status} /></td>
|
||||
<td className="py-2 px-2">
|
||||
{log.commit_sha ? (
|
||||
|
||||
@@ -4939,6 +4939,12 @@ export default {
|
||||
clear: 'Löschen',
|
||||
date: 'Datum',
|
||||
status: 'Status',
|
||||
trigger: 'Typ',
|
||||
triggers: {
|
||||
manual: 'Sicherung (manuell)',
|
||||
scheduled: 'Sicherung (geplant)',
|
||||
restore: 'Wiederherstellung',
|
||||
},
|
||||
commit: 'Commit',
|
||||
|
||||
// Local Backup
|
||||
|
||||
@@ -4987,6 +4987,12 @@ export default {
|
||||
clear: 'Clear',
|
||||
date: 'Date',
|
||||
status: 'Status',
|
||||
trigger: 'Type',
|
||||
triggers: {
|
||||
manual: 'Backup (manual)',
|
||||
scheduled: 'Backup (scheduled)',
|
||||
restore: 'Restore',
|
||||
},
|
||||
commit: 'Commit',
|
||||
|
||||
// Local Backup
|
||||
|
||||
@@ -4947,6 +4947,12 @@ export default {
|
||||
clear: 'Borrar',
|
||||
date: 'Fecha',
|
||||
status: 'Estado',
|
||||
trigger: 'Tipo',
|
||||
triggers: {
|
||||
manual: 'Copia (manual)',
|
||||
scheduled: 'Copia (programada)',
|
||||
restore: 'Restauración',
|
||||
},
|
||||
commit: 'Confirmación',
|
||||
|
||||
// Local Backup
|
||||
|
||||
@@ -4928,6 +4928,12 @@ export default {
|
||||
clear: 'Effacer',
|
||||
date: 'Date',
|
||||
status: 'Statut',
|
||||
trigger: 'Type',
|
||||
triggers: {
|
||||
manual: 'Sauvegarde (manuelle)',
|
||||
scheduled: 'Sauvegarde (planifiée)',
|
||||
restore: 'Restauration',
|
||||
},
|
||||
commit: 'Commit',
|
||||
|
||||
// Local Backup
|
||||
|
||||
@@ -4927,6 +4927,12 @@ export default {
|
||||
clear: 'Cancella',
|
||||
date: 'Data',
|
||||
status: 'Stato',
|
||||
trigger: 'Tipo',
|
||||
triggers: {
|
||||
manual: 'Backup (manuale)',
|
||||
scheduled: 'Backup (pianificato)',
|
||||
restore: 'Ripristino',
|
||||
},
|
||||
commit: 'Commit',
|
||||
|
||||
// Local Backup
|
||||
|
||||
@@ -4939,6 +4939,12 @@ export default {
|
||||
clear: 'クリア',
|
||||
date: '日付',
|
||||
status: 'ステータス',
|
||||
trigger: '種類',
|
||||
triggers: {
|
||||
manual: 'バックアップ(手動)',
|
||||
scheduled: 'バックアップ(スケジュール)',
|
||||
restore: '復元',
|
||||
},
|
||||
commit: 'コミット',
|
||||
|
||||
// Local Backup
|
||||
|
||||
@@ -4702,6 +4702,12 @@ export default {
|
||||
clear: '초기화',
|
||||
date: '날짜',
|
||||
status: '상태',
|
||||
trigger: '유형',
|
||||
triggers: {
|
||||
manual: '백업(수동)',
|
||||
scheduled: '백업(예약)',
|
||||
restore: '복원',
|
||||
},
|
||||
commit: '커밋',
|
||||
localBackup: '로컬 백업',
|
||||
localBackupDescription: '데이터베이스, 아카이브, 업로드 및 모든 파일을 포함한 Bambuddy 데이터의 전체 백업을 만듭니다.',
|
||||
|
||||
@@ -4927,6 +4927,12 @@ export default {
|
||||
clear: 'Limpar',
|
||||
date: 'Data',
|
||||
status: 'Status',
|
||||
trigger: 'Tipo',
|
||||
triggers: {
|
||||
manual: 'Backup manual',
|
||||
scheduled: 'Backup agendado',
|
||||
restore: 'Restauração',
|
||||
},
|
||||
commit: 'Commit',
|
||||
|
||||
// Local Backup
|
||||
|
||||
@@ -4694,6 +4694,12 @@ export default {
|
||||
clear: "Очистить",
|
||||
date: "Дата",
|
||||
status: "Статус",
|
||||
trigger: "Тип",
|
||||
triggers: {
|
||||
manual: "Резервная копия (вручную)",
|
||||
scheduled: "Резервная копия (по расписанию)",
|
||||
restore: "Восстановление",
|
||||
},
|
||||
commit: "Коммит",
|
||||
localBackup: "Локальная резервная копия",
|
||||
localBackupDescription: "Создать полную резервную копию данных Bambuddy, включая базу данных, архивы, загрузки и все файлы.",
|
||||
|
||||
@@ -4916,6 +4916,12 @@ export default {
|
||||
clear: 'Temizle',
|
||||
date: 'Tarih',
|
||||
status: 'Durum',
|
||||
trigger: 'Tür',
|
||||
triggers: {
|
||||
manual: 'Yedek (manuel)',
|
||||
scheduled: 'Yedek (zamanlanmış)',
|
||||
restore: 'Geri yükleme',
|
||||
},
|
||||
commit: 'Commit',
|
||||
|
||||
// Yerel Yedekleme
|
||||
|
||||
@@ -4982,6 +4982,12 @@ export default {
|
||||
clear: "Очистити",
|
||||
date: "Дата",
|
||||
status: "Статус",
|
||||
trigger: "Тип",
|
||||
triggers: {
|
||||
manual: "Резервна копія (вручну)",
|
||||
scheduled: "Резервна копія (за розкладом)",
|
||||
restore: "Відновлення",
|
||||
},
|
||||
commit: "Коміт",
|
||||
|
||||
// Local Backup
|
||||
|
||||
@@ -4927,6 +4927,12 @@ export default {
|
||||
clear: '清除',
|
||||
date: '日期',
|
||||
status: '状态',
|
||||
trigger: '类型',
|
||||
triggers: {
|
||||
manual: '备份(手动)',
|
||||
scheduled: '备份(计划)',
|
||||
restore: '恢复',
|
||||
},
|
||||
commit: '提交',
|
||||
|
||||
// Local Backup
|
||||
|
||||
@@ -4927,6 +4927,12 @@ export default {
|
||||
clear: '清除',
|
||||
date: '日期',
|
||||
status: '狀態',
|
||||
trigger: '類型',
|
||||
triggers: {
|
||||
manual: '備份(手動)',
|
||||
scheduled: '備份(排程)',
|
||||
restore: '還原',
|
||||
},
|
||||
commit: '提交',
|
||||
|
||||
// Local Backup
|
||||
|
||||
Reference in New Issue
Block a user