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:
@@ -227,6 +227,7 @@ func main() {
|
|||||||
slog.Error("create backup engine", "error", err)
|
slog.Error("create backup engine", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
dep.SetPreDeployBackuper(backupEngine)
|
||||||
|
|
||||||
// Clean orphaned backup files and prune on startup.
|
// Clean orphaned backup files and prune on startup.
|
||||||
if cleaned, err := backupEngine.CleanOrphans(); err != nil {
|
if cleaned, err := backupEngine.CleanOrphans(); err != nil {
|
||||||
|
|||||||
@@ -43,11 +43,12 @@ type settingsRequest struct {
|
|||||||
TraefikCertResolver *string `json:"traefik_cert_resolver,omitempty"`
|
TraefikCertResolver *string `json:"traefik_cert_resolver,omitempty"`
|
||||||
TraefikNetwork *string `json:"traefik_network,omitempty"`
|
TraefikNetwork *string `json:"traefik_network,omitempty"`
|
||||||
TraefikAPIURL *string `json:"traefik_api_url,omitempty"`
|
TraefikAPIURL *string `json:"traefik_api_url,omitempty"`
|
||||||
BackupEnabled *bool `json:"backup_enabled,omitempty"`
|
BackupEnabled *bool `json:"backup_enabled,omitempty"`
|
||||||
BackupIntervalHours *int `json:"backup_interval_hours,omitempty"`
|
BackupIntervalHours *int `json:"backup_interval_hours,omitempty"`
|
||||||
BackupRetentionCount *int `json:"backup_retention_count,omitempty"`
|
BackupRetentionCount *int `json:"backup_retention_count,omitempty"`
|
||||||
StatsIntervalSeconds *int `json:"stats_interval_seconds,omitempty"`
|
AutoBackupBeforeDeploy *bool `json:"auto_backup_before_deploy,omitempty"`
|
||||||
StatsRetentionHours *int `json:"stats_retention_hours,omitempty"`
|
StatsIntervalSeconds *int `json:"stats_interval_seconds,omitempty"`
|
||||||
|
StatsRetentionHours *int `json:"stats_retention_hours,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// getSettings handles GET /api/settings.
|
// getSettings handles GET /api/settings.
|
||||||
@@ -86,9 +87,10 @@ func (s *Server) getSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
"traefik_cert_resolver": settings.TraefikCertResolver,
|
"traefik_cert_resolver": settings.TraefikCertResolver,
|
||||||
"traefik_network": settings.TraefikNetwork,
|
"traefik_network": settings.TraefikNetwork,
|
||||||
"traefik_api_url": settings.TraefikAPIURL,
|
"traefik_api_url": settings.TraefikAPIURL,
|
||||||
"backup_enabled": settings.BackupEnabled,
|
"backup_enabled": settings.BackupEnabled,
|
||||||
"backup_interval_hours": settings.BackupIntervalHours,
|
"backup_interval_hours": settings.BackupIntervalHours,
|
||||||
"backup_retention_count": settings.BackupRetentionCount,
|
"backup_retention_count": settings.BackupRetentionCount,
|
||||||
|
"auto_backup_before_deploy": settings.AutoBackupBeforeDeploy,
|
||||||
"stats_interval_seconds": settings.StatsIntervalSeconds,
|
"stats_interval_seconds": settings.StatsIntervalSeconds,
|
||||||
"stats_retention_hours": settings.StatsRetentionHours,
|
"stats_retention_hours": settings.StatsRetentionHours,
|
||||||
"updated_at": settings.UpdatedAt,
|
"updated_at": settings.UpdatedAt,
|
||||||
@@ -243,6 +245,9 @@ func (s *Server) updateSettings(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
updated.BackupRetentionCount = *req.BackupRetentionCount
|
updated.BackupRetentionCount = *req.BackupRetentionCount
|
||||||
}
|
}
|
||||||
|
if req.AutoBackupBeforeDeploy != nil {
|
||||||
|
updated.AutoBackupBeforeDeploy = *req.AutoBackupBeforeDeploy
|
||||||
|
}
|
||||||
if req.StatsIntervalSeconds != nil {
|
if req.StatsIntervalSeconds != nil {
|
||||||
v := *req.StatsIntervalSeconds
|
v := *req.StatsIntervalSeconds
|
||||||
if v != 0 && (v < 5 || v > 300) {
|
if v != 0 && (v < 5 || v > 300) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (e *Engine) BackupDir() string {
|
|||||||
func (e *Engine) CreateBackup(backupType string) (store.Backup, error) {
|
func (e *Engine) CreateBackup(backupType string) (store.Backup, error) {
|
||||||
// Validate backup type to prevent path traversal via filename.
|
// Validate backup type to prevent path traversal via filename.
|
||||||
switch backupType {
|
switch backupType {
|
||||||
case "manual", "auto", "pre-restore":
|
case "manual", "auto", "pre-restore", "pre-deploy":
|
||||||
// valid
|
// valid
|
||||||
default:
|
default:
|
||||||
return store.Backup{}, fmt.Errorf("invalid backup type: %q", backupType)
|
return store.Backup{}, fmt.Errorf("invalid backup type: %q", backupType)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type Deployer struct {
|
|||||||
health *health.Checker
|
health *health.Checker
|
||||||
notifier *notify.Notifier
|
notifier *notify.Notifier
|
||||||
eventBus EventPublisher
|
eventBus EventPublisher
|
||||||
|
backuper PreDeployBackuper // optional; nil disables pre-deploy backups
|
||||||
encKey [32]byte
|
encKey [32]byte
|
||||||
dnsMu sync.RWMutex
|
dnsMu sync.RWMutex
|
||||||
dns dns.Provider // nil when wildcard DNS is active
|
dns dns.Provider // nil when wildcard DNS is active
|
||||||
@@ -46,6 +47,13 @@ type EventPublisher interface {
|
|||||||
Publish(evt events.Event)
|
Publish(evt events.Event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PreDeployBackuper takes a "pre-deploy" Tinyforge DB snapshot before any
|
||||||
|
// deploy starts when the corresponding setting is enabled. Kept as a small
|
||||||
|
// interface so the deployer does not import internal/backup.
|
||||||
|
type PreDeployBackuper interface {
|
||||||
|
CreateBackup(backupType string) (store.Backup, error)
|
||||||
|
}
|
||||||
|
|
||||||
// New creates a new Deployer with all required dependencies.
|
// New creates a new Deployer with all required dependencies.
|
||||||
func New(
|
func New(
|
||||||
dockerClient *docker.Client,
|
dockerClient *docker.Client,
|
||||||
@@ -72,6 +80,30 @@ func (d *Deployer) SetProxyProvider(provider proxy.Provider) {
|
|||||||
d.proxy = provider
|
d.proxy = provider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPreDeployBackuper wires the backup engine in after construction so the
|
||||||
|
// deployer can take a Tinyforge DB snapshot when the
|
||||||
|
// auto_backup_before_deploy setting is enabled. Pass nil to disable.
|
||||||
|
func (d *Deployer) SetPreDeployBackuper(b PreDeployBackuper) {
|
||||||
|
d.backuper = b
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeBackupBeforeDeploy creates a "pre-deploy" Tinyforge DB snapshot when
|
||||||
|
// the setting is enabled. Failures are logged but do not abort the deploy:
|
||||||
|
// missing a backup is preferable to refusing to ship a fix.
|
||||||
|
func (d *Deployer) maybeBackupBeforeDeploy(deployID string, settings store.Settings) {
|
||||||
|
if !settings.AutoBackupBeforeDeploy || d.backuper == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backup, err := d.backuper.CreateBackup("pre-deploy")
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("pre-deploy backup failed", "deploy_id", deployID, "error", err)
|
||||||
|
d.logDeploy(deployID, fmt.Sprintf("Pre-deploy backup failed: %v", err), "warn")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
slog.Info("pre-deploy backup created", "deploy_id", deployID, "backup_id", backup.ID, "filename", backup.Filename)
|
||||||
|
d.logDeploy(deployID, fmt.Sprintf("Pre-deploy backup created: %s", backup.Filename), "info")
|
||||||
|
}
|
||||||
|
|
||||||
// SetDNSProvider sets the DNS provider for managing DNS records during deployments.
|
// SetDNSProvider sets the DNS provider for managing DNS records during deployments.
|
||||||
// Pass nil to disable DNS management (wildcard DNS mode).
|
// Pass nil to disable DNS management (wildcard DNS mode).
|
||||||
func (d *Deployer) SetDNSProvider(provider dns.Provider) {
|
func (d *Deployer) SetDNSProvider(provider dns.Provider) {
|
||||||
@@ -160,6 +192,10 @@ func (d *Deployer) runDeploy(ctx context.Context, project store.Project, stage s
|
|||||||
)
|
)
|
||||||
d.logDeploy(deployID, fmt.Sprintf("Starting deploy of %s:%s for project %s, stage %s", project.Image, imageTag, project.Name, stage.Name), "info")
|
d.logDeploy(deployID, fmt.Sprintf("Starting deploy of %s:%s for project %s, stage %s", project.Image, imageTag, project.Name, stage.Name), "info")
|
||||||
|
|
||||||
|
// Take a pre-deploy DB snapshot if the operator opted in. Runs before
|
||||||
|
// any state-mutating work so a corrupted deploy is recoverable.
|
||||||
|
d.maybeBackupBeforeDeploy(deployID, settings)
|
||||||
|
|
||||||
// Enforce max_instances before deploying.
|
// Enforce max_instances before deploying.
|
||||||
if err := d.enforceMaxInstances(ctx, stage, deployID, settings); err != nil {
|
if err := d.enforceMaxInstances(ctx, stage, deployID, settings); err != nil {
|
||||||
d.logDeploy(deployID, fmt.Sprintf("Failed to enforce max instances: %v", err), "error")
|
d.logDeploy(deployID, fmt.Sprintf("Failed to enforce max instances: %v", err), "error")
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ type Settings struct {
|
|||||||
BackupEnabled bool `json:"backup_enabled"`
|
BackupEnabled bool `json:"backup_enabled"`
|
||||||
BackupIntervalHours int `json:"backup_interval_hours"`
|
BackupIntervalHours int `json:"backup_interval_hours"`
|
||||||
BackupRetentionCount int `json:"backup_retention_count"`
|
BackupRetentionCount int `json:"backup_retention_count"`
|
||||||
|
// AutoBackupBeforeDeploy creates a "pre-deploy" Tinyforge DB backup
|
||||||
|
// at the start of every project deploy. Independent of BackupEnabled
|
||||||
|
// (which governs the periodic auto-backup cron).
|
||||||
|
AutoBackupBeforeDeploy bool `json:"auto_backup_before_deploy"`
|
||||||
StatsIntervalSeconds int `json:"stats_interval_seconds"` // 0 disables collection
|
StatsIntervalSeconds int `json:"stats_interval_seconds"` // 0 disables collection
|
||||||
StatsRetentionHours int `json:"stats_retention_hours"` // 0 disables collection
|
StatsRetentionHours int `json:"stats_retention_hours"` // 0 disables collection
|
||||||
UpdatedAt string `json:"updated_at"`
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
// GetSettings returns the global settings (single-row pattern, always row id=1).
|
// GetSettings returns the global settings (single-row pattern, always row id=1).
|
||||||
func (s *Store) GetSettings() (Settings, error) {
|
func (s *Store) GetSettings() (Settings, error) {
|
||||||
var st Settings
|
var st Settings
|
||||||
var wildcardDNS, npmRemote, backupEnabled int
|
var wildcardDNS, npmRemote, backupEnabled, autoBackupBeforeDeploy int
|
||||||
err := s.db.QueryRow(
|
err := s.db.QueryRow(
|
||||||
`SELECT domain, server_ip, public_ip, network, subdomain_pattern, notification_url,
|
`SELECT domain, server_ip, public_ip, network, subdomain_pattern, notification_url,
|
||||||
notification_secret,
|
notification_secret,
|
||||||
@@ -19,6 +19,7 @@ func (s *Store) GetSettings() (Settings, error) {
|
|||||||
traefik_entrypoint, traefik_cert_resolver, traefik_network, traefik_api_url,
|
traefik_entrypoint, traefik_cert_resolver, traefik_network, traefik_api_url,
|
||||||
image_prune_threshold_mb,
|
image_prune_threshold_mb,
|
||||||
backup_enabled, backup_interval_hours, backup_retention_count,
|
backup_enabled, backup_interval_hours, backup_retention_count,
|
||||||
|
auto_backup_before_deploy,
|
||||||
stats_interval_seconds, stats_retention_hours,
|
stats_interval_seconds, stats_retention_hours,
|
||||||
updated_at
|
updated_at
|
||||||
FROM settings WHERE id = 1`,
|
FROM settings WHERE id = 1`,
|
||||||
@@ -32,6 +33,7 @@ func (s *Store) GetSettings() (Settings, error) {
|
|||||||
&st.TraefikEntrypoint, &st.TraefikCertResolver, &st.TraefikNetwork, &st.TraefikAPIURL,
|
&st.TraefikEntrypoint, &st.TraefikCertResolver, &st.TraefikNetwork, &st.TraefikAPIURL,
|
||||||
&st.ImagePruneThresholdMB,
|
&st.ImagePruneThresholdMB,
|
||||||
&backupEnabled, &st.BackupIntervalHours, &st.BackupRetentionCount,
|
&backupEnabled, &st.BackupIntervalHours, &st.BackupRetentionCount,
|
||||||
|
&autoBackupBeforeDeploy,
|
||||||
&st.StatsIntervalSeconds, &st.StatsRetentionHours,
|
&st.StatsIntervalSeconds, &st.StatsRetentionHours,
|
||||||
&st.UpdatedAt)
|
&st.UpdatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -40,6 +42,7 @@ func (s *Store) GetSettings() (Settings, error) {
|
|||||||
st.WildcardDNS = wildcardDNS != 0
|
st.WildcardDNS = wildcardDNS != 0
|
||||||
st.NpmRemote = npmRemote != 0
|
st.NpmRemote = npmRemote != 0
|
||||||
st.BackupEnabled = backupEnabled != 0
|
st.BackupEnabled = backupEnabled != 0
|
||||||
|
st.AutoBackupBeforeDeploy = autoBackupBeforeDeploy != 0
|
||||||
return st, nil
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +61,10 @@ func (s *Store) UpdateSettings(st Settings) error {
|
|||||||
if st.BackupEnabled {
|
if st.BackupEnabled {
|
||||||
backupEnabled = 1
|
backupEnabled = 1
|
||||||
}
|
}
|
||||||
|
autoBackupBeforeDeploy := 0
|
||||||
|
if st.AutoBackupBeforeDeploy {
|
||||||
|
autoBackupBeforeDeploy = 1
|
||||||
|
}
|
||||||
_, err := s.db.Exec(
|
_, err := s.db.Exec(
|
||||||
`UPDATE settings SET
|
`UPDATE settings SET
|
||||||
domain=?, server_ip=?, public_ip=?, network=?, subdomain_pattern=?, notification_url=?,
|
domain=?, server_ip=?, public_ip=?, network=?, subdomain_pattern=?, notification_url=?,
|
||||||
@@ -70,6 +77,7 @@ func (s *Store) UpdateSettings(st Settings) error {
|
|||||||
traefik_entrypoint=?, traefik_cert_resolver=?, traefik_network=?, traefik_api_url=?,
|
traefik_entrypoint=?, traefik_cert_resolver=?, traefik_network=?, traefik_api_url=?,
|
||||||
image_prune_threshold_mb=?,
|
image_prune_threshold_mb=?,
|
||||||
backup_enabled=?, backup_interval_hours=?, backup_retention_count=?,
|
backup_enabled=?, backup_interval_hours=?, backup_retention_count=?,
|
||||||
|
auto_backup_before_deploy=?,
|
||||||
stats_interval_seconds=?, stats_retention_hours=?,
|
stats_interval_seconds=?, stats_retention_hours=?,
|
||||||
updated_at=?
|
updated_at=?
|
||||||
WHERE id = 1`,
|
WHERE id = 1`,
|
||||||
@@ -83,6 +91,7 @@ func (s *Store) UpdateSettings(st Settings) error {
|
|||||||
st.TraefikEntrypoint, st.TraefikCertResolver, st.TraefikNetwork, st.TraefikAPIURL,
|
st.TraefikEntrypoint, st.TraefikCertResolver, st.TraefikNetwork, st.TraefikAPIURL,
|
||||||
st.ImagePruneThresholdMB,
|
st.ImagePruneThresholdMB,
|
||||||
backupEnabled, st.BackupIntervalHours, st.BackupRetentionCount,
|
backupEnabled, st.BackupIntervalHours, st.BackupRetentionCount,
|
||||||
|
autoBackupBeforeDeploy,
|
||||||
st.StatsIntervalSeconds, st.StatsRetentionHours,
|
st.StatsIntervalSeconds, st.StatsRetentionHours,
|
||||||
st.UpdatedAt,
|
st.UpdatedAt,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -147,6 +147,10 @@ func (s *Store) runMigrations() error {
|
|||||||
`ALTER TABLE stages ADD COLUMN notification_secret TEXT NOT NULL DEFAULT ''`,
|
`ALTER TABLE stages ADD COLUMN notification_secret TEXT NOT NULL DEFAULT ''`,
|
||||||
`ALTER TABLE static_sites ADD COLUMN notification_url TEXT NOT NULL DEFAULT ''`,
|
`ALTER TABLE static_sites ADD COLUMN notification_url TEXT NOT NULL DEFAULT ''`,
|
||||||
`ALTER TABLE static_sites ADD COLUMN notification_secret TEXT NOT NULL DEFAULT ''`,
|
`ALTER TABLE static_sites ADD COLUMN notification_secret TEXT NOT NULL DEFAULT ''`,
|
||||||
|
// Auto-backup before deploy (2026-05-07). When enabled, the deployer
|
||||||
|
// triggers a "pre-deploy" Tinyforge DB backup before any project deploy
|
||||||
|
// so a corrupted deploy is recoverable without data loss.
|
||||||
|
`ALTER TABLE settings ADD COLUMN auto_backup_before_deploy INTEGER NOT NULL DEFAULT 0`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additive stack tables (2026-04-16). Created here rather than in the
|
// Additive stack tables (2026-04-16). Created here rather than in the
|
||||||
|
|||||||
@@ -553,6 +553,9 @@
|
|||||||
"restoreFailed": "Failed to restore backup",
|
"restoreFailed": "Failed to restore backup",
|
||||||
"typeManual": "Manual",
|
"typeManual": "Manual",
|
||||||
"typeAuto": "Auto",
|
"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",
|
"save": "Save",
|
||||||
"saving": "Saving...",
|
"saving": "Saving...",
|
||||||
"saved": "Backup settings saved",
|
"saved": "Backup settings saved",
|
||||||
|
|||||||
@@ -553,6 +553,9 @@
|
|||||||
"restoreFailed": "Не удалось восстановить резервную копию",
|
"restoreFailed": "Не удалось восстановить резервную копию",
|
||||||
"typeManual": "Ручная",
|
"typeManual": "Ручная",
|
||||||
"typeAuto": "Авто",
|
"typeAuto": "Авто",
|
||||||
|
"typePreDeploy": "Перед деплоем",
|
||||||
|
"preDeploy": "Снимок перед каждым деплоем",
|
||||||
|
"preDeployHelp": "Создавать снимок БД Tinyforge в начале каждого деплоя проекта. Независимо от периодического расписания выше; восстанавливается из списка ниже по типу «Перед деплоем».",
|
||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
"saving": "Сохранение...",
|
"saving": "Сохранение...",
|
||||||
"saved": "Настройки копирования сохранены",
|
"saved": "Настройки копирования сохранены",
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ export interface Settings {
|
|||||||
backup_enabled: boolean;
|
backup_enabled: boolean;
|
||||||
backup_interval_hours: number;
|
backup_interval_hours: number;
|
||||||
backup_retention_count: number;
|
backup_retention_count: number;
|
||||||
|
auto_backup_before_deploy: boolean;
|
||||||
stats_interval_seconds: number;
|
stats_interval_seconds: number;
|
||||||
stats_retention_hours: number;
|
stats_retention_hours: number;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
let backupEnabled = $state(false);
|
let backupEnabled = $state(false);
|
||||||
let backupIntervalHours = $state('24');
|
let backupIntervalHours = $state('24');
|
||||||
let backupRetentionCount = $state('10');
|
let backupRetentionCount = $state('10');
|
||||||
|
let autoBackupBeforeDeploy = $state(false);
|
||||||
let backups = $state<BackupInfo[]>([]);
|
let backups = $state<BackupInfo[]>([]);
|
||||||
|
|
||||||
let confirmDeleteId = $state('');
|
let confirmDeleteId = $state('');
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
backupEnabled = settings.backup_enabled ?? false;
|
backupEnabled = settings.backup_enabled ?? false;
|
||||||
backupIntervalHours = String(settings.backup_interval_hours ?? 24);
|
backupIntervalHours = String(settings.backup_interval_hours ?? 24);
|
||||||
backupRetentionCount = String(settings.backup_retention_count ?? 10);
|
backupRetentionCount = String(settings.backup_retention_count ?? 10);
|
||||||
|
autoBackupBeforeDeploy = settings.auto_backup_before_deploy ?? false;
|
||||||
backups = backupList ?? [];
|
backups = backupList ?? [];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toasts.error(err instanceof Error ? err.message : 'Failed to load backup settings');
|
toasts.error(err instanceof Error ? err.message : 'Failed to load backup settings');
|
||||||
@@ -53,7 +55,8 @@
|
|||||||
await updateSettings({
|
await updateSettings({
|
||||||
backup_enabled: backupEnabled,
|
backup_enabled: backupEnabled,
|
||||||
backup_interval_hours: Math.max(1, parseInt(backupIntervalHours, 10) || 24),
|
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'));
|
toasts.success($t('settingsBackup.saved'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -181,6 +184,15 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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">
|
<div class="flex items-center gap-3">
|
||||||
<button onclick={handleSave} disabled={saving}
|
<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">
|
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 text-[var(--text-secondary)]">{formatSize(backup.size_bytes)}</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium
|
<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' ? 'badge-info' : backup.backup_type === 'pre-deploy' ? 'badge-warning' : 'badge-success'}">
|
||||||
{backup.backup_type === 'auto' ? $t('settingsBackup.typeAuto') : $t('settingsBackup.typeManual')}
|
{backup.backup_type === 'auto'
|
||||||
|
? $t('settingsBackup.typeAuto')
|
||||||
|
: backup.backup_type === 'pre-deploy'
|
||||||
|
? $t('settingsBackup.typePreDeploy')
|
||||||
|
: $t('settingsBackup.typeManual')}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-[var(--text-secondary)]">{$fmt.dateTime(toUtcIso(backup.created_at))}</td>
|
<td class="px-4 py-3 text-[var(--text-secondary)]">{$fmt.dateTime(toUtcIso(backup.created_at))}</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user