241 lines
7.7 KiB
TypeScript
Executable File
241 lines
7.7 KiB
TypeScript
Executable File
import { useState } from "react";
|
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { FolderOpen, HardDrive, Trash2 } from "lucide-react";
|
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { FolderBrowserModal } from "./folder-browser-modal";
|
|
|
|
export function BackupDestinationManager() {
|
|
const { toast } = useToast();
|
|
const [path, setPath] = useState("");
|
|
const [deleteId, setDeleteId] = useState<number | null>(null);
|
|
const [browserOpen, setBrowserOpen] = useState(false);
|
|
|
|
// ==============================
|
|
// Queries
|
|
// ==============================
|
|
const { data: destinations = [] } = useQuery({
|
|
queryKey: ["/db/destination"],
|
|
queryFn: async () => {
|
|
const res = await apiRequest(
|
|
"GET",
|
|
"/api/database-management/destination"
|
|
);
|
|
return res.json();
|
|
},
|
|
});
|
|
|
|
const { data: usbSettingData } = useQuery({
|
|
queryKey: ["/db/usb-backup-setting"],
|
|
queryFn: async () => {
|
|
const res = await apiRequest("GET", "/api/database-management/usb-backup-setting");
|
|
return res.json();
|
|
},
|
|
});
|
|
|
|
const usbBackupEnabled = usbSettingData?.usbBackupEnabled ?? false;
|
|
|
|
const usbToggleMutation = useMutation({
|
|
mutationFn: async (enabled: boolean) => {
|
|
const res = await apiRequest("PUT", "/api/database-management/usb-backup-setting", {
|
|
usbBackupEnabled: enabled,
|
|
});
|
|
return res.json();
|
|
},
|
|
onSuccess: (data) => {
|
|
queryClient.setQueryData(["/db/usb-backup-setting"], data);
|
|
toast({
|
|
title: "Setting Saved",
|
|
description: `USB backup ${data.usbBackupEnabled ? "enabled" : "disabled"}.`,
|
|
});
|
|
},
|
|
onError: () => {
|
|
toast({
|
|
title: "Error",
|
|
description: "Failed to update USB backup setting.",
|
|
variant: "destructive",
|
|
});
|
|
},
|
|
});
|
|
|
|
// ==============================
|
|
// Mutations
|
|
// ==============================
|
|
const saveMutation = useMutation({
|
|
mutationFn: async () => {
|
|
const res = await apiRequest(
|
|
"POST",
|
|
"/api/database-management/destination",
|
|
{ path }
|
|
);
|
|
if (!res.ok) throw new Error((await res.json()).error);
|
|
},
|
|
onSuccess: () => {
|
|
toast({ title: "Backup destination saved" });
|
|
setPath("");
|
|
queryClient.invalidateQueries({ queryKey: ["/db/destination"] });
|
|
},
|
|
});
|
|
|
|
const deleteMutation = useMutation({
|
|
mutationFn: async (id: number) => {
|
|
await apiRequest("DELETE", `/api/database-management/destination/${id}`);
|
|
},
|
|
onSuccess: () => {
|
|
toast({ title: "Backup destination deleted" });
|
|
queryClient.invalidateQueries({ queryKey: ["/db/destination"] });
|
|
setDeleteId(null);
|
|
},
|
|
});
|
|
|
|
const backupNowMutation = useMutation({
|
|
mutationFn: async () => {
|
|
const res = await apiRequest("POST", "/api/database-management/backup-path");
|
|
if (!res.ok) {
|
|
const body = await res.json();
|
|
throw new Error(body.details || body.error || "Backup failed");
|
|
}
|
|
return res.json();
|
|
},
|
|
onSuccess: (data) => {
|
|
toast({ title: "Backup complete", description: `Saved: ${data.filename}` });
|
|
queryClient.invalidateQueries({ queryKey: ["/db/status"] });
|
|
},
|
|
onError: (err: any) => {
|
|
toast({ title: "Backup failed", description: err.message, variant: "destructive" });
|
|
},
|
|
});
|
|
|
|
// ==============================
|
|
// Folder browser
|
|
// ==============================
|
|
const handleFolderSelect = (selectedPath: string) => {
|
|
setPath(selectedPath);
|
|
};
|
|
|
|
// ==============================
|
|
// UI
|
|
// ==============================
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>External Backup Destination</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex items-center space-x-3">
|
|
<Switch
|
|
id="usb-backup-toggle"
|
|
checked={usbBackupEnabled}
|
|
onCheckedChange={(checked) => usbToggleMutation.mutate(checked)}
|
|
disabled={usbToggleMutation.isPending}
|
|
/>
|
|
<label
|
|
htmlFor="usb-backup-toggle"
|
|
className="text-sm font-medium text-gray-700 cursor-pointer select-none"
|
|
>
|
|
USB Backup
|
|
</label>
|
|
<span className="text-xs text-gray-400">
|
|
(daily at 9 PM → saves to the "USB Backup" folder on your drive)
|
|
</span>
|
|
</div>
|
|
|
|
<p className="text-sm text-gray-500">
|
|
Enter the root path of your USB drive below. The app will automatically back up to the{" "}
|
|
<span className="font-medium text-gray-700">USB Backup</span> folder inside it every night at 9 PM when the toggle is on.
|
|
</p>
|
|
|
|
<div className="flex gap-2">
|
|
<Input
|
|
placeholder="/media/usb-drive or D:\\Backups"
|
|
value={path}
|
|
onChange={(e) => setPath(e.target.value)}
|
|
/>
|
|
<Button variant="outline" onClick={() => setBrowserOpen(true)} title="Browse folders">
|
|
<FolderOpen className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
<FolderBrowserModal
|
|
open={browserOpen}
|
|
onClose={() => setBrowserOpen(false)}
|
|
onSelect={handleFolderSelect}
|
|
/>
|
|
|
|
<Button
|
|
onClick={() => saveMutation.mutate()}
|
|
disabled={!path || saveMutation.isPending}
|
|
>
|
|
Save Destination
|
|
</Button>
|
|
|
|
<div className="space-y-2">
|
|
{destinations.map((d: any) => (
|
|
<div
|
|
key={d.id}
|
|
className="flex justify-between items-center border rounded p-2"
|
|
>
|
|
<span className="text-sm text-gray-700">{d.path}</span>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
size="sm"
|
|
variant="outline"
|
|
onClick={() => backupNowMutation.mutate()}
|
|
disabled={backupNowMutation.isPending}
|
|
title="Backup now to this destination"
|
|
>
|
|
<HardDrive className="h-4 w-4 mr-1" />
|
|
{backupNowMutation.isPending ? "Backing up..." : "Backup Now"}
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="destructive"
|
|
onClick={() => setDeleteId(d.id)}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Confirm delete dialog */}
|
|
<AlertDialog open={deleteId !== null}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Delete backup destination?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
This will remove the destination and stop automatic backups.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel onClick={() => setDeleteId(null)}>
|
|
Cancel
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={() => deleteId && deleteMutation.mutate(deleteId)}
|
|
>
|
|
Delete
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|