feat: implement backup management features
- Added `createBackupAction`, `verifyDatabaseAction`, and `refreshBackupStatsAction` for handling backup operations. - Introduced `getBackupStats` method in `BackupClient` to retrieve daily backup statistics. - Updated API route to support fetching backup stats. - Integrated backup stats into the `BackupSettingsPage` and visualized them with `BackupTimelineChart`. - Enhanced `BackupSettingsPageClient` to manage backup stats and actions more effectively.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useTransition } from 'react';
|
||||
import { backupClient, BackupListResponse } from '@/clients/backup-client';
|
||||
import { BackupConfig } from '@/services/data-management/backup';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
@@ -8,18 +8,27 @@ import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { Header } from '@/components/ui/Header';
|
||||
import { formatDateForDisplay, parseDate, getToday } from '@/lib/date-utils';
|
||||
import { BackupTimelineChart } from '@/components/backup/BackupTimelineChart';
|
||||
import { createBackupAction, verifyDatabaseAction } from '@/actions/backup';
|
||||
import { parseDate, getToday } from '@/lib/date-utils';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface BackupSettingsPageClientProps {
|
||||
initialData?: BackupListResponse;
|
||||
initialData?: BackupListResponse & {
|
||||
backupStats?: Array<{
|
||||
date: string;
|
||||
manual: number;
|
||||
automatic: number;
|
||||
total: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export default function BackupSettingsPageClient({ initialData }: BackupSettingsPageClientProps) {
|
||||
const [data, setData] = useState<BackupListResponse | null>(initialData || null);
|
||||
const [backupStats, setBackupStats] = useState(initialData?.backupStats || []);
|
||||
const [isLoading, setIsLoading] = useState(!initialData);
|
||||
const [isCreatingBackup, setIsCreatingBackup] = useState(false);
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [showRestoreConfirm, setShowRestoreConfirm] = useState<string | null>(null);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
|
||||
const [config, setConfig] = useState<BackupConfig | null>(initialData?.config || null);
|
||||
@@ -56,10 +65,18 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
||||
const loadData = async () => {
|
||||
try {
|
||||
console.log('🔄 Loading backup data...');
|
||||
const response = await backupClient.listBackups();
|
||||
const [response, newBackupStats] = await Promise.all([
|
||||
backupClient.listBackups(),
|
||||
backupClient.getBackupStats(30)
|
||||
]);
|
||||
console.log('✅ Backup data loaded:', response);
|
||||
console.log('✅ Backup stats loaded:', newBackupStats);
|
||||
|
||||
setData(response);
|
||||
setBackupStats(newBackupStats);
|
||||
setConfig(response.config);
|
||||
|
||||
console.log('✅ States updated - backups count:', response.backups.length, 'stats count:', newBackupStats.length);
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to load backup data:', error);
|
||||
// Afficher l'erreur spécifique à l'utilisateur
|
||||
@@ -71,59 +88,79 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateBackup = async (force: boolean = false) => {
|
||||
setIsCreatingBackup(true);
|
||||
try {
|
||||
const result = await backupClient.createBackup(force);
|
||||
|
||||
if (result === null) {
|
||||
const handleCreateBackup = (force: boolean = false) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
console.log('🔄 Creating backup...');
|
||||
const result = await createBackupAction(force);
|
||||
console.log('✅ Backup action result:', result);
|
||||
|
||||
if (!result.success) {
|
||||
setMessage('backup', {
|
||||
type: 'error',
|
||||
text: result.error || 'Erreur lors de la création de la sauvegarde'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.skipped) {
|
||||
setMessage('backup', {
|
||||
type: 'success',
|
||||
text: result.message || 'Sauvegarde sautée : aucun changement détecté.'
|
||||
});
|
||||
} else {
|
||||
setMessage('backup', {
|
||||
type: 'success',
|
||||
text: result.message || 'Sauvegarde créée avec succès'
|
||||
});
|
||||
}
|
||||
|
||||
// Petit délai pour être sûr que la sauvegarde est bien créée
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Recharger les données manuellement pour être sûr
|
||||
console.log('🔄 Reloading data after backup...');
|
||||
await loadData();
|
||||
console.log('✅ Data reloaded manually');
|
||||
} catch (error) {
|
||||
console.error('❌ Error in handleCreateBackup:', error);
|
||||
setMessage('backup', {
|
||||
type: 'success',
|
||||
text: 'Sauvegarde sautée : aucun changement détecté. Utilisez "Forcer" pour créer malgré tout.'
|
||||
});
|
||||
} else {
|
||||
setMessage('backup', {
|
||||
type: 'success',
|
||||
text: `Sauvegarde créée : ${result.filename}`
|
||||
type: 'error',
|
||||
text: 'Erreur lors de la création de la sauvegarde'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleVerifyDatabase = () => {
|
||||
startTransition(async () => {
|
||||
setMessage('verify', null);
|
||||
const result = await verifyDatabaseAction();
|
||||
|
||||
await loadData();
|
||||
} catch (error) {
|
||||
console.error('Failed to create backup:', error);
|
||||
setMessage('backup', {
|
||||
type: 'error',
|
||||
text: 'Erreur lors de la création de la sauvegarde'
|
||||
});
|
||||
} finally {
|
||||
setIsCreatingBackup(false);
|
||||
}
|
||||
if (result.success) {
|
||||
setMessage('verify', {type: 'success', text: result.message || 'Intégrité vérifiée'});
|
||||
} else {
|
||||
setMessage('verify', {type: 'error', text: result.error || 'Vérification échouée'});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleVerifyDatabase = async () => {
|
||||
setIsVerifying(true);
|
||||
setMessage('verify', null);
|
||||
try {
|
||||
await backupClient.verifyDatabase();
|
||||
setMessage('verify', {type: 'success', text: 'Intégrité vérifiée'});
|
||||
} catch (error) {
|
||||
console.error('Database verification failed:', error);
|
||||
setMessage('verify', {type: 'error', text: 'Vérification échouée'});
|
||||
} finally {
|
||||
setIsVerifying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteBackup = async (filename: string) => {
|
||||
try {
|
||||
await backupClient.deleteBackup(filename);
|
||||
setShowDeleteConfirm(null);
|
||||
setMessage('restore', {type: 'success', text: `Sauvegarde ${filename} supprimée`});
|
||||
await loadData();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete backup:', error);
|
||||
setMessage('restore', {type: 'error', text: 'Suppression échouée'});
|
||||
}
|
||||
const handleDeleteBackup = (filename: string) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
console.log('🔄 Deleting backup:', filename);
|
||||
await backupClient.deleteBackup(filename);
|
||||
setShowDeleteConfirm(null);
|
||||
setMessage('restore', {type: 'success', text: `Sauvegarde ${filename} supprimée`});
|
||||
|
||||
console.log('🔄 Reloading data after deletion...');
|
||||
await loadData();
|
||||
console.log('✅ Data reloaded after deletion');
|
||||
} catch (error) {
|
||||
console.error('Failed to delete backup:', error);
|
||||
setMessage('restore', {type: 'error', text: 'Suppression échouée'});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleRestoreBackup = async (filename: string) => {
|
||||
@@ -192,10 +229,16 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
const formatDate = (date: string | Date): string => {
|
||||
// Format cohérent serveur/client pour éviter les erreurs d'hydratation
|
||||
|
||||
const formatDateWithTime = (date: string | Date): string => {
|
||||
const d = typeof date === 'string' ? parseDate(date) : date;
|
||||
return formatDateForDisplay(d, 'DISPLAY_MEDIUM');
|
||||
return d.toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
@@ -391,17 +434,17 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => handleCreateBackup(false)}
|
||||
disabled={isCreatingBackup}
|
||||
disabled={isPending}
|
||||
className="bg-[var(--primary)] hover:bg-[var(--primary)]/90 text-[var(--primary-foreground)]"
|
||||
>
|
||||
{isCreatingBackup ? 'Création...' : 'Créer sauvegarde'}
|
||||
{isPending ? 'Création...' : 'Créer sauvegarde'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleCreateBackup(true)}
|
||||
disabled={isCreatingBackup}
|
||||
disabled={isPending}
|
||||
className="bg-orange-600 hover:bg-orange-700 text-white"
|
||||
>
|
||||
{isCreatingBackup ? 'Création...' : 'Forcer'}
|
||||
{isPending ? 'Création...' : 'Forcer'}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-xs text-[var(--muted-foreground)]">
|
||||
@@ -413,10 +456,10 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleVerifyDatabase}
|
||||
disabled={isVerifying}
|
||||
disabled={isPending}
|
||||
className="bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] hover:bg-[var(--muted)]"
|
||||
>
|
||||
{isVerifying ? 'Vérification...' : 'Vérifier l\'intégrité'}
|
||||
{isPending ? 'Vérification...' : 'Vérifier l\'intégrité'}
|
||||
</Button>
|
||||
<InlineMessage messageKey="verify" />
|
||||
|
||||
@@ -433,6 +476,13 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Graphique timeline des sauvegardes */}
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
<BackupTimelineChart stats={Array.isArray(backupStats) ? backupStats : []} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Colonne latérale: Statut et historique */}
|
||||
@@ -520,7 +570,7 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
{formatDate(backup.createdAt)}
|
||||
{formatDateWithTime(backup.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user