export function BackupRestoreSection({
searchQuery,
scheduleConfig,
nextScheduledBackup,
lastScheduledBackup,
isTriggeringBackup,
isRestoringBackup,
selectedBackupFile,
backupValidationError,
onScheduleConfigChange,
onTriggerBackup,
onRestoreBackup,
onFileSelect,
}: Readonly<BackupRestoreSectionProps>) {
const [localBackups, setLocalBackups] = useState<BackupFile[]>([]);
const [isLoadingBackups, setIsLoadingBackups] = useState(false);
const [isDeletingBackup, setIsDeletingBackup] = useState<string | null>(null);
const [isRestoringFromList, setIsRestoringFromList] = useState<string | null>(
null,
);
const [isRefreshCooldownActive, setIsRefreshCooldownActive] = useState(false);
const [resolvedDefaultBackupLocation, setResolvedDefaultBackupLocation] =
useState<string>("");
const [isContextMissing, setIsContextMissing] = useState(false);
const [isShowingAllBackups, setIsShowingAllBackups] = useState(false);
const refreshCooldownRef = useRef<NodeJS.Timeout | null>(null);
// Detect if electronBackup context is missing on mount
useEffect(() => {
if (!globalThis.electronBackup) {
setIsContextMissing(true);
console.warn(
"[BackupRestoreSection] electronBackup context is not available - preload may have failed",
);
}
}, []);
// Cleanup cooldown timer on unmount
useEffect(() => {
return () => {
if (refreshCooldownRef.current) {
clearTimeout(refreshCooldownRef.current);
}
};
}, []);
// Fetch the resolved default backup location on mount
useEffect(() => {
const fetchDefaultLocation = async () => {
try {
const result = await globalThis.electronBackup?.getBackupLocation?.();
if (result?.success && result.data) {
setResolvedDefaultBackupLocation(result.data);
} else if (!result?.success) {
console.error(
"[BackupRestoreSection] Error fetching backup location:",
result?.error,
);
toast.error("Failed to load backup location", {
description: truncateToastMessage(
result?.error || "Failed to load backup location",
200,
).component,
});
}
} catch (error) {
console.error(
"[BackupRestoreSection] Error fetching backup location:",
error,
);
toast.error("Failed to load backup location", {
description: truncateToastMessage(
"Failed to load backup location",
200,
).component,
});
}
};
void fetchDefaultLocation();
}, []);
// Load backups from the configured location
const loadBackups = async () => {
setIsLoadingBackups(true);
try {
const result = await globalThis.electronBackup?.listLocalBackups?.();
if (result?.success && result.data) {
setLocalBackups(result.data);
} else if (!result?.success) {
console.error(
"[BackupRestoreSection] Error loading backups:",
result?.error,
);
}
} catch (error) {
console.error("[BackupRestoreSection] Error loading backups:", error);
} finally {
setIsLoadingBackups(false);
}
};
// Debounced refresh handler with cooldown to prevent spam
const handleRefreshBackups = async () => {
if (refreshCooldownRef.current || isLoadingBackups) {
return;
}
await loadBackups();
// Set cooldown: disable button for 1000ms
setIsRefreshCooldownActive(true);
refreshCooldownRef.current = setTimeout(() => {
setIsRefreshCooldownActive(false);
refreshCooldownRef.current = null;
}, 1000);
};
// Load backups on mount and when backup location changes
useEffect(() => {
loadBackups();
}, [scheduleConfig.backupLocation]);
// Listen for backup history updates
useEffect(() => {
const cleanup = globalThis.electronBackup?.onHistoryUpdated?.(() => {
loadBackups();
});
return () => {
cleanup?.();
};
}, []);
const handleDeleteBackup = async (filename: string) => {
if (!confirm(`Delete backup "${filename}"?`)) {
return;
}
setIsDeletingBackup(filename);
try {
const result = await globalThis.electronBackup?.deleteBackup?.(filename);
if (result?.success) {
// Reload backups after deletion
await loadBackups();
toast.success("Backup deleted successfully");
} else {
const errorMsg = result?.error || "Failed to delete backup";
console.error(
"[BackupRestoreSection] Failed to delete backup:",
result?.error,
);
toast.error("Failed to delete backup", {
description: truncateToastMessage(errorMsg, 200).component,
});
}
} catch (error) {
console.error("[BackupRestoreSection] Error deleting backup:", error);
toast.error("Error deleting backup");
} finally {
setIsDeletingBackup(null);
}
};
const handleRestoreFromList = async (backup: BackupFile) => {
if (
!confirm(
`Restore from "${new Date(backup.timestamp).toLocaleString()}"?\n\nWarning: This will overwrite your current data. Make sure you have a backup first.`,
)
) {
return;
}
setIsRestoringFromList(backup.name);
try {
// Call the main process restore IPC directly instead of reconstructing a File object
const result = await globalThis.electronBackup?.restoreFromLocal?.(
backup.name,
{ merge: false },
);
if (result?.success) {
// Trigger the restore workflow with the result
onRestoreBackup();
toast.success("Backup restored successfully. App will reload...");
// Reload backups and clear temp state
setTimeout(() => {
loadBackups();
}, 1000);
} else {
const errorMsg =
result?.errors?.join(", ") || "Failed to restore backup";
console.error("[BackupRestoreSection] Restore failed:", result?.errors);
toast.error(errorMsg);
}
} catch (error) {
console.error("[BackupRestoreSection] Error restoring from list:", error);
toast.error("Error restoring backup");
} finally {
setIsRestoringFromList(null);
}
};
const handleOpenBackupLocation = async () => {
try {
const result = await globalThis.electronBackup?.openBackupLocation?.();
if (!result?.success) {
const errorMsg = result?.error || "Failed to open backup location";
console.error(
"[BackupRestoreSection] Error opening backup location:",
result?.error,
);
toast.error(errorMsg);
}
} catch (error) {
console.error(
"[BackupRestoreSection] Error opening backup location:",
error,
);
toast.error("Error opening backup location");
}
};
const handleBackupLocationChange = async (newLocation: string) => {
try {
const result =
await globalThis.electronBackup?.setBackupLocation?.(newLocation);
if (result?.success) {
onScheduleConfigChange({
...scheduleConfig,
backupLocation: newLocation,
});
// Reload backups from new location
await loadBackups();
toast.success("Backup location updated");
} else {
const errorMsg = result?.error || "Failed to set backup location";
let friendlyMessage = errorMsg;
// Map error codes to friendly messages
if (result?.code === "ENOENT") {
friendlyMessage = "Directory does not exist";
} else if (result?.code === "EACCES") {
friendlyMessage = "Permission denied";
} else if (result?.code === "INVALID_PATH") {
friendlyMessage = "Invalid backup location path";
}
console.error(
"[BackupRestoreSection] Failed to set backup location:",
result?.error,
);
toast.error(friendlyMessage);
}
} catch (error) {
console.error(
"[BackupRestoreSection] Error setting backup location:",
error,
);
toast.error("Error setting backup location");
}
};
return (
<div id="data-backup" className="space-y-6" aria-busy={isLoadingBackups}>
{/* Context Missing Warning */}
{isContextMissing && (
<div className="flex items-start gap-3 rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-900 dark:bg-red-950">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-red-600 dark:text-red-400" />
<div className="text-sm">
<p className="font-medium text-red-900 dark:text-red-300">
Backup features unavailable
</p>
<p className="mt-1 text-xs text-red-800 dark:text-red-400">
The backup context failed to expose. Preload may have failed to
initialize properly.
</p>
</div>
</div>
)}
<div className="grid gap-6 lg:grid-cols-2">
{/* Left Column: Configuration & Actions */}
<div className="space-y-6">
<BackupScheduleCard
searchQuery={searchQuery}
scheduleConfig={scheduleConfig}
onScheduleConfigChange={onScheduleConfigChange}
lastScheduledBackup={lastScheduledBackup}
nextScheduledBackup={nextScheduledBackup}
isTriggeringBackup={isTriggeringBackup}
onTriggerBackup={onTriggerBackup}
/>
<BackupLocationCard
searchQuery={searchQuery}
backupLocation={scheduleConfig.backupLocation}
resolvedDefaultBackupLocation={resolvedDefaultBackupLocation}
onBackupLocationChange={handleBackupLocationChange}
onOpenBackupLocation={handleOpenBackupLocation}
/>
</div>
{/* Right Column: Restore & History */}
<div className="space-y-6">
<RestoreFromFileCard
searchQuery={searchQuery}
selectedBackupFile={selectedBackupFile}
isRestoringBackup={isRestoringBackup}
backupValidationError={backupValidationError}
onFileSelect={onFileSelect}
onRestoreBackup={onRestoreBackup}
/>
<AvailableBackupsCard
searchQuery={searchQuery}
localBackups={localBackups}
isLoadingBackups={isLoadingBackups}
isRefreshCooldownActive={isRefreshCooldownActive}
isShowingAllBackups={isShowingAllBackups}
isRestoringFromList={isRestoringFromList}
isDeletingBackup={isDeletingBackup}
onRefreshBackups={handleRefreshBackups}
onToggleShowAll={() => setIsShowingAllBackups(!isShowingAllBackups)}
onRestoreFromList={handleRestoreFromList}
onDeleteBackup={handleDeleteBackup}
/>
</div>
</div>
</div>
);
}
Backup and restore section component. Handles backup scheduling, location management, file browser, and restore operations.