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) => chooseCandidate(conflict, candidate)}
>
{candidate.source === "global"
? t("settings.backups.existingGlobalValue", "Existing global value")
: `${t("settings.backups.projectValue", "Project value")}: ${candidate.projectId}`}
)}
)}
}
{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.")}
}
{t("settings.backups.currentBackups", "Current Backups")}
{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}{formatBackupDate(backup.createdAt)} {formatBackupSize(backup.size)}
)}
{backupInfo.backups.length > 10 && {t("settings.backups.and", "...and ")}{backupInfo.backups.length - 10}{t("settings.backups.more", " more")} }
}
>) : null}
{backupLoading ? t("settings.backups.creating", "Creating…") : t("settings.backups.backupNow", "Backup Now")}
>);
}
export default DatabaseBackupsSection;