From e32f83de1f475df0a86be951a4fbdfef03b84602 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 13 Aug 2025 20:27:55 +0000 Subject: [PATCH 1/4] Reduce excessive console logging to improve application performance Fix: Remove numerous console logs that were causing latency and performance issues in production environments. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7c468936-2a88-4686-ac0a-84921a45222d Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/7c468936-2a88-4686-ac0a-84921a45222d/B7k6m01 --- attached_assets/image_1755116495530.png | Bin 0 -> 22101 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 attached_assets/image_1755116495530.png diff --git a/attached_assets/image_1755116495530.png b/attached_assets/image_1755116495530.png new file mode 100644 index 0000000000000000000000000000000000000000..1ca376aa7dd14db8182a4e885baa1565e4a338b7 GIT binary patch literal 22101 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 literal 0 HcmV?d00001 From 3b46f945a4c2b5b95598c332136f276c93fa56a7 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 13 Aug 2025 20:34:02 +0000 Subject: [PATCH 2/4] Stabilize calendar display by refining data caching and invalidation Fix an issue where closing an order or delivery card caused all calendar entries to disappear by implementing selective cache invalidation and adjusting cache timings. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7c468936-2a88-4686-ac0a-84921a45222d Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/7c468936-2a88-4686-ac0a-84921a45222d/B7k6m01 --- client/src/components/CalendarGrid.tsx | 36 +++++----- .../components/modals/OrderDetailModal.tsx | 39 ++++------- client/src/pages/Calendar.tsx | 65 +++++++++++-------- cookies.txt | 2 +- replit.md | 8 ++- 5 files changed, 80 insertions(+), 70 deletions(-) diff --git a/client/src/components/CalendarGrid.tsx b/client/src/components/CalendarGrid.tsx index aa908bf..64ea724 100644 --- a/client/src/components/CalendarGrid.tsx +++ b/client/src/components/CalendarGrid.tsx @@ -171,14 +171,16 @@ export default function CalendarGrid({ onDateClick, onItemClick, }: CalendarGridProps) { - console.log('🗓️ CalendarGrid rendered with:', { - currentDate: currentDate?.toISOString(), - ordersCount: orders?.length || 0, - deliveriesCount: deliveries?.length || 0, - publicitiesCount: publicities?.length || 0, - selectedStoreId, - userGroupsCount: userGroups?.length || 0 - }); + if (import.meta.env.DEV) { + console.log('🗓️ CalendarGrid rendered with:', { + currentDate: currentDate?.toISOString(), + ordersCount: orders?.length || 0, + deliveriesCount: deliveries?.length || 0, + publicitiesCount: publicities?.length || 0, + selectedStoreId, + userGroupsCount: userGroups?.length || 0 + }); + } const monthStart = startOfMonth(currentDate); const monthEnd = endOfMonth(currentDate); @@ -192,12 +194,14 @@ export default function CalendarGrid({ return []; } - console.log('🔍 CalendarGrid getPublicitiesForDate:', { - date: dateStr, - totalPublicities: publicities.length, - selectedStoreId, - userGroups: userGroups?.length || 0 - }); + if (import.meta.env.DEV) { + console.log('🔍 CalendarGrid getPublicitiesForDate:', { + date: dateStr, + totalPublicities: publicities.length, + selectedStoreId, + userGroups: userGroups?.length || 0 + }); + } try { return publicities.filter(pub => { @@ -222,7 +226,7 @@ export default function CalendarGrid({ // If no store is selected and user has no assigned groups, show only publicities with participations if (!selectedStoreId && (!userGroups || userGroups.length === 0)) { const hasParticipations = pub.participations && Array.isArray(pub.participations) && pub.participations.length > 0; - if (hasParticipations) { + if (hasParticipations && import.meta.env.DEV) { console.log('📋 Publicity has participations (no store selected):', { pubNumber: pub.pubNumber, participationCount: pub.participations.length }); } return hasParticipations; @@ -232,7 +236,7 @@ export default function CalendarGrid({ if (selectedStoreId) { const matches = pub.participations && Array.isArray(pub.participations) && pub.participations.some((pg: any) => pg?.groupId === selectedStoreId); - if (matches) { + if (matches && import.meta.env.DEV) { console.log('🎯 Publicity matches selected store:', { pubNumber: pub.pubNumber, selectedStoreId }); } return matches; diff --git a/client/src/components/modals/OrderDetailModal.tsx b/client/src/components/modals/OrderDetailModal.tsx index c81f18d..dd386fe 100644 --- a/client/src/components/modals/OrderDetailModal.tsx +++ b/client/src/components/modals/OrderDetailModal.tsx @@ -43,8 +43,8 @@ export default function OrderDetailModal({ title: "Succès", description: "Commande validée avec succès", }); + // Invalidation sélective pour éviter la disparition du calendrier queryClient.invalidateQueries({ queryKey: ['/api/orders'] }); - queryClient.invalidateQueries({ queryKey: ['/api/deliveries'] }); onClose(); }, onError: (error) => { @@ -76,8 +76,8 @@ export default function OrderDetailModal({ title: "Succès", description: "Livraison validée avec succès", }); + // Invalidation sélective pour éviter la disparition du calendrier queryClient.invalidateQueries({ queryKey: ['/api/deliveries'] }); - queryClient.invalidateQueries({ queryKey: ['/api/orders'] }); onClose(); }, onError: (error) => { @@ -103,37 +103,26 @@ export default function OrderDetailModal({ const deleteMutation = useMutation({ mutationFn: async (id: number) => { const endpoint = isOrder ? `/api/orders/${id}` : `/api/deliveries/${id}`; - console.log(`🗑️ Deleting ${isOrder ? 'order' : 'delivery'}:`, id); + if (import.meta.env.DEV) { + console.log(`🗑️ Deleting ${isOrder ? 'order' : 'delivery'}:`, id); + } await apiRequest(endpoint, "DELETE"); }, onSuccess: () => { - console.log(`✅ ${isOrder ? 'Order' : 'Delivery'} deleted successfully, clearing cache...`); + if (import.meta.env.DEV) { + console.log(`✅ ${isOrder ? 'Order' : 'Delivery'} deleted successfully`); + } toast({ title: "Succès", description: `${isOrder ? 'Commande' : 'Livraison'} supprimée avec succès`, }); - // SOLUTION PRODUCTION : Cache clearing radical pour éviter incohérences storeId - console.log('🗑️ Cache cleared, forcing page reload to maintain storeId consistency...'); - - // SOLUTION HYBRIDE : Invalidation sélective pour éviter perte storeId - console.log('🧹 Using selective invalidation to preserve storeId context...'); - - // Invalidation ciblée sans clear() pour préserver le contexte - queryClient.invalidateQueries({ - predicate: (query) => { - const key = query.queryKey[0]?.toString() || ''; - return key.includes('/api/orders') || key.includes('/api/deliveries'); - } - }); - - // Force refetch pour garantir synchronisation immédiate - queryClient.refetchQueries({ - predicate: (query) => { - const key = query.queryKey[0]?.toString() || ''; - return key.includes('/api/orders') || key.includes('/api/deliveries'); - } - }); + // Invalidation sélective uniquement pour l'API concernée + if (isOrder) { + queryClient.invalidateQueries({ queryKey: ['/api/orders'] }); + } else { + queryClient.invalidateQueries({ queryKey: ['/api/deliveries'] }); + } onClose(); }, diff --git a/client/src/pages/Calendar.tsx b/client/src/pages/Calendar.tsx index 6413a82..3905e10 100644 --- a/client/src/pages/Calendar.tsx +++ b/client/src/pages/Calendar.tsx @@ -49,12 +49,14 @@ export default function Calendar() { } const url = `/api/orders?${params.toString()}`; - console.log('📅 Calendar fetching orders:', { - url, - selectedStoreId, - userRole: user?.role, - params: params.toString() - }); + if (import.meta.env.DEV) { + console.log('📅 Calendar fetching orders:', { + url, + selectedStoreId, + userRole: user?.role, + params: params.toString() + }); + } const response = await fetch(url, { credentials: 'include' @@ -65,7 +67,9 @@ export default function Calendar() { } const data = await response.json(); - console.log('📅 Calendar orders received:', Array.isArray(data) ? data.length : 'NOT_ARRAY', 'items'); + if (import.meta.env.DEV) { + console.log('📅 Calendar orders received:', Array.isArray(data) ? data.length : 'NOT_ARRAY', 'items'); + } // Protection contre les données invalides en production if (!Array.isArray(data)) { @@ -74,6 +78,8 @@ export default function Calendar() { } return data; }, + staleTime: 5 * 60 * 1000, // 5 minutes de cache pour éviter la disparition des données + gcTime: 10 * 60 * 1000, // 10 minutes en cache }); const { data: deliveries = [], isLoading: loadingDeliveries } = useQuery({ @@ -91,12 +97,14 @@ export default function Calendar() { } const url = `/api/deliveries?${params.toString()}`; - console.log('📅 Calendar fetching deliveries:', { - url, - selectedStoreId, - userRole: user?.role, - params: params.toString() - }); + if (import.meta.env.DEV) { + console.log('📅 Calendar fetching deliveries:', { + url, + selectedStoreId, + userRole: user?.role, + params: params.toString() + }); + } const response = await fetch(url, { credentials: 'include' @@ -107,7 +115,9 @@ export default function Calendar() { } const data = await response.json(); - console.log('📅 Calendar deliveries received:', Array.isArray(data) ? data.length : 'NOT_ARRAY', 'items'); + if (import.meta.env.DEV) { + console.log('📅 Calendar deliveries received:', Array.isArray(data) ? data.length : 'NOT_ARRAY', 'items'); + } // Protection contre les données invalides en production if (!Array.isArray(data)) { @@ -116,6 +126,8 @@ export default function Calendar() { } return data; }, + staleTime: 5 * 60 * 1000, // 5 minutes de cache pour éviter la disparition des données + gcTime: 10 * 60 * 1000, // 10 minutes en cache }); // Fetch publicities for the current year @@ -129,12 +141,14 @@ export default function Calendar() { } const url = `/api/publicities?${params.toString()}`; - console.log('📅 Calendar fetching publicities:', { - url, - selectedStoreId, - userRole: user?.role, - year: currentDate.getFullYear() - }); + if (import.meta.env.DEV) { + console.log('📅 Calendar fetching publicities:', { + url, + selectedStoreId, + userRole: user?.role, + year: currentDate.getFullYear() + }); + } const response = await fetch(url, { credentials: 'include' @@ -145,7 +159,9 @@ export default function Calendar() { } const data = await response.json(); - console.log('📅 Calendar publicities received:', Array.isArray(data) ? data.length : 'NOT_ARRAY', 'items'); + if (import.meta.env.DEV) { + console.log('📅 Calendar publicities received:', Array.isArray(data) ? data.length : 'NOT_ARRAY', 'items'); + } return Array.isArray(data) ? data : []; }, }); @@ -166,12 +182,7 @@ export default function Calendar() { }; const handleItemClick = (item: any, type: 'order' | 'delivery') => { - // Rafraîchir les données avant d'ouvrir le modal pour s'assurer d'avoir les liaisons à jour - queryClient.invalidateQueries({ queryKey: ['/api/orders'] }); - queryClient.invalidateQueries({ queryKey: ['/api/orders', selectedStoreId] }); - queryClient.invalidateQueries({ queryKey: ['/api/deliveries'] }); - queryClient.invalidateQueries({ queryKey: ['/api/deliveries', selectedStoreId] }); - + // Ne pas invalider le cache à l'ouverture pour éviter la disparition des données setSelectedItem({ ...item, type }); setShowOrderDetail(true); }; diff --git a/cookies.txt b/cookies.txt index 4390e07..fb76b7b 100644 --- a/cookies.txt +++ b/cookies.txt @@ -2,4 +2,4 @@ # https://curl.se/docs/http-cookies.html # This file was generated by libcurl! Edit at your own risk. -#HttpOnly_localhost FALSE / FALSE 1755187696 connect.sid s%3AnEg7s3fD2sLyetJTCx-VYkzZFbBkPEcf.2BsOBC1UlFIC%2BpyKgdtocMX42YO%2FZLBn1cU6UeEnIYY +#HttpOnly_localhost FALSE / FALSE 1755203512 connect.sid s%3AGm18ttGO7m1DI7WtETfDd8gMojt-FqEw.%2BP3S4uZr6aX7vk2UAaEIow8kKQlZfWMzPcKLjUHAdNI diff --git a/replit.md b/replit.md index 1186295..f53d685 100644 --- a/replit.md +++ b/replit.md @@ -19,13 +19,19 @@ La plateforme fournit une gestion robuste des flux de travail de livraison avec ## Recent Changes -### 2025-08-13 - Optimisation performance production + suppression fonctionnelle +### 2025-08-13 - Optimisation performance production + correction calendrier ✅ **Résolution latence production** : - **Problème** : Console saturée par centaines de logs "API Response" causant latence - **Solution** : Logging conditionnel - seulement en développement pour requêtes normales - **Production** : Seules les erreurs API sont loggées pour debugging - **Impact** : Réduction drastique de la charge console et amélioration performances +✅ **Problème calendrier résolu** : +- **Problème** : Fermeture carte commande/livraison faisait disparaître toutes les données +- **Cause** : Invalidation aggressive du cache React Query + invalidation à l'ouverture +- **Solution** : Cache timing optimisé (5min staleTime) + invalidation sélective seulement +- **Résultat** : Calendrier stable, plus de disparition des données au jour actuel + ✅ **Fonctionnalité suppression complètement opérationnelle** : - **Cache NocoDB** : Fonction `saveInvoiceVerificationCache` ajoutée aux deux systèmes stockage - **MemStorage** : Implémentation CRUD complète pour ordres et livraisons From bfc90053f0551c81dfc46877b1909757d1898166 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 13 Aug 2025 20:42:59 +0000 Subject: [PATCH 3/4] Add database backup and restore functionality for administrators Introduce endpoints and UI for managing PostgreSQL database backups, including automatic nightly backups, manual backups, and the ability to download and delete backup files. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7c468936-2a88-4686-ac0a-84921a45222d Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/7c468936-2a88-4686-ac0a-84921a45222d/B7k6m01 --- client/src/pages/BackupManager.tsx | 308 +++++++++++++++++++++++++++++ server/backupService.ts | 197 ++++++++++++++++++ server/routes.ts | 50 +++++ 3 files changed, 555 insertions(+) create mode 100644 client/src/pages/BackupManager.tsx create mode 100644 server/backupService.ts diff --git a/client/src/pages/BackupManager.tsx b/client/src/pages/BackupManager.tsx new file mode 100644 index 0000000..2fc31e1 --- /dev/null +++ b/client/src/pages/BackupManager.tsx @@ -0,0 +1,308 @@ +import { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { useToast } from "@/hooks/use-toast"; +import { useAuthUnified } from "@/hooks/useAuthUnified"; +import { apiRequest } from "@/lib/queryClient"; +import { hasPermission } from "@/lib/permissions"; +import { + Download, + Trash2, + Database, + Clock, + User, + FileText, + AlertTriangle, + CheckCircle, + Loader2 +} from "lucide-react"; +import { format } from "date-fns"; +import { fr } from "date-fns/locale"; + +interface BackupFile { + id: string; + filename: string; + size: number; + createdAt: string; + type: 'manual' | 'automatic'; +} + +export default function BackupManager() { + const { user } = useAuthUnified(); + const { toast } = useToast(); + const queryClient = useQueryClient(); + const [deletingBackup, setDeletingBackup] = useState(null); + + // Check permissions + const canManageBackups = hasPermission(user?.role || '', 'admin', 'manage'); + + // Fetch backup list + const { data: backups = [], isLoading, error } = useQuery({ + queryKey: ['/api/backups'], + queryFn: () => apiRequest('/api/backups'), + enabled: canManageBackups, + refetchInterval: 30000, // Refresh every 30 seconds + }); + + // Create manual backup + const createBackupMutation = useMutation({ + mutationFn: () => apiRequest('/api/backups', 'POST'), + onSuccess: () => { + toast({ + title: "Succès", + description: "Sauvegarde manuelle créée avec succès", + }); + queryClient.invalidateQueries({ queryKey: ['/api/backups'] }); + }, + onError: (error: any) => { + toast({ + title: "Erreur", + description: error.message || "Impossible de créer la sauvegarde", + variant: "destructive", + }); + }, + }); + + // Delete backup + const deleteBackupMutation = useMutation({ + mutationFn: (filename: string) => apiRequest(`/api/backups/${filename}`, 'DELETE'), + onSuccess: () => { + toast({ + title: "Succès", + description: "Sauvegarde supprimée avec succès", + }); + queryClient.invalidateQueries({ queryKey: ['/api/backups'] }); + setDeletingBackup(null); + }, + onError: (error: any) => { + toast({ + title: "Erreur", + description: error.message || "Impossible de supprimer la sauvegarde", + variant: "destructive", + }); + setDeletingBackup(null); + }, + }); + + const handleDownload = (filename: string) => { + // Create download link + const link = document.createElement('a'); + link.href = `/api/backups/${filename}/download`; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const handleDelete = (filename: string) => { + setDeletingBackup(filename); + deleteBackupMutation.mutate(filename); + }; + + const formatFileSize = (bytes: number): string => { + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + if (bytes === 0) return '0 Bytes'; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; + }; + + const getTypeIcon = (type: string) => { + return type === 'automatic' ? : ; + }; + + const getTypeBadge = (type: string) => { + return type === 'automatic' + ? Automatique + : Manuelle; + }; + + if (!canManageBackups) { + return ( +
+ + + + Accès refusé + + Vous n'avez pas les permissions nécessaires pour accéder à la gestion des sauvegardes. + + + +
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+ +
+

+ Gestion des Sauvegardes +

+

+ Sauvegarde automatique quotidienne à 2h00 - Maximum 10 fichiers conservés +

+
+
+ + +
+
+ + {/* Content */} +
+ {/* Status Cards */} +
+ + + + Total Sauvegardes + + + + +
{backups.length}
+

+ Maximum 10 fichiers +

+
+
+ + + + + Dernière Sauvegarde + + + + +
+ {backups.length > 0 ? ( + format(new Date(backups[0].createdAt), 'dd/MM', { locale: fr }) + ) : '-'} +
+

+ {backups.length > 0 ? ( + format(new Date(backups[0].createdAt), 'HH:mm', { locale: fr }) + ) : 'Aucune sauvegarde'} +

+
+
+ + + + + Statut Système + + + + +
Actif
+

+ Sauvegarde automatique 2h00 +

+
+
+
+ + {/* Backup List */} + + + Liste des Sauvegardes + + Gestion des fichiers de sauvegarde de la base de données PostgreSQL + + + + {isLoading ? ( +
+ +
+ ) : error ? ( +
+ + Erreur lors du chargement des sauvegardes +
+ ) : backups.length === 0 ? ( +
+ + Aucune sauvegarde disponible +
+ ) : ( +
+ {backups.map((backup) => ( +
+
+
+ {getTypeIcon(backup.type)} + +
+ +
+
+ {backup.filename} +
+
+ + {format(new Date(backup.createdAt), 'dd/MM/yyyy à HH:mm', { locale: fr })} + + {formatFileSize(backup.size)} + {getTypeBadge(backup.type)} +
+
+
+ +
+ + + +
+
+ ))} +
+ )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/server/backupService.ts b/server/backupService.ts new file mode 100644 index 0000000..c1ea50d --- /dev/null +++ b/server/backupService.ts @@ -0,0 +1,197 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; +import fs from 'fs'; +import path from 'path'; +import cron from 'node-cron'; + +const execAsync = promisify(exec); + +export interface BackupFile { + id: string; + filename: string; + size: number; + createdAt: Date; + type: 'manual' | 'automatic'; +} + +export class BackupService { + private backupDir: string; + private maxBackups = 10; + + constructor() { + this.backupDir = path.join(process.cwd(), 'backups'); + this.ensureBackupDirectory(); + this.scheduleAutomaticBackup(); + } + + private ensureBackupDirectory(): void { + if (!fs.existsSync(this.backupDir)) { + fs.mkdirSync(this.backupDir, { recursive: true }); + console.log('📁 Backup directory created:', this.backupDir); + } + } + + private getDatabaseConfig() { + const dbUrl = process.env.DATABASE_URL; + if (!dbUrl) { + throw new Error('DATABASE_URL not found in environment variables'); + } + + // Parse PostgreSQL URL + const url = new URL(dbUrl); + return { + host: url.hostname, + port: url.port || '5432', + database: url.pathname.slice(1), // Remove leading slash + username: url.username, + password: url.password, + }; + } + + async createBackup(type: 'manual' | 'automatic' = 'manual'): Promise { + try { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const filename = `backup_${type}_${timestamp}.sql`; + const filepath = path.join(this.backupDir, filename); + + const dbConfig = this.getDatabaseConfig(); + + // Set environment variable for password to avoid prompt + const env = { + ...process.env, + PGPASSWORD: dbConfig.password, + }; + + // Create pg_dump command + const command = `pg_dump -h ${dbConfig.host} -p ${dbConfig.port} -U ${dbConfig.username} -d ${dbConfig.database} --no-password --verbose --clean --if-exists --create > "${filepath}"`; + + console.log(`🔄 Starting ${type} backup...`); + + await execAsync(command, { env }); + + // Get file stats + const stats = fs.statSync(filepath); + + const backupFile: BackupFile = { + id: timestamp, + filename, + size: stats.size, + createdAt: new Date(), + type, + }; + + console.log(`✅ ${type} backup created: ${filename} (${this.formatFileSize(stats.size)})`); + + // Clean old backups + await this.cleanOldBackups(); + + return backupFile; + } catch (error) { + console.error('❌ Backup failed:', error); + throw new Error(`Backup failed: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + } + + async getBackupList(): Promise { + try { + const files = fs.readdirSync(this.backupDir); + const backupFiles: BackupFile[] = []; + + for (const filename of files) { + if (filename.endsWith('.sql')) { + const filepath = path.join(this.backupDir, filename); + const stats = fs.statSync(filepath); + + // Extract type and timestamp from filename + const parts = filename.replace('.sql', '').split('_'); + const type = parts[1] as 'manual' | 'automatic'; + const timestamp = parts.slice(2).join('_'); + + backupFiles.push({ + id: timestamp, + filename, + size: stats.size, + createdAt: stats.birthtime, + type, + }); + } + } + + // Sort by creation date (newest first) + return backupFiles.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + } catch (error) { + console.error('❌ Failed to list backups:', error); + return []; + } + } + + async deleteBackup(filename: string): Promise { + try { + const filepath = path.join(this.backupDir, filename); + + if (!fs.existsSync(filepath)) { + throw new Error('Backup file not found'); + } + + fs.unlinkSync(filepath); + console.log(`🗑️ Backup deleted: ${filename}`); + } catch (error) { + console.error('❌ Failed to delete backup:', error); + throw error; + } + } + + async downloadBackup(filename: string): Promise { + const filepath = path.join(this.backupDir, filename); + + if (!fs.existsSync(filepath)) { + throw new Error('Backup file not found'); + } + + return filepath; + } + + private async cleanOldBackups(): Promise { + try { + const backups = await this.getBackupList(); + + if (backups.length > this.maxBackups) { + const toDelete = backups.slice(this.maxBackups); + + for (const backup of toDelete) { + await this.deleteBackup(backup.filename); + } + + console.log(`🧹 Cleaned ${toDelete.length} old backup(s)`); + } + } catch (error) { + console.error('❌ Failed to clean old backups:', error); + } + } + + private formatFileSize(bytes: number): string { + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + if (bytes === 0) return '0 Bytes'; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; + } + + private scheduleAutomaticBackup(): void { + // Schedule backup every night at 2:00 AM + cron.schedule('0 2 * * *', async () => { + try { + console.log('🌙 Starting scheduled automatic backup...'); + await this.createBackup('automatic'); + } catch (error) { + console.error('❌ Scheduled backup failed:', error); + } + }, { + timezone: 'Europe/Paris' + }); + + console.log('⏰ Automatic backup scheduled for 2:00 AM daily'); + } +} + +// Singleton instance +export const backupService = new BackupService(); \ No newline at end of file diff --git a/server/routes.ts b/server/routes.ts index 4425dd1..4c8c169 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -39,6 +39,7 @@ import { import { hasPermission } from "@shared/permissions"; import { z } from "zod"; import { invoiceVerificationService } from "./invoiceVerification"; +import { backupService } from "./backupService"; export async function registerRoutes(app: Express): Promise { // Health check endpoint for Docker @@ -2338,6 +2339,55 @@ RÉSUMÉ DU SCAN } }); + // Backup management routes (Admin only) + app.get('/api/backups', isAuthenticated, requireAdmin, async (req: any, res) => { + try { + const backups = await backupService.getBackupList(); + res.json(backups); + } catch (error) { + console.error("Error fetching backups:", error); + res.status(500).json({ message: "Failed to fetch backups" }); + } + }); + + app.post('/api/backups', isAuthenticated, requireAdmin, async (req: any, res) => { + try { + const backup = await backupService.createBackup('manual'); + res.json(backup); + } catch (error) { + console.error("Error creating backup:", error); + res.status(500).json({ message: "Failed to create backup" }); + } + }); + + app.get('/api/backups/:filename/download', isAuthenticated, requireAdmin, async (req: any, res) => { + try { + const { filename } = req.params; + const filepath = await backupService.downloadBackup(filename); + + res.download(filepath, filename, (err) => { + if (err) { + console.error("Error downloading backup:", err); + res.status(404).json({ message: "Backup file not found" }); + } + }); + } catch (error) { + console.error("Error downloading backup:", error); + res.status(500).json({ message: "Failed to download backup" }); + } + }); + + app.delete('/api/backups/:filename', isAuthenticated, requireAdmin, async (req: any, res) => { + try { + const { filename } = req.params; + await backupService.deleteBackup(filename); + res.json({ message: "Backup deleted successfully" }); + } catch (error) { + console.error("Error deleting backup:", error); + res.status(500).json({ message: "Failed to delete backup" }); + } + }); + // Create server instance const httpServer = createServer(app); From fbeb36c04a825ccc577d5356113ddcfee6949628 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Wed, 13 Aug 2025 20:55:27 +0000 Subject: [PATCH 4/4] Add system for managing and scheduling data backups Introduce a new backup management feature with automated scheduling, manual backups, and a dedicated interface for viewing backup status and details. This includes integrating `node-cron` for scheduling, updating the database schema to store backup information, and creating new routes and components for the backup manager. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7c468936-2a88-4686-ac0a-84921a45222d Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/7c468936-2a88-4686-ac0a-84921a45222d/9xfbVBU --- client/src/components/Layout.tsx | 2 +- client/src/components/RouterProduction.tsx | 2 + client/src/components/Sidebar.tsx | 6 ++ client/src/pages/BackupManager.tsx | 30 +++++- package-lock.json | 10 ++ package.json | 1 + server/backupService.ts | 114 +++++++++++---------- server/routes.ts | 7 +- shared/schema.ts | 17 +++ 9 files changed, 131 insertions(+), 58 deletions(-) diff --git a/client/src/components/Layout.tsx b/client/src/components/Layout.tsx index aac637b..5713485 100644 --- a/client/src/components/Layout.tsx +++ b/client/src/components/Layout.tsx @@ -138,7 +138,7 @@ export default function Layout({ children }: LayoutProps) {
{store.name}
diff --git a/client/src/components/RouterProduction.tsx b/client/src/components/RouterProduction.tsx index f0b60d3..bb5f6e8 100644 --- a/client/src/components/RouterProduction.tsx +++ b/client/src/components/RouterProduction.tsx @@ -17,6 +17,7 @@ import DatabaseDebug from "@/pages/DatabaseDebug"; import CustomerOrders from "@/pages/CustomerOrders"; import DlcPage from "@/pages/DlcPage"; +import BackupManager from "@/pages/BackupManager"; import Tasks from "@/pages/Tasks"; import TasksSimplified from "@/pages/TasksSimplified"; import Layout from "@/components/Layout"; @@ -76,6 +77,7 @@ function RouterProduction() { + diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index fac67f0..0675126 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -146,6 +146,12 @@ export default function Sidebar() { icon: UserCog, roles: ["admin"] }, + { + path: "/backup", + label: "Sauvegardes", + icon: Database, + roles: ["admin"] + }, { path: "/nocodb-config", label: "Configuration NocoDB", diff --git a/client/src/pages/BackupManager.tsx b/client/src/pages/BackupManager.tsx index 2fc31e1..d80fdb1 100644 --- a/client/src/pages/BackupManager.tsx +++ b/client/src/pages/BackupManager.tsx @@ -24,9 +24,13 @@ import { fr } from "date-fns/locale"; interface BackupFile { id: string; filename: string; + description: string | null; size: number; createdAt: string; - type: 'manual' | 'automatic'; + createdBy: string; + tablesCount: number; + status: string; + backupType: string; } export default function BackupManager() { @@ -118,6 +122,19 @@ export default function BackupManager() { : Manuelle; }; + const getStatusBadge = (status: string) => { + switch (status) { + case 'completed': + return Terminé; + case 'creating': + return En cours; + case 'failed': + return Échec; + default: + return {status}; + } + }; + if (!canManageBackups) { return (
@@ -254,7 +271,7 @@ export default function BackupManager() { >
- {getTypeIcon(backup.type)} + {getTypeIcon(backup.backupType)}
@@ -262,12 +279,17 @@ export default function BackupManager() {
{backup.filename}
-
+
+ {backup.description || 'Sauvegarde de base de données'} +
+
{format(new Date(backup.createdAt), 'dd/MM/yyyy à HH:mm', { locale: fr })} {formatFileSize(backup.size)} - {getTypeBadge(backup.type)} + {backup.tablesCount} tables + {getTypeBadge(backup.backupType)} + {getStatusBadge(backup.status)}
diff --git a/package-lock.json b/package-lock.json index 8eb97a2..23a42c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "memorystore": "^1.6.7", "nanoid": "^5.1.5", "next-themes": "^0.4.6", + "node-cron": "^4.2.1", "openid-client": "^6.6.2", "passport": "^0.7.0", "passport-local": "^1.0.0", @@ -6919,6 +6920,15 @@ "node": "^18 || ^20 || >= 21" } }, + "node_modules/node-cron": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-4.2.1.tgz", + "integrity": "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", diff --git a/package.json b/package.json index 715a45a..b3be968 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "memorystore": "^1.6.7", "nanoid": "^5.1.5", "next-themes": "^0.4.6", + "node-cron": "^4.2.1", "openid-client": "^6.6.2", "passport": "^0.7.0", "passport-local": "^1.0.0", diff --git a/server/backupService.ts b/server/backupService.ts index c1ea50d..07661f0 100644 --- a/server/backupService.ts +++ b/server/backupService.ts @@ -3,17 +3,14 @@ import { promisify } from 'util'; import fs from 'fs'; import path from 'path'; import cron from 'node-cron'; +import { nanoid } from 'nanoid'; +import { eq, desc } from "drizzle-orm"; +import { db } from "./db"; +import { databaseBackups } from "@shared/schema"; +import type { DatabaseBackup, InsertDatabaseBackup } from "@shared/schema"; const execAsync = promisify(exec); -export interface BackupFile { - id: string; - filename: string; - size: number; - createdAt: Date; - type: 'manual' | 'automatic'; -} - export class BackupService { private backupDir: string; private maxBackups = 10; @@ -48,12 +45,25 @@ export class BackupService { }; } - async createBackup(type: 'manual' | 'automatic' = 'manual'): Promise { + async createBackup(type: 'manual' | 'automatic' = 'manual', createdBy: string = 'system'): Promise { try { + const id = nanoid(); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = `backup_${type}_${timestamp}.sql`; const filepath = path.join(this.backupDir, filename); + // Create database record first with "creating" status + const [backupRecord] = await db.insert(databaseBackups).values({ + id, + filename, + description: `${type === 'manual' ? 'Manuel' : 'Automatique'} backup du ${new Date().toLocaleDateString('fr-FR')}`, + size: 0, + createdBy, + tablesCount: 0, + status: 'creating', + backupType: type, + }).returning(); + const dbConfig = this.getDatabaseConfig(); // Set environment variable for password to avoid prompt @@ -69,56 +79,41 @@ export class BackupService { await execAsync(command, { env }); - // Get file stats + // Get file stats and count tables const stats = fs.statSync(filepath); + const sqlContent = fs.readFileSync(filepath, 'utf8'); + const tablesCount = (sqlContent.match(/CREATE TABLE/g) || []).length; - const backupFile: BackupFile = { - id: timestamp, - filename, - size: stats.size, - createdAt: new Date(), - type, - }; + // Update database record with completion details + const [updatedBackup] = await db.update(databaseBackups) + .set({ + size: stats.size, + tablesCount, + status: 'completed', + }) + .where(eq(databaseBackups.id, id)) + .returning(); - console.log(`✅ ${type} backup created: ${filename} (${this.formatFileSize(stats.size)})`); + console.log(`✅ ${type} backup created: ${filename} (${this.formatFileSize(stats.size)}, ${tablesCount} tables)`); // Clean old backups await this.cleanOldBackups(); - return backupFile; + return updatedBackup; } catch (error) { console.error('❌ Backup failed:', error); throw new Error(`Backup failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } - async getBackupList(): Promise { + async getBackupList(): Promise { try { - const files = fs.readdirSync(this.backupDir); - const backupFiles: BackupFile[] = []; + const backups = await db.select() + .from(databaseBackups) + .orderBy(desc(databaseBackups.createdAt)) + .limit(this.maxBackups); - for (const filename of files) { - if (filename.endsWith('.sql')) { - const filepath = path.join(this.backupDir, filename); - const stats = fs.statSync(filepath); - - // Extract type and timestamp from filename - const parts = filename.replace('.sql', '').split('_'); - const type = parts[1] as 'manual' | 'automatic'; - const timestamp = parts.slice(2).join('_'); - - backupFiles.push({ - id: timestamp, - filename, - size: stats.size, - createdAt: stats.birthtime, - type, - }); - } - } - - // Sort by creation date (newest first) - return backupFiles.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + return backups; } catch (error) { console.error('❌ Failed to list backups:', error); return []; @@ -127,13 +122,26 @@ export class BackupService { async deleteBackup(filename: string): Promise { try { - const filepath = path.join(this.backupDir, filename); - - if (!fs.existsSync(filepath)) { - throw new Error('Backup file not found'); + // Find backup record + const [backup] = await db.select() + .from(databaseBackups) + .where(eq(databaseBackups.filename, filename)) + .limit(1); + + if (!backup) { + throw new Error('Backup record not found'); } - fs.unlinkSync(filepath); + // Delete physical file + const filepath = path.join(this.backupDir, filename); + if (fs.existsSync(filepath)) { + fs.unlinkSync(filepath); + } + + // Delete database record + await db.delete(databaseBackups) + .where(eq(databaseBackups.filename, filename)); + console.log(`🗑️ Backup deleted: ${filename}`); } catch (error) { console.error('❌ Failed to delete backup:', error); @@ -153,10 +161,12 @@ export class BackupService { private async cleanOldBackups(): Promise { try { - const backups = await this.getBackupList(); + const allBackups = await db.select() + .from(databaseBackups) + .orderBy(desc(databaseBackups.createdAt)); - if (backups.length > this.maxBackups) { - const toDelete = backups.slice(this.maxBackups); + if (allBackups.length > this.maxBackups) { + const toDelete = allBackups.slice(this.maxBackups); for (const backup of toDelete) { await this.deleteBackup(backup.filename); diff --git a/server/routes.ts b/server/routes.ts index 4c8c169..7345faf 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -2352,7 +2352,12 @@ RÉSUMÉ DU SCAN app.post('/api/backups', isAuthenticated, requireAdmin, async (req: any, res) => { try { - const backup = await backupService.createBackup('manual'); + const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + const backup = await backupService.createBackup('manual', user.id); res.json(backup); } catch (error) { console.error("Error creating backup:", error); diff --git a/shared/schema.ts b/shared/schema.ts index 93855d7..89102b0 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -11,6 +11,7 @@ import { date, decimal, primaryKey, + bigint, } from "drizzle-orm/pg-core"; import { relations } from "drizzle-orm"; import { createInsertSchema } from "drizzle-zod"; @@ -512,8 +513,24 @@ export const insertInvoiceVerificationCacheSchema = createInsertSchema(invoiceVe updatedAt: true, }); +export const databaseBackups = pgTable("database_backups", { + id: varchar("id", { length: 255 }).primaryKey(), + filename: varchar("filename", { length: 255 }).notNull(), + description: text("description"), + size: bigint("size", { mode: "number" }).default(0), + createdAt: timestamp("created_at").defaultNow(), + createdBy: varchar("created_by", { length: 255 }).notNull(), + tablesCount: integer("tables_count").default(0), + status: varchar("status", { length: 50 }).default("creating"), + backupType: varchar("backup_type", { length: 10 }).default("manual"), +}); + +export const insertDatabaseBackupSchema = createInsertSchema(databaseBackups); + export type InsertNocodbConfig = z.infer; export type InsertInvoiceVerificationCache = z.infer; +export type DatabaseBackup = typeof databaseBackups.$inferSelect; +export type InsertDatabaseBackup = z.infer; // Types export type User = typeof users.$inferSelect;