import { useState, useEffect } 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Copy, Eye, EyeOff, RefreshCw, Network, RotateCcw, HardDrive } from "lucide-react"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; const HOUR_OPTIONS = Array.from({ length: 24 }, (_, h) => { const label = h === 0 ? "12:00 AM (midnight)" : h < 12 ? `${h}:00 AM` : h === 12 ? "12:00 PM (noon)" : `${h - 12}:00 PM`; return { value: h, label }; }); export function NetworkBackupManager() { return ( Network Backup Rclone API Key ); } // ============================================================ // Tab 1: Rclone // ============================================================ function RcloneBackupSection() { const { toast } = useToast(); // Server (source) state const [serverEnabled, setServerEnabled] = useState(false); const [serverPort, setServerPort] = useState(8080); // Receiver state const [receiverEnabled, setReceiverEnabled] = useState(false); const [receiverSyncHour, setReceiverSyncHour] = useState(21); const [sourceIp, setSourceIp] = useState(""); const [sourcePort, setSourcePort] = useState(8080); // Auto-import state const [autoImportEnabled, setAutoImportEnabled] = useState(false); const [autoImportHour, setAutoImportHour] = useState(22); const [formLoaded, setFormLoaded] = useState(false); const { data: rcloneConfig } = useQuery({ queryKey: ["/db/rclone-config"], queryFn: async () => { const res = await apiRequest("GET", "/api/database-management/rclone-config"); return res.json(); }, }); const { data: rcloneStatus } = useQuery({ queryKey: ["/db/rclone-status"], queryFn: async () => { const res = await apiRequest("GET", "/api/database-management/rclone-status"); return res.json() as Promise<{ installed: boolean; serverRunning: boolean }>; }, refetchInterval: 5000, }); useEffect(() => { if (rcloneConfig && !formLoaded) { setServerEnabled(rcloneConfig.serverEnabled ?? false); setServerPort(rcloneConfig.serverPort ?? 8080); setReceiverEnabled(rcloneConfig.receiverEnabled ?? false); setReceiverSyncHour(rcloneConfig.receiverSyncHour ?? 21); setSourceIp(rcloneConfig.sourceIp ?? ""); setSourcePort(rcloneConfig.sourcePort ?? 8080); setAutoImportEnabled(rcloneConfig.autoImportEnabled ?? false); setAutoImportHour(rcloneConfig.autoImportHour ?? 22); setFormLoaded(true); } }, [rcloneConfig, formLoaded]); const saveMutation = useMutation({ mutationFn: async () => { const res = await apiRequest("PUT", "/api/database-management/rclone-config", { serverEnabled, serverPort, receiverEnabled, receiverSyncHour, sourceIp, sourcePort, autoImportEnabled, autoImportHour, }); return res.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/db/rclone-config"] }); toast({ title: "Rclone settings saved" }); }, onError: () => { toast({ title: "Error", description: "Failed to save rclone settings.", variant: "destructive" }); }, }); const pullNowMutation = useMutation({ mutationFn: async () => { const res = await apiRequest("POST", "/api/database-management/rclone-pull-now"); if (!res.ok) { const body = await res.json(); throw new Error(body.details || body.error || "Pull failed"); } return res.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/db/rclone-config"] }); toast({ title: "Rclone pull complete", description: "Backup files copied from source PC." }); }, onError: (err: any) => { queryClient.invalidateQueries({ queryKey: ["/db/rclone-config"] }); toast({ title: "Rclone pull failed", description: err.message, variant: "destructive" }); }, }); const importNowMutation = useMutation({ mutationFn: async () => { const res = await apiRequest("POST", "/api/database-management/auto-import-now"); if (!res.ok) { const body = await res.json(); throw new Error(body.details || body.error || "Import failed"); } return res.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/db/rclone-config"] }); toast({ title: "Import complete", description: "Latest backup restored to database." }); }, onError: (err: any) => { queryClient.invalidateQueries({ queryKey: ["/db/rclone-config"] }); toast({ title: "Import failed", description: err.message, variant: "destructive" }); }, }); const serverRunning = rcloneStatus?.serverRunning ?? false; return (
{/* ── Source PC: Serve backups via WebDAV ── */}

Source PC — Serve Backups

{serverRunning ? "Running" : "Stopped"}

Enable this on the source PC to serve the backups/ folder via WebDAV. The backup PC will connect to this machine to pull files.

setServerPort(Number(e.target.value))} />
{/* ── Receiver PC: Pull backups from source ── */}

Receiver PC — Pull Backups

Enable this on the backup PC to pull backup files from the source PC's WebDAV server at a scheduled time each day.

setSourceIp(e.target.value)} />
setSourcePort(Number(e.target.value))} />
{/* ── Auto-Import: restore latest backup to database ── */}

Auto-Import Database

Automatically restore the latest backup file from the backups/ folder into this PC's database after rclone finishes pulling.

{/* Save all settings */}
); } function RcloneSyncStatus() { const { data } = useQuery({ queryKey: ["/db/rclone-config"], queryFn: async () => { const res = await apiRequest("GET", "/api/database-management/rclone-config"); return res.json(); }, }); if (!data?.lastSyncAt) return null; const date = new Date(data.lastSyncAt).toLocaleString(); const ok = data.lastSyncStatus === "success"; return (
{ok ? "Last pull: " : "Last pull failed: "}{date} {!ok && data.lastSyncError && ( {data.lastSyncError} )}
); } function AutoImportStatus() { const { data } = useQuery({ queryKey: ["/db/rclone-config"], queryFn: async () => { const res = await apiRequest("GET", "/api/database-management/rclone-config"); return res.json(); }, }); if (!data?.lastImportAt) return null; const date = new Date(data.lastImportAt).toLocaleString(); const ok = data.lastImportStatus === "success"; return (
{ok ? "Last import: " : "Last import failed: "}{date} {!ok && data.lastImportError && ( {data.lastImportError} )}
); } // ============================================================ // Tab 2: API Key (existing behavior) // ============================================================ function ApiKeyBackupSection() { const { toast } = useToast(); const [showKey, setShowKey] = useState(false); const [showReceiverKey, setShowReceiverKey] = useState(false); const [confirmRegenOpen, setConfirmRegenOpen] = useState(false); const [enabled, setEnabled] = useState(false); const [syncHour, setSyncHour] = useState(0); const [sourceUrl, setSourceUrl] = useState(""); const [receiverApiKey, setReceiverApiKey] = useState(""); const [formLoaded, setFormLoaded] = useState(false); const { data: keyData } = useQuery({ queryKey: ["/db/network-backup-key"], queryFn: async () => { const res = await apiRequest("GET", "/api/database-management/network-backup-key"); return res.json() as Promise<{ apiKey: string }>; }, }); const { data: syncConfig } = useQuery({ queryKey: ["/db/network-sync-config"], queryFn: async () => { const res = await apiRequest("GET", "/api/database-management/network-sync-config"); return res.json(); }, }); useEffect(() => { if (syncConfig && !formLoaded) { setEnabled(syncConfig.enabled ?? false); setSyncHour(syncConfig.syncHour ?? 0); setSourceUrl(syncConfig.sourceUrl ?? ""); setReceiverApiKey(syncConfig.apiKey ?? ""); setFormLoaded(true); } }, [syncConfig, formLoaded]); const regenMutation = useMutation({ mutationFn: async () => { const res = await apiRequest("POST", "/api/database-management/network-backup-key/regenerate"); return res.json() as Promise<{ apiKey: string }>; }, onSuccess: (data) => { queryClient.setQueryData(["/db/network-backup-key"], data); setConfirmRegenOpen(false); toast({ title: "API key regenerated", description: "Update the key on your backup PC." }); }, onError: () => { toast({ title: "Error", description: "Failed to regenerate key.", variant: "destructive" }); }, }); const saveMutation = useMutation({ mutationFn: async () => { const res = await apiRequest("PUT", "/api/database-management/network-sync-config", { enabled, syncHour, sourceUrl: sourceUrl.trim(), apiKey: receiverApiKey.trim(), }); return res.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/db/network-sync-config"] }); toast({ title: "Network sync settings saved" }); }, onError: () => { toast({ title: "Error", description: "Failed to save settings.", variant: "destructive" }); }, }); const syncNowMutation = useMutation({ mutationFn: async () => { const res = await apiRequest("POST", "/api/database-management/network-sync-now"); if (!res.ok) { const body = await res.json(); throw new Error(body.details || body.error || "Sync failed"); } return res.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/db/network-sync-config"] }); toast({ title: "Sync complete", description: "Database synced from source PC." }); }, onError: (err: any) => { queryClient.invalidateQueries({ queryKey: ["/db/network-sync-config"] }); toast({ title: "Sync failed", description: err.message, variant: "destructive" }); }, }); const displayKey = keyData?.apiKey ?? ""; const maskedKey = displayKey ? "••••••••-••••-••••-••••-" + displayKey.slice(-12) : "—"; return (
{/* Source role */}

This Machine's Backup Key

Share this key with the backup PC so it can pull a copy of this machine's database. The key survives database restores (stored in a local file).

{/* Receiver role */}

Sync from Another PC

Configure this machine to pull a fresh copy of the database and all uploaded files (patient photos, cloud storage, documents) from another PC at a scheduled time each day. Enter the source PC's URL (e.g. http://192.168.0.94 — no port number) and the Backup Key shown in the source PC's Network Backup section.

setSourceUrl(e.target.value)} />
setReceiverApiKey(e.target.value)} />
{/* Confirm regenerate dialog */} Regenerate backup key? The old key will stop working immediately. You will need to update the API key on any backup PC that is currently configured to sync from this machine. setConfirmRegenOpen(false)}>Cancel regenMutation.mutate()} disabled={regenMutation.isPending} > Regenerate
); } function ApiKeySyncStatus() { const { data } = useQuery({ queryKey: ["/db/network-sync-config"], queryFn: async () => { const res = await apiRequest("GET", "/api/database-management/network-sync-config"); return res.json(); }, }); if (!data?.lastSyncAt) return null; const date = new Date(data.lastSyncAt).toLocaleString(); const ok = data.lastSyncStatus === "success"; return (
{ok ? "Last sync: " : "Last sync failed: "}{date} {!ok && data.lastSyncError && ( {data.lastSyncError} )}
); }