• Backup and restore section component. Handles backup scheduling, location management, file browser, and restore operations.

    Parameters

    Returns Element

    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>
    );
    }