feat: add configurable backup time for local and USB backups

Replace hardcoded 8 PM / 9 PM backup schedules with user-selectable
hour dropdowns. Adds autoBackupHour and usbBackupHour fields to User
model. Cron jobs now check every hour against the configured time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-24 23:37:34 -04:00
parent 5e881c9ff7
commit 70b5e2ba47
5 changed files with 74 additions and 76 deletions

View File

@@ -55,26 +55,19 @@ async function getAdminUser() {
export const startBackupCron = () => { export const startBackupCron = () => {
// ============================================================ // ============================================================
// 8 PM — Local automatic backup to apps/Backend/backups/ // Every hour — Local automatic backup (runs when hour matches setting)
// ============================================================ // ============================================================
cron.schedule("0 20 * * *", async () => { cron.schedule("0 * * * *", async () => {
console.log("🔄 [8 PM] Running local auto-backup...");
ensureLocalBackupDir();
const admin = await getAdminUser(); const admin = await getAdminUser();
if (!admin) { if (!admin) return;
console.warn("No admin user found, skipping local backup."); if (!admin.autoBackupEnabled) return;
return;
}
if (!admin.autoBackupEnabled) { const currentHour = new Date().getHours();
console.log("✅ [8 PM] Auto-backup is disabled for admin, skipped."); const backupHour = admin.autoBackupHour ?? 20;
const startedAt = new Date(); if (currentHour !== backupHour) return;
const log = await cronJobLogStorage.createJobLog("local-backup", startedAt);
await cronJobLogStorage.completeJobLog(log.id, "skipped", new Date()); console.log(`🔄 [${backupHour}:00] Running local auto-backup...`);
await storage.deleteNotificationsByType(admin.id, "BACKUP"); ensureLocalBackupDir();
return;
}
const startedAt = new Date(); const startedAt = new Date();
const log = await cronJobLogStorage.createJobLog("local-backup", startedAt); const log = await cronJobLogStorage.createJobLog("local-backup", startedAt);
@@ -97,30 +90,21 @@ export const startBackupCron = () => {
"❌ Automatic backup failed. Please check the server backup folder." "❌ Automatic backup failed. Please check the server backup folder."
); );
} }
console.log("✅ [8 PM] Local backup complete.");
}); });
// ============================================================ // ============================================================
// 9 PM — USB backup to the "USB Backup" folder on the drive // Every hour — USB backup (runs when hour matches setting)
// ============================================================ // ============================================================
cron.schedule("0 21 * * *", async () => { cron.schedule("0 * * * *", async () => {
console.log("🔄 [9 PM] Running USB backup...");
const admin = await getAdminUser(); const admin = await getAdminUser();
if (!admin) { if (!admin) return;
console.warn("No admin user found, skipping USB backup."); if (!admin.usbBackupEnabled) return;
return;
}
if (!admin.usbBackupEnabled) { const currentHour = new Date().getHours();
console.log("✅ [9 PM] USB backup is disabled for admin, skipped."); const backupHour = admin.usbBackupHour ?? 21;
const startedAt = new Date(); if (currentHour !== backupHour) return;
const log = await cronJobLogStorage.createJobLog("usb-backup", startedAt);
await cronJobLogStorage.completeJobLog(log.id, "skipped", new Date()); console.log(`🔄 [${backupHour}:00] Running USB backup...`);
await storage.deleteNotificationsByType(admin.id, "BACKUP");
return;
}
const startedAt = new Date(); const startedAt = new Date();
const log = await cronJobLogStorage.createJobLog("usb-backup", startedAt); const log = await cronJobLogStorage.createJobLog("usb-backup", startedAt);
@@ -172,8 +156,6 @@ export const startBackupCron = () => {
"❌ USB backup failed. Please check the USB drive and try again." "❌ USB backup failed. Please check the USB drive and try again."
); );
} }
console.log("✅ [9 PM] USB backup complete.");
}); });
// ============================================================ // ============================================================

View File

@@ -308,7 +308,7 @@ router.get("/usb-backup-setting", async (req, res) => {
const user = await storage.getUser(userId); const user = await storage.getUser(userId);
if (!user) return res.status(404).json({ error: "User not found" }); if (!user) return res.status(404).json({ error: "User not found" });
res.json({ usbBackupEnabled: user.usbBackupEnabled }); res.json({ usbBackupEnabled: user.usbBackupEnabled, usbBackupHour: user.usbBackupHour ?? 21 });
}); });
// PUT usb backup setting // PUT usb backup setting
@@ -316,15 +316,15 @@ router.put("/usb-backup-setting", async (req, res) => {
const userId = req.user?.id; const userId = req.user?.id;
if (!userId) return res.status(401).json({ error: "Unauthorized" }); if (!userId) return res.status(401).json({ error: "Unauthorized" });
const { usbBackupEnabled } = req.body; const { usbBackupEnabled, usbBackupHour } = req.body;
if (typeof usbBackupEnabled !== "boolean") { const patch: any = {};
return res.status(400).json({ error: "usbBackupEnabled must be a boolean" }); if (typeof usbBackupEnabled === "boolean") patch.usbBackupEnabled = usbBackupEnabled;
} if (typeof usbBackupHour === "number") patch.usbBackupHour = usbBackupHour;
const updated = await storage.updateUser(userId, { usbBackupEnabled }); const updated = await storage.updateUser(userId, patch);
if (!updated) return res.status(404).json({ error: "User not found" }); if (!updated) return res.status(404).json({ error: "User not found" });
res.json({ usbBackupEnabled: updated.usbBackupEnabled }); res.json({ usbBackupEnabled: updated.usbBackupEnabled, usbBackupHour: updated.usbBackupHour ?? 21 });
}); });
// GET auto backup setting // GET auto backup setting
@@ -335,7 +335,7 @@ router.get("/auto-backup-setting", async (req, res) => {
const user = await storage.getUser(userId); const user = await storage.getUser(userId);
if (!user) return res.status(404).json({ error: "User not found" }); if (!user) return res.status(404).json({ error: "User not found" });
res.json({ autoBackupEnabled: user.autoBackupEnabled }); res.json({ autoBackupEnabled: user.autoBackupEnabled, autoBackupHour: user.autoBackupHour ?? 20 });
}); });
// PUT auto backup setting // PUT auto backup setting
@@ -343,15 +343,15 @@ router.put("/auto-backup-setting", async (req, res) => {
const userId = req.user?.id; const userId = req.user?.id;
if (!userId) return res.status(401).json({ error: "Unauthorized" }); if (!userId) return res.status(401).json({ error: "Unauthorized" });
const { autoBackupEnabled } = req.body; const { autoBackupEnabled, autoBackupHour } = req.body;
if (typeof autoBackupEnabled !== "boolean") { const patch: any = {};
return res.status(400).json({ error: "autoBackupEnabled must be a boolean" }); if (typeof autoBackupEnabled === "boolean") patch.autoBackupEnabled = autoBackupEnabled;
} if (typeof autoBackupHour === "number") patch.autoBackupHour = autoBackupHour;
const updated = await storage.updateUser(userId, { autoBackupEnabled }); const updated = await storage.updateUser(userId, patch);
if (!updated) return res.status(404).json({ error: "User not found" }); if (!updated) return res.status(404).json({ error: "User not found" });
res.json({ autoBackupEnabled: updated.autoBackupEnabled }); res.json({ autoBackupEnabled: updated.autoBackupEnabled, autoBackupHour: updated.autoBackupHour ?? 20 });
}); });
router.post("/backup-path", async (req, res) => { router.post("/backup-path", async (req, res) => {

View File

@@ -48,20 +48,16 @@ export function BackupDestinationManager() {
}); });
const usbBackupEnabled = usbSettingData?.usbBackupEnabled ?? false; const usbBackupEnabled = usbSettingData?.usbBackupEnabled ?? false;
const usbBackupHour = usbSettingData?.usbBackupHour ?? 21;
const usbToggleMutation = useMutation({ const usbToggleMutation = useMutation({
mutationFn: async (enabled: boolean) => { mutationFn: async (patch: { usbBackupEnabled?: boolean; usbBackupHour?: number }) => {
const res = await apiRequest("PUT", "/api/database-management/usb-backup-setting", { const res = await apiRequest("PUT", "/api/database-management/usb-backup-setting", patch);
usbBackupEnabled: enabled,
});
return res.json(); return res.json();
}, },
onSuccess: (data) => { onSuccess: (data) => {
queryClient.setQueryData(["/db/usb-backup-setting"], data); queryClient.setQueryData(["/db/usb-backup-setting"], data);
toast({ toast({ title: "Setting Saved" });
title: "Setting Saved",
description: `USB backup ${data.usbBackupEnabled ? "enabled" : "disabled"}.`,
});
}, },
onError: () => { onError: () => {
toast({ toast({
@@ -136,11 +132,11 @@ export function BackupDestinationManager() {
<CardTitle>External Backup Destination</CardTitle> <CardTitle>External Backup Destination</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex items-center space-x-3"> <div className="flex flex-wrap items-center gap-3">
<Switch <Switch
id="usb-backup-toggle" id="usb-backup-toggle"
checked={usbBackupEnabled} checked={usbBackupEnabled}
onCheckedChange={(checked) => usbToggleMutation.mutate(checked)} onCheckedChange={(checked) => usbToggleMutation.mutate({ usbBackupEnabled: checked })}
disabled={usbToggleMutation.isPending} disabled={usbToggleMutation.isPending}
/> />
<label <label
@@ -149,14 +145,24 @@ export function BackupDestinationManager() {
> >
USB Backup USB Backup
</label> </label>
<span className="text-xs text-gray-400"> <div className="flex items-center gap-2">
(daily at 9 PM saves to the &quot;USB Backup&quot; folder on your drive) <label className="text-sm text-gray-600">at</label>
</span> <select
className="border rounded px-2 py-1 text-sm text-gray-700 bg-white"
value={usbBackupHour}
onChange={(e) => usbToggleMutation.mutate({ usbBackupHour: Number(e.target.value) })}
>
{Array.from({ length: 24 }, (_, h) => {
const label = h === 0 ? "12:00 AM" : h < 12 ? `${h}:00 AM` : h === 12 ? "12:00 PM" : `${h - 12}:00 PM`;
return <option key={h} value={h}>{label}</option>;
})}
</select>
</div>
</div> </div>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500">
Enter the root path of your USB drive below. The app will automatically back up to the{" "} 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. <span className="font-medium text-gray-700">USB Backup</span> folder inside it at the scheduled time when the toggle is on.
</p> </p>
<div className="flex gap-2"> <div className="flex gap-2">

View File

@@ -88,20 +88,16 @@ export default function DatabaseManagementPage() {
}); });
const autoBackupEnabled = autoBackupData?.autoBackupEnabled ?? true; const autoBackupEnabled = autoBackupData?.autoBackupEnabled ?? true;
const autoBackupHour = autoBackupData?.autoBackupHour ?? 20;
const autoBackupMutation = useMutation({ const autoBackupMutation = useMutation({
mutationFn: async (enabled: boolean) => { mutationFn: async (patch: { autoBackupEnabled?: boolean; autoBackupHour?: number }) => {
const res = await apiRequest("PUT", "/api/database-management/auto-backup-setting", { const res = await apiRequest("PUT", "/api/database-management/auto-backup-setting", patch);
autoBackupEnabled: enabled,
});
return res.json(); return res.json();
}, },
onSuccess: (data) => { onSuccess: (data) => {
queryClient.setQueryData(["/db/auto-backup-setting"], data); queryClient.setQueryData(["/db/auto-backup-setting"], data);
toast({ toast({ title: "Setting Saved" });
title: "Setting Saved",
description: `Automatic backup ${data.autoBackupEnabled ? "enabled" : "disabled"}.`,
});
}, },
onError: () => { onError: () => {
toast({ toast({
@@ -215,11 +211,11 @@ export default function DatabaseManagementPage() {
including patients, appointments, claims, and all related data. including patients, appointments, claims, and all related data.
</p> </p>
<div className="flex items-center space-x-3"> <div className="flex flex-wrap items-center gap-3">
<Switch <Switch
id="auto-backup-toggle" id="auto-backup-toggle"
checked={autoBackupEnabled} checked={autoBackupEnabled}
onCheckedChange={(checked) => autoBackupMutation.mutate(checked)} onCheckedChange={(checked) => autoBackupMutation.mutate({ autoBackupEnabled: checked })}
disabled={autoBackupMutation.isPending} disabled={autoBackupMutation.isPending}
/> />
<label <label
@@ -228,7 +224,19 @@ export default function DatabaseManagementPage() {
> >
Automatic Backup Automatic Backup
</label> </label>
<span className="text-xs text-gray-400">(daily at 8 PM to server backup folder)</span> <div className="flex items-center gap-2">
<label className="text-sm text-gray-600">at</label>
<select
className="border rounded px-2 py-1 text-sm text-gray-700 bg-white"
value={autoBackupHour}
onChange={(e) => autoBackupMutation.mutate({ autoBackupHour: Number(e.target.value) })}
>
{Array.from({ length: 24 }, (_, h) => {
const label = h === 0 ? "12:00 AM" : h < 12 ? `${h}:00 AM` : h === 12 ? "12:00 PM" : `${h - 12}:00 PM`;
return <option key={h} value={h}>{label}</option>;
})}
</select>
</div>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">

View File

@@ -23,7 +23,9 @@ model User {
username String @unique username String @unique
password String password String
autoBackupEnabled Boolean @default(true) autoBackupEnabled Boolean @default(true)
autoBackupHour Int @default(20)
usbBackupEnabled Boolean @default(false) usbBackupEnabled Boolean @default(false)
usbBackupHour Int @default(21)
patients Patient[] patients Patient[]
appointments Appointment[] appointments Appointment[]
staff Staff[] staff Staff[]