feat(backup): take Tinyforge DB snapshot before every deploy

Adds an opt-in "auto_backup_before_deploy" setting that triggers a
"pre-deploy" backup at the start of every project deploy via the deploy
pipeline (covers both the async HTTP path and the sync poller/webhook
path). Failures are logged to the deploy log but do not abort — missing
a backup is preferable to refusing to ship a fix.

- store: settings.auto_backup_before_deploy column + scan/update wiring
- backup: accept "pre-deploy" as a valid backup_type
- deployer: small PreDeployBackuper interface, hooked into runDeploy
  right after settings load and before any state-mutating work
- api: settings request/response surface the new flag
- web: ToggleSwitch on the backup settings page; "Pre-deploy" badge
  variant in the backup list (badge-warning so it stands out)
- i18n: en/ru strings for the toggle, help text, and badge label
This commit is contained in:
2026-05-07 02:14:26 +03:00
parent 0405ecd9ce
commit 8b886ddf2b
11 changed files with 95 additions and 13 deletions
+3
View File
@@ -553,6 +553,9 @@
"restoreFailed": "Failed to restore backup",
"typeManual": "Manual",
"typeAuto": "Auto",
"typePreDeploy": "Pre-deploy",
"preDeploy": "Backup before every deploy",
"preDeployHelp": "Take a Tinyforge DB snapshot at the start of every project deploy. Independent of the periodic schedule above; restorable from this list under the \"Pre-deploy\" type.",
"save": "Save",
"saving": "Saving...",
"saved": "Backup settings saved",
+3
View File
@@ -553,6 +553,9 @@
"restoreFailed": "Не удалось восстановить резервную копию",
"typeManual": "Ручная",
"typeAuto": "Авто",
"typePreDeploy": "Перед деплоем",
"preDeploy": "Снимок перед каждым деплоем",
"preDeployHelp": "Создавать снимок БД Tinyforge в начале каждого деплоя проекта. Независимо от периодического расписания выше; восстанавливается из списка ниже по типу «Перед деплоем».",
"save": "Сохранить",
"saving": "Сохранение...",
"saved": "Настройки копирования сохранены",
+1
View File
@@ -134,6 +134,7 @@ export interface Settings {
backup_enabled: boolean;
backup_interval_hours: number;
backup_retention_count: number;
auto_backup_before_deploy: boolean;
stats_interval_seconds: number;
stats_retention_hours: number;
updated_at: string;
+19 -3
View File
@@ -19,6 +19,7 @@
let backupEnabled = $state(false);
let backupIntervalHours = $state('24');
let backupRetentionCount = $state('10');
let autoBackupBeforeDeploy = $state(false);
let backups = $state<BackupInfo[]>([]);
let confirmDeleteId = $state('');
@@ -38,6 +39,7 @@
backupEnabled = settings.backup_enabled ?? false;
backupIntervalHours = String(settings.backup_interval_hours ?? 24);
backupRetentionCount = String(settings.backup_retention_count ?? 10);
autoBackupBeforeDeploy = settings.auto_backup_before_deploy ?? false;
backups = backupList ?? [];
} catch (err) {
toasts.error(err instanceof Error ? err.message : 'Failed to load backup settings');
@@ -53,7 +55,8 @@
await updateSettings({
backup_enabled: backupEnabled,
backup_interval_hours: Math.max(1, parseInt(backupIntervalHours, 10) || 24),
backup_retention_count: Math.max(1, parseInt(backupRetentionCount, 10) || 10)
backup_retention_count: Math.max(1, parseInt(backupRetentionCount, 10) || 10),
auto_backup_before_deploy: autoBackupBeforeDeploy
});
toasts.success($t('settingsBackup.saved'));
} catch (err) {
@@ -181,6 +184,15 @@
</div>
{/if}
<!-- Pre-deploy backup toggle: independent of the periodic auto-backup. -->
<div class="flex items-center gap-3">
<ToggleSwitch bind:checked={autoBackupBeforeDeploy} label={$t('settingsBackup.preDeploy')} />
<div>
<span class="text-sm font-medium text-[var(--text-primary)]">{$t('settingsBackup.preDeploy')}</span>
<p class="text-xs text-[var(--text-tertiary)]">{$t('settingsBackup.preDeployHelp')}</p>
</div>
</div>
<div class="flex items-center gap-3">
<button onclick={handleSave} disabled={saving}
class="inline-flex items-center gap-2 rounded-lg bg-[var(--color-brand-600)] px-4 py-2.5 text-sm font-medium text-white shadow-sm transition-all duration-150 hover:bg-[var(--color-brand-700)] disabled:opacity-50 active:animate-press">
@@ -233,8 +245,12 @@
<td class="px-4 py-3 text-[var(--text-secondary)]">{formatSize(backup.size_bytes)}</td>
<td class="px-4 py-3">
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium
{backup.backup_type === 'auto' ? 'badge-info' : 'badge-success'}">
{backup.backup_type === 'auto' ? $t('settingsBackup.typeAuto') : $t('settingsBackup.typeManual')}
{backup.backup_type === 'auto' ? 'badge-info' : backup.backup_type === 'pre-deploy' ? 'badge-warning' : 'badge-success'}">
{backup.backup_type === 'auto'
? $t('settingsBackup.typeAuto')
: backup.backup_type === 'pre-deploy'
? $t('settingsBackup.typePreDeploy')
: $t('settingsBackup.typeManual')}
</span>
</td>
<td class="px-4 py-3 text-[var(--text-secondary)]">{$fmt.dateTime(toUtcIso(backup.created_at))}</td>