import { useTranslation } from "react-i18next"; import type { BackupListResponse } from "../../../api"; import { SettingsToggleRow } from "../SettingsToggleRow"; import { SettingsNumberRow } from "../SettingsNumberRow"; import { SettingsTextRow } from "../SettingsTextRow"; import type { BackupSettingsMigrationCandidate, BackupSettingsMigrationConflict } from "@fusion/core"; import type { SectionBaseProps } from "./context"; import { LoadingSpinner } from "../../LoadingSpinner"; import "./DatabaseBackupsSection.css"; const formatBackupSize = (size: number): string => size > 1024 * 1024 ? `${(size / (1024 * 1024)).toFixed(1)} MB` : `${(size / 1024).toFixed(1)} KB`; const formatBackupDate = (value: string): string => { const date = new Date(value); return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); }; export interface DatabaseBackupsSectionProps extends SectionBaseProps { backupInfo: BackupListResponse | null; backupLoading: boolean; onBackupNow: () => void; } /* FNXC:SettingsBackups 2026-07-16-14:35: The global PostgreSQL cluster contains every project, so database controls and the one shared backup destination live in this global section. Memory snapshots remain in the project section. */ export function DatabaseBackupsSection({ form, setForm, backupInfo, backupLoading, onBackupNow }: DatabaseBackupsSectionProps) { const { t } = useTranslation("app"); const conflicts = form.backupSettingsMigrationConflicts ?? []; const chooseCandidate = (conflict: BackupSettingsMigrationConflict, candidate: BackupSettingsMigrationCandidate) => { setForm((current) => ({ ...current, [conflict.key]: candidate.value, backupSettingsMigrationConflicts: conflicts.filter((entry) => entry.key !== conflict.key), })); }; return (<> {conflicts.length > 0 &&
{t("settings.backups.migrationConflict", "Choose database backup settings")}

{t("settings.backups.migrationConflictHelp", "Existing projects used different backup settings. Choose the value to use globally.")}

{conflicts.map((conflict) =>
{conflict.key} {conflict.candidates.map((candidate, index) => )}
)}
}

{t("settings.backups.databaseBackups", "Database Backups")}

{t("settings.database.advanced", "Advanced database settings")} setForm((f) => ({ ...f, embeddedPostgresMaxConnections: v ?? undefined }))} error={form.embeddedPostgresMaxConnections !== undefined && (form.embeddedPostgresMaxConnections < 32 || form.embeddedPostgresMaxConnections > 2000) ? t("settings.database.embeddedConnectionCapError", "Enter a value between 32 and 2,000.") : undefined} />
setForm((f) => ({ ...f, autoBackupEnabled: v === true }))} /> setForm((f) => ({ ...f, autoBackupSchedule: v ?? "" }))} error={form.autoBackupSchedule && !/^[\s\d*,/-]+$/.test(form.autoBackupSchedule) ? t("settings.backups.invalidCronExpressionFormat", "Invalid cron expression format") : undefined} /> setForm((f) => ({ ...f, autoBackupRetention: v ?? undefined }))} error={form.autoBackupRetention !== undefined && (form.autoBackupRetention < 1 || form.autoBackupRetention > 100) ? t("settings.backups.mustBeBetween1And100", "Must be between 1 and 100") : undefined} /> setForm((f) => ({ ...f, autoBackupDir: v ?? "" }))} error={form.autoBackupDir && form.autoBackupDir.includes("..") ? t("settings.backups.pathCannotContainParentDirectoryTraversal", "Path cannot contain parent directory traversal (..)") : undefined} /> {backupLoading ? (
) : backupInfo ? (<> {/* FNXC:SettingsBackups 2026-08-13-23:51: The inventory must show both stored backups and central-routine evidence so a blank list cannot conceal a schedule failure. */}
{t("settings.backups.scheduleStatus", "Automatic backup schedule")}
{t("settings.backups.status", "Status")}
{backupInfo.schedule.enabled ? t("settings.backups.on", "On") : t("settings.backups.off", "Off")}
{t("settings.backups.cron", "Cron")}
{backupInfo.schedule.cronExpression}
{backupInfo.schedule.nextRunAt &&
{t("settings.backups.nextRun", "Next run")}
{formatBackupDate(backupInfo.schedule.nextRunAt)}
} {backupInfo.schedule.lastRunAt &&
{t("settings.backups.lastRun", "Last run")}
{formatBackupDate(backupInfo.schedule.lastRunAt)} — {backupInfo.schedule.lastRunSucceeded ? t("settings.backups.succeeded", "Succeeded") : t("settings.backups.failed", "Failed")}
} {backupInfo.schedule.lastRunOutput &&
{t("settings.backups.lastOutput", "Last output")}
{backupInfo.schedule.lastRunOutput}
} {backupInfo.schedule.runCount !== undefined &&
{t("settings.backups.runCount", "Run count")}
{backupInfo.schedule.runCount}
}
{backupInfo.schedule.enabled && !backupInfo.schedule.routineRegistered &&
{t("settings.backups.scheduleNotRegistered", "No backup schedule is registered. Restart Fusion or re-save Database Backup settings to register it.")}
}
{backupInfo.count}{t("settings.backups.backups", "backups")}
{formatBackupSize(backupInfo.totalSize)}{t("settings.backups.totalSize", "total size")}
{backupInfo.listError &&

{backupInfo.listError}

} {backupInfo.count === 0 ?

{t("settings.backups.noBackupsYet", "No backups yet — use Backup Now or enable automatic backups.")}

:
    {backupInfo.backups.slice().sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt)).slice(0, 10).map((backup) =>
  • {backup.filename}{formatBackupSize(backup.size)}
  • )} {backupInfo.backups.length > 10 &&
  • {t("settings.backups.and", "...and ")}{backupInfo.backups.length - 10}{t("settings.backups.more", " more")}
  • }
}
) : null}
); } export default DatabaseBackupsSection;