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 { Copy, Eye, EyeOff, RefreshCw, Network, RotateCcw } 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() { const { toast } = useToast(); const [showKey, setShowKey] = useState(false); const [confirmRegenOpen, setConfirmRegenOpen] = useState(false); // receiver form state (initialised from query) const [enabled, setEnabled] = useState(false); const [syncHour, setSyncHour] = useState(0); const [sourceUrl, setSourceUrl] = useState(""); const [receiverApiKey, setReceiverApiKey] = useState(""); const [formLoaded, setFormLoaded] = useState(false); // ============================== // Queries // ============================== 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 }>; }, }); useQuery({ queryKey: ["/db/network-sync-config"], queryFn: async () => { const res = await apiRequest("GET", "/api/database-management/network-sync-config"); const data = await res.json(); if (!formLoaded) { setEnabled(data.enabled ?? false); setSyncHour(data.syncHour ?? 0); setSourceUrl(data.sourceUrl ?? ""); setReceiverApiKey(data.apiKey ?? ""); setFormLoaded(true); } return data; }, }); // ============================== // Mutations // ============================== 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 ( Network Backup {/* ── Section A: Source role (this machine's API key) ── */}

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).

{/* ── Section B: Receiver role (pull config) ── */}

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 toggle + time picker on same row */}
setSourceUrl(e.target.value)} />
setReceiverApiKey(e.target.value)} />
{/* Last sync status */}
{/* 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 SyncStatus() { 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: {date} {!ok && data.lastSyncError && ( {data.lastSyncError} )}
); }