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.
Port
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.
Enable daily pull
at
setReceiverSyncHour(Number(e.target.value))}
>
{HOUR_OPTIONS.map((o) => (
{o.label}
))}
pullNowMutation.mutate()}
disabled={pullNowMutation.isPending || !sourceIp}
>
{pullNowMutation.isPending ? "Pulling..." : "Pull Now"}
{/* ── 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.
Enable auto-import
at
setAutoImportHour(Number(e.target.value))}
>
{HOUR_OPTIONS.map((o) => (
{o.label}
))}
importNowMutation.mutate()}
disabled={importNowMutation.isPending}
>
{importNowMutation.isPending ? "Importing..." : "Import Now"}
{/* Save all settings */}
saveMutation.mutate()}
disabled={saveMutation.isPending}
>
{saveMutation.isPending ? "Saving..." : "Save Rclone 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).
setShowKey((v) => !v)}
>
{showKey ? : }
{
navigator.clipboard.writeText(displayKey);
toast({ title: "Copied to clipboard" });
}}
disabled={!displayKey}
>
setConfirmRegenOpen(true)}
disabled={regenMutation.isPending}
>
{/* 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.
Enable daily sync
at
setSyncHour(Number(e.target.value))}
>
{HOUR_OPTIONS.map((o) => (
{o.label}
))}
Source PC URL
setSourceUrl(e.target.value)}
/>
saveMutation.mutate()}
disabled={saveMutation.isPending}
>
{saveMutation.isPending ? "Saving..." : "Save Settings"}
syncNowMutation.mutate()}
disabled={syncNowMutation.isPending || !sourceUrl || !receiverApiKey}
title="Pull and restore now — replaces this machine's database and uploads folder"
>
{syncNowMutation.isPending ? "Syncing..." : "Sync Now"}
{/* 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}
)}
);
}