* fix(backup): make backup settings actually apply (#871) - Wire the What-to-Backup toggles into the walker: honor backup_include_thumbnails / backup_include_photos (opt-out, default ON) and accept the UI's backup_include_archives spelling for the archived gate (the engine expected _archived, so the Archives checkbox silently never worked). - Fix the 167.6 TB dashboard size: file_size_bytes is a bigint that node-postgres returns as a string, and the S3 path concatenated it onto the byte counter; coerce to Number at the source. - Compute the real next scheduled run (cron-parser) and return it as nextBackup; the UI read a field the API never sent and rendered a hardcoded 'Not scheduled'. A named schedule label now beats the stray default cron the UI always sent, which silently turned weekly schedules into daily 03:00 runs. - Never back up filesystem noise (.nfs* silly-renames, .DS_Store, Thumbs.db) and honor backup_exclude_patterns in the walker (previously rsync-only). - Remove the compression/encryption toggles from the configuration UI: no backend implementation exists, and collecting an encryption passphrase while uploading plaintext is a false promise. Closes #871 * fix(backup): close the review gaps in the settings wiring - The UI's backup_include_archives now beats the migration-seeded backup_include_archived: every install has the singular key seeded true, so the alias-only-when-absent lookup made unchecking Archives a no-op. - rsync destinations now receive the de-selected What-to-Backup paths and the noise filters as anchored --exclude args; previously rsync synced the whole storage root and the walker's selection only shaped the manifest, which then misreported what was actually transferred. - Escape regex metacharacters in the walker's glob matcher: '.nfs*' compiled to /^.nfs.*$/ whose leading dot matched any character, so files like anfs-photo.jpg were silently dropped from backups. - The Backup Coverage report now uses the same gate as the walker (new 'skipped-by-setting' status) instead of re-implementing it without the opt-out toggles and the archives alias. * fix(backup): make the coverage diagnostics agree with the walker - The coverage table shows the alias-aware flag value the gate actually used, instead of the seeded backup_include_archived shadowed by the UI's plural key (true next to a 'Gated off' badge). - skipped-by-setting paths are now counted in the coverage summary (backend, TS contract, summary card, EN/DE locales) so the totals reconcile again when Photos or Thumbnails is unchecked. - The form's thumbnail default now matches the backend's never-saved fallback (include): the checkbox no longer shows 'off' while thumbnails are being backed up, and saving an unrelated setting no longer flips the backup scope. * fix(backup): keep custom crons, exclude disabled rows from rsync, normalize flag display - Saving a named schedule no longer wipes the stored custom cron: the backend already prefers the label, so the cron field stays inert for named schedules and is preserved for switching back to Custom. A custom schedule now validates the 5-field expression before saving (the backend silently fell back to daily 02:00 on a blank value). - resolveExcludedBackupPaths now also returns rows disabled via include_in_default, so rsync excludes them; the enabled-only loader hid them and rsync transferred their contents anyway. - The coverage table normalizes flag values like the walker does — Boolean('false') displayed true beside a gated-off badge.
This commit is contained in:
@@ -5,7 +5,6 @@ import {
|
||||
Server,
|
||||
Cloud,
|
||||
HardDrive,
|
||||
AlertCircle,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Wifi,
|
||||
@@ -102,7 +101,9 @@ export const BackupConfiguration: React.FC<BackupConfigurationProps> = ({ config
|
||||
backup_include_database: true,
|
||||
backup_include_photos: true,
|
||||
backup_include_archives: true,
|
||||
backup_include_thumbnails: false,
|
||||
// Matches the backend never-saved fallback (include everything) so the
|
||||
// form does not show "off" while thumbnails are in fact being backed up.
|
||||
backup_include_thumbnails: true,
|
||||
backup_include_temp: false,
|
||||
backup_compression: true,
|
||||
backup_encryption: false,
|
||||
@@ -111,8 +112,7 @@ export const BackupConfiguration: React.FC<BackupConfigurationProps> = ({ config
|
||||
|
||||
const [showSecrets, setShowSecrets] = useState({
|
||||
s3_secret_key: false,
|
||||
ssh_key: false,
|
||||
encryption_passphrase: false
|
||||
ssh_key: false
|
||||
});
|
||||
|
||||
const [testingConnection, setTestingConnection] = useState(false);
|
||||
@@ -152,6 +152,15 @@ export const BackupConfiguration: React.FC<BackupConfigurationProps> = ({ config
|
||||
return;
|
||||
}
|
||||
|
||||
// A custom schedule needs a real 5-field cron — the backend silently
|
||||
// falls back to daily 02:00 otherwise. For named schedules the stored
|
||||
// cron is kept (the backend prefers the label), so switching back to
|
||||
// Custom keeps the previously saved expression.
|
||||
if (formData.backup_schedule === 'custom' && !/^\s*\S+(\s+\S+){4}\s*$/.test(formData.backup_schedule_cron)) {
|
||||
toast.error(t('backup.configuration.messages.invalidCron', 'Please enter a valid cron expression (5 fields)'));
|
||||
return;
|
||||
}
|
||||
|
||||
onSave(formData);
|
||||
};
|
||||
|
||||
@@ -546,69 +555,6 @@ export const BackupConfiguration: React.FC<BackupConfigurationProps> = ({ config
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Advanced Options */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.configuration.advancedOptions.title')}</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.backup_compression}
|
||||
onChange={(e) => handleChange('backup_compression', e.target.checked)}
|
||||
className="h-4 w-4 text-primary focus:ring-primary border-neutral-300 dark:border-neutral-600 rounded bg-white dark:bg-neutral-700"
|
||||
/>
|
||||
<div className="ml-3">
|
||||
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">{t('backup.configuration.advancedOptions.compression')}</span>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">{t('backup.configuration.advancedOptions.compressionHelp')}</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center mb-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.backup_encryption}
|
||||
onChange={(e) => handleChange('backup_encryption', e.target.checked)}
|
||||
className="h-4 w-4 text-primary focus:ring-primary border-neutral-300 dark:border-neutral-600 rounded bg-white dark:bg-neutral-700"
|
||||
/>
|
||||
<div className="ml-3">
|
||||
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">{t('backup.configuration.advancedOptions.encryption')}</span>
|
||||
<p className="text-xs text-neutral-500 dark:text-neutral-400">{t('backup.configuration.advancedOptions.encryptionHelp')}</p>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{formData.backup_encryption && (
|
||||
<div className="ml-7">
|
||||
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
|
||||
{t('backup.configuration.advancedOptions.encryptionPassphrase')}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showSecrets.encryption_passphrase ? 'text' : 'password'}
|
||||
value={formData.backup_encryption_passphrase}
|
||||
onChange={(e) => handleChange('backup_encryption_passphrase', e.target.value)}
|
||||
placeholder={t('backup.configuration.advancedOptions.encryptionPassphraseHelp')}
|
||||
required={formData.backup_encryption}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSecrets(prev => ({ ...prev, encryption_passphrase: !prev.encryption_passphrase }))}
|
||||
className="absolute top-1/2 -translate-y-1/2 right-2 text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300"
|
||||
>
|
||||
{showSecrets.encryption_passphrase ? <EyeOff size={20} /> : <Eye size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-red-600">
|
||||
<AlertCircle className="inline h-3 w-3 mr-1" />
|
||||
{t('backup.configuration.advancedOptions.encryptionPassphraseHelp')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
|
||||
@@ -258,6 +258,12 @@ const SummaryCard: React.FC<{
|
||||
value={String(summary.skippedByFeatureFlagCount)}
|
||||
/>
|
||||
)}
|
||||
{summary.skippedBySettingCount > 0 && (
|
||||
<Row
|
||||
label={t('backup.coverage.summary.skippedBySetting', 'Skipped (backup settings)')}
|
||||
value={String(summary.skippedBySettingCount)}
|
||||
/>
|
||||
)}
|
||||
{summary.missingOnDiskCount > 0 && (
|
||||
<Row
|
||||
label={t('backup.coverage.summary.missingOnDisk', 'Missing on disk')}
|
||||
@@ -380,6 +386,10 @@ const CoverageBadge: React.FC<{ coverage: BackupPathCoverage }> = ({ coverage })
|
||||
tone: 'neutral',
|
||||
label: t('backup.coverage.coverage.skippedByFlag', 'Gated off'),
|
||||
},
|
||||
'skipped-by-setting': {
|
||||
tone: 'neutral',
|
||||
label: t('backup.coverage.coverage.skippedBySetting', 'Off (backup settings)'),
|
||||
},
|
||||
'missing-on-disk': {
|
||||
tone: 'amber',
|
||||
label: t('backup.coverage.coverage.missingOnDisk', 'Missing on disk'),
|
||||
|
||||
Reference in New Issue
Block a user