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:
60
src/actions/backup.ts
Normal file
60
src/actions/backup.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { backupService } from '@/services/data-management/backup';
|
||||||
|
import { revalidatePath } from 'next/cache';
|
||||||
|
|
||||||
|
export async function createBackupAction(force: boolean = false) {
|
||||||
|
try {
|
||||||
|
const result = await backupService.createBackup('manual', force);
|
||||||
|
|
||||||
|
// Invalider le cache de la page pour forcer le rechargement des données SSR
|
||||||
|
revalidatePath('/settings/backup');
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
skipped: true,
|
||||||
|
message: 'Sauvegarde sautée : aucun changement détecté. Utilisez "Forcer" pour créer malgré tout.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
message: `Sauvegarde créée : ${result.filename}`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create backup:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Erreur lors de la création de la sauvegarde'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyDatabaseAction() {
|
||||||
|
try {
|
||||||
|
await backupService.verifyDatabaseHealth();
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Intégrité vérifiée'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Database verification failed:', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Vérification échouée'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshBackupStatsAction() {
|
||||||
|
try {
|
||||||
|
// Cette action sert juste à revalider le cache
|
||||||
|
revalidatePath('/settings/backup');
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh backup stats:', error);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,16 @@ export async function GET(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action === 'stats') {
|
||||||
|
const days = parseInt(searchParams.get('days') || '30');
|
||||||
|
const stats = await backupService.getBackupStats(days);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: stats
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
console.log('🔄 API GET /api/backups called');
|
console.log('🔄 API GET /api/backups called');
|
||||||
|
|
||||||
// Test de la configuration d'abord
|
// Test de la configuration d'abord
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export default async function BackupSettingsPage() {
|
|||||||
const backups = await backupService.listBackups();
|
const backups = await backupService.listBackups();
|
||||||
const schedulerStatus = backupScheduler.getStatus();
|
const schedulerStatus = backupScheduler.getStatus();
|
||||||
const config = backupService.getConfig();
|
const config = backupService.getConfig();
|
||||||
|
const backupStats = await backupService.getBackupStats(30);
|
||||||
|
|
||||||
const initialData = {
|
const initialData = {
|
||||||
backups,
|
backups,
|
||||||
@@ -18,6 +19,7 @@ export default async function BackupSettingsPage() {
|
|||||||
nextBackup: schedulerStatus.nextBackup ? schedulerStatus.nextBackup.toISOString() : null,
|
nextBackup: schedulerStatus.nextBackup ? schedulerStatus.nextBackup.toISOString() : null,
|
||||||
},
|
},
|
||||||
config,
|
config,
|
||||||
|
backupStats,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -109,6 +109,24 @@ export class BackupClient {
|
|||||||
const response = await httpClient.get<{ data: { logs: string[] } }>(`${this.baseUrl}?action=logs&maxLines=${maxLines}`);
|
const response = await httpClient.get<{ data: { logs: string[] } }>(`${this.baseUrl}?action=logs&maxLines=${maxLines}`);
|
||||||
return response.data.logs;
|
return response.data.logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les statistiques de sauvegarde par jour
|
||||||
|
*/
|
||||||
|
async getBackupStats(days: number = 30): Promise<Array<{
|
||||||
|
date: string;
|
||||||
|
manual: number;
|
||||||
|
automatic: number;
|
||||||
|
total: number;
|
||||||
|
}>> {
|
||||||
|
const response = await httpClient.get<{ data: Array<{
|
||||||
|
date: string;
|
||||||
|
manual: number;
|
||||||
|
automatic: number;
|
||||||
|
total: number;
|
||||||
|
}> }>(`${this.baseUrl}?action=stats&days=${days}`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const backupClient = new BackupClient();
|
export const backupClient = new BackupClient();
|
||||||
|
|||||||
211
src/components/backup/BackupTimelineChart.tsx
Normal file
211
src/components/backup/BackupTimelineChart.tsx
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
interface BackupStats {
|
||||||
|
date: string;
|
||||||
|
manual: number;
|
||||||
|
automatic: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BackupTimelineChartProps {
|
||||||
|
stats?: BackupStats[];
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BackupTimelineChart({ stats = [], className = '' }: BackupTimelineChartProps) {
|
||||||
|
// Protection contre les stats non-array
|
||||||
|
const safeStats = Array.isArray(stats) ? stats : [];
|
||||||
|
const error = safeStats.length === 0 ? 'Aucune donnée disponible' : null;
|
||||||
|
|
||||||
|
// Convertir les stats en map pour accès rapide
|
||||||
|
const statsMap = new Map(safeStats.map(s => [s.date, s]));
|
||||||
|
|
||||||
|
// Générer les 30 derniers jours
|
||||||
|
const days = Array.from({ length: 30 }, (_, i) => {
|
||||||
|
const date = new Date();
|
||||||
|
date.setDate(date.getDate() - (29 - i));
|
||||||
|
// Utiliser la date locale pour éviter les décalages UTC
|
||||||
|
const localDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
|
||||||
|
return localDate.toISOString().split('T')[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Organiser en semaines (5 semaines de 6 jours + quelques jours)
|
||||||
|
const weeks = [];
|
||||||
|
for (let i = 0; i < days.length; i += 7) {
|
||||||
|
weeks.push(days.slice(i, i + 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const formatDateFull = (dateStr: string) => {
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
return date.toLocaleDateString('fr-FR', {
|
||||||
|
weekday: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className={`p-4 sm:p-6 ${className}`}>
|
||||||
|
<div className="text-gray-500 text-sm text-center py-8">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`p-4 sm:p-6 w-full ${className}`}>
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||||
|
💾 Activité de sauvegarde (30 derniers jours)
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Vue en ligne avec indicateurs clairs */}
|
||||||
|
<div className="mb-6">
|
||||||
|
{/* En-têtes des jours */}
|
||||||
|
<div className="grid grid-cols-7 gap-1 mb-2">
|
||||||
|
{['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'].map(day => (
|
||||||
|
<div key={day} className="text-xs text-center text-gray-500 font-medium py-1">
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grille des jours avec indicateurs visuels */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
{weeks.map((week, weekIndex) => (
|
||||||
|
<div key={weekIndex} className="grid grid-cols-7 gap-1">
|
||||||
|
{week.map((day) => {
|
||||||
|
const stat = statsMap.get(day) || { date: day, manual: 0, automatic: 0, total: 0 };
|
||||||
|
const hasManual = stat.manual > 0;
|
||||||
|
const hasAuto = stat.automatic > 0;
|
||||||
|
const dayNumber = new Date(day).getDate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={day} className="group relative">
|
||||||
|
<div className={`
|
||||||
|
relative h-8 rounded border-2 transition-all duration-200 cursor-pointer flex items-center justify-center text-xs font-medium
|
||||||
|
${stat.total === 0
|
||||||
|
? 'bg-gray-50 dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-400'
|
||||||
|
: 'border-transparent'
|
||||||
|
}
|
||||||
|
`}>
|
||||||
|
{/* Jour du mois */}
|
||||||
|
<span className={`relative z-10 ${stat.total > 0 ? 'text-white font-bold' : ''}`}>
|
||||||
|
{dayNumber}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Fond selon le type */}
|
||||||
|
{stat.total > 0 && (
|
||||||
|
<div className={`
|
||||||
|
absolute inset-0 rounded
|
||||||
|
${hasManual && hasAuto
|
||||||
|
? 'bg-gradient-to-br from-blue-500 to-green-500'
|
||||||
|
: hasManual
|
||||||
|
? 'bg-blue-500'
|
||||||
|
: 'bg-green-500'
|
||||||
|
}
|
||||||
|
`}></div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Indicateurs visuels pour l'intensité */}
|
||||||
|
{stat.total > 0 && stat.total > 1 && (
|
||||||
|
<div className="absolute -top-1 -right-1 bg-orange-500 text-white rounded-full w-4 h-4 flex items-center justify-center text-xs font-bold">
|
||||||
|
{stat.total > 9 ? '9+' : stat.total}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tooltip détaillé */}
|
||||||
|
<div className="absolute bottom-full mb-2 left-1/2 transform -translate-x-1/2 bg-black text-white text-xs rounded py-2 px-3 opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-20">
|
||||||
|
<div className="font-semibold">{formatDateFull(day)}</div>
|
||||||
|
{stat.total > 0 ? (
|
||||||
|
<div className="mt-1 space-y-1">
|
||||||
|
{stat.manual > 0 && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 bg-blue-400 rounded-full"></div>
|
||||||
|
<span>Manuel: {stat.manual}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{stat.automatic > 0 && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-2 h-2 bg-green-400 rounded-full"></div>
|
||||||
|
<span>Auto: {stat.automatic}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="font-semibold border-t border-gray-600 pt-1">
|
||||||
|
Total: {stat.total}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-gray-300 mt-1">Aucune sauvegarde</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Légende claire */}
|
||||||
|
<div className="mb-6 p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
||||||
|
<h4 className="text-sm font-medium mb-3 text-gray-700 dark:text-gray-300">Légende</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-6 h-6 bg-blue-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div>
|
||||||
|
<span>Manuel seul</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-6 h-6 bg-green-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div>
|
||||||
|
<span>Auto seul</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-6 h-6 bg-gradient-to-br from-blue-500 to-green-500 rounded flex items-center justify-center text-white text-xs font-bold">15</div>
|
||||||
|
<span>Manuel + Auto</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="w-6 h-6 bg-gray-200 dark:bg-gray-700 border-2 border-gray-300 dark:border-gray-600 rounded flex items-center justify-center text-gray-500 text-xs">15</div>
|
||||||
|
<span>Aucune</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
💡 Le badge orange indique le nombre total quand > 1
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Statistiques résumées */}
|
||||||
|
<div className="grid grid-cols-3 gap-3 text-center">
|
||||||
|
<div className="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
|
||||||
|
<div className="text-xl font-bold text-blue-600">
|
||||||
|
{safeStats.reduce((sum, s) => sum + s.manual, 0)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-blue-600 font-medium">Manuelles</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||||
|
<div className="text-xl font-bold text-green-600">
|
||||||
|
{safeStats.reduce((sum, s) => sum + s.automatic, 0)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-green-600 font-medium">Automatiques</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
|
||||||
|
<div className="text-xl font-bold text-purple-600">
|
||||||
|
{safeStats.reduce((sum, s) => sum + s.total, 0)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-purple-600 font-medium">Total</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useTransition } from 'react';
|
||||||
import { backupClient, BackupListResponse } from '@/clients/backup-client';
|
import { backupClient, BackupListResponse } from '@/clients/backup-client';
|
||||||
import { BackupConfig } from '@/services/data-management/backup';
|
import { BackupConfig } from '@/services/data-management/backup';
|
||||||
import { Button } from '@/components/ui/Button';
|
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 { Input } from '@/components/ui/Input';
|
||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import { Header } from '@/components/ui/Header';
|
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';
|
import Link from 'next/link';
|
||||||
|
|
||||||
interface BackupSettingsPageClientProps {
|
interface BackupSettingsPageClientProps {
|
||||||
initialData?: BackupListResponse;
|
initialData?: BackupListResponse & {
|
||||||
|
backupStats?: Array<{
|
||||||
|
date: string;
|
||||||
|
manual: number;
|
||||||
|
automatic: number;
|
||||||
|
total: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BackupSettingsPageClient({ initialData }: BackupSettingsPageClientProps) {
|
export default function BackupSettingsPageClient({ initialData }: BackupSettingsPageClientProps) {
|
||||||
const [data, setData] = useState<BackupListResponse | null>(initialData || null);
|
const [data, setData] = useState<BackupListResponse | null>(initialData || null);
|
||||||
|
const [backupStats, setBackupStats] = useState(initialData?.backupStats || []);
|
||||||
const [isLoading, setIsLoading] = useState(!initialData);
|
const [isLoading, setIsLoading] = useState(!initialData);
|
||||||
const [isCreatingBackup, setIsCreatingBackup] = useState(false);
|
const [isPending, startTransition] = useTransition();
|
||||||
const [isVerifying, setIsVerifying] = useState(false);
|
|
||||||
const [showRestoreConfirm, setShowRestoreConfirm] = useState<string | null>(null);
|
const [showRestoreConfirm, setShowRestoreConfirm] = useState<string | null>(null);
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
|
||||||
const [config, setConfig] = useState<BackupConfig | null>(initialData?.config || null);
|
const [config, setConfig] = useState<BackupConfig | null>(initialData?.config || null);
|
||||||
@@ -56,10 +65,18 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
|||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
console.log('🔄 Loading backup data...');
|
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 data loaded:', response);
|
||||||
|
console.log('✅ Backup stats loaded:', newBackupStats);
|
||||||
|
|
||||||
setData(response);
|
setData(response);
|
||||||
|
setBackupStats(newBackupStats);
|
||||||
setConfig(response.config);
|
setConfig(response.config);
|
||||||
|
|
||||||
|
console.log('✅ States updated - backups count:', response.backups.length, 'stats count:', newBackupStats.length);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Failed to load backup data:', error);
|
console.error('❌ Failed to load backup data:', error);
|
||||||
// Afficher l'erreur spécifique à l'utilisateur
|
// Afficher l'erreur spécifique à l'utilisateur
|
||||||
@@ -71,59 +88,79 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateBackup = async (force: boolean = false) => {
|
const handleCreateBackup = (force: boolean = false) => {
|
||||||
setIsCreatingBackup(true);
|
startTransition(async () => {
|
||||||
try {
|
try {
|
||||||
const result = await backupClient.createBackup(force);
|
console.log('🔄 Creating backup...');
|
||||||
|
const result = await createBackupAction(force);
|
||||||
if (result === null) {
|
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', {
|
setMessage('backup', {
|
||||||
type: 'success',
|
type: 'error',
|
||||||
text: 'Sauvegarde sautée : aucun changement détecté. Utilisez "Forcer" pour créer malgré tout.'
|
text: 'Erreur lors de la création de la sauvegarde'
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setMessage('backup', {
|
|
||||||
type: 'success',
|
|
||||||
text: `Sauvegarde créée : ${result.filename}`
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVerifyDatabase = () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
setMessage('verify', null);
|
||||||
|
const result = await verifyDatabaseAction();
|
||||||
|
|
||||||
await loadData();
|
if (result.success) {
|
||||||
} catch (error) {
|
setMessage('verify', {type: 'success', text: result.message || 'Intégrité vérifiée'});
|
||||||
console.error('Failed to create backup:', error);
|
} else {
|
||||||
setMessage('backup', {
|
setMessage('verify', {type: 'error', text: result.error || 'Vérification échouée'});
|
||||||
type: 'error',
|
}
|
||||||
text: 'Erreur lors de la création de la sauvegarde'
|
});
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsCreatingBackup(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVerifyDatabase = async () => {
|
const handleDeleteBackup = (filename: string) => {
|
||||||
setIsVerifying(true);
|
startTransition(async () => {
|
||||||
setMessage('verify', null);
|
try {
|
||||||
try {
|
console.log('🔄 Deleting backup:', filename);
|
||||||
await backupClient.verifyDatabase();
|
await backupClient.deleteBackup(filename);
|
||||||
setMessage('verify', {type: 'success', text: 'Intégrité vérifiée'});
|
setShowDeleteConfirm(null);
|
||||||
} catch (error) {
|
setMessage('restore', {type: 'success', text: `Sauvegarde ${filename} supprimée`});
|
||||||
console.error('Database verification failed:', error);
|
|
||||||
setMessage('verify', {type: 'error', text: 'Vérification échouée'});
|
console.log('🔄 Reloading data after deletion...');
|
||||||
} finally {
|
await loadData();
|
||||||
setIsVerifying(false);
|
console.log('✅ Data reloaded after deletion');
|
||||||
}
|
} catch (error) {
|
||||||
};
|
console.error('Failed to delete backup:', error);
|
||||||
|
setMessage('restore', {type: 'error', text: 'Suppression échouée'});
|
||||||
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 handleRestoreBackup = async (filename: string) => {
|
const handleRestoreBackup = async (filename: string) => {
|
||||||
@@ -192,10 +229,16 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
|||||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
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;
|
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) {
|
if (isLoading) {
|
||||||
@@ -391,17 +434,17 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleCreateBackup(false)}
|
onClick={() => handleCreateBackup(false)}
|
||||||
disabled={isCreatingBackup}
|
disabled={isPending}
|
||||||
className="bg-[var(--primary)] hover:bg-[var(--primary)]/90 text-[var(--primary-foreground)]"
|
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>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleCreateBackup(true)}
|
onClick={() => handleCreateBackup(true)}
|
||||||
disabled={isCreatingBackup}
|
disabled={isPending}
|
||||||
className="bg-orange-600 hover:bg-orange-700 text-white"
|
className="bg-orange-600 hover:bg-orange-700 text-white"
|
||||||
>
|
>
|
||||||
{isCreatingBackup ? 'Création...' : 'Forcer'}
|
{isPending ? 'Création...' : 'Forcer'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-[var(--muted-foreground)]">
|
<div className="text-xs text-[var(--muted-foreground)]">
|
||||||
@@ -413,10 +456,10 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleVerifyDatabase}
|
onClick={handleVerifyDatabase}
|
||||||
disabled={isVerifying}
|
disabled={isPending}
|
||||||
className="bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)] hover:bg-[var(--muted)]"
|
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>
|
</Button>
|
||||||
<InlineMessage messageKey="verify" />
|
<InlineMessage messageKey="verify" />
|
||||||
|
|
||||||
@@ -433,6 +476,13 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Graphique timeline des sauvegardes */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<BackupTimelineChart stats={Array.isArray(backupStats) ? backupStats : []} />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Colonne latérale: Statut et historique */}
|
{/* Colonne latérale: Statut et historique */}
|
||||||
@@ -520,7 +570,7 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||||
{formatDate(backup.createdAt)}
|
{formatDateWithTime(backup.createdAt)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export class BackupUtils {
|
|||||||
* Génère un nom de fichier de backup
|
* Génère un nom de fichier de backup
|
||||||
*/
|
*/
|
||||||
static generateBackupFilename(type: 'manual' | 'automatic'): string {
|
static generateBackupFilename(type: 'manual' | 'automatic'): string {
|
||||||
const timestamp = getToday().toISOString().replace(/[:.]/g, '-');
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
return `towercontrol_${type}_${timestamp}.db`;
|
return `towercontrol_${type}_${timestamp}.db`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ export class BackupService {
|
|||||||
id: backupId,
|
id: backupId,
|
||||||
filename: path.basename(finalPath),
|
filename: path.basename(finalPath),
|
||||||
size: stats.size,
|
size: stats.size,
|
||||||
createdAt: getToday(),
|
createdAt: new Date(), // Utiliser l'heure actuelle au lieu de getToday()
|
||||||
type,
|
type,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
databaseHash,
|
databaseHash,
|
||||||
@@ -290,7 +290,7 @@ export class BackupService {
|
|||||||
// Sauvegarder les métadonnées du backup
|
// Sauvegarder les métadonnées du backup
|
||||||
await this.saveBackupMetadata(path.basename(finalPath), {
|
await this.saveBackupMetadata(path.basename(finalPath), {
|
||||||
databaseHash,
|
databaseHash,
|
||||||
createdAt: getToday(),
|
createdAt: new Date(), // Utiliser l'heure actuelle
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -314,7 +314,7 @@ export class BackupService {
|
|||||||
id: backupId,
|
id: backupId,
|
||||||
filename,
|
filename,
|
||||||
size: 0,
|
size: 0,
|
||||||
createdAt: getToday(),
|
createdAt: new Date(), // Utiliser l'heure actuelle même en cas d'erreur
|
||||||
type,
|
type,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
error: errorMessage,
|
error: errorMessage,
|
||||||
@@ -565,6 +565,59 @@ export class BackupService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les statistiques de sauvegarde par jour pour les N derniers jours
|
||||||
|
*/
|
||||||
|
async getBackupStats(days: number = 30): Promise<Array<{
|
||||||
|
date: string;
|
||||||
|
manual: number;
|
||||||
|
automatic: number;
|
||||||
|
total: number;
|
||||||
|
}>> {
|
||||||
|
try {
|
||||||
|
const backups = await this.listBackups();
|
||||||
|
const now = new Date();
|
||||||
|
const stats: { [date: string]: { manual: number; automatic: number; total: number } } = {};
|
||||||
|
|
||||||
|
// Initialiser les stats pour chaque jour
|
||||||
|
for (let i = 0; i < days; i++) {
|
||||||
|
const date = new Date(now);
|
||||||
|
date.setDate(date.getDate() - i);
|
||||||
|
// Utiliser la date locale pour éviter les décalages UTC
|
||||||
|
const localDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
|
||||||
|
const dateStr = localDate.toISOString().split('T')[0]; // Format YYYY-MM-DD
|
||||||
|
stats[dateStr] = { manual: 0, automatic: 0, total: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compter les sauvegardes par jour et par type
|
||||||
|
backups.forEach(backup => {
|
||||||
|
// Utiliser la date locale pour éviter les décalages UTC
|
||||||
|
const backupDate = new Date(backup.createdAt.getTime() - backup.createdAt.getTimezoneOffset() * 60000)
|
||||||
|
.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
if (stats[backupDate]) {
|
||||||
|
if (backup.type === 'manual') {
|
||||||
|
stats[backupDate].manual++;
|
||||||
|
} else {
|
||||||
|
stats[backupDate].automatic++;
|
||||||
|
}
|
||||||
|
stats[backupDate].total++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convertir en tableau et trier par date
|
||||||
|
return Object.entries(stats)
|
||||||
|
.map(([date, counts]) => ({
|
||||||
|
date,
|
||||||
|
...counts
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting backup stats:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instance singleton
|
// Instance singleton
|
||||||
|
|||||||
Reference in New Issue
Block a user