From 70b5e2ba47b5903ca48beff96afdd311981765ec Mon Sep 17 00:00:00 2001 From: ff Date: Wed, 24 Jun 2026 23:37:34 -0400 Subject: [PATCH] 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 --- apps/Backend/src/cron/backupCheck.ts | 56 +++++++------------ .../Backend/src/routes/database-management.ts | 28 +++++----- .../backup-destination-manager.tsx | 34 ++++++----- .../src/pages/database-management-page.tsx | 30 ++++++---- packages/db/prisma/schema.prisma | 2 + 5 files changed, 74 insertions(+), 76 deletions(-) diff --git a/apps/Backend/src/cron/backupCheck.ts b/apps/Backend/src/cron/backupCheck.ts index 0bc4f030..989aa203 100755 --- a/apps/Backend/src/cron/backupCheck.ts +++ b/apps/Backend/src/cron/backupCheck.ts @@ -55,26 +55,19 @@ async function getAdminUser() { 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 () => { - console.log("🔄 [8 PM] Running local auto-backup..."); - ensureLocalBackupDir(); - + cron.schedule("0 * * * *", async () => { const admin = await getAdminUser(); - if (!admin) { - console.warn("No admin user found, skipping local backup."); - return; - } + if (!admin) return; + if (!admin.autoBackupEnabled) return; - if (!admin.autoBackupEnabled) { - console.log("✅ [8 PM] Auto-backup is disabled for admin, skipped."); - const startedAt = new Date(); - const log = await cronJobLogStorage.createJobLog("local-backup", startedAt); - await cronJobLogStorage.completeJobLog(log.id, "skipped", new Date()); - await storage.deleteNotificationsByType(admin.id, "BACKUP"); - return; - } + const currentHour = new Date().getHours(); + const backupHour = admin.autoBackupHour ?? 20; + if (currentHour !== backupHour) return; + + console.log(`🔄 [${backupHour}:00] Running local auto-backup...`); + ensureLocalBackupDir(); const startedAt = new Date(); const log = await cronJobLogStorage.createJobLog("local-backup", startedAt); @@ -97,30 +90,21 @@ export const startBackupCron = () => { "❌ 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 () => { - console.log("🔄 [9 PM] Running USB backup..."); - + cron.schedule("0 * * * *", async () => { const admin = await getAdminUser(); - if (!admin) { - console.warn("No admin user found, skipping USB backup."); - return; - } + if (!admin) return; + if (!admin.usbBackupEnabled) return; - if (!admin.usbBackupEnabled) { - console.log("✅ [9 PM] USB backup is disabled for admin, skipped."); - const startedAt = new Date(); - const log = await cronJobLogStorage.createJobLog("usb-backup", startedAt); - await cronJobLogStorage.completeJobLog(log.id, "skipped", new Date()); - await storage.deleteNotificationsByType(admin.id, "BACKUP"); - return; - } + const currentHour = new Date().getHours(); + const backupHour = admin.usbBackupHour ?? 21; + if (currentHour !== backupHour) return; + + console.log(`🔄 [${backupHour}:00] Running USB backup...`); const startedAt = new Date(); 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." ); } - - console.log("✅ [9 PM] USB backup complete."); }); // ============================================================ diff --git a/apps/Backend/src/routes/database-management.ts b/apps/Backend/src/routes/database-management.ts index a29458bb..472d35ba 100755 --- a/apps/Backend/src/routes/database-management.ts +++ b/apps/Backend/src/routes/database-management.ts @@ -308,7 +308,7 @@ router.get("/usb-backup-setting", async (req, res) => { const user = await storage.getUser(userId); 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 @@ -316,15 +316,15 @@ router.put("/usb-backup-setting", async (req, res) => { const userId = req.user?.id; if (!userId) return res.status(401).json({ error: "Unauthorized" }); - const { usbBackupEnabled } = req.body; - if (typeof usbBackupEnabled !== "boolean") { - return res.status(400).json({ error: "usbBackupEnabled must be a boolean" }); - } + const { usbBackupEnabled, usbBackupHour } = req.body; + const patch: any = {}; + 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" }); - res.json({ usbBackupEnabled: updated.usbBackupEnabled }); + res.json({ usbBackupEnabled: updated.usbBackupEnabled, usbBackupHour: updated.usbBackupHour ?? 21 }); }); // GET auto backup setting @@ -335,7 +335,7 @@ router.get("/auto-backup-setting", async (req, res) => { const user = await storage.getUser(userId); 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 @@ -343,15 +343,15 @@ router.put("/auto-backup-setting", async (req, res) => { const userId = req.user?.id; if (!userId) return res.status(401).json({ error: "Unauthorized" }); - const { autoBackupEnabled } = req.body; - if (typeof autoBackupEnabled !== "boolean") { - return res.status(400).json({ error: "autoBackupEnabled must be a boolean" }); - } + const { autoBackupEnabled, autoBackupHour } = req.body; + const patch: any = {}; + 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" }); - res.json({ autoBackupEnabled: updated.autoBackupEnabled }); + res.json({ autoBackupEnabled: updated.autoBackupEnabled, autoBackupHour: updated.autoBackupHour ?? 20 }); }); router.post("/backup-path", async (req, res) => { diff --git a/apps/Frontend/src/components/database-management/backup-destination-manager.tsx b/apps/Frontend/src/components/database-management/backup-destination-manager.tsx index bba3f3b4..6f3a3f45 100755 --- a/apps/Frontend/src/components/database-management/backup-destination-manager.tsx +++ b/apps/Frontend/src/components/database-management/backup-destination-manager.tsx @@ -48,20 +48,16 @@ export function BackupDestinationManager() { }); const usbBackupEnabled = usbSettingData?.usbBackupEnabled ?? false; + const usbBackupHour = usbSettingData?.usbBackupHour ?? 21; const usbToggleMutation = useMutation({ - mutationFn: async (enabled: boolean) => { - const res = await apiRequest("PUT", "/api/database-management/usb-backup-setting", { - usbBackupEnabled: enabled, - }); + mutationFn: async (patch: { usbBackupEnabled?: boolean; usbBackupHour?: number }) => { + const res = await apiRequest("PUT", "/api/database-management/usb-backup-setting", patch); return res.json(); }, onSuccess: (data) => { queryClient.setQueryData(["/db/usb-backup-setting"], data); - toast({ - title: "Setting Saved", - description: `USB backup ${data.usbBackupEnabled ? "enabled" : "disabled"}.`, - }); + toast({ title: "Setting Saved" }); }, onError: () => { toast({ @@ -136,11 +132,11 @@ export function BackupDestinationManager() { External Backup Destination -
+
usbToggleMutation.mutate(checked)} + onCheckedChange={(checked) => usbToggleMutation.mutate({ usbBackupEnabled: checked })} disabled={usbToggleMutation.isPending} /> - - (daily at 9 PM → saves to the "USB Backup" folder on your drive) - +
+ + +

Enter the root path of your USB drive below. The app will automatically back up to the{" "} - USB Backup folder inside it every night at 9 PM when the toggle is on. + USB Backup folder inside it at the scheduled time when the toggle is on.

diff --git a/apps/Frontend/src/pages/database-management-page.tsx b/apps/Frontend/src/pages/database-management-page.tsx index 03a73823..ed913672 100755 --- a/apps/Frontend/src/pages/database-management-page.tsx +++ b/apps/Frontend/src/pages/database-management-page.tsx @@ -88,20 +88,16 @@ export default function DatabaseManagementPage() { }); const autoBackupEnabled = autoBackupData?.autoBackupEnabled ?? true; + const autoBackupHour = autoBackupData?.autoBackupHour ?? 20; const autoBackupMutation = useMutation({ - mutationFn: async (enabled: boolean) => { - const res = await apiRequest("PUT", "/api/database-management/auto-backup-setting", { - autoBackupEnabled: enabled, - }); + mutationFn: async (patch: { autoBackupEnabled?: boolean; autoBackupHour?: number }) => { + const res = await apiRequest("PUT", "/api/database-management/auto-backup-setting", patch); return res.json(); }, onSuccess: (data) => { queryClient.setQueryData(["/db/auto-backup-setting"], data); - toast({ - title: "Setting Saved", - description: `Automatic backup ${data.autoBackupEnabled ? "enabled" : "disabled"}.`, - }); + toast({ title: "Setting Saved" }); }, onError: () => { toast({ @@ -215,11 +211,11 @@ export default function DatabaseManagementPage() { including patients, appointments, claims, and all related data.

-
+
autoBackupMutation.mutate(checked)} + onCheckedChange={(checked) => autoBackupMutation.mutate({ autoBackupEnabled: checked })} disabled={autoBackupMutation.isPending} /> - (daily at 8 PM to server backup folder) +
+ + +
diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index d33eba00..4e615660 100755 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -23,7 +23,9 @@ model User { username String @unique password String autoBackupEnabled Boolean @default(true) + autoBackupHour Int @default(20) usbBackupEnabled Boolean @default(false) + usbBackupHour Int @default(21) patients Patient[] appointments Appointment[] staff Staff[]