feat: add auto-import toggle to restore latest backup after rclone pull
Adds autoImportEnabled/autoImportHour to rclone config. A separate hourly cron finds the latest .zip in backups/ and restores it to the database (drop schema, psql restore, apply migrations). Frontend shows toggle + time picker + Import Now button in the Receiver PC section. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,10 @@ function RcloneBackupSection() {
|
||||
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({
|
||||
@@ -107,6 +111,8 @@ function RcloneBackupSection() {
|
||||
setReceiverSyncHour(rcloneConfig.receiverSyncHour ?? 21);
|
||||
setSourceIp(rcloneConfig.sourceIp ?? "");
|
||||
setSourcePort(rcloneConfig.sourcePort ?? 8080);
|
||||
setAutoImportEnabled(rcloneConfig.autoImportEnabled ?? false);
|
||||
setAutoImportHour(rcloneConfig.autoImportHour ?? 22);
|
||||
setFormLoaded(true);
|
||||
}
|
||||
}, [rcloneConfig, formLoaded]);
|
||||
@@ -120,6 +126,8 @@ function RcloneBackupSection() {
|
||||
receiverSyncHour,
|
||||
sourceIp,
|
||||
sourcePort,
|
||||
autoImportEnabled,
|
||||
autoImportHour,
|
||||
});
|
||||
return res.json();
|
||||
},
|
||||
@@ -151,6 +159,25 @@ function RcloneBackupSection() {
|
||||
},
|
||||
});
|
||||
|
||||
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 (
|
||||
@@ -271,6 +298,60 @@ function RcloneBackupSection() {
|
||||
<RcloneSyncStatus />
|
||||
</div>
|
||||
|
||||
{/* ── Auto-Import: restore latest backup to database ── */}
|
||||
<div className="space-y-3 border rounded-lg p-4">
|
||||
<p className="text-sm font-semibold text-gray-800">Auto-Import Database</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
Automatically restore the latest backup file from the <code>backups/</code> folder
|
||||
into this PC's database after rclone finishes pulling.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="auto-import-toggle"
|
||||
checked={autoImportEnabled}
|
||||
onCheckedChange={setAutoImportEnabled}
|
||||
/>
|
||||
<label
|
||||
htmlFor="auto-import-toggle"
|
||||
className="text-sm font-medium text-gray-700 cursor-pointer select-none"
|
||||
>
|
||||
Enable auto-import
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-gray-600 whitespace-nowrap">at</label>
|
||||
<select
|
||||
className="border rounded px-2 py-1 text-sm text-gray-700 bg-white"
|
||||
value={autoImportHour}
|
||||
onChange={(e) => setAutoImportHour(Number(e.target.value))}
|
||||
>
|
||||
{HOUR_OPTIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => importNowMutation.mutate()}
|
||||
disabled={importNowMutation.isPending}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4 mr-1" />
|
||||
{importNowMutation.isPending ? "Importing..." : "Import Now"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<AutoImportStatus />
|
||||
</div>
|
||||
|
||||
{/* Save all settings */}
|
||||
<Button
|
||||
onClick={() => saveMutation.mutate()}
|
||||
@@ -306,6 +387,30 @@ function RcloneSyncStatus() {
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={`text-xs rounded p-2 ${ok ? "bg-green-50 text-green-700" : "bg-red-50 text-red-700"}`}>
|
||||
{ok ? "Last import: " : "Last import failed: "}{date}
|
||||
{!ok && data.lastImportError && (
|
||||
<span className="block mt-0.5 text-red-500">{data.lastImportError}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Tab 2: API Key (existing behavior)
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user