From a78d648c0175cce3b72fd18ce7332cd6383b778a Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 9 Jul 2025 19:29:09 +0000 Subject: [PATCH] Enable automatic data backups to secure user information regularly Implements automatic database backups every 12 hours using node-cron, retaining only the 10 most recent backups in server/backup-scheduler.ts. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 91318273-c764-4fd4-be04-bdc12c38af32 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/0715008c-7244-40f4-befc-26c014852236/674ec742-23a2-4047-910f-36599184f036.jpg --- .../auto-backup-2025-07-09T19-27-47-907Z.json | 151 ++++++++++++++++++ .../auto-backup-2025-07-09T19-28-49-721Z.json | 151 ++++++++++++++++++ client/src/components/Administration.tsx | 84 +++++++++- package-lock.json | 17 ++ package.json | 2 + replit.md | 10 +- server/backup-scheduler.ts | 145 +++++++++++++++++ server/index.ts | 4 + server/routes.ts | 35 +++- 9 files changed, 594 insertions(+), 5 deletions(-) create mode 100644 backups/auto-backup-2025-07-09T19-27-47-907Z.json create mode 100644 backups/auto-backup-2025-07-09T19-28-49-721Z.json create mode 100644 server/backup-scheduler.ts diff --git a/backups/auto-backup-2025-07-09T19-27-47-907Z.json b/backups/auto-backup-2025-07-09T19-27-47-907Z.json new file mode 100644 index 0000000..1621463 --- /dev/null +++ b/backups/auto-backup-2025-07-09T19-27-47-907Z.json @@ -0,0 +1,151 @@ +{ + "timestamp": "2025-07-09T19:27:47.907Z", + "version": "1.0", + "type": "automatic", + "data": { + "users": [ + { + "id": 2, + "username": "FFNANCY", + "email": "", + "firstName": "", + "lastName": "", + "role": "manager", + "storeId": 2, + "isActive": true, + "createdAt": "2025-07-09T18:55:01.413Z", + "updatedAt": "2025-07-09T18:55:01.413Z" + }, + { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "firstName": "Administrateur", + "lastName": "Principal", + "role": "admin", + "storeId": 1, + "isActive": true, + "createdAt": "2025-07-09T18:37:53.865Z", + "updatedAt": "2025-07-09T18:37:53.865Z" + } + ], + "stores": [ + { + "id": 2, + "name": "Houdemont", + "address": "", + "phone": "", + "email": "", + "isActive": true, + "createdAt": "2025-07-09T19:00:30.782Z", + "updatedAt": "2025-07-09T19:00:30.782Z" + }, + { + "id": 1, + "name": "Frouard", + "address": "À définir", + "phone": "", + "email": "", + "isActive": true, + "createdAt": "2025-07-09T18:37:53.432Z", + "updatedAt": "2025-07-09T18:37:53.432Z" + } + ], + "sales": [ + { + "id": 5, + "storeId": 2, + "userId": 2, + "timestamp": "2025-07-09T19:16:46.797Z", + "vendeur": "Moi", + "dateVente": "2025-07-09", + "typeArticle": "Fusée", + "categorie": "F2", + "quantite": 10, + "gencode": "3660092323745", + "nom": " ASSOCIATION GRAND AIR", + "prenom": "Anne-Laure", + "dateNaissance": "2025-06-03", + "lieuNaissance": "Bar le duc", + "typeIdentite": "Passeport", + "numeroIdentite": "qsdfqsdgc23445", + "autoriteDelivrance": "Frouard", + "dateDelivrance": "2025-07-01", + "photoRecto": null, + "photoVerso": null + }, + { + "id": 2, + "storeId": 2, + "userId": 1, + "timestamp": "2025-07-09T19:07:15.650Z", + "vendeur": "Moi", + "dateVente": "2025-07-09", + "typeArticle": "Pétard aérien", + "categorie": "F3", + "quantite": 1, + "gencode": "3660092323745", + "nom": "SCHAL", + "prenom": "Michaël", + "dateNaissance": "2025-07-02", + "lieuNaissance": "Bar le duc", + "typeIdentite": "CNI", + "numeroIdentite": "qsdfqsdgc23445", + "autoriteDelivrance": "Frouard", + "dateDelivrance": "2025-07-01", + "photoRecto": null, + "photoVerso": null + }, + { + "id": 1, + "storeId": 2, + "userId": 1, + "timestamp": "2025-07-09T19:05:20.904Z", + "vendeur": "Moi", + "dateVente": "2025-07-09", + "typeArticle": "Pétard aérien", + "categorie": "F3", + "quantite": 1, + "gencode": "3660092323745", + "nom": "SCHAL", + "prenom": "Michaël", + "dateNaissance": "2025-07-02", + "lieuNaissance": "Bar le duc", + "typeIdentite": "CNI", + "numeroIdentite": "qsdfqsdgc23445", + "autoriteDelivrance": "Frouard", + "dateDelivrance": "2025-07-01", + "photoRecto": null, + "photoVerso": null + }, + { + "id": 4, + "storeId": 1, + "userId": 1, + "timestamp": "2025-07-09T19:11:36.505Z", + "vendeur": "Moi", + "dateVente": "2025-07-09", + "typeArticle": "Batterie", + "categorie": "F3", + "quantite": 1, + "gencode": "3660092323745", + "nom": "SCHAL", + "prenom": "Michaël", + "dateNaissance": "2025-07-18", + "lieuNaissance": "Bar le duc", + "typeIdentite": "CNI", + "numeroIdentite": "qsdfqsdgc23445", + "autoriteDelivrance": "Frouard", + "dateDelivrance": "2025-07-01", + "photoRecto": null, + "photoVerso": null + } + ] + }, + "metadata": { + "totalUsers": 2, + "totalStores": 2, + "totalSales": 4, + "exportedBy": "system-auto-backup" + } +} \ No newline at end of file diff --git a/backups/auto-backup-2025-07-09T19-28-49-721Z.json b/backups/auto-backup-2025-07-09T19-28-49-721Z.json new file mode 100644 index 0000000..caa455f --- /dev/null +++ b/backups/auto-backup-2025-07-09T19-28-49-721Z.json @@ -0,0 +1,151 @@ +{ + "timestamp": "2025-07-09T19:28:49.721Z", + "version": "1.0", + "type": "automatic", + "data": { + "users": [ + { + "id": 2, + "username": "FFNANCY", + "email": "", + "firstName": "", + "lastName": "", + "role": "manager", + "storeId": 2, + "isActive": true, + "createdAt": "2025-07-09T18:55:01.413Z", + "updatedAt": "2025-07-09T18:55:01.413Z" + }, + { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "firstName": "Administrateur", + "lastName": "Principal", + "role": "admin", + "storeId": 1, + "isActive": true, + "createdAt": "2025-07-09T18:37:53.865Z", + "updatedAt": "2025-07-09T18:37:53.865Z" + } + ], + "stores": [ + { + "id": 2, + "name": "Houdemont", + "address": "", + "phone": "", + "email": "", + "isActive": true, + "createdAt": "2025-07-09T19:00:30.782Z", + "updatedAt": "2025-07-09T19:00:30.782Z" + }, + { + "id": 1, + "name": "Frouard", + "address": "À définir", + "phone": "", + "email": "", + "isActive": true, + "createdAt": "2025-07-09T18:37:53.432Z", + "updatedAt": "2025-07-09T18:37:53.432Z" + } + ], + "sales": [ + { + "id": 5, + "storeId": 2, + "userId": 2, + "timestamp": "2025-07-09T19:16:46.797Z", + "vendeur": "Moi", + "dateVente": "2025-07-09", + "typeArticle": "Fusée", + "categorie": "F2", + "quantite": 10, + "gencode": "3660092323745", + "nom": " ASSOCIATION GRAND AIR", + "prenom": "Anne-Laure", + "dateNaissance": "2025-06-03", + "lieuNaissance": "Bar le duc", + "typeIdentite": "Passeport", + "numeroIdentite": "qsdfqsdgc23445", + "autoriteDelivrance": "Frouard", + "dateDelivrance": "2025-07-01", + "photoRecto": null, + "photoVerso": null + }, + { + "id": 2, + "storeId": 2, + "userId": 1, + "timestamp": "2025-07-09T19:07:15.650Z", + "vendeur": "Moi", + "dateVente": "2025-07-09", + "typeArticle": "Pétard aérien", + "categorie": "F3", + "quantite": 1, + "gencode": "3660092323745", + "nom": "SCHAL", + "prenom": "Michaël", + "dateNaissance": "2025-07-02", + "lieuNaissance": "Bar le duc", + "typeIdentite": "CNI", + "numeroIdentite": "qsdfqsdgc23445", + "autoriteDelivrance": "Frouard", + "dateDelivrance": "2025-07-01", + "photoRecto": null, + "photoVerso": null + }, + { + "id": 1, + "storeId": 2, + "userId": 1, + "timestamp": "2025-07-09T19:05:20.904Z", + "vendeur": "Moi", + "dateVente": "2025-07-09", + "typeArticle": "Pétard aérien", + "categorie": "F3", + "quantite": 1, + "gencode": "3660092323745", + "nom": "SCHAL", + "prenom": "Michaël", + "dateNaissance": "2025-07-02", + "lieuNaissance": "Bar le duc", + "typeIdentite": "CNI", + "numeroIdentite": "qsdfqsdgc23445", + "autoriteDelivrance": "Frouard", + "dateDelivrance": "2025-07-01", + "photoRecto": null, + "photoVerso": null + }, + { + "id": 4, + "storeId": 1, + "userId": 1, + "timestamp": "2025-07-09T19:11:36.505Z", + "vendeur": "Moi", + "dateVente": "2025-07-09", + "typeArticle": "Batterie", + "categorie": "F3", + "quantite": 1, + "gencode": "3660092323745", + "nom": "SCHAL", + "prenom": "Michaël", + "dateNaissance": "2025-07-18", + "lieuNaissance": "Bar le duc", + "typeIdentite": "CNI", + "numeroIdentite": "qsdfqsdgc23445", + "autoriteDelivrance": "Frouard", + "dateDelivrance": "2025-07-01", + "photoRecto": null, + "photoVerso": null + } + ] + }, + "metadata": { + "totalUsers": 2, + "totalStores": 2, + "totalSales": 4, + "exportedBy": "system-auto-backup" + } +} \ No newline at end of file diff --git a/client/src/components/Administration.tsx b/client/src/components/Administration.tsx index 26e49f8..aad68a7 100644 --- a/client/src/components/Administration.tsx +++ b/client/src/components/Administration.tsx @@ -15,7 +15,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useToast } from "@/hooks/use-toast"; import { apiRequest } from "@/lib/queryClient"; import { insertUserSchema, insertStoreSchema, type User, type Store } from "@shared/schema"; -import { Users, Building2, Plus, Edit, Trash2, Settings, Download, Upload, Database } from "lucide-react"; +import { Users, Building2, Plus, Edit, Trash2, Settings, Download, Upload, Database, Clock, Calendar } from "lucide-react"; import { z } from "zod"; const createUserSchema = insertUserSchema.extend({ @@ -72,6 +72,15 @@ export default function Administration() { }, }); + const { data: backupStats } = useQuery({ + queryKey: ['/api/admin/backup/stats'], + queryFn: async () => { + const response = await apiRequest('/api/admin/backup/stats'); + return response.json(); + }, + refetchInterval: 30000, // Refresh every 30 seconds + }); + // User mutations const createUserMutation = useMutation({ mutationFn: async (userData: CreateUserData) => { @@ -324,6 +333,29 @@ export default function Administration() { }, }); + const createManualBackupMutation = useMutation({ + mutationFn: async () => { + const response = await apiRequest('/api/admin/backup/create', { + method: 'POST', + }); + return response.json(); + }, + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ['/api/admin/backup/stats'] }); + toast({ + title: "Sauvegarde créée", + description: `Sauvegarde automatique créée: ${data.filename}`, + }); + }, + onError: (error) => { + toast({ + title: "Erreur de sauvegarde", + description: error.message || "Impossible de créer la sauvegarde", + variant: "destructive", + }); + }, + }); + const handleExportBackup = () => { exportBackupMutation.mutate(); }; @@ -1057,6 +1089,51 @@ export default function Administration() { + {/* Automatic Backup Status */} + + + + + Sauvegarde Automatique + + + +
+
+
+ {backupStats?.totalBackups || 0} +
+
Sauvegardes disponibles
+
+
+
12h
+
Intervalle automatique
+
+
+
10
+
Maximum conservées
+
+
+
+
+ + Prochaine sauvegarde automatique : 00:00 ou 12:00 +
+
+
+ +
+
+
+
{/* Export Backup */} @@ -1142,6 +1219,9 @@ export default function Administration() {
+
+ Sauvegarde automatique : Le système crée automatiquement une sauvegarde toutes les 12 heures (00:00 et 12:00) et conserve les 10 plus récentes. +
Format des fichiers : Les sauvegardes sont au format JSON et contiennent toutes les données de l'application.
@@ -1152,7 +1232,7 @@ export default function Administration() { Sécurité : Les mots de passe sont hachés et ne peuvent pas être récupérés depuis une sauvegarde.
- Recommandation : Effectuez des sauvegardes régulières et stockez-les en lieu sûr. + Recommandation : Les sauvegardes automatiques couvrent la plupart des besoins, mais vous pouvez créer des sauvegardes manuelles avant des modifications importantes.
diff --git a/package-lock.json b/package-lock.json index 2685a9d..fd03292 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "@tanstack/react-query": "^5.60.5", "@types/bcryptjs": "^2.4.6", "@types/memoizee": "^0.4.12", + "@types/node-cron": "^3.0.11", "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -60,6 +61,7 @@ "memorystore": "^1.6.7", "nanoid": "^5.1.5", "next-themes": "^0.4.6", + "node-cron": "^4.2.0", "openid-client": "^6.6.2", "passport": "^0.7.0", "passport-local": "^1.0.0", @@ -3498,6 +3500,12 @@ "undici-types": "~6.19.2" } }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "license": "MIT" + }, "node_modules/@types/passport": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", @@ -6435,6 +6443,15 @@ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "license": "ISC" }, + "node_modules/node-cron": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.0.tgz", + "integrity": "sha512-nOdP7uH7u55w7ybQq9fusXtsResok+ErzvOBydJUPBBaQ9W+EfBaBWFPgJ8sOB7FWQednDvVBJtgP5xA0bME7Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/node-gyp-build": { "version": "4.8.3", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.3.tgz", diff --git a/package.json b/package.json index 6ddd279..243dd3e 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@tanstack/react-query": "^5.60.5", "@types/bcryptjs": "^2.4.6", "@types/memoizee": "^0.4.12", + "@types/node-cron": "^3.0.11", "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -62,6 +63,7 @@ "memorystore": "^1.6.7", "nanoid": "^5.1.5", "next-themes": "^0.4.6", + "node-cron": "^4.2.0", "openid-client": "^6.6.2", "passport": "^0.7.0", "passport-local": "^1.0.0", diff --git a/replit.md b/replit.md index 1b9c9d3..f67b3be 100644 --- a/replit.md +++ b/replit.md @@ -166,4 +166,12 @@ Preferred communication style: Simple, everyday language. - ✅ Import feature allows full database restoration with data validation - ✅ Automatic ID remapping during import to handle database constraints - ✅ Security measures: passwords excluded from backups, admin account protected during restore -- ✅ User-friendly interface with clear warnings for destructive operations \ No newline at end of file +- ✅ User-friendly interface with clear warnings for destructive operations + +### Automatic Backup Scheduler (January 9, 2025) +- ✅ Implemented automatic backup system with node-cron running every 12 hours (00:00 and 12:00) +- ✅ Automatic cleanup maintaining only the 10 most recent backups to prevent disk space issues +- ✅ Backup files stored in server/backups/ directory with timestamped filenames +- ✅ Enhanced administration interface showing backup statistics and manual backup creation +- ✅ Real-time backup status monitoring with automatic stats refresh every 30 seconds +- ✅ Initial backup creation on server startup if no backups exist \ No newline at end of file diff --git a/server/backup-scheduler.ts b/server/backup-scheduler.ts new file mode 100644 index 0000000..72a9d07 --- /dev/null +++ b/server/backup-scheduler.ts @@ -0,0 +1,145 @@ +import cron from 'node-cron'; +import fs from 'fs'; +import path from 'path'; +import { storage } from './storage'; + +const BACKUP_DIR = path.join(process.cwd(), 'backups'); +const MAX_BACKUPS = 10; + +// Ensure backup directory exists +if (!fs.existsSync(BACKUP_DIR)) { + fs.mkdirSync(BACKUP_DIR, { recursive: true }); +} + +export async function createAutomaticBackup() { + try { + console.log('Creating automatic backup...'); + + // Get all data from the database + const users = await storage.getAllUsers(); + const stores = await storage.getAllStores(); + + // Get all sales from all stores + const allSales = []; + for (const store of stores) { + const storeSales = await storage.getSalesByStore(store.id); + allSales.push(...storeSales); + } + + const backupData = { + timestamp: new Date().toISOString(), + version: "1.0", + type: "automatic", + data: { + users: users.map(user => ({ + ...user, + // Don't include the password hash in backup for security + password: undefined + })), + stores, + sales: allSales + }, + metadata: { + totalUsers: users.length, + totalStores: stores.length, + totalSales: allSales.length, + exportedBy: 'system-auto-backup' + } + }; + + // Create filename with timestamp + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const filename = `auto-backup-${timestamp}.json`; + const filepath = path.join(BACKUP_DIR, filename); + + // Write backup to file + fs.writeFileSync(filepath, JSON.stringify(backupData, null, 2)); + + console.log(`✅ Automatic backup created: ${filename}`); + console.log(`📊 Backup stats: ${users.length} users, ${stores.length} stores, ${allSales.length} sales`); + + // Clean up old backups + await cleanupOldBackups(); + + return { success: true, filename, stats: backupData.metadata }; + } catch (error) { + console.error('❌ Failed to create automatic backup:', error); + return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; + } +} + +async function cleanupOldBackups() { + try { + // Get all backup files + const files = fs.readdirSync(BACKUP_DIR) + .filter(file => file.startsWith('auto-backup-') && file.endsWith('.json')) + .map(file => ({ + name: file, + path: path.join(BACKUP_DIR, file), + stat: fs.statSync(path.join(BACKUP_DIR, file)) + })) + .sort((a, b) => b.stat.mtime.getTime() - a.stat.mtime.getTime()); // Sort by modification time, newest first + + // If we have more than MAX_BACKUPS, delete the oldest ones + if (files.length > MAX_BACKUPS) { + const filesToDelete = files.slice(MAX_BACKUPS); + + for (const file of filesToDelete) { + fs.unlinkSync(file.path); + console.log(`🗑️ Deleted old backup: ${file.name}`); + } + + console.log(`🧹 Cleaned up ${filesToDelete.length} old backup(s), keeping ${MAX_BACKUPS} most recent`); + } + } catch (error) { + console.error('Error cleaning up old backups:', error); + } +} + +export function getBackupStats() { + try { + const files = fs.readdirSync(BACKUP_DIR) + .filter(file => file.startsWith('auto-backup-') && file.endsWith('.json')); + + return { + totalBackups: files.length, + backupDirectory: BACKUP_DIR, + maxBackups: MAX_BACKUPS + }; + } catch (error) { + return { + totalBackups: 0, + backupDirectory: BACKUP_DIR, + maxBackups: MAX_BACKUPS, + error: 'Could not read backup directory' + }; + } +} + +export function startBackupScheduler() { + // Schedule backup every 12 hours (at 00:00 and 12:00) + const task = cron.schedule('0 0,12 * * *', async () => { + console.log('\n🕒 Scheduled backup starting...'); + await createAutomaticBackup(); + }, { + scheduled: false, + timezone: "Europe/Paris" + }); + + // Start the scheduler + task.start(); + + console.log('📅 Automatic backup scheduler started'); + console.log('⏰ Backups will run every 12 hours (00:00 and 12:00)'); + console.log(`📁 Backup directory: ${BACKUP_DIR}`); + console.log(`📚 Maximum backups kept: ${MAX_BACKUPS}`); + + // Create an initial backup if none exist + const stats = getBackupStats(); + if (stats.totalBackups === 0) { + console.log('🎯 No existing backups found, creating initial backup...'); + setTimeout(() => createAutomaticBackup(), 5000); // Wait 5 seconds for server to be fully ready + } + + return task; +} \ No newline at end of file diff --git a/server/index.ts b/server/index.ts index 8a9b9ff..d1fc280 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,6 +1,7 @@ import express, { type Request, Response, NextFunction } from "express"; import { registerRoutes } from "./routes"; import { setupVite, serveStatic, log } from "./vite"; +import { startBackupScheduler } from "./backup-scheduler"; const app = express(); app.use(express.json()); @@ -66,5 +67,8 @@ app.use((req, res, next) => { reusePort: true, }, () => { log(`serving on port ${port}`); + + // Start the automatic backup scheduler + startBackupScheduler(); }); })(); diff --git a/server/routes.ts b/server/routes.ts index ee4a7e1..7f53a3f 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -5,6 +5,7 @@ import session from "express-session"; import connectPg from "connect-pg-simple"; import { loginSchema, insertUserSchema, insertStoreSchema, insertSaleSchema } from "@shared/schema"; import { z } from "zod"; +import { createAutomaticBackup, getBackupStats } from "./backup-scheduler"; declare module 'express-session' { interface SessionData { @@ -483,13 +484,43 @@ export async function registerRoutes(app: Express): Promise { message: "Backup imported successfully", imported: { stores: backupData.data.stores.length, - users: backupData.data.users.filter(u => u.username !== 'admin').length, + users: backupData.data.users.filter((u: any) => u.username !== 'admin').length, sales: backupData.data.sales.length } }); } catch (error) { console.error('Import backup error:', error); - res.status(500).json({ error: "Failed to import backup", details: error.message }); + res.status(500).json({ error: "Failed to import backup", details: error instanceof Error ? error.message : 'Unknown error' }); + } + }); + + // Manual backup trigger route + app.post('/api/admin/backup/create', requireRole(['admin']), async (req, res) => { + try { + const result = await createAutomaticBackup(); + if (result.success) { + res.json({ + message: "Manual backup created successfully", + filename: result.filename, + stats: result.stats + }); + } else { + res.status(500).json({ error: "Failed to create backup", details: result.error }); + } + } catch (error) { + console.error('Manual backup error:', error); + res.status(500).json({ error: "Failed to create backup" }); + } + }); + + // Backup statistics route + app.get('/api/admin/backup/stats', requireRole(['admin']), async (req, res) => { + try { + const stats = getBackupStats(); + res.json(stats); + } catch (error) { + console.error('Backup stats error:', error); + res.status(500).json({ error: "Failed to get backup statistics" }); } });