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
+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>